1import { assert, describe, it } from "@effect/vitest"
2import { Schema } from "effect"
3import { Multipart } from "effect/http"
4import {
5 HttpApi,
6 HttpApiEndpoint,
7 HttpApiError,
8 HttpApiGroup,
9 HttpApiMiddleware,
10 HttpApiSchema,
11 OpenApi
12} from "effect/http-api"
13
14describe("OpenAPI spec", () => {
15 describe("api", () => {
16 it("annotate", () => {
17 const Api = HttpApi.make("api")
18 .annotate(OpenApi.Title, "title")
19 .annotate(OpenApi.Version, "version")
20 .annotate(OpenApi.Description, "description")
21 .annotate(OpenApi.License, { name: "license" })
22 .annotate(OpenApi.Summary, "summary")
23 .annotate(OpenApi.Servers, [{ url: "https://example.com" }])
24 const spec = OpenApi.fromApi(Api)
25 assert.deepStrictEqual(spec.info.title, "title")
26 assert.deepStrictEqual(spec.info.version, "version")
27 assert.deepStrictEqual(spec.info.description, "description")
28 assert.deepStrictEqual(spec.info.license, { name: "license" })
29 assert.deepStrictEqual(spec.info.summary, "summary")
30 assert.deepStrictEqual(spec.servers, [{ url: "https://example.com" }])
31 })
32
33 it("annotateMerge", () => {
34 const Api = HttpApi.make("api").annotateMerge(OpenApi.annotations({
35 title: "title",
36 version: "version",
37 description: "description",
38 license: { name: "license" },
39 summary: "summary",
40 servers: [{ url: "https://example.com" }]
41 }))
42 const spec = OpenApi.fromApi(Api)
43 assert.deepStrictEqual(spec.info.title, "title")
44 assert.deepStrictEqual(spec.info.version, "version")
45 assert.deepStrictEqual(spec.info.description, "description")
46 assert.deepStrictEqual(spec.info.license, { name: "license" })
47 assert.deepStrictEqual(spec.info.summary, "summary")
48 assert.deepStrictEqual(spec.servers, [{ url: "https://example.com" }])
49 })
50
51 it("AdditionalSchemas", () => {
52 const Api = HttpApi.make("api")
53 .annotate(HttpApi.AdditionalSchemas, [Schema.String.annotate({ identifier: "A" })])
54 const spec = OpenApi.fromApi(Api)
55 assert.deepStrictEqual(spec.components.schemas, {
56 A: {
57 "type": "string"
58 }
59 })
60 })
61
62 it("Override", () => {
63 const Api = HttpApi.make("api")
64 .annotate(OpenApi.Override, {
65 tags: [{ name: "A" }]
66 })
67 const spec = OpenApi.fromApi(Api)
68 assert.deepStrictEqual(spec.tags, [{ name: "A" }])
69 })
70
71 it("Transform", () => {
72 const Api = HttpApi.make("api")
73 .annotate(OpenApi.Transform, (spec) => ({
74 ...spec,
75 tags: [{ name: "A" }]
76 }))
77 const spec = OpenApi.fromApi(Api)
78 assert.deepStrictEqual(spec.tags, [{ name: "A" }])
79 })
80 })
81
82 describe("group", () => {
83 it("annotations", () => {
84 const Api = HttpApi.make("api")
85 .add(
86 HttpApiGroup.make("group")
87 // Add a description for the group
88 .annotate(OpenApi.Description, "my description")
89 // Provide external documentation links
90 .annotate(OpenApi.ExternalDocs, {
91 url: "http://example.com",
92 description: "example"
93 })
94 // Override parts of the final output
95 .annotate(OpenApi.Override, { name: "my name" })
96 // Transform the final specification for this group
97 .annotate(OpenApi.Transform, (spec) => ({
98 ...spec,
99 name: spec.name + "-transformed"
100 }))
101 )
102 .add(
103 HttpApiGroup.make("excluded")
104 // Exclude the group from the final specification
105 .annotate(OpenApi.Exclude, true)
106 )
107 const spec = OpenApi.fromApi(Api)
108 assert.deepStrictEqual(spec, {
109 "openapi": "3.1.0",
110 "info": {
111 "title": "Api",
112 "version": "0.0.1"
113 },
114 "paths": {},
115 "components": {
116 "schemas": {},
117 "securitySchemes": {}
118 },
119 "security": [],
120 "tags": [
121 {
122 "name": "my name-transformed",
123 "description": "my description",
124 "externalDocs": {
125 "url": "http://example.com",
126 "description": "example"
127 }
128 }
129 ]
130 })
131 })
132
133 it("top level group", () => {
134 const Api = HttpApi.make("api").add(
135 // Mark the group as top-level
136 HttpApiGroup.make("group", { topLevel: true }).add(
137 HttpApiEndpoint.get("get", "/", {
138 success: Schema.String
139 })
140 )
141 )
142 const spec = OpenApi.fromApi(Api)
143 assert.deepStrictEqual(spec.paths, {
144 "/": {
145 "get": {
146 "tags": [
147 "group"
148 ],
149 "operationId": "get",
150 "parameters": [],
151 "security": [],
152 "responses": {
153 "200": {
154 "description": "Success",
155 "content": {
156 "application/json": {
157 "schema": {
158 "type": "string"
159 }
160 }
161 }
162 }
163 }
164 }
165 }
166 })
167 })
168 })
169
170 describe("endpoint", () => {
171 describe("annotations", () => {
172 it("endpoint annotations", () => {
173 const Api = HttpApi.make("api").add(
174 HttpApiGroup.make("group")
175 .add(
176 HttpApiEndpoint.get("get", "/", {
177 success: Schema.String
178 })
179 // Add a description
180 .annotate(OpenApi.Description, "my description")
181 // Provide a summary
182 .annotate(OpenApi.Summary, "my summary")
183 // Mark the endpoint as deprecated
184 .annotate(OpenApi.Deprecated, true)
185 // Provide external documentation
186 .annotate(OpenApi.ExternalDocs, {
187 url: "http://example.com",
188 description: "example"
189 })
190 )
191 .add(
192 HttpApiEndpoint.get("excluded", "/excluded", {
193 success: Schema.String
194 })
195 // Exclude this endpoint from the final specification
196 .annotate(OpenApi.Exclude, true)
197 )
198 )
199 const spec = OpenApi.fromApi(Api)
200 assert.deepStrictEqual(spec, {
201 "openapi": "3.1.0",
202 "info": {
203 "title": "Api",
204 "version": "0.0.1"
205 },
206 "paths": {
207 "/": {
208 "get": {
209 "tags": [
210 "group"
211 ],
212 "operationId": "group.get",
213 "parameters": [],
214 "security": [],
215 "responses": {
216 "200": {
217 "description": "Success",
218 "content": {
219 "application/json": {
220 "schema": {
221 "type": "string"
222 }
223 }
224 }
225 }
226 },
227 "description": "my description",
228 "summary": "my summary",
229 "deprecated": true,
230 "externalDocs": {
231 "url": "http://example.com",
232 "description": "example"
233 }
234 }
235 }
236 },
237 "components": {
238 "schemas": {},
239 "securitySchemes": {}
240 },
241 "security": [],
242 "tags": [
243 {
244 "name": "group"
245 }
246 ]
247 })
248 })
249
250 it("success description", () => {
251 const User = Schema.Struct({
252 id: Schema.Finite,
253 name: Schema.String
254 }).annotate({ identifier: "User" })
255
256 const Api = HttpApi.make("api").add(
257 HttpApiGroup.make("group").add(
258 HttpApiEndpoint.get("getUsers", "/users", {
259 success: Schema.Array(User).annotate({
260 description: "Returns an array of users"
261 })
262 })
263 )
264 )
265 const spec = OpenApi.fromApi(Api)
266 assert.deepStrictEqual(spec.paths, {
267 "/users": {
268 "get": {
269 "tags": [
270 "group"
271 ],
272 "operationId": "group.getUsers",
273 "parameters": [],
274 "security": [],
275 "responses": {
276 "200": {
277 "description": "Returns an array of users",
278 "content": {
279 "application/json": {
280 "schema": {
281 "type": "array",
282 "items": {
283 "$ref": "#/components/schemas/User"
284 },
285 "description": "Returns an array of users"
286 }
287 }
288 }
289 }
290 }
291 }
292 }
293 })
294 assert.deepStrictEqual(spec.components.schemas, {
295 User: {
296 "type": "object",
297 "properties": {
298 "id": {
299 "type": "number"
300 },
301 "name": {
302 "type": "string"
303 }
304 },
305 "required": [
306 "id",
307 "name"
308 ],
309 "additionalProperties": false
310 }
311 })
312 })
313 })
314
315 it("catch all path", () => {
316 const Api = HttpApi.make("api")
317 .add(
318 HttpApiGroup.make("group")
319 .add(
320 HttpApiEndpoint.get("a", "*", {
321 success: Schema.String
322 })
323 )
324 )
325 const spec = OpenApi.fromApi(Api)
326 assert.deepStrictEqual(spec.paths["*"].get?.responses, {
327 "200": {
328 description: "Success",
329 content: {
330 "application/json": {
331 schema: {
332 "type": "string"
333 }
334 }
335 }
336 }
337 })
338 })
339
340 it("middleware error", () => {
341 class M extends HttpApiMiddleware.Service<M>()("Http/Logger", {
342 error: Schema.String
343 .pipe(
344 HttpApiSchema.status(405),
345 HttpApiSchema.asText()
346 )
347 }) {}
348
349 const Api = HttpApi.make("api")
350 .add(
351 HttpApiGroup.make("group")
352 .add(
353 HttpApiEndpoint.get("a", "/a", {
354 success: Schema.Finite
355 })
356 ).middleware(M)
357 )
358 const spec = OpenApi.fromApi(Api)
359 assert.deepStrictEqual(spec.paths["/a"].get?.responses, {
360 "200": {
361 description: "Success",
362 content: {
363 "application/json": {
364 schema: {
365 "type": "number"
366 }
367 }
368 }
369 },
370 "405": {
371 description: "Error",
372 content: {
373 "text/plain": {
374 schema: {
375 "type": "string"
376 }
377 }
378 }
379 }
380 })
381 })
382
383 describe("params option", () => {
384 it("GET", () => {
385 const Api = HttpApi.make("api")
386 .add(
387 HttpApiGroup.make("group")
388 .add(
389 HttpApiEndpoint.get("a", "/a/:id", {
390 params: {
391 id: Schema.FiniteFromString
392 }
393 })
394 )
395 )
396 const spec = OpenApi.fromApi(Api)
397 assert.deepStrictEqual(spec.paths["/a/{id}"].get?.parameters, [
398 {
399 name: "id",
400 in: "path",
401 schema: {
402 "type": "string"
403 },
404 required: true
405 }
406 ])
407 })
408 })
409
410 describe("payload option", () => {
411 describe("GET", () => {
412 it("query parameters", () => {
413 const Api = HttpApi.make("api")
414 .add(
415 HttpApiGroup.make("group")
416 .add(
417 HttpApiEndpoint.get("a", "/a", {
418 payload: {
419 required: Schema.FiniteFromString,
420 optionalKey: Schema.optionalKey(Schema.FiniteFromString),
421 optional: Schema.optional(Schema.FiniteFromString)
422 }
423 })
424 )
425 )
426 const spec = OpenApi.fromApi(Api)
427 assert.deepStrictEqual(spec.paths["/a"].get?.parameters, [
428 {
429 name: "required",
430 in: "query",
431 schema: {
432 "type": "string"
433 },
434 required: true
435 },
436 {
437 name: "optionalKey",
438 in: "query",
439 schema: {
440 "type": "string"
441 },
442 required: false
443 },
444 {
445 name: "optional",
446 in: "query",
447 schema: {
448 "anyOf": [
449 { "type": "string" },
450 { "type": "null" }
451 ]
452 },
453 required: false
454 }
455 ])
456 })
457 })
458
459 describe("POST", () => {
460 it("NoContent", () => {
461 const Api = HttpApi.make("api")
462 .add(
463 HttpApiGroup.make("group")
464 .add(HttpApiEndpoint.post("a", "/a", { payload: HttpApiSchema.NoContent }))
465 )
466 const spec = OpenApi.fromApi(Api)
467 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody, undefined)
468 })
469
470 it("NoContent + payload", () => {
471 const Api = HttpApi.make("api")
472 .add(
473 HttpApiGroup.make("group")
474 .add(HttpApiEndpoint.post("a", "/a", { payload: [HttpApiSchema.NoContent, Schema.String] }))
475 )
476 const spec = OpenApi.fromApi(Api)
477 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
478 "application/json": {
479 schema: {
480 "type": "string"
481 }
482 }
483 })
484 })
485
486 it("Json (default)", () => {
487 const Api = HttpApi.make("api")
488 .add(
489 HttpApiGroup.make("group")
490 .add(
491 HttpApiEndpoint.post("a", "/a", {
492 payload: Schema.String
493 })
494 )
495 )
496 const spec = OpenApi.fromApi(Api)
497 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
498 "application/json": {
499 schema: {
500 "type": "string"
501 }
502 }
503 })
504 })
505
506 describe("encodings", () => {
507 it("asMultipart", () => {
508 const Api = HttpApi.make("api")
509 .add(
510 HttpApiGroup.make("group")
511 .add(
512 HttpApiEndpoint.post("a", "/a", {
513 payload: Schema.String.pipe(HttpApiSchema.asMultipart())
514 })
515 )
516 )
517 const spec = OpenApi.fromApi(Api)
518 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
519 "multipart/form-data": {
520 schema: {
521 "type": "string"
522 }
523 }
524 })
525 })
526
527 it("asMultipartStream", () => {
528 const Api = HttpApi.make("api")
529 .add(
530 HttpApiGroup.make("group")
531 .add(
532 HttpApiEndpoint.post("a", "/a", {
533 payload: Schema.String.pipe(HttpApiSchema.asMultipartStream())
534 })
535 )
536 )
537 const spec = OpenApi.fromApi(Api)
538 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
539 "multipart/form-data": {
540 schema: {
541 "type": "string"
542 }
543 }
544 })
545 })
546
547 it("asJson + contentType", () => {
548 const Api = HttpApi.make("api")
549 .add(
550 HttpApiGroup.make("group")
551 .add(
552 HttpApiEndpoint.post("a", "/a", {
553 payload: Schema.String.pipe(
554 HttpApiSchema.asJson({ contentType: "application/problem+json" })
555 )
556 })
557 )
558 )
559 const spec = OpenApi.fromApi(Api)
560 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
561 "application/problem+json": {
562 schema: {
563 "type": "string"
564 }
565 }
566 })
567 })
568
569 it("asText", () => {
570 const Api = HttpApi.make("api")
571 .add(
572 HttpApiGroup.make("group")
573 .add(
574 HttpApiEndpoint.post("a", "/a", {
575 payload: Schema.String.pipe(HttpApiSchema.asText())
576 })
577 )
578 )
579 const spec = OpenApi.fromApi(Api)
580 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
581 "text/plain": {
582 schema: {
583 "type": "string"
584 }
585 }
586 })
587 })
588
589 it("asFormUrlEncoded", () => {
590 const Api = HttpApi.make("api")
591 .add(
592 HttpApiGroup.make("group")
593 .add(
594 HttpApiEndpoint.post("a", "/a", {
595 payload: Schema.Struct({ a: Schema.String }).pipe(
596 HttpApiSchema.asFormUrlEncoded()
597 )
598 })
599 )
600 )
601 const spec = OpenApi.fromApi(Api)
602 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
603 "application/x-www-form-urlencoded": {
604 schema: {
605 "type": "object",
606 "properties": {
607 "a": {
608 "type": "string"
609 }
610 },
611 "required": ["a"],
612 "additionalProperties": false
613 }
614 }
615 })
616 })
617
618 it("asUint8Array", () => {
619 const Api = HttpApi.make("api")
620 .add(
621 HttpApiGroup.make("group")
622 .add(
623 HttpApiEndpoint.post("a", "/a", {
624 payload: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array())
625 })
626 )
627 )
628 const spec = OpenApi.fromApi(Api)
629 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
630 "application/octet-stream": {
631 schema: {
632 "type": "string",
633 "format": "binary"
634 }
635 }
636 })
637 })
638
639 it("array of schemas with different encodings", () => {
640 const Api = HttpApi.make("api")
641 .add(
642 HttpApiGroup.make("group")
643 .add(
644 HttpApiEndpoint.post("a", "/a", {
645 payload: [
646 Schema.Struct({ a: Schema.String }), // application/json
647 Schema.String.pipe(HttpApiSchema.asText()), // text/plain
648 Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()) // application/octet-stream
649 ]
650 })
651 )
652 )
653 const spec = OpenApi.fromApi(Api)
654 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
655 "application/json": {
656 schema: {
657 "type": "object",
658 "properties": {
659 "a": {
660 "type": "string"
661 }
662 },
663 "required": ["a"],
664 "additionalProperties": false
665 }
666 },
667 "text/plain": {
668 schema: {
669 "type": "string"
670 }
671 },
672 "application/octet-stream": {
673 schema: {
674 "type": "string",
675 "format": "binary"
676 }
677 }
678 })
679 })
680
681 it("asMultipart with FilesSchema", () => {
682 const Api = HttpApi.make("api")
683 .add(
684 HttpApiGroup.make("group")
685 .add(
686 HttpApiEndpoint.post("a", "/a", {
687 payload: Multipart.FilesSchema.pipe(HttpApiSchema.asMultipart())
688 })
689 )
690 )
691 const spec = OpenApi.fromApi(Api)
692 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
693 "multipart/form-data": {
694 schema: {
695 type: "array",
696 items: {
697 type: "string",
698 format: "binary"
699 }
700 }
701 }
702 })
703 })
704
705 it("asMultipart with SingleFileSchema", () => {
706 const Api = HttpApi.make("api")
707 .add(
708 HttpApiGroup.make("group")
709 .add(
710 HttpApiEndpoint.post("a", "/a", {
711 payload: Multipart.SingleFileSchema.pipe(HttpApiSchema.asMultipart())
712 })
713 )
714 )
715 const spec = OpenApi.fromApi(Api)
716 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
717 "multipart/form-data": {
718 schema: {
719 type: "string",
720 format: "binary"
721 }
722 }
723 })
724 })
725
726 it("asMultipart with SingleFileSchema in Struct", () => {
727 const Api = HttpApi.make("api")
728 .add(
729 HttpApiGroup.make("group")
730 .add(
731 HttpApiEndpoint.post("a", "/a", {
732 payload: Schema.Struct({
733 file: Multipart.SingleFileSchema
734 }).pipe(HttpApiSchema.asMultipart())
735 })
736 )
737 )
738 const spec = OpenApi.fromApi(Api)
739 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
740 "multipart/form-data": {
741 schema: {
742 type: "object",
743 properties: {
744 file: {
745 type: "string",
746 format: "binary"
747 }
748 },
749 required: ["file"],
750 additionalProperties: false
751 }
752 }
753 })
754 })
755
756 it("FilesSchema in non-multipart payload should not be converted to binary", () => {
757 const Api = HttpApi.make("api")
758 .add(
759 HttpApiGroup.make("group")
760 .add(
761 HttpApiEndpoint.post("a", "/a", {
762 payload: Schema.Struct({
763 files: Multipart.FilesSchema
764 })
765 })
766 )
767 )
768 const spec = OpenApi.fromApi(Api)
769 assert.deepStrictEqual(spec.paths["/a"].post?.requestBody?.content, {
770 "application/json": {
771 schema: {
772 type: "object",
773 properties: {
774 files: {
775 type: "array",
776 items: {
777 type: "object",
778 properties: {
779 key: { "type": "string" },
780 name: { "type": "string" },
781 contentType: { type: "string", contentEncoding: "binary" },
782 path: { "type": "string" }
783 },
784 required: ["key", "name", "contentType", "path"],
785 additionalProperties: false
786 }
787 }
788 },
789 required: ["files"],
790 additionalProperties: false
791 }
792 }
793 })
794 })
795 })
796 })
797 })
798
799 describe("success option", () => {
800 it("no content (default)", () => {
801 const Api = HttpApi.make("api")
802 .add(
803 HttpApiGroup.make("group")
804 .add(HttpApiEndpoint.get("a", "/a"))
805 )
806 const spec = OpenApi.fromApi(Api)
807 assert.deepStrictEqual(spec.paths["/a"].get?.responses["204"], {
808 description: "<No Content>"
809 })
810 })
811
812 it("schema", () => {
813 const Api = HttpApi.make("api")
814 .add(
815 HttpApiGroup.make("group")
816 .add(HttpApiEndpoint.get("a", "/a", {
817 success: Schema.String
818 }))
819 )
820 const spec = OpenApi.fromApi(Api)
821 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
822 "application/json": {
823 schema: {
824 "type": "string"
825 }
826 }
827 })
828 })
829
830 describe("NoContent", () => {
831 it("makeNoContent(204)", () => {
832 const Api = HttpApi.make("api")
833 .add(
834 HttpApiGroup.make("group")
835 .add(HttpApiEndpoint.get("a", "/a", {
836 success: HttpApiSchema.Empty(204)
837 }))
838 )
839 const spec = OpenApi.fromApi(Api)
840 assert.deepStrictEqual(spec.paths["/a"].get?.responses["204"], {
841 description: "<No Content>"
842 })
843 })
844
845 it("NoContent", () => {
846 const Api = HttpApi.make("api")
847 .add(
848 HttpApiGroup.make("group")
849 .add(HttpApiEndpoint.get("a", "/a", {
850 success: HttpApiSchema.NoContent
851 }))
852 )
853 const spec = OpenApi.fromApi(Api)
854 assert.deepStrictEqual(spec.paths["/a"].get?.responses["204"], {
855 description: "<No Content>"
856 })
857 })
858
859 it("Created", () => {
860 const Api = HttpApi.make("api")
861 .add(
862 HttpApiGroup.make("group")
863 .add(HttpApiEndpoint.get("a", "/a", {
864 success: HttpApiSchema.Created
865 }))
866 )
867 const spec = OpenApi.fromApi(Api)
868 assert.deepStrictEqual(spec.paths["/a"].get?.responses["201"], {
869 description: "<No Content>"
870 })
871 })
872
873 it("status(\"Created\")", () => {
874 const Api = HttpApi.make("api")
875 .add(
876 HttpApiGroup.make("group")
877 .add(HttpApiEndpoint.get("a", "/a", {
878 success: Schema.Void.pipe(HttpApiSchema.status("Created"))
879 }))
880 )
881 const spec = OpenApi.fromApi(Api)
882 assert.deepStrictEqual(spec.paths["/a"].get?.responses["201"], {
883 description: "<No Content>"
884 })
885 })
886
887 it("Accepted", () => {
888 const Api = HttpApi.make("api")
889 .add(
890 HttpApiGroup.make("group")
891 .add(HttpApiEndpoint.get("a", "/a", {
892 success: HttpApiSchema.Accepted
893 }))
894 )
895 const spec = OpenApi.fromApi(Api)
896 assert.deepStrictEqual(spec.paths["/a"].get?.responses["202"], {
897 description: "<No Content>"
898 })
899 })
900 })
901
902 describe("encodings", () => {
903 it("asJson + contentType", () => {
904 const Api = HttpApi.make("api")
905 .add(
906 HttpApiGroup.make("group")
907 .add(
908 HttpApiEndpoint.get("a", "/a", {
909 success: Schema.String.pipe(
910 HttpApiSchema.asJson({ contentType: "application/problem+json" })
911 )
912 })
913 )
914 )
915 const spec = OpenApi.fromApi(Api)
916 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
917 "application/problem+json": {
918 schema: {
919 "type": "string"
920 }
921 }
922 })
923 })
924
925 it("asText", () => {
926 const Api = HttpApi.make("api")
927 .add(
928 HttpApiGroup.make("group")
929 .add(
930 HttpApiEndpoint.get("a", "/a", {
931 success: Schema.String.pipe(HttpApiSchema.asText())
932 })
933 )
934 )
935 const spec = OpenApi.fromApi(Api)
936 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
937 "text/plain": {
938 schema: {
939 "type": "string"
940 }
941 }
942 })
943 })
944
945 it("asFormUrlEncoded", () => {
946 const Api = HttpApi.make("api")
947 .add(
948 HttpApiGroup.make("group")
949 .add(
950 HttpApiEndpoint.get("a", "/a", {
951 success: Schema.Struct({ a: Schema.String }).pipe(
952 HttpApiSchema.asFormUrlEncoded()
953 )
954 })
955 )
956 )
957 const spec = OpenApi.fromApi(Api)
958 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
959 "application/x-www-form-urlencoded": {
960 schema: {
961 "type": "object",
962 "properties": {
963 "a": {
964 "type": "string"
965 }
966 },
967 "required": ["a"],
968 "additionalProperties": false
969 }
970 }
971 })
972 })
973
974 it("asUint8Array", () => {
975 const Api = HttpApi.make("api")
976 .add(
977 HttpApiGroup.make("group")
978 .add(
979 HttpApiEndpoint.get("a", "/a", {
980 success: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array())
981 })
982 )
983 )
984 const spec = OpenApi.fromApi(Api)
985 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
986 "application/octet-stream": {
987 schema: {
988 "type": "string",
989 "format": "binary"
990 }
991 }
992 })
993 })
994
995 it("StreamSse", () => {
996 const Api = HttpApi.make("api")
997 .add(
998 HttpApiGroup.make("group")
999 .add(
1000 HttpApiEndpoint.get("a", "/a", {
1001 success: HttpApiSchema.StreamSse({
1002 events: Schema.Struct({
1003 event: Schema.Literal("message"),
1004 data: Schema.String
1005 }),
1006 error: Schema.Struct({
1007 message: Schema.String
1008 })
1009 })
1010 })
1011 )
1012 )
1013 const spec = OpenApi.fromApi(Api)
1014 const mediaType = spec.paths["/a"].get?.responses["200"].content?.["text/event-stream"]
1015 const stream = mediaType?.["x-effect-stream"]
1016 assert.strictEqual(stream?.encoding, "sse")
1017 if (stream?.encoding !== "sse") {
1018 assert.fail("expected SSE stream metadata")
1019 }
1020 assert.strictEqual(stream.failureEvent, "effect/http-api/stream/failure")
1021 assert.strictEqual(typeof stream.causeSchema, "object")
1022 assert.strictEqual(typeof stream.errorSchema, "object")
1023 })
1024
1025 it("StreamSse with annotated status", () => {
1026 const Api = HttpApi.make("api")
1027 .add(
1028 HttpApiGroup.make("group")
1029 .add(
1030 HttpApiEndpoint.get("a", "/a", {
1031 success: HttpApiSchema.status(202)(
1032 HttpApiSchema.StreamSse({
1033 events: Schema.Struct({
1034 event: Schema.Literal("message"),
1035 data: Schema.String
1036 }),
1037 error: Schema.Struct({
1038 message: Schema.String
1039 })
1040 })
1041 )
1042 })
1043 )
1044 )
1045 const spec = OpenApi.fromApi(Api)
1046 assert.strictEqual(spec.paths["/a"].get?.responses["200"], undefined)
1047 assert.strictEqual(
1048 spec.paths["/a"].get?.responses["202"].content?.["text/event-stream"]?.["x-effect-stream"]?.encoding,
1049 "sse"
1050 )
1051 })
1052
1053 it("StreamUint8Array", () => {
1054 const Api = HttpApi.make("api")
1055 .add(
1056 HttpApiGroup.make("group")
1057 .add(
1058 HttpApiEndpoint.get("a", "/a", {
1059 success: HttpApiSchema.StreamUint8Array()
1060 })
1061 )
1062 )
1063 const spec = OpenApi.fromApi(Api)
1064 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
1065 "application/octet-stream": {
1066 schema: {
1067 "type": "string",
1068 "format": "binary"
1069 },
1070 "x-effect-stream": {
1071 encoding: "uint8array"
1072 }
1073 }
1074 })
1075 })
1076
1077 it("StreamUint8Array with annotated status", () => {
1078 const Api = HttpApi.make("api")
1079 .add(
1080 HttpApiGroup.make("group")
1081 .add(
1082 HttpApiEndpoint.get("a", "/a", {
1083 success: HttpApiSchema.status(206)(HttpApiSchema.StreamUint8Array())
1084 })
1085 )
1086 )
1087 const spec = OpenApi.fromApi(Api)
1088 assert.strictEqual(spec.paths["/a"].get?.responses["200"], undefined)
1089 assert.deepStrictEqual(spec.paths["/a"].get?.responses["206"].content, {
1090 "application/octet-stream": {
1091 schema: {
1092 "type": "string",
1093 "format": "binary"
1094 },
1095 "x-effect-stream": {
1096 encoding: "uint8array"
1097 }
1098 }
1099 })
1100 })
1101
1102 it("union with top level encoding", () => {
1103 const Api = HttpApi.make("api")
1104 .add(
1105 HttpApiGroup.make("group")
1106 .add(
1107 HttpApiEndpoint.get("a", "/a", {
1108 success: Schema.Union([
1109 Schema.Struct({ a: Schema.String }),
1110 Schema.Struct({ b: Schema.String })
1111 ]).pipe(HttpApiSchema.asFormUrlEncoded())
1112 })
1113 )
1114 )
1115 const spec = OpenApi.fromApi(Api)
1116 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
1117 "application/x-www-form-urlencoded": {
1118 schema: {
1119 "anyOf": [
1120 {
1121 "type": "object",
1122 "properties": {
1123 "a": {
1124 "type": "string"
1125 }
1126 },
1127 "required": ["a"],
1128 "additionalProperties": false
1129 },
1130 {
1131 "type": "object",
1132 "properties": {
1133 "b": {
1134 "type": "string"
1135 }
1136 },
1137 "required": ["b"],
1138 "additionalProperties": false
1139 }
1140 ]
1141 }
1142 }
1143 })
1144 })
1145
1146 it("asJson + astText", () => {
1147 const Api = HttpApi.make("api")
1148 .add(
1149 HttpApiGroup.make("group")
1150 .add(
1151 HttpApiEndpoint.get("a", "/a", {
1152 success: [
1153 Schema.String,
1154 Schema.String.pipe(
1155 HttpApiSchema.asText()
1156 )
1157 ]
1158 })
1159 )
1160 )
1161 const spec = OpenApi.fromApi(Api)
1162 assert.deepStrictEqual(spec.paths["/a"].get?.responses["200"].content, {
1163 "application/json": {
1164 "schema": {
1165 "type": "string"
1166 }
1167 },
1168 "text/plain": {
1169 "schema": {
1170 "type": "string"
1171 }
1172 }
1173 })
1174 })
1175 })
1176 })
1177
1178 describe("error option", () => {
1179 it("should deduplicate the descriptions", () => {
1180 class Group extends HttpApiGroup.make("users")
1181 .add(HttpApiEndpoint.post("a", "/a", {
1182 error: HttpApiError.BadRequestNoContent
1183 }))
1184 {}
1185
1186 class Api extends HttpApi.make("api").add(Group) {}
1187 const spec = OpenApi.fromApi(Api)
1188 assert.deepStrictEqual(spec.paths["/a"].post?.responses["400"], {
1189 description: "BadRequest"
1190 })
1191 assert.deepStrictEqual(spec.components.schemas, {})
1192 })
1193
1194 describe("1 endopoint", () => {
1195 it("no identifier annotation", () => {
1196 class Group extends HttpApiGroup.make("users")
1197 .add(HttpApiEndpoint.post("a", "/a", {
1198 payload: Schema.String,
1199 success: Schema.String,
1200 error: Schema.String
1201 }))
1202 {}
1203
1204 class Api extends HttpApi.make("api").add(Group) {}
1205 const spec = OpenApi.fromApi(Api)
1206 assert.deepStrictEqual(spec.paths["/a"].post?.responses, {
1207 "200": {
1208 description: "Success",
1209 content: {
1210 "application/json": {
1211 schema: {
1212 "type": "string"
1213 }
1214 }
1215 }
1216 },
1217 "500": {
1218 description: "Error",
1219 content: {
1220 "application/json": {
1221 schema: {
1222 "type": "string"
1223 }
1224 }
1225 }
1226 }
1227 })
1228 assert.deepStrictEqual(spec.components.schemas, {})
1229 })
1230
1231 it("identifier annotation", () => {
1232 class Group extends HttpApiGroup.make("users")
1233 .add(HttpApiEndpoint.post("a", "/a", {
1234 payload: Schema.String,
1235 success: Schema.String,
1236 error: Schema.String.annotate({ identifier: "id" })
1237 }))
1238 {}
1239
1240 class Api extends HttpApi.make("api").add(Group) {}
1241 const spec = OpenApi.fromApi(Api)
1242 assert.deepStrictEqual(spec.paths["/a"].post?.responses, {
1243 "200": {
1244 description: "Success",
1245 content: {
1246 "application/json": {
1247 schema: {
1248 "type": "string"
1249 }
1250 }
1251 }
1252 },
1253 "500": {
1254 description: "id",
1255 content: {
1256 "application/json": {
1257 schema: {
1258 "$ref": "#/components/schemas/id"
1259 }
1260 }
1261 }
1262 }
1263 })
1264 assert.deepStrictEqual(spec.components.schemas, {
1265 id: {
1266 "type": "string"
1267 }
1268 })
1269 })
1270
1271 it("httpApiStatus annotation", () => {
1272 class Group extends HttpApiGroup.make("users")
1273 .add(HttpApiEndpoint.post("a", "/a", {
1274 payload: Schema.String,
1275 success: Schema.String,
1276 error: Schema.String.annotate({ httpApiStatus: 400 })
1277 }))
1278 {}
1279
1280 class Api extends HttpApi.make("api").add(Group) {}
1281 const spec = OpenApi.fromApi(Api)
1282 assert.deepStrictEqual(spec.paths["/a"].post?.responses, {
1283 "200": {
1284 description: "Success",
1285 content: {
1286 "application/json": {
1287 schema: {
1288 "type": "string"
1289 }
1290 }
1291 }
1292 },
1293 "400": {
1294 description: "Error",
1295 content: {
1296 "application/json": {
1297 schema: {
1298 "type": "string"
1299 }
1300 }
1301 }
1302 }
1303 })
1304 assert.deepStrictEqual(spec.components.schemas, {})
1305 })
1306 })
1307
1308 describe("2 endopoints", () => {
1309 it("no identifier annotation", () => {
1310 const E = Schema.String
1311 class Group extends HttpApiGroup.make("users")
1312 .add(HttpApiEndpoint.post("a", "/a", {
1313 payload: Schema.String,
1314 success: Schema.String,
1315 error: E
1316 }))
1317 .add(HttpApiEndpoint.post("b", "/b", {
1318 payload: Schema.String,
1319 success: Schema.String,
1320 error: E
1321 }))
1322 {}
1323
1324 class Api extends HttpApi.make("api").add(Group) {}
1325 const spec = OpenApi.fromApi(Api)
1326 assert.deepStrictEqual(spec.paths["/a"].post?.responses, {
1327 "200": {
1328 description: "Success",
1329 content: {
1330 "application/json": {
1331 schema: {
1332 "type": "string"
1333 }
1334 }
1335 }
1336 },
1337 "500": {
1338 description: "Error",
1339 content: {
1340 "application/json": {
1341 schema: {
1342 "type": "string"
1343 }
1344 }
1345 }
1346 }
1347 })
1348 assert.deepStrictEqual(spec.paths["/b"].post?.responses, {
1349 "200": {
1350 description: "Success",
1351 content: {
1352 "application/json": {
1353 schema: {
1354 "type": "string"
1355 }
1356 }
1357 }
1358 },
1359 "500": {
1360 description: "Error",
1361 content: {
1362 "application/json": {
1363 schema: {
1364 "type": "string"
1365 }
1366 }
1367 }
1368 }
1369 })
1370 assert.deepStrictEqual(spec.components.schemas, {})
1371 })
1372
1373 it("identifier annotation", () => {
1374 const E = Schema.String.annotate({ identifier: "id" })
1375 class Group extends HttpApiGroup.make("users")
1376 .add(HttpApiEndpoint.post("a", "/a", {
1377 payload: Schema.String,
1378 success: Schema.String,
1379 error: E
1380 }))
1381 .add(HttpApiEndpoint.post("b", "/b", {
1382 payload: Schema.String,
1383 success: Schema.String,
1384 error: E
1385 }))
1386 {}
1387
1388 class Api extends HttpApi.make("api").add(Group) {}
1389 const spec = OpenApi.fromApi(Api)
1390 assert.deepStrictEqual(spec.paths["/a"].post?.responses, {
1391 "200": {
1392 description: "Success",
1393 content: {
1394 "application/json": {
1395 schema: {
1396 "type": "string"
1397 }
1398 }
1399 }
1400 },
1401 "500": {
1402 description: "id",
1403 content: {
1404 "application/json": {
1405 schema: {
1406 "$ref": "#/components/schemas/id"
1407 }
1408 }
1409 }
1410 }
1411 })
1412 assert.deepStrictEqual(spec.paths["/b"].post?.responses, {
1413 "200": {
1414 description: "Success",
1415 content: {
1416 "application/json": {
1417 schema: {
1418 "type": "string"
1419 }
1420 }
1421 }
1422 },
1423 "500": {
1424 description: "id",
1425 content: {
1426 "application/json": {
1427 schema: {
1428 "$ref": "#/components/schemas/id"
1429 }
1430 }
1431 }
1432 }
1433 })
1434 assert.deepStrictEqual(spec.components.schemas, {
1435 id: {
1436 "type": "string"
1437 }
1438 })
1439 })
1440
1441 it("httpApiStatus annotation", () => {
1442 const E = Schema.String.annotate({ httpApiStatus: 400 })
1443 class Group extends HttpApiGroup.make("users")
1444 .add(HttpApiEndpoint.post("a", "/a", {
1445 payload: Schema.String,
1446 success: Schema.String,
1447 error: E
1448 }))
1449 .add(HttpApiEndpoint.post("b", "/b", {
1450 payload: Schema.String,
1451 success: Schema.String,
1452 error: E
1453 }))
1454 {}
1455
1456 class Api extends HttpApi.make("api").add(Group) {}
1457 const spec = OpenApi.fromApi(Api)
1458 assert.deepStrictEqual(spec.paths["/a"].post?.responses, {
1459 "200": {
1460 description: "Success",
1461 content: {
1462 "application/json": {
1463 schema: {
1464 "type": "string"
1465 }
1466 }
1467 }
1468 },
1469 "400": {
1470 description: "Error",
1471 content: {
1472 "application/json": {
1473 schema: {
1474 "type": "string"
1475 }
1476 }
1477 }
1478 }
1479 })
1480 assert.deepStrictEqual(spec.paths["/b"].post?.responses, {
1481 "200": {
1482 description: "Success",
1483 content: {
1484 "application/json": {
1485 schema: {
1486 "type": "string"
1487 }
1488 }
1489 }
1490 },
1491 "400": {
1492 description: "Error",
1493 content: {
1494 "application/json": {
1495 schema: {
1496 "type": "string"
1497 }
1498 }
1499 }
1500 }
1501 })
1502 assert.deepStrictEqual(spec.components.schemas, {})
1503 })
1504 })
1505
1506 describe("No Content", () => {
1507 it("makeNoContent(400)", () => {
1508 const Api = HttpApi.make("api")
1509 .add(
1510 HttpApiGroup.make("group")
1511 .add(
1512 HttpApiEndpoint.get("a", "/a", {
1513 error: HttpApiSchema.Empty(400)
1514 })
1515 )
1516 )
1517 const spec = OpenApi.fromApi(Api)
1518 assert.deepStrictEqual(spec.paths["/a"].get?.responses["400"], {
1519 description: "<No Content>"
1520 })
1521 })
1522
1523 it("Empty(401)", () => {
1524 const Api = HttpApi.make("api")
1525 .add(
1526 HttpApiGroup.make("group")
1527 .add(
1528 HttpApiEndpoint.get("a", "/a", {
1529 error: HttpApiSchema.Empty(401)
1530 })
1531 )
1532 )
1533 const spec = OpenApi.fromApi(Api)
1534 assert.deepStrictEqual(spec.paths["/a"].get?.responses["401"], {
1535 description: "<No Content>"
1536 })
1537 })
1538
1539 it("UnauthorizedNoContent", () => {
1540 const Api = HttpApi.make("api")
1541 .add(
1542 HttpApiGroup.make("group")
1543 .add(
1544 HttpApiEndpoint.get("a", "/a", {
1545 error: HttpApiError.UnauthorizedNoContent
1546 })
1547 )
1548 )
1549 const spec = OpenApi.fromApi(Api)
1550 assert.deepStrictEqual(spec.paths["/a"].get?.responses["401"], {
1551 description: "Unauthorized"
1552 })
1553 })
1554
1555 it("BadRequestNoContent", () => {
1556 const Api = HttpApi.make("api")
1557 .add(
1558 HttpApiGroup.make("group")
1559 .add(
1560 HttpApiEndpoint.get("a", "/a", {
1561 error: HttpApiError.BadRequestNoContent
1562 })
1563 )
1564 )
1565 const spec = OpenApi.fromApi(Api)
1566 assert.deepStrictEqual(spec.paths["/a"].get?.responses["400"], {
1567 description: "BadRequest"
1568 })
1569 })
1570 })
1571 })
1572 })
1573})