1import { assert, describe, it } from "@effect/vitest"
2import { Schema, type SchemaRepresentation } from "effect"
3import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/http-api"
4
5describe("OpenApi representation v2 consumer", () => {
6 it("uses canonical JSON codecs for additional declaration schemas", () => {
7 const AdditionalDate = Schema.Date.annotate({ identifier: "AdditionalDate" })
8 const Api = HttpApi.make("Api").annotate(HttpApi.AdditionalSchemas, [AdditionalDate])
9
10 assert.deepStrictEqual(OpenApi.fromApi(Api).components.schemas, {
11 AdditionalDate: { $ref: "#/components/schemas/AdditionalDateEncoded" },
12 AdditionalDateEncoded: { type: "string" }
13 })
14 })
15
16 it("applies the reference policy without removing additional schema components", () => {
17 const AdditionalDate = Schema.Date.annotate({ identifier: "AdditionalDate" })
18 const Api = HttpApi.make("Api").annotate(HttpApi.AdditionalSchemas, [AdditionalDate]).add(
19 HttpApiGroup.make("test").add(
20 HttpApiEndpoint.get("date", "/date", { success: AdditionalDate })
21 )
22 )
23 const spec = OpenApi.fromApi(Api, { referencePolicy: () => undefined })
24
25 assert.deepStrictEqual(spec.components.schemas, {
26 AdditionalDate: { type: "string" }
27 })
28 assert.deepStrictEqual(
29 spec.paths["/date"]?.get?.responses[200]?.content?.["application/json"]?.schema,
30 { type: "string" }
31 )
32 })
33
34 it("uses canonical JSON codecs for response declaration schemas", () => {
35 const Api = HttpApi.make("Api").add(
36 HttpApiGroup.make("test").add(
37 HttpApiEndpoint.get("date", "/date", { success: Schema.Date })
38 )
39 )
40
41 assert.deepStrictEqual(
42 OpenApi.fromApi(Api).paths["/date"]?.get?.responses[200]?.content?.["application/json"]?.schema,
43 { type: "string" }
44 )
45 })
46
47 it("deduplicates JSON encoding definitions across regular and SSE responses", () => {
48 const Content = Schema.Struct({ text: Schema.String }).annotate({ identifier: "Tool.Content" })
49 const Api = HttpApi.make("Api").add(
50 HttpApiGroup.make("test").add(
51 HttpApiEndpoint.get("content", "/content", {
52 success: Schema.fromJsonString(Content)
53 }),
54 HttpApiEndpoint.get("stream", "/stream", {
55 success: HttpApiSchema.StreamSse({ data: Content })
56 })
57 )
58 )
59 const spec = OpenApi.fromApi(Api)
60
61 assert.deepStrictEqual(spec.components.schemas, {
62 "Tool.ContentEncoded": {
63 type: "string",
64 contentMediaType: "application/json"
65 }
66 })
67 assert.deepStrictEqual(
68 spec.paths["/content"]?.get?.responses[200]?.content?.["application/json"]?.schema,
69 { $ref: "#/components/schemas/Tool.ContentEncoded" }
70 )
71 assert.deepStrictEqual(
72 spec.paths["/stream"]?.get?.responses[200]?.content?.["text/event-stream"]?.schema,
73 {
74 type: "object",
75 properties: {
76 id: { anyOf: [{ type: "string" }, { type: "null" }] },
77 event: { type: "string" },
78 data: { $ref: "#/components/schemas/Tool.ContentEncoded" }
79 },
80 required: ["event", "data"],
81 additionalProperties: false
82 }
83 )
84 })
85
86 it("projects request and response schemas to the encoded side", () => {
87 const Api = HttpApi.make("Api").add(
88 HttpApiGroup.make("test").add(
89 HttpApiEndpoint.post("create", "/create", {
90 payload: Schema.FiniteFromString,
91 success: Schema.FiniteFromString
92 })
93 )
94 )
95
96 const spec = OpenApi.fromApi(Api)
97
98 assert.deepStrictEqual(
99 spec.paths["/create"]?.post?.requestBody?.content["application/json"]?.schema,
100 { type: "string" }
101 )
102 assert.deepStrictEqual(
103 spec.paths["/create"]?.post?.responses[200]?.content?.["application/json"]?.schema,
104 { type: "string" }
105 )
106 })
107
108 it("uses custom JSON Schema compiler annotations", () => {
109 const CustomString = Schema.String.check(Schema.makeFilter<string>((value) => value.length >= 2, {
110 representation: {
111 id: "test/openapi/minTwoCharacters",
112 payload: null
113 },
114 toJsonSchema: () => ({ minLength: 2 })
115 }))
116 const Api = HttpApi.make("Api").add(
117 HttpApiGroup.make("test").add(
118 HttpApiEndpoint.post("create", "/create", { payload: CustomString })
119 )
120 )
121
122 assert.deepStrictEqual(
123 OpenApi.fromApi(Api).paths["/create"]?.post?.requestBody?.content["application/json"]?.schema,
124 {
125 type: "string",
126 minLength: 2
127 }
128 )
129 })
130
131 it("shares definitions and returns cached copies by API identity", () => {
132 const Shared = Schema.Struct({ value: Schema.FiniteFromString }).annotate({ identifier: "Shared" })
133 const Api = HttpApi.make("Api").add(
134 HttpApiGroup.make("test").add(
135 HttpApiEndpoint.post("shared", "/shared", {
136 payload: Schema.Struct({ first: Shared, second: Shared }),
137 success: Shared
138 })
139 )
140 )
141
142 const first = OpenApi.fromApi(Api)
143 const second = OpenApi.fromApi(Api)
144
145 assert.notStrictEqual(second, first)
146 assert.deepStrictEqual(second, first)
147 assert.deepStrictEqual(first.components.schemas.Shared, {
148 type: "object",
149 properties: { value: { type: "string" } },
150 required: ["value"],
151 additionalProperties: false
152 })
153 })
154
155 it("retains referenced parameter schemas without emitting the parameter container", () => {
156 const Filter = Schema.Struct({ value: Schema.String }).annotate({ identifier: "Filter" })
157 const Query = Schema.Struct({ filter: Filter }).annotate({ identifier: "Query" })
158 const Api = HttpApi.make("Api").add(
159 HttpApiGroup.make("test").add(
160 HttpApiEndpoint.get("list", "/list", { query: Query })
161 )
162 )
163
164 const spec = OpenApi.fromApi(Api)
165
166 assert.deepStrictEqual(spec.paths["/list"]?.get?.parameters, [
167 { name: "filter", in: "query", required: true, schema: { $ref: "#/components/schemas/Filter" } }
168 ])
169 assert.deepStrictEqual(spec.components.schemas, {
170 Filter: {
171 type: "object",
172 properties: { value: { type: "string" } },
173 required: ["value"],
174 additionalProperties: false
175 }
176 })
177 })
178
179 it("caches generated specs by API and options identity", () => {
180 const Api = HttpApi.make("Api").add(
181 HttpApiGroup.make("test").add(
182 HttpApiEndpoint.get("value", "/value", {
183 success: Schema.Struct({ value: Schema.String })
184 })
185 )
186 )
187 let calls = 0
188 const referencePolicy: SchemaRepresentation.ReferencePolicy = (input) => {
189 calls++
190 return input.identifier
191 }
192 const options = { referencePolicy }
193
194 OpenApi.fromApi(Api, options)
195 const callsAfterFirstGeneration = calls
196 assert.isAbove(callsAfterFirstGeneration, 0)
197
198 OpenApi.fromApi(Api, options)
199 assert.strictEqual(calls, callsAfterFirstGeneration)
200
201 OpenApi.fromApi(Api, { referencePolicy })
202 assert.isAbove(calls, callsAfterFirstGeneration)
203 })
204})