@@ -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,6 +38,10 @@ 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
@@ -62,6 +72,8 @@ module Yoga.HTTP.API.Route.OpenAPI
6272 , class CollectRouteSchemaNameRL
6373 , class ValidateNoDuplicatesRL
6474 , class ValidateSchemaNames
75+ -- , class RenderCallbacks
76+ -- , renderCallbacks
6577 ) where
6678
6779import Prelude
@@ -82,10 +94,11 @@ import Prim.RowList (class RowToList, RowList, Cons, Nil)
8294import Prim.RowList as RL
8395import Type.Proxy (Proxy(..))
8496import Unsafe.Coerce (unsafeCoerce)
85−import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
86−import Yoga.HTTP.API.Route.Encoding (JSON, FormData, NoBody)
97+import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, ApiKeyCookie, DigestAuth)
98+import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainText, XML, CustomContentType, NoBody)
8799import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
88−import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, 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)
100+import Yoga.HTTP.API.Route.OpenAPIMetadata (Description, Example, Format, Minimum, Maximum, Pattern, MinLength, MaxLength, Title, Nullable, Default, Deprecated, Enum, Schema, Callback, Examples, Link, 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, class HasOperationMetadata, operationMetadata, class HasLinks, links)
101+import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
89102import Yoga.HTTP.API.Route.Response (class ToResponse)
90103import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
91104import Yoga.JSON (class WriteForeign, writeImpl)
@@ -263,6 +276,7 @@ else instance
263276 , HasDefault ty
264277 , HasDeprecated ty
265278 , HasEnum ty
279+ , HasExamples ty
266280 , RenderHeadersSchemaRL tail tailRow
267281 , Row.Cons name ty tailRow headers
268282 , Row.Lacks name tailRow
@@ -293,7 +307,7 @@ else instance
293307 , schema
294308 , description: description p
295309 , deprecated: if deprecated p then Just true else Nothing
296− , examples: Nothing
310+ , examples: examples p
297311
298312 }
299313 rest = renderHeadersSchemaRL (Proxy :: Proxy tail)
@@ -456,6 +470,66 @@ else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecate
456470else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
457471 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
458472
473+--------------------------------------------------------------------------------
474+-- Security Detection for Cookies
475+--------------------------------------------------------------------------------
476+
477+-- | Detect security requirements in request cookies
478+-- | Returns an array of security requirement objects (e.g., [{ sessionIdCookie: [] }])
479+class DetectCookieSecurity (cookies :: Row Type) where
480+ detectCookieSecurity :: Proxy cookies -> Array Foreign
481+
482+instance (RowToList cookies rl, DetectCookieSecurityRL rl) => DetectCookieSecurity cookies where
483+ detectCookieSecurity _ = detectCookieSecurityRL (Proxy :: Proxy rl)
484+
485+-- | Helper class using RowList to find security-related cookies
486+class DetectCookieSecurityRL (rl :: RowList Type) where
487+ detectCookieSecurityRL :: Proxy rl -> Array Foreign
488+
489+-- Base case: no cookies = no security
490+instance DetectCookieSecurityRL RL.Nil where
491+ detectCookieSecurityRL _ = []
492+
493+-- Case: ApiKeyCookie found
494+instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name ApiKeyCookie tail) where
495+ detectCookieSecurityRL _ =
496+ let
497+ cookieName = reflectSymbol (Proxy :: Proxy name)
498+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
499+ in
500+ [ apiKeyCookie ]
501+
502+-- Case: ApiKeyCookie wrapped in Description
503+else instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name (Description desc ApiKeyCookie) tail) where
504+ detectCookieSecurityRL _ =
505+ let
506+ cookieName = reflectSymbol (Proxy :: Proxy name)
507+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
508+ in
509+ [ apiKeyCookie ]
510+
511+-- Case: ApiKeyCookie wrapped in Example
512+else instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name (Example ex ApiKeyCookie) tail) where
513+ detectCookieSecurityRL _ =
514+ let
515+ cookieName = reflectSymbol (Proxy :: Proxy name)
516+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
517+ in
518+ [ apiKeyCookie ]
519+
520+-- Case: ApiKeyCookie wrapped in Deprecated
521+else instance (DetectCookieSecurityRL tail, IsSymbol name) => DetectCookieSecurityRL (RL.Cons name (Deprecated ApiKeyCookie) tail) where
522+ detectCookieSecurityRL _ =
523+ let
524+ cookieName = reflectSymbol (Proxy :: Proxy name)
525+ apiKeyCookie = unsafeCoerce $ FObject.singleton (cookieName <> "Cookie") ([] :: Array String)
526+ in
527+ [ apiKeyCookie ]
528+
529+-- Case: Non-security cookie, recurse
530+else instance (DetectCookieSecurityRL tail, HeaderValueType ty) => DetectCookieSecurityRL (RL.Cons name ty tail) where
531+ detectCookieSecurityRL _ = detectCookieSecurityRL (Proxy :: Proxy tail)
532+
459533--------------------------------------------------------------------------------
460534-- Path Parameters Schema Generation
461535--------------------------------------------------------------------------------
@@ -491,6 +565,7 @@ instance
491565 , HasDefault ty
492566 , HasDeprecated ty
493567 , HasEnum ty
568+ , HasExamples ty
494569 , RenderPathParamsSchemaRL tail tailRow
495570 , Row.Cons name ty tailRow params
496571 , Row.Lacks name tailRow
@@ -521,7 +596,7 @@ instance
521596 , schema
522597 , description: description p
523598 , deprecated: if deprecated p then Just true else Nothing
524− , examples: Nothing
599+ , examples: examples p
525600
526601 }
527602 rest = renderPathParamsSchemaRL (Proxy :: Proxy tail)
@@ -563,6 +638,7 @@ instance
563638 , HasDefault ty
564639 , HasDeprecated ty
565640 , HasEnum ty
641+ , HasExamples ty
566642 , RenderQueryParamsSchemaRL tail tailRow
567643 , Row.Cons name ty tailRow params
568644 , Row.Lacks name tailRow
@@ -593,13 +669,102 @@ instance
593669 , schema
594670 , description: description p
595671 , deprecated: if deprecated p then Just true else Nothing
596− , examples: Nothing
672+ , examples: examples p
597673
598674 }
599675 rest = renderQueryParamsSchemaRL (Proxy :: Proxy tail)
600676 in
601677 [ param ] <> rest
602678
679+--------------------------------------------------------------------------------
680+-- Cookie Parameters Schema Generation
681+--------------------------------------------------------------------------------
682+
683+-- | Render cookie parameters row as OpenAPI parameter array
684+class RenderCookieParamsSchema (params :: Row Type) where
685+ renderCookieParamsSchema :: Proxy params -> Array Foreign
686+
687+instance (RowToList params rl, RenderCookieParamsSchemaRL rl params) => RenderCookieParamsSchema params where
688+ renderCookieParamsSchema _ = renderCookieParamsSchemaRL (Proxy :: Proxy rl)
689+
690+-- | Helper class using RowList
691+class RenderCookieParamsSchemaRL (rl :: RowList Type) (params :: Row Type) | rl -> params where
692+ renderCookieParamsSchemaRL :: Proxy rl -> Array Foreign
693+
694+instance RenderCookieParamsSchemaRL RL.Nil () where
695+ renderCookieParamsSchemaRL _ = []
696+
697+-- Skip ApiKeyCookie cookies (they're handled as security schemes, not regular cookies)
698+instance (RenderCookieParamsSchemaRL tail tailRow, Row.Cons name ApiKeyCookie tailRow params, Row.Lacks name tailRow) => RenderCookieParamsSchemaRL (RL.Cons name ApiKeyCookie tail) params where
699+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
700+
701+-- Skip ApiKeyCookie wrapped in Description
702+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
703+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
704+
705+-- Skip ApiKeyCookie wrapped in Example
706+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
707+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
708+
709+-- Skip ApiKeyCookie wrapped in Deprecated
710+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
711+ renderCookieParamsSchemaRL _ = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
712+
713+-- Cookie parameter instance
714+else instance
715+ ( IsSymbol name
716+ , HeaderValueType ty
717+ , HasDescription ty
718+ , HasExample ty
719+ , HasFormat ty
720+ , HasMinimum ty
721+ , HasMaximum ty
722+ , HasPattern ty
723+ , HasMinLength ty
724+ , HasMaxLength ty
725+ , HasTitle ty
726+ , HasNullable ty
727+ , HasDefault ty
728+ , HasDeprecated ty
729+ , HasEnum ty
730+ , HasExamples ty
731+ , RenderCookieParamsSchemaRL tail tailRow
732+ , Row.Cons name ty tailRow params
733+ , Row.Lacks name tailRow
734+ ) =>
735+ RenderCookieParamsSchemaRL (RL.Cons name ty tail) params where
736+ renderCookieParamsSchemaRL _ =
737+ let
738+ p = Proxy :: Proxy ty
739+ schema = buildSchema
740+ { type: headerValueType p
741+ , format: format p
742+ , example: example p
743+ , examples: Nothing
744+ , minimum: minimum p
745+ , maximum: maximum p
746+ , pattern: pattern p
747+ , minLength: minLength p
748+ , maxLength: maxLength p
749+ , title: title p
750+ , nullable: nullable p
751+ , default: default p
752+ , enum: enum p
753+ }
754+ param = buildParameter
755+ { name: reflectSymbol (Proxy :: Proxy name)
756+ , in: "cookie"
757+ , required: false
758+ , schema
759+ , description: description p
760+ , deprecated: if deprecated p then Just true else Nothing
761+ , examples: examples p
762+
763+ }
764+ rest = renderCookieParamsSchemaRL (Proxy :: Proxy tail)
765+ in
766+ [ param ] <> rest
767+
603768--------------------------------------------------------------------------------
604769-- JSON Schema Generation (Type Introspection)
605770--------------------------------------------------------------------------------
@@ -739,6 +904,22 @@ instance RenderJSONSchema a => RenderJSONSchema (JSON a) where
739904instance RenderJSONSchema a => RenderJSONSchema (FormData a) where
740905 renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
741906
907+-- MultipartFormData encoding wrapper (unwrap and render inner type)
908+instance RenderJSONSchema a => RenderJSONSchema (MultipartFormData a) where
909+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
910+
911+-- PlainText encoding wrapper (renders as string)
912+instance RenderJSONSchema (PlainText a) where
913+ renderJSONSchema _ = unsafeCoerce { type: "string" }
914+
915+-- XML encoding wrapper (unwrap and render inner type)
916+instance RenderJSONSchema a => RenderJSONSchema (XML a) where
917+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
918+
919+-- CustomContentType encoding wrapper (unwrap and render inner type)
920+instance RenderJSONSchema a => RenderJSONSchema (CustomContentType mime a) where
921+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
922+
742923-- NoBody encoding (no schema)
743924instance RenderJSONSchema NoBody where
744925 renderJSONSchema _ = unsafeCoerce { type: "null" }
@@ -810,6 +991,10 @@ instance (RenderJSONSchema a, HasEnum (Enum a)) => RenderJSONSchema (Enum a) whe
810991 in
811992 unsafeCoerce withEnum
812993
994+-- Examples wrapper: render inner type (examples are handled elsewhere)
995+instance RenderJSONSchema a => RenderJSONSchema (Examples examplesRow a) where
996+ renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
997+
813998-- Schema wrapper: generates $ref instead of inline schema
814999instance (IsSymbol name, RenderJSONSchema a) => RenderJSONSchema (Schema name a) where
8151000 renderJSONSchema _ =
@@ -853,6 +1038,103 @@ instance
8531038 in
8541039 [ fieldName ] <> rest
8551040
1041+--------------------------------------------------------------------------------
1042+-- Content Type Extraction
1043+--------------------------------------------------------------------------------
1044+
1045+-- | Extract MIME type from encoding wrapper types
1046+class GetContentType (encoding :: Type) where
1047+ getContentType :: Proxy encoding -> String
1048+
1049+instance GetContentType NoBody where
1050+ getContentType _ = ""
1051+
1052+instance GetContentType (JSON a) where
1053+ getContentType _ = "application/json"
1054+
1055+instance GetContentType (FormData a) where
1056+ getContentType _ = "application/x-www-form-urlencoded"
1057+
1058+instance GetContentType (MultipartFormData a) where
1059+ getContentType _ = "multipart/form-data"
1060+
1061+instance GetContentType (PlainText a) where
1062+ getContentType _ = "text/plain"
1063+
1064+instance GetContentType (XML a) where
1065+ getContentType _ = "application/xml"
1066+
1067+instance IsSymbol mime => GetContentType (CustomContentType mime a) where
1068+ getContentType _ = reflectSymbol (Proxy :: Proxy mime)
1069+
1070+-- For unwrapped types (backward compatibility), default to JSON
1071+instance GetContentType String where
1072+ getContentType _ = "application/json"
1073+
1074+instance GetContentType Int where
1075+ getContentType _ = "application/json"
1076+
1077+instance GetContentType Number where
1078+ getContentType _ = "application/json"
1079+
1080+instance GetContentType Boolean where
1081+ getContentType _ = "application/json"
1082+
1083+instance GetContentType Unit where
1084+ getContentType _ = "application/json"
1085+
1086+instance GetContentType a => GetContentType (Array a) where
1087+ getContentType _ = "application/json"
1088+
1089+instance GetContentType a => GetContentType (Maybe a) where
1090+ getContentType _ = "application/json"
1091+
1092+instance GetContentType (Record row) where
1093+ getContentType _ = "application/json"
1094+
1095+-- Metadata wrappers: pass through to inner type
1096+instance GetContentType a => GetContentType (Description desc a) where
1097+ getContentType _ = getContentType (Proxy :: Proxy a)
1098+
1099+instance GetContentType a => GetContentType (Example ex a) where
1100+ getContentType _ = getContentType (Proxy :: Proxy a)
1101+
1102+instance GetContentType a => GetContentType (Format fmt a) where
1103+ getContentType _ = getContentType (Proxy :: Proxy a)
1104+
1105+instance GetContentType a => GetContentType (Nullable a) where
1106+ getContentType _ = getContentType (Proxy :: Proxy a)
1107+
1108+instance GetContentType a => GetContentType (Deprecated a) where
1109+ getContentType _ = getContentType (Proxy :: Proxy a)
1110+
1111+instance GetContentType a => GetContentType (Enum a) where
1112+ getContentType _ = getContentType (Proxy :: Proxy a)
1113+
1114+instance GetContentType a => GetContentType (Schema name a) where
1115+ getContentType _ = getContentType (Proxy :: Proxy a)
1116+
1117+instance GetContentType a => GetContentType (Minimum v a) where
1118+ getContentType _ = getContentType (Proxy :: Proxy a)
1119+
1120+instance GetContentType a => GetContentType (Maximum v a) where
1121+ getContentType _ = getContentType (Proxy :: Proxy a)
1122+
1123+instance GetContentType a => GetContentType (Pattern pat a) where
1124+ getContentType _ = getContentType (Proxy :: Proxy a)
1125+
1126+instance GetContentType a => GetContentType (MinLength v a) where
1127+ getContentType _ = getContentType (Proxy :: Proxy a)
1128+
1129+instance GetContentType a => GetContentType (MaxLength v a) where
1130+ getContentType _ = getContentType (Proxy :: Proxy a)
1131+
1132+instance GetContentType a => GetContentType (Title t a) where
1133+ getContentType _ = getContentType (Proxy :: Proxy a)
1134+
1135+instance GetContentType a => GetContentType (Default val a) where
1136+ getContentType _ = getContentType (Proxy :: Proxy a)
1137+
8561138--------------------------------------------------------------------------------
8571139-- Request Body Schema Generation
8581140--------------------------------------------------------------------------------
@@ -886,6 +1168,45 @@ instance RenderJSONSchema a => RenderRequestBodySchema (FormData a) where
8861168 }
8871169 }
8881170
1171+-- MultipartFormData: request body with multipart/form-data content type
1172+instance RenderJSONSchema a => RenderRequestBodySchema (MultipartFormData a) where
1173+ renderRequestBodySchema _ = Just
1174+ { required: true
1175+ , content: unsafeCoerce
1176+ { "multipart/form-data":
1177+ { schema: renderJSONSchema (Proxy :: Proxy a) }
1178+ }
1179+ }
1180+
1181+-- PlainText: request body with text/plain content type
1182+instance RenderRequestBodySchema (PlainText a) where
1183+ renderRequestBodySchema _ = Just
1184+ { required: true
1185+ , content: unsafeCoerce
1186+ { "text/plain":
1187+ { schema: unsafeCoerce { type: "string" } }
1188+ }
1189+ }
1190+
1191+-- XML: request body with application/xml content type
1192+instance RenderJSONSchema a => RenderRequestBodySchema (XML a) where
1193+ renderRequestBodySchema _ = Just
1194+ { required: true
1195+ , content: unsafeCoerce
1196+ { "application/xml":
1197+ { schema: renderJSONSchema (Proxy :: Proxy a) }
1198+ }
1199+ }
1200+
1201+-- CustomContentType: request body with custom MIME type
1202+instance (IsSymbol mime, RenderJSONSchema a) => RenderRequestBodySchema (CustomContentType mime a) where
1203+ renderRequestBodySchema _ =
1204+ let
1205+ mimeType = reflectSymbol (Proxy :: Proxy mime)
1206+ contentObj = FObject.singleton mimeType (unsafeCoerce { schema: renderJSONSchema (Proxy :: Proxy a) })
1207+ in
1208+ Just { required: true, content: unsafeCoerce contentObj }
1209+
8891210--------------------------------------------------------------------------------
8901211-- Response Headers Schema Generation
8911212--------------------------------------------------------------------------------
@@ -982,26 +1303,22 @@ class RenderResponseSchema (headers :: Row Type) (body :: Type) where
9821303 -> { "200" ::
9831304 { description :: String
9841305 , headers :: FObject.Object ResponseHeaderObject
985− , content ::
986− { "application/json" ::
987− { schema :: Foreign }
988− }
1306+ , content :: Foreign
9891307 }
9901308 }
9911309
992−instance (RenderResponseHeadersSchema headers, RenderJSONSchema body) => RenderResponseSchema headers body where
1310+instance (RenderResponseHeadersSchema headers, RenderJSONSchema body, GetContentType body) => RenderResponseSchema headers body where
9931311 renderResponseSchema headersProxy bodyProxy =
9941312 let
9951313 headers = renderResponseHeadersSchema headersProxy
9961314 bodySchema = renderJSONSchema bodyProxy
1315+ contentType = getContentType bodyProxy
1316+ contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
9971317 in
9981318 { "200":
9991319 { description: "Successful response"
10001320 , headers: headers
1001− , content:
1002− { "application/json":
1003− { schema: bodySchema }
1004− }
1321+ , content: unsafeCoerce contentObj
10051322 }
10061323 }
10071324
@@ -1013,7 +1330,7 @@ instance (RenderResponseHeadersSchema headers, RenderJSONSchema body) => RenderR
10131330type ResponseObject =
10141331 { description :: String
10151332 , headers :: FObject.Object ResponseHeaderObject
1016− , content :: { "application/json" :: { schema :: Foreign } }
1333+ , content :: Foreign
10171334 }
10181335
10191336-- | Render variant response schema as OpenAPI responses object
@@ -1043,6 +1360,8 @@ instance renderVariantResponseSchemaRLCons ::
10431360 , ToResponse recordType headers body
10441361 , RenderResponseHeadersSchema headers
10451362 , RenderJSONSchema body
1363+ , GetContentType body
1364+ -- , HasLinks recordType
10461365 , RenderVariantResponseSchemaRL tail
10471366 ) =>
10481367 RenderVariantResponseSchemaRL (Cons label recordType tail) where
@@ -1052,27 +1371,84 @@ instance renderVariantResponseSchemaRLCons ::
10521371 statusCodeStr = statusCodeToString statusCode
10531372 headersObj = renderResponseHeadersSchema (Proxy :: Proxy headers)
10541373 bodySchema = renderJSONSchema (Proxy :: Proxy body)
1055− -- TODO: Links support (incomplete feature)
1374+ contentType = getContentType (Proxy :: Proxy body)
1375+ contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
10561376 -- linksArray = links (Proxy :: Proxy recordType)
10571377 -- linksObj =
1058− -- if Array.null linksArray then Nothing
1059− -- else Just $ FObject.fromFoldable $ linksArray <#> \link ->
1378+ -- if Array.null linksArray then FObject.empty
1379+ -- else FObject.fromFoldable $ linksArray <#> \link ->
10601380 -- Tuple link.name $ unsafeCoerce $ FObject.fromFoldable
10611381 -- [ Tuple "operationId" (unsafeCoerce link.operationId)
10621382 -- , Tuple "parameters" link.parameters
10631383 -- ]
1064− responseObj =
1384+ responseObjBase =
10651385 { description: "Successful response"
10661386 , headers: headersObj
1067− , content:
1068− { "application/json":
1069− { schema: bodySchema }
1070− }
1387+ , content: unsafeCoerce contentObj
10711388 }
1389+ responseObj = unsafeCoerce responseObjBase
1390+ -- if FObject.isEmpty linksObj then unsafeCoerce responseObjBase
1391+ -- else unsafeCoerce $ FObject.insert "links" (unsafeCoerce linksObj) (unsafeCoerce responseObjBase :: FObject.Object Foreign)
10721392 rest = renderVariantResponseSchemaRL (Proxy :: Proxy tail)
10731393 in
10741394 FObject.insert statusCodeStr responseObj rest
10751395
1396+-- --------------------------------------------------------------------------------
1397+-- -- RenderCallbacks: Render full callback objects for OpenAPI
1398+-- --------------------------------------------------------------------------------
1399+
1400+-- | Render callbacks with full method, requestBody, and responses information.
1401+-- | This typeclass walks through Callback wrappers and renders complete callback objects.
1402+class RenderCallbacks (ty :: Type) where
1403+ renderCallbacks :: Proxy ty -> FObject.Object Foreign
1404+
1405+-- Callback wrapper: render this callback and recurse
1406+instance
1407+ ( IsSymbol name
1408+ , IsSymbol expression
1409+ , RenderMethod method
1410+ , RenderRequestBodySchema requestBody
1411+ , RowToList responseRow rl
1412+ , RenderVariantResponseSchemaRL rl
1413+ , RenderCallbacks inner
1414+ ) =>
1415+ RenderCallbacks (Callback inner name expression method requestBody responseRow) where
1416+ renderCallbacks _ =
1417+ let
1418+ callbackName = reflectSymbol (Proxy :: Proxy name)
1419+ callbackExpr = reflectSymbol (Proxy :: Proxy expression)
1420+ callbackMethodStr = String.toLower (renderMethod (Proxy :: Proxy method))
1421+
1422+ -- Build request body if present
1423+ reqBodyObj = case renderRequestBodySchema (Proxy :: Proxy requestBody) of
1424+ Nothing -> FObject.empty
1425+ Just { required, content } ->
1426+ FObject.fromFoldable
1427+ [ Tuple "required" (unsafeCoerce required)
1428+ , Tuple "content" content
1429+ ]
1430+
1431+ -- Build responses
1432+ respSchemas = renderVariantResponseSchemaRL (Proxy :: Proxy rl)
1433+
1434+ -- Build operation object for this callback
1435+ operationObj = FObject.fromFoldable $
1436+ [ Tuple "responses" (unsafeCoerce respSchemas) ] <>
1437+ (if FObject.isEmpty reqBodyObj then [] else [ Tuple "requestBody" (unsafeCoerce reqBodyObj) ])
1438+
1439+ -- Build the callback structure: { expression: { method: operation } }
1440+ methodObj = FObject.singleton callbackMethodStr (unsafeCoerce operationObj)
1441+ expressionObj = FObject.singleton callbackExpr (unsafeCoerce methodObj)
1442+
1443+ -- Get inner callbacks
1444+ innerCallbacks = renderCallbacks (Proxy :: Proxy inner)
1445+ in
1446+ FObject.insert callbackName (unsafeCoerce expressionObj) innerCallbacks
1447+
1448+-- Base case: no callbacks
1449+else instance RenderCallbacks ty where
1450+ renderCallbacks _ = FObject.empty
1451+
10761452--------------------------------------------------------------------------------
10771453-- ToOpenAPI
10781454--------------------------------------------------------------------------------
@@ -1130,6 +1506,22 @@ instance CollectSchemas a => CollectSchemas (JSON a) where
11301506instance CollectSchemas a => CollectSchemas (FormData a) where
11311507 collectSchemas _ = collectSchemas (Proxy :: Proxy a)
11321508
1509+-- MultipartFormData wrapper: unwrap and recurse
1510+instance CollectSchemas a => CollectSchemas (MultipartFormData a) where
1511+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1512+
1513+-- PlainText wrapper: no nested schemas to collect
1514+instance CollectSchemas (PlainText a) where
1515+ collectSchemas _ = FObject.empty
1516+
1517+-- XML wrapper: unwrap and recurse
1518+instance CollectSchemas a => CollectSchemas (XML a) where
1519+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1520+
1521+-- CustomContentType wrapper: unwrap and recurse
1522+instance CollectSchemas a => CollectSchemas (CustomContentType mime a) where
1523+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1524+
11331525-- NoBody: no schemas
11341526instance CollectSchemas NoBody where
11351527 collectSchemas _ = FObject.empty
@@ -1186,6 +1578,10 @@ instance CollectSchemas a => CollectSchemas (Deprecated a) where
11861578instance CollectSchemas a => CollectSchemas (Enum a) where
11871579 collectSchemas _ = collectSchemas (Proxy :: Proxy a)
11881580
1581+-- Examples wrapper: recurse through to find schemas
1582+instance CollectSchemas a => CollectSchemas (Examples examplesRow a) where
1583+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1584+
11891585-- | Helper class to collect schemas from record fields
11901586class CollectSchemasRL (rl :: RowList Type) where
11911587 collectSchemasRL :: Proxy rl -> FObject.Object Foreign
@@ -1270,6 +1666,10 @@ instance (RowToList row rl, CollectSchemaNamesRL rl names) => CollectSchemaNames
12701666
12711667instance CollectSchemaNames a names => CollectSchemaNames (JSON a) names
12721668instance CollectSchemaNames a names => CollectSchemaNames (FormData a) names
1669+instance CollectSchemaNames a names => CollectSchemaNames (MultipartFormData a) names
1670+instance CollectSchemaNames (PlainText a) ()
1671+instance CollectSchemaNames a names => CollectSchemaNames (XML a) names
1672+instance CollectSchemaNames a names => CollectSchemaNames (CustomContentType mime a) names
12731673instance CollectSchemaNames NoBody ()
12741674
12751675instance
@@ -1293,6 +1693,7 @@ instance CollectSchemaNames a names => CollectSchemaNames (Nullable a) names
12931693instance CollectSchemaNames a names => CollectSchemaNames (Default val a) names
12941694instance CollectSchemaNames a names => CollectSchemaNames (Deprecated a) names
12951695instance CollectSchemaNames a names => CollectSchemaNames (Enum a) names
1696+instance CollectSchemaNames a names => CollectSchemaNames (Examples examplesRow a) names
12961697
12971698-- | Helper class to collect schema names from record fields
12981699class CollectSchemaNamesRL (rl :: RowList Type) (names :: Row Type) | rl -> names
@@ -1458,8 +1859,18 @@ buildSecuritySchemes ops =
14581859 , scheme: "digest"
14591860 }
14601861 _ ->
1461− -- API Key (header names ending with "ApiKey")
1462− if String.take (String.length key - 6) key /= "" && String.drop (String.length key - 6) key == "ApiKey" then
1862+ -- API Key in cookie (names ending with "Cookie")
1863+ if String.take (String.length key - 6) key /= "" && String.drop (String.length key - 6) key == "Cookie" then
1864+ let
1865+ cookieName = String.take (String.length key - 6) key
1866+ in
1867+ Just $ Tuple key $ unsafeCoerce
1868+ { type: "apiKey"
1869+ , in: "cookie"
1870+ , name: cookieName
1871+ }
1872+ -- API Key in header (names ending with "ApiKey")
1873+ else if String.take (String.length key - 6) key /= "" && String.drop (String.length key - 6) key == "ApiKey" then
14631874 let
14641875 headerName = String.take (String.length key - 6) key
14651876 in
@@ -1501,7 +1912,12 @@ buildOpenAPISpec
15011912 . CollectOperations routes
15021913 => CollectRouteSchemas routes
15031914 => ValidateSchemaNames routes
1504− => { title :: String, version :: String }
1915+ => { title :: String
1916+ , version :: String
1917+ , description :: Maybe String
1918+ , contact :: Maybe { name :: Maybe String, url :: Maybe String, email :: Maybe String }
1919+ , license :: Maybe { name :: String, url :: Maybe String }
1920+ }
15051921 -> OpenAPISpec
15061922buildOpenAPISpec info = buildOpenAPISpec' @routes info { servers: Nothing }
15071923
@@ -1511,7 +1927,12 @@ buildOpenAPISpec'
15111927 . CollectOperations routes
15121928 => CollectRouteSchemas routes
15131929 => ValidateSchemaNames routes
1514− => { title :: String, version :: String }
1930+ => { title :: String
1931+ , version :: String
1932+ , description :: Maybe String
1933+ , contact :: Maybe { name :: Maybe String, url :: Maybe String, email :: Maybe String }
1934+ , license :: Maybe { name :: String, url :: Maybe String }
1935+ }
15151936 -> { servers :: Maybe (Array ServerObject) }
15161937 -> OpenAPISpec
15171938buildOpenAPISpec' info config =
@@ -1528,9 +1949,45 @@ buildOpenAPISpec' info config =
15281949 (if FObject.isEmpty schemas then [] else [ Tuple "schemas" (unsafeCoerce schemas) ])
15291950 <> (if FObject.isEmpty securitySchemes then [] else [ Tuple "securitySchemes" (unsafeCoerce securitySchemes) ])
15301951
1952+ -- Build info object with optional fields
1953+ infoBase = FObject.fromFoldable
1954+ [ Tuple "title" (writeImpl info.title)
1955+ , Tuple "version" (writeImpl info.version)
1956+ ]
1957+ infoWithDescription = case info.description of
1958+ Nothing -> infoBase
1959+ Just desc -> FObject.insert "description" (writeImpl desc) infoBase
1960+ infoWithContact = case info.contact of
1961+ Nothing -> infoWithDescription
1962+ Just contact ->
1963+ let
1964+ contactBase = FObject.empty
1965+ withName = case contact.name of
1966+ Nothing -> contactBase
1967+ Just name -> FObject.insert "name" (writeImpl name) contactBase
1968+ withUrl = case contact.url of
1969+ Nothing -> withName
1970+ Just url -> FObject.insert "url" (writeImpl url) withName
1971+ withEmail = case contact.email of
1972+ Nothing -> withUrl
1973+ Just email -> FObject.insert "email" (writeImpl email) withUrl
1974+ in
1975+ if FObject.isEmpty withEmail then infoWithDescription
1976+ else FObject.insert "contact" (unsafeCoerce withEmail) infoWithDescription
1977+ infoWithLicense = case info.license of
1978+ Nothing -> infoWithContact
1979+ Just license ->
1980+ let
1981+ licenseBase = FObject.singleton "name" (writeImpl license.name)
1982+ licenseWithUrl = case license.url of
1983+ Nothing -> licenseBase
1984+ Just url -> FObject.insert "url" (writeImpl url) licenseBase
1985+ in
1986+ FObject.insert "license" (unsafeCoerce licenseWithUrl) infoWithContact
1987+
15311988 baseSpec = FObject.fromFoldable
15321989 [ Tuple "openapi" (unsafeCoerce "3.0.0")
1533− , Tuple "info" (unsafeCoerce { title: info.title, version: info.version })
1990+ , Tuple "info" (unsafeCoerce infoWithLicense)
15341991 , Tuple "paths" (unsafeCoerce paths)
15351992 ]
15361993 withComponents = case components of