@@ -11,6 +11,12 @@ module Yoga.HTTP.API.Route.OpenAPI
1111 , renderQueryParamsSchema
1212 , class RenderQueryParamsSchemaRL
1313 , renderQueryParamsSchemaRL
14+ , class RenderCookieParamsSchema
15+ , renderCookieParamsSchema
16+ , class RenderCookieParamsSchemaRL
17+ , renderCookieParamsSchemaRL
18+ , class GetContentType
19+ , getContentType
1420 , class RenderRequestBodySchema
1521 , renderRequestBodySchema
1622 , class RenderResponseHeadersSchema
@@ -32,29 +38,58 @@ module Yoga.HTTP.API.Route.OpenAPI
3238 , detectSecurity
3339 , class DetectSecurityRL
3440 , detectSecurityRL
41+ , class DetectCookieSecurity
42+ , detectCookieSecurity
43+ , class DetectCookieSecurityRL
44+ , detectCookieSecurityRL
3545 , class ToOpenAPI
3646 , toOpenAPIImpl
3747 , toOpenAPI
3848 , class CollectOperations
3949 , collectOperations
50+ , class CollectNamedOperationsRL
51+ , collectNamedOperationsRL
4052 , OperationEntry
4153 , buildOpenAPISpec
4254 , buildOpenAPISpec'
4355 , ServerObject
4456 , OpenAPISpec
4557 , ResponseHeaderObject
58+ , class CollectSchemas
59+ , collectSchemas
60+ , class CollectSchemasRL
61+ , collectSchemasRL
62+ , class CollectVariantSchemasRL
63+ , collectVariantSchemasRL
64+ , class CollectRouteSchemas
65+ , collectRouteSchemas
66+ , class CollectRouteSchemasRL
67+ , collectRouteSchemasRL
68+ , class CollectSchemaNames
69+ , class CollectSchemaNamesRL
70+ , class CollectVariantSchemaNames
71+ , class CollectRouteSchemaNames
72+ , class CollectRouteSchemaNameRL
73+ , class ValidateNoDuplicatesRL
74+ , class ValidateSchemaNames
75+ -- , class RenderCallbacks
76+ -- , renderCallbacks
4677 ) where
4778
4879import Prelude
4980
81+import Data.Array as Array
5082import Data.Foldable (foldl)
5183import Data.Maybe (Maybe(..), maybe)
84+import Data.String as String
85+import Data.Identity (Identity)
86+import Data.Undefined.NoProblem (Opt, toMaybe, undefined)
87+import Yoga.HTTP.API.UnionTrick (UndefinedOr, uorToMaybe, class CoerceFields, coerceFields)
5288import Data.String.Regex (replace)
5389import Data.String.Regex.Flags (global)
5490import Data.String.Regex.Unsafe (unsafeRegex)
5591import Data.Symbol (class IsSymbol, reflectSymbol)
5692import Data.Tuple (Tuple(..))
57−import Data.Tuple.Nested (type (/\), (/\))
5893import Foreign (Foreign)
5994import Foreign.Object as FObject
6095import Prim.Row as Row
@@ -62,10 +97,11 @@ import Prim.RowList (class RowToList, RowList, Cons, Nil)
6297import Prim.RowList as RL
6398import Type.Proxy (Proxy(..))
6499import Unsafe.Coerce (unsafeCoerce)
65−import Yoga.HTTP.API.Route.BearerToken (BearerToken)
66−import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
100+import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, ApiKeyCookie, DigestAuth)
101+import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType, NoBody)
67102import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
68−import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, class HasDescription, description, class HasExample, example, class HasFormat, format, class HasDeprecated, deprecated, class HasMinimum, minimum, class HasMaximum, maximum, class HasPattern, pattern, class HasMinLength, minLength, class HasMaxLength, maxLength, class HasTitle, title, class HasNullable, nullable, class HasDefault, default, class HasEnum, enum, class HasOperationMetadata, operationMetadata)
103+import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, Callback, Examples, class HasDescription, description, class HasExample, example, class HasFormat, format, class HasDeprecated, deprecated, class HasMinimum, minimum, class HasMaximum, maximum, class HasPattern, pattern, class HasMinLength, minLength, class HasMaxLength, maxLength, class HasTitle, title, class HasNullable, nullable, class HasDefault, default, class HasEnum, enum, class HasExamples, examples)
104+import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
69105import Yoga.HTTP.API.Route.Response (class ToResponse)
70106import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
71107import Yoga.JSON (class WriteForeign, writeImpl)
@@ -79,6 +115,7 @@ buildSchema
79115 :: { type :: String
80116 , format :: Maybe String
81117 , example :: Maybe String
118+ , examples :: Maybe (FObject.Object Foreign)
82119 , minimum :: Maybe Int
83120 , maximum :: Maybe Int
84121 , pattern :: Maybe String
@@ -120,20 +157,28 @@ buildParameter
120157 , schema :: Foreign
121158 , description :: Maybe String
122159 , deprecated :: Maybe Boolean
160+ , examples :: Maybe (FObject.Object Foreign)
123161 }
124162 -> Foreign
125−buildParameter { name, in: inStr, required, schema, description: descMaybe, deprecated: deprecatedMaybe } =
163+buildParameter { name, in: inStr, required, schema, description: descMaybe, deprecated: deprecatedMaybe, examples: examplesMaybe } =
126164 let
165+ ins :: forall a. String -> a -> FObject.Object Foreign -> FObject.Object Foreign
166+ ins key val obj = FObject.insert key (unsafeCoerce val) obj
167+
168+ insMaybe :: forall a. String -> Maybe a -> FObject.Object Foreign -> FObject.Object Foreign
169+ insMaybe key valMaybe obj = maybe obj (\v -> ins key v obj) valMaybe
170+
127171 base = FObject.fromFoldable
128172 [ Tuple "name" (unsafeCoerce name)
129173 , Tuple "in" (unsafeCoerce inStr)
130174 , Tuple "required" (unsafeCoerce required)
131175 , Tuple "schema" schema
132176 ]
133− withDesc = maybe base (\d -> FObject.insert "description" (unsafeCoerce d) base) descMaybe
134− withDeprecated = maybe withDesc (\dep -> FObject.insert "deprecated" (unsafeCoerce dep) withDesc) deprecatedMaybe
135177 in
136− unsafeCoerce withDeprecated
178+ unsafeCoerce $ base
179+ # insMaybe "description" descMaybe
180+ # insMaybe "deprecated" deprecatedMaybe
181+ # insMaybe "examples" examplesMaybe
137182
138183--------------------------------------------------------------------------------
139184-- OpenAPI Generation
@@ -153,8 +198,72 @@ class RenderHeadersSchemaRL (rl :: RowList Type) (headers :: Row Type) | rl -> h
153198instance RenderHeadersSchemaRL RL.Nil () where
154199 renderHeadersSchemaRL _ = []
155200
201+-- Skip BearerToken headers (they're handled as security schemes, not regular headers)
202+instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name BearerToken tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name BearerToken tail) headers where
203+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
204+
205+-- Skip BearerToken wrapped in Description
206+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc BearerToken) tail) headers where
207+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
208+
209+-- Skip BearerToken wrapped in Example
210+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex BearerToken) tail) headers where
211+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
212+
213+-- Skip BearerToken wrapped in Deprecated
214+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BearerToken) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BearerToken) tail) headers where
215+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
216+
217+-- Skip BasicAuth headers (they're handled as security schemes, not regular headers)
218+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name BasicAuth tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name BasicAuth tail) headers where
219+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
220+
221+-- Skip BasicAuth wrapped in Description
222+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc BasicAuth) tail) headers where
223+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
224+
225+-- Skip BasicAuth wrapped in Example
226+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex BasicAuth) tail) headers where
227+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
228+
229+-- Skip BasicAuth wrapped in Deprecated
230+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated BasicAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated BasicAuth) tail) headers where
231+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
232+
233+-- Skip ApiKeyHeader headers (they're handled as security schemes, not regular headers)
234+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name ApiKeyHeader tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name ApiKeyHeader tail) headers where
235+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
236+
237+-- Skip ApiKeyHeader wrapped in Description
238+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc ApiKeyHeader) tail) headers where
239+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
240+
241+-- Skip ApiKeyHeader wrapped in Example
242+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex ApiKeyHeader) tail) headers where
243+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
244+
245+-- Skip ApiKeyHeader wrapped in Deprecated
246+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated ApiKeyHeader) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated ApiKeyHeader) tail) headers where
247+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
248+
249+-- Skip DigestAuth headers (they're handled as security schemes, not regular headers)
250+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name DigestAuth tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name DigestAuth tail) headers where
251+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
252+
253+-- Skip DigestAuth wrapped in Description
254+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Description desc DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Description desc DigestAuth) tail) headers where
255+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
256+
257+-- Skip DigestAuth wrapped in Example
258+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Example ex DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Example ex DigestAuth) tail) headers where
259+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
260+
261+-- Skip DigestAuth wrapped in Deprecated
262+else instance (RenderHeadersSchemaRL tail tailRow, Row.Cons name (Deprecated DigestAuth) tailRow headers, Row.Lacks name tailRow) => RenderHeadersSchemaRL (RL.Cons name (Deprecated DigestAuth) tail) headers where
263+ renderHeadersSchemaRL _ = renderHeadersSchemaRL (Proxy :: Proxy tail)
264+
156265-- Required header (non-Maybe type)
157−instance
266+else instance
158267 ( IsSymbol name
159268 , HeaderValueType ty
160269 , HasDescription ty
@@ -170,6 +279,7 @@ instance
170279 , HasDefault ty
171280 , HasDeprecated ty
172281 , HasEnum ty
282+ , HasExamples ty
173283 , RenderHeadersSchemaRL tail tailRow
174284 , Row.Cons name ty tailRow headers
175285 , Row.Lacks name tailRow
@@ -182,6 +292,7 @@ instance
182292 { type: headerValueType p
183293 , format: format p
184294 , example: example p
295+ , examples: Nothing
185296 , minimum: minimum p
186297 , maximum: maximum p
187298 , pattern: pattern p
@@ -199,6 +310,8 @@ instance
199310 , schema
200311 , description: description p
201312 , deprecated: if deprecated p then Just true else Nothing
313+ , examples: examples p
314+
202315 }
203316 rest = renderHeadersSchemaRL (Proxy :: Proxy tail)
204317 in
@@ -256,10 +369,170 @@ else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecate
256369 in
257370 [ bearerAuth ]
258371
372+-- Case: BasicAuth found
373+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name BasicAuth tail) where
374+ detectSecurityRL _ =
375+ let
376+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
377+ in
378+ [ basicAuth ]
379+
380+-- Case: BasicAuth wrapped in Description
381+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc BasicAuth) tail) where
382+ detectSecurityRL _ =
383+ let
384+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
385+ in
386+ [ basicAuth ]
387+
388+-- Case: BasicAuth wrapped in Example
389+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex BasicAuth) tail) where
390+ detectSecurityRL _ =
391+ let
392+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
393+ in
394+ [ basicAuth ]
395+
396+-- Case: BasicAuth wrapped in Deprecated
397+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated BasicAuth) tail) where
398+ detectSecurityRL _ =
399+ let
400+ basicAuth = unsafeCoerce $ FObject.singleton "basicAuth" ([] :: Array String)
401+ in
402+ [ basicAuth ]
403+
404+-- Case: ApiKeyHeader found
405+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name ApiKeyHeader tail) where
406+ detectSecurityRL _ =
407+ let
408+ keyName = reflectSymbol (Proxy :: Proxy name)
409+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
410+ in
411+ [ apiKeyAuth ]
412+
413+-- Case: ApiKeyHeader wrapped in Description
414+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Description desc ApiKeyHeader) tail) where
415+ detectSecurityRL _ =
416+ let
417+ keyName = reflectSymbol (Proxy :: Proxy name)
418+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
419+ in
420+ [ apiKeyAuth ]
421+
422+-- Case: ApiKeyHeader wrapped in Example
423+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Example ex ApiKeyHeader) tail) where
424+ detectSecurityRL _ =
425+ let
426+ keyName = reflectSymbol (Proxy :: Proxy name)
427+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
428+ in
429+ [ apiKeyAuth ]
430+
431+-- Case: ApiKeyHeader wrapped in Deprecated
432+else instance (DetectSecurityRL tail, IsSymbol name) => DetectSecurityRL (RL.Cons name (Deprecated ApiKeyHeader) tail) where
433+ detectSecurityRL _ =
434+ let
435+ keyName = reflectSymbol (Proxy :: Proxy name)
436+ apiKeyAuth = unsafeCoerce $ FObject.singleton (keyName <> "ApiKey") ([] :: Array String)
437+ in
438+ [ apiKeyAuth ]
439+
440+-- Case: DigestAuth found
441+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name DigestAuth tail) where
442+ detectSecurityRL _ =
443+ let
444+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
445+ in
446+ [ digestAuth ]
447+
448+-- Case: DigestAuth wrapped in Description
449+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Description desc DigestAuth) tail) where
450+ detectSecurityRL _ =
451+ let
452+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
453+ in
454+ [ digestAuth ]
455+
456+-- Case: DigestAuth wrapped in Example
457+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Example ex DigestAuth) tail) where
458+ detectSecurityRL _ =
459+ let
460+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
461+ in
462+ [ digestAuth ]
463+
464+-- Case: DigestAuth wrapped in Deprecated
465+else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecated DigestAuth) tail) where
466+ detectSecurityRL _ =
467+ let
468+ digestAuth = unsafeCoerce $ FObject.singleton "digestAuth" ([] :: Array String)
469+ in
470+ [ digestAuth ]
471+
259472-- Case: Non-security header, recurse
260473else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
261474 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
262475
476+--------------------------------------------------------------------------------
477+-- Security Detection for Cookies
478+--------------------------------------------------------------------------------
479+
480+-- | Detect security requirements in request cookies
481+-- | Returns an array of security requirement objects (e.g., [{ sessionIdCookie: [] }])
482+class DetectCookieSecurity (cookies :: Row Type) where
483+ detectCookieSecurity :: Proxy cookies -> Array Foreign
484+
485+instance (RowToList cookies rl, DetectCookieSecurityRL rl) => DetectCookieSecurity cookies where
486+ detectCookieSecurity _ = detectCookieSecurityRL (Proxy :: Proxy rl)
487+
488+-- | Helper class using RowList to find security-related cookies
489+class DetectCookieSecurityRL (rl :: RowList Type) where
490+ detectCookieSecurityRL :: Proxy rl -> Array Foreign
491+
492+-- Base case: no cookies = no security
493+instance DetectCookieSecurityRL RL.Nil where
494+ detectCookieSecurityRL _ = []
495+
496+-- Case: ApiKeyCookie found
497+instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name ApiKeyCookie tail) where
498+ detectCookieSecurityRL _ =
499+ let
500+ cookieName = reflectSymbol (Proxy :: Proxy name)
501+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
502+ in
503+ [ apiKeyCookie ]
504+
505+-- Case: ApiKeyCookie wrapped in Description
506+else instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name (Description desc ApiKeyCookie) tail) where
507+ detectCookieSecurityRL _ =
508+ let
509+ cookieName = reflectSymbol (Proxy :: Proxy name)
510+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
511+ in
512+ [ apiKeyCookie ]
513+
514+-- Case: ApiKeyCookie wrapped in Example
515+else instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name (Example ex ApiKeyCookie) tail) where
516+ detectCookieSecurityRL _ =
517+ let
518+ cookieName = reflectSymbol (Proxy :: Proxy name)
519+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
520+ in
521+ [ apiKeyCookie ]
522+
523+-- Case: ApiKeyCookie wrapped in Deprecated
524+else instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name (Deprecated ApiKeyCookie) tail) where
525+ detectCookieSecurityRL _ =
526+ let
527+ cookieName = reflectSymbol (Proxy :: Proxy name)
528+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
529+ in
530+ [ apiKeyCookie ]
531+
532+-- Case: Non-security cookie, recurse
533+else instance (DetectCookieSecurityRL tail, HeaderValueType ty) => DetectCookieSecurityRL (RL.Cons name ty tail) where
534+ detectCookieSecurityRL _ = detectCookieSecurityRL (Proxy :: Proxy tail)
535+
263536--------------------------------------------------------------------------------
264537-- Path Parameters Schema Generation
265538--------------------------------------------------------------------------------
@@ -295,6 +568,7 @@ instance
295568 , HasDefault ty
296569 , HasDeprecated ty
297570 , HasEnum ty
571+ , HasExamples ty
298572 , RenderPathParamsSchemaRL tail tailRow
299573 , Row.Cons name ty tailRow params
300574 , Row.Lacks name tailRow
@@ -307,6 +581,7 @@ instance
307581 { type: headerValueType p
308582 , format: format p
309583 , example: example p
584+ , examples: Nothing
310585 , minimum: minimum p
311586 , maximum: maximum p
312587 , pattern: pattern p
@@ -324,6 +599,8 @@ instance
324599 , schema
325600 , description: description p
326601 , deprecated: if deprecated p then Just true else Nothing
602+ , examples: examples p
603+
327604 }
328605 rest = renderPathParamsSchemaRL (Proxy :: Proxy tail)
329606 in
@@ -364,6 +641,7 @@ instance
364641 , HasDefault ty
365642 , HasDeprecated ty
366643 , HasEnum ty
644+ , HasExamples ty
367645 , RenderQueryParamsSchemaRL tail tailRow
368646 , Row.Cons name ty tailRow params
369647 , Row.Lacks name tailRow
@@ -376,6 +654,7 @@ instance
376654 { type: headerValueType p
377655 , format: format p
378656 , example: example p
657+ , examples: Nothing
379658 , minimum: minimum p
380659 , maximum: maximum p
381660 , pattern: pattern p
@@ -393,16 +672,108 @@ instance
393672 , schema
394673 , description: description p
395674 , deprecated: if deprecated p then Just true else Nothing
675+ , examples: examples p
676+
396677 }
397678 rest = renderQueryParamsSchemaRL (Proxy :: Proxy tail)
398679 in
399680 [ param ] <> rest
400681
682+--------------------------------------------------------------------------------
683+-- Cookie Parameters Schema Generation
684+--------------------------------------------------------------------------------
685+
686+-- | Render cookie parameters row as OpenAPI parameter array
687+class RenderCookieParamsSchema (params :: Row Type) where
688+ renderCookieParamsSchema :: Proxy params -> Array Foreign
689+
690+instance (RowToList params rl, RenderCookieParamsSchemaRL rl params) => RenderCookieParamsSchema params where
691+ renderCookieParamsSchema _ = renderCookieParamsSchemaRL (Proxy :: Proxy rl)
692+
693+-- | Helper class using RowList
694+class RenderCookieParamsSchemaRL (rl :: RowList Type) (params :: Row Type) | rl -> params where
695+ renderCookieParamsSchemaRL :: Proxy rl -> Array Foreign
696+
697+instance RenderCookieParamsSchemaRL RL.Nil () where
698+ renderCookieParamsSchemaRL _ = []
699+
700+-- Skip ApiKeyCookie cookies (they're handled as security schemes, not regular cookies)
701+instance (RenderCookieParamsSchemaRL tail tailRow, Row.Cons name ApiKeyCookie tailRow params, Row.Lacks name tailRow) => RenderCookieParamsSchemaRL (RL.Cons name ApiKeyCookie tail) params where
702+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
703+
704+-- Skip ApiKeyCookie wrapped in Description
705+else instance (RenderCookieParamsSchemaRL tail tailRow, Row.Cons name (Description desc ApiKeyCookie) tailRow params, Row.Lacks name tailRow) => RenderCookieParamsSchemaRL (RL.Cons name (Description desc ApiKeyCookie) tail) params where
706+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
707+
708+-- Skip ApiKeyCookie wrapped in Example
709+else instance (RenderCookieParamsSchemaRL tail tailRow, Row.Cons name (Example ex ApiKeyCookie) tailRow params, Row.Lacks name tailRow) => RenderCookieParamsSchemaRL (RL.Cons name (Example ex ApiKeyCookie) tail) params where
710+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
711+
712+-- Skip ApiKeyCookie wrapped in Deprecated
713+else instance (RenderCookieParamsSchemaRL tail tailRow, Row.Cons name (Deprecated ApiKeyCookie) tailRow params, Row.Lacks name tailRow) => RenderCookieParamsSchemaRL (RL.Cons name (Deprecated ApiKeyCookie) tail) params where
714+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
715+
716+-- Cookie parameter instance
717+else instance
718+ ( IsSymbol name
719+ , HeaderValueType ty
720+ , HasDescription ty
721+ , HasExample ty
722+ , HasFormat ty
723+ , HasMinimum ty
724+ , HasMaximum ty
725+ , HasPattern ty
726+ , HasMinLength ty
727+ , HasMaxLength ty
728+ , HasTitle ty
729+ , HasNullable ty
730+ , HasDefault ty
731+ , HasDeprecated ty
732+ , HasEnum ty
733+ , HasExamples ty
734+ , RenderCookieParamsSchemaRL tail tailRow
735+ , Row.Cons name ty tailRow params
736+ , Row.Lacks name tailRow
737+ ) =>
738+ RenderCookieParamsSchemaRL (RL.Cons name ty tail) params where
739+ renderCookieParamsSchemaRL _ =
740+ let
741+ p = Proxy :: Proxy ty
742+ schema = buildSchema
743+ { type: headerValueType p
744+ , format: format p
745+ , example: example p
746+ , examples: Nothing
747+ , minimum: minimum p
748+ , maximum: maximum p
749+ , pattern: pattern p
750+ , minLength: minLength p
751+ , maxLength: maxLength p
752+ , title: title p
753+ , nullable: nullable p
754+ , default: default p
755+ , enum: enum p
756+ }
757+ param = buildParameter
758+ { name: reflectSymbol (Proxy :: Proxy name)
759+ , in: "cookie"
760+ , required: false
761+ , schema
762+ , description: description p
763+ , deprecated: if deprecated p then Just true else Nothing
764+ , examples: examples p
765+
766+ }
767+ rest = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
768+ in
769+ [ param ] <> rest
770+
401771--------------------------------------------------------------------------------
402772-- JSON Schema Generation (Type Introspection)
403773--------------------------------------------------------------------------------
404774
405775-- | Render a PureScript type as an OpenAPI JSON schema
776+class RenderJSONSchema :: forall k. k -> Constraint
406777class RenderJSONSchema ty where
407778 renderJSONSchema :: Proxy ty -> Foreign
408779
@@ -411,6 +782,7 @@ instance RenderJSONSchema String where
411782 renderJSONSchema _ = buildSchema
412783 { type: "string"
413784 , format: Nothing
785+ , examples: Nothing
414786 , example: Nothing
415787 , minimum: Nothing
416788 , maximum: Nothing
@@ -427,6 +799,7 @@ instance RenderJSONSchema Int where
427799 renderJSONSchema _ = buildSchema
428800 { type: "integer"
429801 , format: Nothing
802+ , examples: Nothing
430803 , example: Nothing
431804 , minimum: Nothing
432805 , maximum: Nothing
@@ -443,6 +816,7 @@ instance RenderJSONSchema Number where
443816 renderJSONSchema _ = buildSchema
444817 { type: "number"
445818 , format: Nothing
819+ , examples: Nothing
446820 , example: Nothing
447821 , minimum: Nothing
448822 , maximum: Nothing
@@ -459,6 +833,7 @@ instance RenderJSONSchema Boolean where
459833 renderJSONSchema _ = buildSchema
460834 { type: "boolean"
461835 , format: Nothing
836+ , examples: Nothing
462837 , example: Nothing
463838 , minimum: Nothing
464839 , maximum: Nothing
@@ -475,6 +850,7 @@ instance renderJSONSchemaUnit :: RenderJSONSchema Unit where
475850 renderJSONSchema _ = buildSchema
476851 { type: "null"
477852 , format: Nothing
853+ , examples: Nothing
478854 , example: Nothing
479855 , minimum: Nothing
480856 , maximum: Nothing
@@ -532,6 +908,22 @@ instance RenderJSONSchema a => RenderJSONSchema (JSON a) where
532908instance RenderJSONSchema a => RenderJSONSchema (FormData a) where
533909 renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
534910
911+-- MultipartFormData encoding wrapper (unwrap and render inner type)
912+instance RenderJSONSchema a => RenderJSONSchema (MultipartFormData a) where
913+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
914+
915+-- PlainText encoding wrapper (renders as string)
916+instance RenderJSONSchema (PlainText a) where
917+ renderJSONSchema _ = unsafeCoerce { type: "string" }
918+
919+-- XML encoding wrapper (unwrap and render inner type)
920+instance RenderJSONSchema a => RenderJSONSchema (XML a) where
921+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
922+
923+-- CustomContentType encoding wrapper (unwrap and render inner type)
924+instance RenderJSONSchema a => RenderJSONSchema (CustomContentType mime a) where
925+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
926+
535927-- NoBody encoding (no schema)
536928instance RenderJSONSchema NoBody where
537929 renderJSONSchema _ = unsafeCoerce { type: "null" }
@@ -603,6 +995,18 @@ instance (RenderJSONSchema a, HasEnum (Enum a)) => RenderJSONSchema (Enum a) whe
603995 in
604996 unsafeCoerce withEnum
605997
998+-- Examples wrapper: render inner type (examples are handled elsewhere)
999+instance RenderJSONSchema a => RenderJSONSchema (Examples examplesRow a) where
1000+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
1001+
1002+-- Schema wrapper: generates $ref instead of inline schema
1003+instance (IsSymbol name, RenderJSONSchema a) => RenderJSONSchema (Schema name a) where
1004+ renderJSONSchema _ =
1005+ let
1006+ schemaName = reflectSymbol (Proxy :: Proxy name)
1007+ in
1008+ unsafeCoerce $ FObject.singleton "$ref" (unsafeCoerce $ "#/components/schemas/" <> schemaName)
1009+
6061010-- Other metadata wrappers (Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Default)
6071011-- are similar but require more complex constraints, so we'll skip them for now in body schemas
6081012
@@ -638,6 +1042,103 @@ instance
6381042 in
6391043 [ fieldName ] <> rest
6401044
1045+--------------------------------------------------------------------------------
1046+-- Content Type Extraction
1047+--------------------------------------------------------------------------------
1048+
1049+-- | Extract MIME type from encoding wrapper types
1050+class GetContentType (encoding :: Type) where
1051+ getContentType :: Proxy encoding -> String
1052+
1053+instance GetContentType NoBody where
1054+ getContentType _ = ""
1055+
1056+instance GetContentType (JSON a) where
1057+ getContentType _ = "application/json"
1058+
1059+instance GetContentType (FormData a) where
1060+ getContentType _ = "application/x-www-form-urlencoded"
1061+
1062+instance GetContentType (MultipartFormData a) where
1063+ getContentType _ = "multipart/form-data"
1064+
1065+instance GetContentType (PlainText a) where
1066+ getContentType _ = "text/plain"
1067+
1068+instance GetContentType (XML a) where
1069+ getContentType _ = "application/xml"
1070+
1071+instance IsSymbol mime => GetContentType (CustomContentType mime a) where
1072+ getContentType _ = reflectSymbol (Proxy :: Proxy mime)
1073+
1074+-- For unwrapped types (backward compatibility), default to JSON
1075+instance GetContentType String where
1076+ getContentType _ = "application/json"
1077+
1078+instance GetContentType Int where
1079+ getContentType _ = "application/json"
1080+
1081+instance GetContentType Number where
1082+ getContentType _ = "application/json"
1083+
1084+instance GetContentType Boolean where
1085+ getContentType _ = "application/json"
1086+
1087+instance GetContentType Unit where
1088+ getContentType _ = "application/json"
1089+
1090+instance GetContentType a => GetContentType (Array a) where
1091+ getContentType _ = "application/json"
1092+
1093+instance GetContentType a => GetContentType (Maybe a) where
1094+ getContentType _ = "application/json"
1095+
1096+instance GetContentType (Record row) where
1097+ getContentType _ = "application/json"
1098+
1099+-- Metadata wrappers: pass through to inner type
1100+instance GetContentType a => GetContentType (Description desc a) where
1101+ getContentType _ = getContentType (Proxy :: Proxy a)
1102+
1103+instance GetContentType a => GetContentType (Example ex a) where
1104+ getContentType _ = getContentType (Proxy :: Proxy a)
1105+
1106+instance GetContentType a => GetContentType (Format fmt a) where
1107+ getContentType _ = getContentType (Proxy :: Proxy a)
1108+
1109+instance GetContentType a => GetContentType (Nullable a) where
1110+ getContentType _ = getContentType (Proxy :: Proxy a)
1111+
1112+instance GetContentType a => GetContentType (Deprecated a) where
1113+ getContentType _ = getContentType (Proxy :: Proxy a)
1114+
1115+instance GetContentType a => GetContentType (Enum a) where
1116+ getContentType _ = getContentType (Proxy :: Proxy a)
1117+
1118+instance GetContentType a => GetContentType (Schema name a) where
1119+ getContentType _ = getContentType (Proxy :: Proxy a)
1120+
1121+instance GetContentType a => GetContentType (Minimum v a) where
1122+ getContentType _ = getContentType (Proxy :: Proxy a)
1123+
1124+instance GetContentType a => GetContentType (Maximum v a) where
1125+ getContentType _ = getContentType (Proxy :: Proxy a)
1126+
1127+instance GetContentType a => GetContentType (Pattern pat a) where
1128+ getContentType _ = getContentType (Proxy :: Proxy a)
1129+
1130+instance GetContentType a => GetContentType (MinLength v a) where
1131+ getContentType _ = getContentType (Proxy :: Proxy a)
1132+
1133+instance GetContentType a => GetContentType (MaxLength v a) where
1134+ getContentType _ = getContentType (Proxy :: Proxy a)
1135+
1136+instance GetContentType a => GetContentType (Title t a) where
1137+ getContentType _ = getContentType (Proxy :: Proxy a)
1138+
1139+instance GetContentType a => GetContentType (Default val a) where
1140+ getContentType _ = getContentType (Proxy :: Proxy a)
1141+
6411142--------------------------------------------------------------------------------
6421143-- Request Body Schema Generation
6431144--------------------------------------------------------------------------------
@@ -671,6 +1172,45 @@ instance RenderJSONSchema a => RenderRequestBodySchema (FormData a) where
6711172 }
6721173 }
6731174
1175+-- MultipartFormData: request body with multipart/form-data content type
1176+instance RenderJSONSchema a => RenderRequestBodySchema (MultipartFormData a) where
1177+ renderRequestBodySchema _ = Just
1178+ { required: true
1179+ , content: unsafeCoerce
1180+ { "multipart/form-data":
1181+ { schema: renderJSONSchema (Proxy :: Proxy a) }
1182+ }
1183+ }
1184+
1185+-- PlainText: request body with text/plain content type
1186+instance RenderRequestBodySchema (PlainText a) where
1187+ renderRequestBodySchema _ = Just
1188+ { required: true
1189+ , content: unsafeCoerce
1190+ { "text/plain":
1191+ { schema: unsafeCoerce { type: "string" } }
1192+ }
1193+ }
1194+
1195+-- XML: request body with application/xml content type
1196+instance RenderJSONSchema a => RenderRequestBodySchema (XML a) where
1197+ renderRequestBodySchema _ = Just
1198+ { required: true
1199+ , content: unsafeCoerce
1200+ { "application/xml":
1201+ { schema: renderJSONSchema (Proxy :: Proxy a) }
1202+ }
1203+ }
1204+
1205+-- CustomContentType: request body with custom MIME type
1206+instance (IsSymbol mime, RenderJSONSchema a) => RenderRequestBodySchema (CustomContentType mime a) where
1207+ renderRequestBodySchema _ =
1208+ let
1209+ mimeType = reflectSymbol (Proxy :: Proxy mime)
1210+ contentObj = FObject.singleton mimeType (unsafeCoerce { schema: renderJSONSchema (Proxy :: Proxy a) })
1211+ in
1212+ Just { required: true, content: unsafeCoerce contentObj }
1213+
6741214--------------------------------------------------------------------------------
6751215-- Response Headers Schema Generation
6761216--------------------------------------------------------------------------------
@@ -767,26 +1307,22 @@ class RenderResponseSchema (headers :: Row Type) (body :: Type) where
7671307 -> { "200" ::
7681308 { description :: String
7691309 , headers :: FObject.Object ResponseHeaderObject
770− , content ::
771− { "application/json" ::
772− { schema :: Foreign }
773− }
1310+ , content :: Foreign
7741311 }
7751312 }
7761313
777−instance (RenderResponseHeadersSchema headers, RenderJSONSchema body) => RenderResponseSchema headers body where
1314+instance (RenderResponseHeadersSchema headers, RenderJSONSchema body, GetContentType body) => RenderResponseSchema headers body where
7781315 renderResponseSchema headersProxy bodyProxy =
7791316 let
7801317 headers = renderResponseHeadersSchema headersProxy
7811318 bodySchema = renderJSONSchema bodyProxy
1319+ contentType = getContentType bodyProxy
1320+ contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
7821321 in
7831322 { "200":
7841323 { description: "Successful response"
7851324 , headers: headers
786− , content:
787− { "application/json":
788− { schema: bodySchema }
789− }
1325+ , content: unsafeCoerce contentObj
7901326 }
7911327 }
7921328
@@ -798,7 +1334,7 @@ instance (RenderResponseHeadersSchema headers, RenderJSONSchema body) => RenderR
7981334type ResponseObject =
7991335 { description :: String
8001336 , headers :: FObject.Object ResponseHeaderObject
801− , content :: { "application/json" :: { schema :: Foreign } }
1337+ , content :: Foreign
8021338 }
8031339
8041340-- | Render variant response schema as OpenAPI responses object
@@ -828,6 +1364,8 @@ instance renderVariantResponseSchemaRLCons ::
8281364 , ToResponse recordType headers body
8291365 , RenderResponseHeadersSchema headers
8301366 , RenderJSONSchema body
1367+ , GetContentType body
1368+ -- , HasLinks recordType
8311369 , RenderVariantResponseSchemaRL tail
8321370 ) =>
8331371 RenderVariantResponseSchemaRL (Cons label recordType tail) where
@@ -837,18 +1375,84 @@ instance renderVariantResponseSchemaRLCons ::
8371375 statusCodeStr = statusCodeToString statusCode
8381376 headersObj = renderResponseHeadersSchema (Proxy :: Proxy headers)
8391377 bodySchema = renderJSONSchema (Proxy :: Proxy body)
840− responseObj =
1378+ contentType = getContentType (Proxy :: Proxy body)
1379+ contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
1380+ -- linksArray = links (Proxy :: Proxy recordType)
1381+ -- linksObj =
1382+ -- if Array.null linksArray then FObject.empty
1383+ -- else FObject.fromFoldable $ linksArray <#> \link ->
1384+ -- Tuple link.name $ unsafeCoerce $ FObject.fromFoldable
1385+ -- [ Tuple "operationId" (unsafeCoerce link.operationId)
1386+ -- , Tuple "parameters" link.parameters
1387+ -- ]
1388+ responseObjBase =
8411389 { description: "Successful response"
8421390 , headers: headersObj
843− , content:
844− { "application/json":
845− { schema: bodySchema }
846− }
1391+ , content: unsafeCoerce contentObj
8471392 }
1393+ responseObj = unsafeCoerce responseObjBase
1394+ -- if FObject.isEmpty linksObj then unsafeCoerce responseObjBase
1395+ -- else unsafeCoerce $ FObject.insert "links" (unsafeCoerce linksObj) (unsafeCoerce responseObjBase :: FObject.Object Foreign)
8481396 rest = renderVariantResponseSchemaRL (Proxy :: Proxy tail)
8491397 in
8501398 FObject.insert statusCodeStr responseObj rest
8511399
1400+-- --------------------------------------------------------------------------------
1401+-- -- RenderCallbacks: Render full callback objects for OpenAPI
1402+-- --------------------------------------------------------------------------------
1403+
1404+-- | Render callbacks with full method, requestBody, and responses information.
1405+-- | This typeclass walks through Callback wrappers and renders complete callback objects.
1406+class RenderCallbacks (ty :: Type) where
1407+ renderCallbacks :: Proxy ty -> FObject.Object Foreign
1408+
1409+-- Callback wrapper: render this callback and recurse
1410+instance
1411+ ( IsSymbol name
1412+ , IsSymbol expression
1413+ , RenderMethod method
1414+ , RenderRequestBodySchema requestBody
1415+ , RowToList responseRow rl
1416+ , RenderVariantResponseSchemaRL rl
1417+ , RenderCallbacks inner
1418+ ) =>
1419+ RenderCallbacks (Callback inner name expression method requestBody responseRow) where
1420+ renderCallbacks _ =
1421+ let
1422+ callbackName = reflectSymbol (Proxy :: Proxy name)
1423+ callbackExpr = reflectSymbol (Proxy :: Proxy expression)
1424+ callbackMethodStr = String.toLower (renderMethod (Proxy :: Proxy method))
1425+
1426+ -- Build request body if present
1427+ reqBodyObj = case renderRequestBodySchema (Proxy :: Proxy requestBody) of
1428+ Nothing -> FObject.empty
1429+ Just { required, content } ->
1430+ FObject.fromFoldable
1431+ [ Tuple "required" (unsafeCoerce required)
1432+ , Tuple "content" content
1433+ ]
1434+
1435+ -- Build responses
1436+ respSchemas = renderVariantResponseSchemaRL (Proxy :: Proxy rl)
1437+
1438+ -- Build operation object for this callback
1439+ operationObj = FObject.fromFoldable $
1440+ [ Tuple "responses" (unsafeCoerce respSchemas) ] <>
1441+ (if FObject.isEmpty reqBodyObj then [] else [ Tuple "requestBody" (unsafeCoerce reqBodyObj) ])
1442+
1443+ -- Build the callback structure: { expression: { method: operation } }
1444+ methodObj = FObject.singleton callbackMethodStr (unsafeCoerce operationObj)
1445+ expressionObj = FObject.singleton callbackExpr (unsafeCoerce methodObj)
1446+
1447+ -- Get inner callbacks
1448+ innerCallbacks = renderCallbacks (Proxy :: Proxy inner)
1449+ in
1450+ FObject.insert callbackName (unsafeCoerce expressionObj) innerCallbacks
1451+
1452+-- Base case: no callbacks
1453+else instance RenderCallbacks ty where
1454+ renderCallbacks _ = FObject.empty
1455+
8521456--------------------------------------------------------------------------------
8531457-- ToOpenAPI
8541458--------------------------------------------------------------------------------
@@ -862,6 +1466,307 @@ class ToOpenAPI (route :: Type) where
8621466toOpenAPI :: forall @a. ToOpenAPI a => String
8631467toOpenAPI = toOpenAPIImpl (Proxy :: Proxy a)
8641468
1469+--------------------------------------------------------------------------------
1470+-- CollectSchemas: Collect all Schema wrappers for component extraction
1471+--------------------------------------------------------------------------------
1472+
1473+-- | Collects all Schema wrappers found in a type and returns their definitions
1474+class CollectSchemas (ty :: Type) where
1475+ collectSchemas :: Proxy ty -> FObject.Object Foreign
1476+
1477+-- Base cases: primitives don't contain schemas
1478+instance CollectSchemas String where
1479+ collectSchemas _ = FObject.empty
1480+
1481+instance CollectSchemas Int where
1482+ collectSchemas _ = FObject.empty
1483+
1484+instance CollectSchemas Number where
1485+ collectSchemas _ = FObject.empty
1486+
1487+instance CollectSchemas Boolean where
1488+ collectSchemas _ = FObject.empty
1489+
1490+instance CollectSchemas Unit where
1491+ collectSchemas _ = FObject.empty
1492+
1493+-- Array: recurse into item type
1494+instance CollectSchemas a => CollectSchemas (Array a) where
1495+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1496+
1497+-- Maybe: recurse into inner type
1498+instance CollectSchemas a => CollectSchemas (Maybe a) where
1499+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1500+
1501+-- Record: recurse through all fields
1502+instance (RowToList row rl, CollectSchemasRL rl) => CollectSchemas (Record row) where
1503+ collectSchemas _ = collectSchemasRL (Proxy :: Proxy rl)
1504+
1505+-- JSON wrapper: unwrap and recurse
1506+instance CollectSchemas a => CollectSchemas (JSON a) where
1507+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1508+
1509+-- FormData wrapper: unwrap and recurse
1510+instance CollectSchemas a => CollectSchemas (FormData a) where
1511+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1512+
1513+-- MultipartFormData wrapper: unwrap and recurse
1514+instance CollectSchemas a => CollectSchemas (MultipartFormData a) where
1515+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1516+
1517+-- PlainText wrapper: no nested schemas to collect
1518+instance CollectSchemas (PlainText a) where
1519+ collectSchemas _ = FObject.empty
1520+
1521+-- XML wrapper: unwrap and recurse
1522+instance CollectSchemas a => CollectSchemas (XML a) where
1523+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1524+
1525+-- CustomContentType wrapper: unwrap and recurse
1526+instance CollectSchemas a => CollectSchemas (CustomContentType mime a) where
1527+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1528+
1529+-- NoBody: no schemas
1530+instance CollectSchemas NoBody where
1531+ collectSchemas _ = FObject.empty
1532+
1533+-- Schema wrapper: COLLECT THIS + recurse into inner type
1534+instance (IsSymbol name, RenderJSONSchema inner, CollectSchemas inner) => CollectSchemas (Schema name inner) where
1535+ collectSchemas _ =
1536+ let
1537+ schemaName = reflectSymbol (Proxy :: Proxy name)
1538+ -- Render the INNER type (not the Schema wrapper) for the component definition
1539+ innerSchema = renderJSONSchema (Proxy :: Proxy inner)
1540+ -- Also collect any nested schemas
1541+ nestedSchemas = collectSchemas (Proxy :: Proxy inner)
1542+ in
1543+ FObject.insert schemaName innerSchema nestedSchemas
1544+
1545+-- Metadata wrappers: recurse through to find schemas
1546+instance CollectSchemas a => CollectSchemas (Description desc a) where
1547+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1548+
1549+instance CollectSchemas a => CollectSchemas (Example ex a) where
1550+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1551+
1552+instance CollectSchemas a => CollectSchemas (Format fmt a) where
1553+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1554+
1555+instance CollectSchemas a => CollectSchemas (Minimum v a) where
1556+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1557+
1558+instance CollectSchemas a => CollectSchemas (Maximum v a) where
1559+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1560+
1561+instance CollectSchemas a => CollectSchemas (Pattern pat a) where
1562+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1563+
1564+instance CollectSchemas a => CollectSchemas (MinLength v a) where
1565+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1566+
1567+instance CollectSchemas a => CollectSchemas (MaxLength v a) where
1568+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1569+
1570+instance CollectSchemas a => CollectSchemas (Title t a) where
1571+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1572+
1573+instance CollectSchemas a => CollectSchemas (Nullable a) where
1574+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1575+
1576+instance CollectSchemas a => CollectSchemas (Default val a) where
1577+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1578+
1579+instance CollectSchemas a => CollectSchemas (Deprecated a) where
1580+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1581+
1582+instance CollectSchemas a => CollectSchemas (Enum a) where
1583+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1584+
1585+-- Examples wrapper: recurse through to find schemas
1586+instance CollectSchemas a => CollectSchemas (Examples examplesRow a) where
1587+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1588+
1589+-- | Helper class to collect schemas from record fields
1590+class CollectSchemasRL (rl :: RowList Type) where
1591+ collectSchemasRL :: Proxy rl -> FObject.Object Foreign
1592+
1593+instance CollectSchemasRL Nil where
1594+ collectSchemasRL _ = FObject.empty
1595+
1596+instance (IsSymbol name, CollectSchemas ty, CollectSchemasRL tail) => CollectSchemasRL (Cons name ty tail) where
1597+ collectSchemasRL _ =
1598+ let
1599+ fieldSchemas = collectSchemas (Proxy :: Proxy ty)
1600+ restSchemas = collectSchemasRL (Proxy :: Proxy tail)
1601+ in
1602+ FObject.union fieldSchemas restSchemas
1603+
1604+-- | Collects schemas from all response bodies in a variant
1605+class CollectVariantSchemasRL (rl :: RowList Type) where
1606+ collectVariantSchemasRL :: Proxy rl -> FObject.Object Foreign
1607+
1608+instance CollectVariantSchemasRL Nil where
1609+ collectVariantSchemasRL _ = FObject.empty
1610+
1611+instance
1612+ ( ToResponse recordType headers body
1613+ , CollectSchemas body
1614+ , CollectVariantSchemasRL tail
1615+ ) =>
1616+ CollectVariantSchemasRL (Cons label recordType tail) where
1617+ collectVariantSchemasRL _ =
1618+ let
1619+ bodySchemas = collectSchemas (Proxy :: Proxy body)
1620+ restSchemas = collectVariantSchemasRL (Proxy :: Proxy tail)
1621+ in
1622+ FObject.union bodySchemas restSchemas
1623+
1624+-- | Collects schemas from a route's request and response types
1625+class CollectRouteSchemas (route :: Type) where
1626+ collectRouteSchemas :: Proxy route -> FObject.Object Foreign
1627+
1628+-- | Helper class for collecting schemas from a record of routes
1629+class CollectRouteSchemasRL (rl :: RowList Type) where
1630+ collectRouteSchemasRL :: Proxy rl -> FObject.Object Foreign
1631+
1632+instance CollectRouteSchemasRL Nil where
1633+ collectRouteSchemasRL _ = FObject.empty
1634+
1635+instance
1636+ ( CollectRouteSchemas routeType
1637+ , CollectRouteSchemasRL tail
1638+ ) =>
1639+ CollectRouteSchemasRL (Cons label routeType tail) where
1640+ collectRouteSchemasRL _ =
1641+ let
1642+ routeSchemas = collectRouteSchemas (Proxy :: Proxy routeType)
1643+ restSchemas = collectRouteSchemasRL (Proxy :: Proxy tail)
1644+ in
1645+ FObject.union routeSchemas restSchemas
1646+
1647+-- Record instance: iterate record fields via RowList
1648+instance (RowToList row rl, CollectRouteSchemasRL rl) => CollectRouteSchemas (Record row) where
1649+ collectRouteSchemas _ = collectRouteSchemasRL (Proxy :: Proxy rl)
1650+
1651+--------------------------------------------------------------------------------
1652+-- CollectSchemaNames: Type-level collision detection
1653+--------------------------------------------------------------------------------
1654+
1655+-- | Collects schema names at the type level as a Row.
1656+-- | Used with Row.Union + Row.Nub to detect name collisions at compile time.
1657+-- | Two Schema wrappers with the same name but different types cause a compile error.
1658+class CollectSchemaNames (ty :: Type) (names :: Row Type) | ty -> names
1659+
1660+instance CollectSchemaNames String ()
1661+instance CollectSchemaNames Int ()
1662+instance CollectSchemaNames Number ()
1663+instance CollectSchemaNames Boolean ()
1664+instance CollectSchemaNames Unit ()
1665+
1666+instance CollectSchemaNames a names => CollectSchemaNames (Array a) names
1667+instance CollectSchemaNames a names => CollectSchemaNames (Maybe a) names
1668+
1669+instance (RowToList row rl, CollectSchemaNamesRL rl names) => CollectSchemaNames (Record row) names
1670+
1671+instance CollectSchemaNames a names => CollectSchemaNames (JSON a) names
1672+instance CollectSchemaNames a names => CollectSchemaNames (FormData a) names
1673+instance CollectSchemaNames a names => CollectSchemaNames (MultipartFormData a) names
1674+instance CollectSchemaNames (PlainText a) ()
1675+instance CollectSchemaNames a names => CollectSchemaNames (XML a) names
1676+instance CollectSchemaNames a names => CollectSchemaNames (CustomContentType mime a) names
1677+instance CollectSchemaNames NoBody ()
1678+
1679+instance
1680+ ( CollectSchemaNames inner innerNames
1681+ , Row.Cons name inner innerNames names
1682+ , Row.Lacks name innerNames
1683+ ) =>
1684+ CollectSchemaNames (Schema name inner) names
1685+
1686+-- Metadata wrappers: recurse through
1687+instance CollectSchemaNames a names => CollectSchemaNames (Description desc a) names
1688+instance CollectSchemaNames a names => CollectSchemaNames (Example ex a) names
1689+instance CollectSchemaNames a names => CollectSchemaNames (Format fmt a) names
1690+instance CollectSchemaNames a names => CollectSchemaNames (Minimum v a) names
1691+instance CollectSchemaNames a names => CollectSchemaNames (Maximum v a) names
1692+instance CollectSchemaNames a names => CollectSchemaNames (Pattern pat a) names
1693+instance CollectSchemaNames a names => CollectSchemaNames (MinLength v a) names
1694+instance CollectSchemaNames a names => CollectSchemaNames (MaxLength v a) names
1695+instance CollectSchemaNames a names => CollectSchemaNames (Title t a) names
1696+instance CollectSchemaNames a names => CollectSchemaNames (Nullable a) names
1697+instance CollectSchemaNames a names => CollectSchemaNames (Default val a) names
1698+instance CollectSchemaNames a names => CollectSchemaNames (Deprecated a) names
1699+instance CollectSchemaNames a names => CollectSchemaNames (Enum a) names
1700+instance CollectSchemaNames a names => CollectSchemaNames (Examples examplesRow a) names
1701+
1702+-- | Helper class to collect schema names from record fields
1703+class CollectSchemaNamesRL (rl :: RowList Type) (names :: Row Type) | rl -> names
1704+
1705+instance CollectSchemaNamesRL Nil ()
1706+
1707+instance
1708+ ( CollectSchemaNames ty fieldNames
1709+ , CollectSchemaNamesRL tail tailNames
1710+ , Row.Union fieldNames tailNames names
1711+ ) =>
1712+ CollectSchemaNamesRL (Cons name ty tail) names
1713+
1714+-- | Collects schema names from all response bodies in a variant
1715+class CollectVariantSchemaNames (rl :: RowList Type) (names :: Row Type) | rl -> names
1716+
1717+instance CollectVariantSchemaNames Nil ()
1718+
1719+instance
1720+ ( ToResponse recordType headers body
1721+ , CollectSchemaNames body bodyNames
1722+ , CollectVariantSchemaNames tail tailNames
1723+ , Row.Union bodyNames tailNames names
1724+ ) =>
1725+ CollectVariantSchemaNames (Cons label recordType tail) names
1726+
1727+-- | Collects schema names from a route's request and response types
1728+class CollectRouteSchemaNames (route :: Type) (names :: Row Type) | route -> names
1729+
1730+-- | Helper for collecting schema names from a record of routes
1731+class CollectRouteSchemaNameRL (rl :: RowList Type) (names :: Row Type) | rl -> names
1732+
1733+instance CollectRouteSchemaNameRL Nil ()
1734+
1735+instance
1736+ ( CollectRouteSchemaNames routeType routeNames
1737+ , CollectRouteSchemaNameRL tail tailNames
1738+ , Row.Union routeNames tailNames names
1739+ ) =>
1740+ CollectRouteSchemaNameRL (Cons label routeType tail) names
1741+
1742+-- Record instance
1743+instance (RowToList row rl, CollectRouteSchemaNameRL rl names) => CollectRouteSchemaNames (Record row) names
1744+
1745+-- | Walks a RowList and checks each entry against the full row.
1746+-- | If the RowList has duplicate labels from Union, Cons will unify their types.
1747+-- | Same name + same type: Cons succeeds. Same name + different type: Cons fails.
1748+class ValidateNoDuplicatesRL (rl :: RowList Type) (full :: Row Type)
1749+
1750+instance ValidateNoDuplicatesRL Nil full
1751+
1752+instance
1753+ ( Row.Cons name ty _rest full
1754+ , ValidateNoDuplicatesRL tail full
1755+ ) =>
1756+ ValidateNoDuplicatesRL (Cons name ty tail) full
1757+
1758+-- | Validates that all Schema wrappers in a route collection have unique names.
1759+-- | Same name with same type is allowed (deduplication).
1760+-- | Same name with different types is a compile error.
1761+class ValidateSchemaNames (routes :: Type)
1762+
1763+instance
1764+ ( CollectRouteSchemaNames routes names
1765+ , RowToList names rl
1766+ , ValidateNoDuplicatesRL rl names
1767+ ) =>
1768+ ValidateSchemaNames routes
1769+
8651770--------------------------------------------------------------------------------
8661771-- CollectOperations: Walk a type-level structure of routes
8671772--------------------------------------------------------------------------------
@@ -873,17 +1778,42 @@ type OperationEntry =
8731778 }
8741779
8751780-- | Collect OpenAPI operations from a type-level structure of routes.
876−-- | Use Tuple (/\) to combine multiple routes:
877−-- | CollectOperations (RouteA /\ RouteB /\ RouteC)
1781+-- | Use a Record to combine multiple named routes:
1782+-- | CollectOperations { getUser :: RouteA, createUser :: RouteB }
8781783class CollectOperations (routes :: Type) where
8791784 collectOperations :: Proxy routes -> Array OperationEntry
8801785
881−-- Tuple: recurse into both sides
882−instance (CollectOperations a, CollectOperations b) => CollectOperations (Tuple a b) where
883− collectOperations _ = collectOperations (Proxy :: Proxy a) <> collectOperations (Proxy :: Proxy b)
884−
8851786-- Note: Route instance is defined in Route.purs to avoid import cycles
8861787
1788+--------------------------------------------------------------------------------
1789+-- CollectNamedOperationsRL: Walk a record RowList and inject operationId
1790+--------------------------------------------------------------------------------
1791+
1792+class CollectNamedOperationsRL (rl :: RowList Type) where
1793+ collectNamedOperationsRL :: Proxy rl -> Array OperationEntry
1794+
1795+instance CollectNamedOperationsRL Nil where
1796+ collectNamedOperationsRL _ = []
1797+
1798+instance
1799+ ( IsSymbol label
1800+ , CollectOperations routeType
1801+ , CollectNamedOperationsRL tail
1802+ ) =>
1803+ CollectNamedOperationsRL (Cons label routeType tail) where
1804+ collectNamedOperationsRL _ = do
1805+ let
1806+ name = reflectSymbol (Proxy :: Proxy label)
1807+ entries = collectOperations (Proxy :: Proxy routeType)
1808+ namedEntries = entries <#> \entry ->
1809+ entry { operation = setOperationId name entry.operation }
1810+ rest = collectNamedOperationsRL (Proxy :: Proxy tail)
1811+ namedEntries <> rest
1812+
1813+-- Record instance: iterate record fields via RowList
1814+instance (RowToList row rl, CollectNamedOperationsRL rl) => CollectOperations (Record row) where
1815+ collectOperations _ = collectNamedOperationsRL (Proxy :: Proxy rl)
1816+
8871817--------------------------------------------------------------------------------
8881818-- buildOpenAPISpec: Assemble a complete OpenAPI 3.0 document
8891819--------------------------------------------------------------------------------
@@ -894,32 +1824,70 @@ foreign import data OpenAPISpec :: Type
8941824instance WriteForeign OpenAPISpec where
8951825 writeImpl = unsafeCoerce
8961826
897−-- | Detect if any operations use bearer token authentication
1827+-- | Detect which security schemes are used across all operations
8981828-- | and build the securitySchemes component
8991829buildSecuritySchemes :: Array OperationEntry -> FObject.Object Foreign
9001830buildSecuritySchemes ops =
9011831 let
902− hasSecurity = ops # foldl checkOp false
1832+ securityKeys = ops # foldl collectSecurityKeys []
9031833 where
904− checkOp acc entry =
1834+ collectSecurityKeys acc entry =
9051835 let
9061836 opObj = unsafeCoerce (unsafeParseJSON entry.operation) :: FObject.Object Foreign
9071837 securityField = FObject.lookup "security" opObj
9081838 in
909− acc || case securityField of
910− Nothing -> false
911− Just _ -> true
912− in
913− if hasSecurity then
914− FObject.singleton "bearerAuth"
915− ( unsafeCoerce
1839+ case securityField of
1840+ Nothing -> acc
1841+ Just secArray ->
1842+ let
1843+ secList = unsafeCoerce secArray :: Array (FObject.Object Foreign)
1844+ keys = secList >>= FObject.keys
1845+ in
1846+ acc <> keys
1847+
1848+ uniqueKeys = Array.nub securityKeys
1849+ schemes = uniqueKeys <#> \key ->
1850+ let
1851+ scheme = case key of
1852+ "bearerAuth" -> Just $ Tuple key $ unsafeCoerce
9161853 { type: "http"
9171854 , scheme: "bearer"
9181855 , bearerFormat: "JWT"
9191856 }
920− )
921− else
922− FObject.empty
1857+ "basicAuth" -> Just $ Tuple key $ unsafeCoerce
1858+ { type: "http"
1859+ , scheme: "basic"
1860+ }
1861+ "digestAuth" -> Just $ Tuple key $ unsafeCoerce
1862+ { type: "http"
1863+ , scheme: "digest"
1864+ }
1865+ _ ->
1866+ -- API Key in cookie (names ending with "Cookie")
1867+ if String.take (String.length key - 6) key /= "" && String.drop (String.length key - 6) key == "Cookie" then
1868+ let
1869+ cookieName = String.take (String.length key - 6) key
1870+ in
1871+ Just $ Tuple key $ unsafeCoerce
1872+ { type: "apiKey"
1873+ , in: "cookie"
1874+ , name: cookieName
1875+ }
1876+ -- API Key in header (names ending with "ApiKey")
1877+ else if String.take (String.length key - 6) key /= "" && String.drop (String.length key - 6) key == "ApiKey" then
1878+ let
1879+ headerName = String.take (String.length key - 6) key
1880+ in
1881+ Just $ Tuple key $ unsafeCoerce
1882+ { type: "apiKey"
1883+ , in: "header"
1884+ , name: headerName
1885+ }
1886+ else Nothing
1887+ in
1888+ scheme
1889+ in
1890+ FObject.fromFoldable $ Array.catMaybes schemes
9231891
9241892-- | Type for OpenAPI server object
9251893type ServerObject =
@@ -927,55 +1895,115 @@ type ServerObject =
9271895 , description :: Maybe String
9281896 }
9291897
1898+-- | Row type for contact information (all fields will be coerced)
1899+type ContactInfoR =
1900+ ( name :: String
1901+ , url :: String
1902+ , email :: String
1903+ )
1904+
1905+-- | Contact info record
1906+type ContactInfo = { | ContactInfoR }
1907+
1908+-- | Row type for license information
1909+type LicenseInfoR =
1910+ ( name :: String
1911+ , url :: String
1912+ )
1913+
1914+-- | License info record
1915+type LicenseInfo = { | LicenseInfoR }
1916+
1917+-- | Row type for OpenAPI info object
1918+type OpenAPIInfoR f =
1919+ ( title :: String
1920+ , version :: String
1921+ , description :: f String
1922+ , contact :: f ContactInfo
1923+ , license :: f LicenseInfo
1924+ )
1925+
1926+-- | OpenAPI info with all required fields
1927+type OpenAPIInfo = { | OpenAPIInfoR Identity }
1928+
1929+-- | OpenAPI info with optional fields (using UndefinedOr)
1930+type OpenAPIInfoUor = { | OpenAPIInfoR UndefinedOr }
1931+
9301932-- | Build a complete OpenAPI 3.0 spec from a type-level collection of routes.
9311933-- |
9321934-- | Examples:
933−-- | -- Basic usage:
934−-- | buildOpenAPISpec @(HealthRoute /\ UserRoute)
1935+-- | -- Basic usage (omit all optional fields):
1936+-- | buildOpenAPISpec @MyAPI
9351937-- | { title: "My API", version: "1.0.0" }
9361938-- |
937−-- | -- With servers:
938−-- | buildOpenAPISpec' @(HealthRoute /\ UserRoute)
1939+-- | -- With optional fields:
1940+-- | buildOpenAPISpec @MyAPI
9391941-- | { title: "My API"
9401942-- | , version: "1.0.0"
941−-- | , servers: Just
942−-- | [ { url: "https://api.example.com", description: Just "Production server" }
943−-- | , { url: "https://staging-api.example.com", description: Just "Staging server" }
944−-- | ]
1943+-- | , description: "A comprehensive API"
1944+-- | , contact: { name: "Support", email: "api@example.com" }
9451945-- | }
1946+-- |
1947+-- | Note: Uses the CoerceFields typeclass for type-safe coercion of optional fields.
1948+-- | For nested records (contact/license), use `undefined` from Literals.Undefined for optional fields.
1949+-- | The typeclass verifies at compile-time that each field can be safely coerced.
9461950buildOpenAPISpec
947− :: forall @routes
1951+ :: forall @routes r
9481952 . CollectOperations routes
949− => { title :: String, version :: String }
1953+ => CollectRouteSchemas routes
1954+ => ValidateSchemaNames routes
1955+ => CoerceFields r (OpenAPIInfoR UndefinedOr)
1956+ => { | r }
9501957 -> OpenAPISpec
951−buildOpenAPISpec info = buildOpenAPISpec' @routes info { servers: Nothing }
1958+buildOpenAPISpec given = buildOpenAPISpec' @routes (coerceFields given) { servers: undefined }
9521959
9531960-- | Build a complete OpenAPI 3.0 spec with optional servers configuration.
9541961buildOpenAPISpec'
9551962 :: forall @routes
9561963 . CollectOperations routes
957− => { title :: String, version :: String }
958− -> { servers :: Maybe (Array ServerObject) }
1964+ => CollectRouteSchemas routes
1965+ => ValidateSchemaNames routes
1966+ => OpenAPIInfoUor
1967+ -> { servers :: Opt (Array ServerObject) }
9591968 -> OpenAPISpec
9601969buildOpenAPISpec' info config =
9611970 let
9621971 ops = collectOperations (Proxy :: Proxy routes)
9631972 paths = groupByPath ops
9641973 securitySchemes = buildSecuritySchemes ops
1974+ schemas = collectRouteSchemas (Proxy :: Proxy routes)
1975+
1976+ -- Build components object with both schemas and securitySchemes
9651977 components =
966− if FObject.isEmpty securitySchemes then Nothing
967− else Just { securitySchemes }
1978+ if FObject.isEmpty schemas && FObject.isEmpty securitySchemes then Nothing
1979+ else Just $ unsafeCoerce $ FObject.fromFoldable $
1980+ (if FObject.isEmpty schemas then [] else [ Tuple "schemas" (unsafeCoerce schemas) ])
1981+ <> (if FObject.isEmpty securitySchemes then [] else [ Tuple "securitySchemes" (unsafeCoerce securitySchemes) ])
1982+
1983+ -- Build info object with optional fields
1984+ infoBase = FObject.fromFoldable
1985+ [ Tuple "title" (writeImpl info.title)
1986+ , Tuple "version" (writeImpl info.version)
1987+ ]
1988+ infoWithDescription = maybe infoBase (\desc -> FObject.insert "description" (writeImpl desc) infoBase) (uorToMaybe info.description)
1989+ infoWithContact = maybe infoWithDescription
1990+ ( \contact ->
1991+ FObject.insert "contact" (writeImpl contact) infoWithDescription
1992+ )
1993+ (uorToMaybe info.contact)
1994+ infoWithLicense = maybe infoWithContact
1995+ ( \license ->
1996+ FObject.insert "license" (writeImpl license) infoWithContact
1997+ )
1998+ (uorToMaybe info.license)
1999+
9682000 baseSpec = FObject.fromFoldable
9692001 [ Tuple "openapi" (unsafeCoerce "3.0.0")
970− , Tuple "info" (unsafeCoerce { title: info.title, version: info.version })
2002+ , Tuple "info" (unsafeCoerce infoWithLicense)
9712003 , Tuple "paths" (unsafeCoerce paths)
9722004 ]
973− withComponents = case components of
974− Nothing -> baseSpec
975− Just c -> FObject.insert "components" (unsafeCoerce c) baseSpec
976− withServers = case config.servers of
977− Nothing -> withComponents
978− Just servers -> FObject.insert "servers" (writeImpl servers) withComponents
2005+ withComponents = maybe baseSpec (\c -> FObject.insert "components" c baseSpec) components
2006+ withServers = maybe withComponents (\servers -> FObject.insert "servers" (writeImpl servers) withComponents) (toMaybe config.servers)
9792007 in
9802008 unsafeCoerce $ withServers
9812009
@@ -1003,3 +2031,4 @@ groupByPath = foldl insertOp FObject.empty
10032031-- FFI helpers
10042032foreign import unsafeParseJSON :: String -> Foreign
10052033foreign import stripKeys :: Array String -> Foreign -> Foreign
2034+foreign import setOperationId :: String -> String -> String