@@ -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
@@ -70,6 +82,9 @@ import Data.Array as Array
7082import Data.Foldable (foldl)
7183import Data.Maybe (Maybe(..), maybe)
7284import 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)
7388import Data.String.Regex (replace)
7489import Data.String.Regex.Flags (global)
7590import Data.String.Regex.Unsafe (unsafeRegex)
@@ -82,10 +97,11 @@ import Prim.RowList (class RowToList, RowList, Cons, Nil)
8297import Prim.RowList as RL
8398import Type.Proxy (Proxy(..))
8499import Unsafe.Coerce (unsafeCoerce)
85−import Yoga.HTTP.API.Route.Auth (BearerToken, BasicAuth, ApiKeyHeader, DigestAuth)
86−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)
87102import 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)
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)
89105import Yoga.HTTP.API.Route.Response (class ToResponse)
90106import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
91107import Yoga.JSON (class WriteForeign, writeImpl)
@@ -263,6 +279,7 @@ else instance
263279 , HasDefault ty
264280 , HasDeprecated ty
265281 , HasEnum ty
282+ , HasExamples ty
266283 , RenderHeadersSchemaRL tail tailRow
267284 , Row.Cons name ty tailRow headers
268285 , Row.Lacks name tailRow
@@ -293,7 +310,7 @@ else instance
293310 , schema
294311 , description: description p
295312 , deprecated: if deprecated p then Just true else Nothing
296− , examples: Nothing
313+ , examples: examples p
297314
298315 }
299316 rest = renderHeadersSchemaRL (Proxy :: Proxy tail)
@@ -456,6 +473,66 @@ else instance DetectSecurityRL tail => DetectSecurityRL (RL.Cons name (Deprecate
456473else instance (DetectSecurityRL tail, HeaderValueType ty) => DetectSecurityRL (RL.Cons name ty tail) where
457474 detectSecurityRL _ = detectSecurityRL (Proxy :: Proxy tail)
458475
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+
459536--------------------------------------------------------------------------------
460537-- Path Parameters Schema Generation
461538--------------------------------------------------------------------------------
@@ -491,6 +568,7 @@ instance
491568 , HasDefault ty
492569 , HasDeprecated ty
493570 , HasEnum ty
571+ , HasExamples ty
494572 , RenderPathParamsSchemaRL tail tailRow
495573 , Row.Cons name ty tailRow params
496574 , Row.Lacks name tailRow
@@ -521,7 +599,7 @@ instance
521599 , schema
522600 , description: description p
523601 , deprecated: if deprecated p then Just true else Nothing
524− , examples: Nothing
602+ , examples: examples p
525603
526604 }
527605 rest = renderPathParamsSchemaRL (Proxy :: Proxy tail)
@@ -563,6 +641,7 @@ instance
563641 , HasDefault ty
564642 , HasDeprecated ty
565643 , HasEnum ty
644+ , HasExamples ty
566645 , RenderQueryParamsSchemaRL tail tailRow
567646 , Row.Cons name ty tailRow params
568647 , Row.Lacks name tailRow
@@ -593,18 +672,108 @@ instance
593672 , schema
594673 , description: description p
595674 , deprecated: if deprecated p then Just true else Nothing
596− , examples: Nothing
675+ , examples: examples p
597676
598677 }
599678 rest = renderQueryParamsSchemaRL (Proxy :: Proxy tail)
600679 in
601680 [ param ] <> rest
602681
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+
603771--------------------------------------------------------------------------------
604772-- JSON Schema Generation (Type Introspection)
605773--------------------------------------------------------------------------------
606774
607775-- | Render a PureScript type as an OpenAPI JSON schema
776+class RenderJSONSchema :: forall k. k -> Constraint
608777class RenderJSONSchema ty where
609778 renderJSONSchema :: Proxy ty -> Foreign
610779
@@ -739,6 +908,22 @@ instance RenderJSONSchema a => RenderJSONSchema (JSON a) where
739908instance RenderJSONSchema a => RenderJSONSchema (FormData a) where
740909 renderJSONSchema _ = renderJSONSchema (Proxy :: Proxy a)
741910
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+
742927-- NoBody encoding (no schema)
743928instance RenderJSONSchema NoBody where
744929 renderJSONSchema _ = unsafeCoerce { type: "null" }
@@ -810,6 +995,10 @@ instance (RenderJSONSchema a, HasEnum (Enum a)) => RenderJSONSchema (Enum a) whe
810995 in
811996 unsafeCoerce withEnum
812997
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+
8131002-- Schema wrapper: generates $ref instead of inline schema
8141003instance (IsSymbol name, RenderJSONSchema a) => RenderJSONSchema (Schema name a) where
8151004 renderJSONSchema _ =
@@ -853,6 +1042,103 @@ instance
8531042 in
8541043 [ fieldName ] <> rest
8551044
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+
8561142--------------------------------------------------------------------------------
8571143-- Request Body Schema Generation
8581144--------------------------------------------------------------------------------
@@ -886,6 +1172,45 @@ instance RenderJSONSchema a => RenderRequestBodySchema (FormData a) where
8861172 }
8871173 }
8881174
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+
8891214--------------------------------------------------------------------------------
8901215-- Response Headers Schema Generation
8911216--------------------------------------------------------------------------------
@@ -982,26 +1307,22 @@ class RenderResponseSchema (headers :: Row Type) (body :: Type) where
9821307 -> { "200" ::
9831308 { description :: String
9841309 , headers :: FObject.Object ResponseHeaderObject
985− , content ::
986− { "application/json" ::
987− { schema :: Foreign }
988− }
1310+ , content :: Foreign
9891311 }
9901312 }
9911313
992−instance (RenderResponseHeadersSchema headers, RenderJSONSchema body) => RenderResponseSchema headers body where
1314+instance (RenderResponseHeadersSchema headers, RenderJSONSchema body, GetContentType body) => RenderResponseSchema headers body where
9931315 renderResponseSchema headersProxy bodyProxy =
9941316 let
9951317 headers = renderResponseHeadersSchema headersProxy
9961318 bodySchema = renderJSONSchema bodyProxy
1319+ contentType = getContentType bodyProxy
1320+ contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
9971321 in
9981322 { "200":
9991323 { description: "Successful response"
10001324 , headers: headers
1001− , content:
1002− { "application/json":
1003− { schema: bodySchema }
1004− }
1325+ , content: unsafeCoerce contentObj
10051326 }
10061327 }
10071328
@@ -1013,7 +1334,7 @@ instance (RenderResponseHeadersSchema headers, RenderJSONSchema body) => RenderR
10131334type ResponseObject =
10141335 { description :: String
10151336 , headers :: FObject.Object ResponseHeaderObject
1016− , content :: { "application/json" :: { schema :: Foreign } }
1337+ , content :: Foreign
10171338 }
10181339
10191340-- | Render variant response schema as OpenAPI responses object
@@ -1043,6 +1364,8 @@ instance renderVariantResponseSchemaRLCons ::
10431364 , ToResponse recordType headers body
10441365 , RenderResponseHeadersSchema headers
10451366 , RenderJSONSchema body
1367+ , GetContentType body
1368+ -- , HasLinks recordType
10461369 , RenderVariantResponseSchemaRL tail
10471370 ) =>
10481371 RenderVariantResponseSchemaRL (Cons label recordType tail) where
@@ -1052,27 +1375,84 @@ instance renderVariantResponseSchemaRLCons ::
10521375 statusCodeStr = statusCodeToString statusCode
10531376 headersObj = renderResponseHeadersSchema (Proxy :: Proxy headers)
10541377 bodySchema = renderJSONSchema (Proxy :: Proxy body)
1055− -- TODO: Links support (incomplete feature)
1378+ contentType = getContentType (Proxy :: Proxy body)
1379+ contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
10561380 -- linksArray = links (Proxy :: Proxy recordType)
10571381 -- linksObj =
1058− -- if Array.null linksArray then Nothing
1059− -- else Just $ FObject.fromFoldable $ linksArray <#> \link ->
1382+ -- if Array.null linksArray then FObject.empty
1383+ -- else FObject.fromFoldable $ linksArray <#> \link ->
10601384 -- Tuple link.name $ unsafeCoerce $ FObject.fromFoldable
10611385 -- [ Tuple "operationId" (unsafeCoerce link.operationId)
10621386 -- , Tuple "parameters" link.parameters
10631387 -- ]
1064− responseObj =
1388+ responseObjBase =
10651389 { description: "Successful response"
10661390 , headers: headersObj
1067− , content:
1068− { "application/json":
1069− { schema: bodySchema }
1070− }
1391+ , content: unsafeCoerce contentObj
10711392 }
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)
10721396 rest = renderVariantResponseSchemaRL (Proxy :: Proxy tail)
10731397 in
10741398 FObject.insert statusCodeStr responseObj rest
10751399
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+
10761456--------------------------------------------------------------------------------
10771457-- ToOpenAPI
10781458--------------------------------------------------------------------------------
@@ -1130,6 +1510,22 @@ instance CollectSchemas a => CollectSchemas (JSON a) where
11301510instance CollectSchemas a => CollectSchemas (FormData a) where
11311511 collectSchemas _ = collectSchemas (Proxy :: Proxy a)
11321512
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+
11331529-- NoBody: no schemas
11341530instance CollectSchemas NoBody where
11351531 collectSchemas _ = FObject.empty
@@ -1186,6 +1582,10 @@ instance CollectSchemas a => CollectSchemas (Deprecated a) where
11861582instance CollectSchemas a => CollectSchemas (Enum a) where
11871583 collectSchemas _ = collectSchemas (Proxy :: Proxy a)
11881584
1585+-- Examples wrapper: recurse through to find schemas
1586+instance CollectSchemas a => CollectSchemas (Examples examplesRow a) where
1587+ collectSchemas _ = collectSchemas (Proxy :: Proxy a)
1588+
11891589-- | Helper class to collect schemas from record fields
11901590class CollectSchemasRL (rl :: RowList Type) where
11911591 collectSchemasRL :: Proxy rl -> FObject.Object Foreign
@@ -1270,6 +1670,10 @@ instance (RowToList row rl, CollectSchemaNamesRL rl names) => CollectSchemaNames
12701670
12711671instance CollectSchemaNames a names => CollectSchemaNames (JSON a) names
12721672instance 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
12731677instance CollectSchemaNames NoBody ()
12741678
12751679instance
@@ -1293,6 +1697,7 @@ instance CollectSchemaNames a names => CollectSchemaNames (Nullable a) names
12931697instance CollectSchemaNames a names => CollectSchemaNames (Default val a) names
12941698instance CollectSchemaNames a names => CollectSchemaNames (Deprecated a) names
12951699instance CollectSchemaNames a names => CollectSchemaNames (Enum a) names
1700+instance CollectSchemaNames a names => CollectSchemaNames (Examples examplesRow a) names
12961701
12971702-- | Helper class to collect schema names from record fields
12981703class CollectSchemaNamesRL (rl :: RowList Type) (names :: Row Type) | rl -> names
@@ -1458,8 +1863,18 @@ buildSecuritySchemes ops =
14581863 , scheme: "digest"
14591864 }
14601865 _ ->
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
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
14631878 let
14641879 headerName = String.take (String.length key - 6) key
14651880 in
@@ -1480,30 +1895,67 @@ type ServerObject =
14801895 , description :: Maybe String
14811896 }
14821897
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+
14831932-- | Build a complete OpenAPI 3.0 spec from a type-level collection of routes.
14841933-- |
14851934-- | Examples:
1486−-- | -- Basic usage:
1935+-- | -- Basic usage (omit all optional fields):
14871936-- | buildOpenAPISpec @MyAPI
14881937-- | { title: "My API", version: "1.0.0" }
14891938-- |
1490−-- | -- With servers:
1491−-- | buildOpenAPISpec' @MyAPI
1939+-- | -- With optional fields:
1940+-- | buildOpenAPISpec @MyAPI
14921941-- | { title: "My API"
14931942-- | , version: "1.0.0"
1494−-- | , servers: Just
1495−-- | [ { url: "https://api.example.com", description: Just "Production server" }
1496−-- | , { url: "https://staging-api.example.com", description: Just "Staging server" }
1497−-- | ]
1943+-- | , description: "A comprehensive API"
1944+-- | , contact: { name: "Support", email: "api@example.com" }
14981945-- | }
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.
14991950buildOpenAPISpec
1500− :: forall @routes
1951+ :: forall @routes r
15011952 . CollectOperations routes
15021953 => CollectRouteSchemas routes
15031954 => ValidateSchemaNames routes
1504− => { title :: String, version :: String }
1955+ => CoerceFields r (OpenAPIInfoR UndefinedOr)
1956+ => { | r }
15051957 -> OpenAPISpec
1506−buildOpenAPISpec info = buildOpenAPISpec' @routes info { servers: Nothing }
1958+buildOpenAPISpec given = buildOpenAPISpec' @routes (coerceFields given) { servers: undefined }
15071959
15081960-- | Build a complete OpenAPI 3.0 spec with optional servers configuration.
15091961buildOpenAPISpec'
@@ -1511,8 +1963,8 @@ buildOpenAPISpec'
15111963 . CollectOperations routes
15121964 => CollectRouteSchemas routes
15131965 => ValidateSchemaNames routes
1514− => { title :: String, version :: String }
1515− -> { servers :: Maybe (Array ServerObject) }
1966+ => OpenAPIInfoUor
1967+ -> { servers :: Opt (Array ServerObject) }
15161968 -> OpenAPISpec
15171969buildOpenAPISpec' info config =
15181970 let
@@ -1528,17 +1980,30 @@ buildOpenAPISpec' info config =
15281980 (if FObject.isEmpty schemas then [] else [ Tuple "schemas" (unsafeCoerce schemas) ])
15291981 <> (if FObject.isEmpty securitySchemes then [] else [ Tuple "securitySchemes" (unsafeCoerce securitySchemes) ])
15301982
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+
15312000 baseSpec = FObject.fromFoldable
15322001 [ Tuple "openapi" (unsafeCoerce "3.0.0")
1533− , Tuple "info" (unsafeCoerce { title: info.title, version: info.version })
2002+ , Tuple "info" (unsafeCoerce infoWithLicense)
15342003 , Tuple "paths" (unsafeCoerce paths)
15352004 ]
1536− withComponents = case components of
1537− Nothing -> baseSpec
1538− Just c -> FObject.insert "components" c baseSpec
1539− withServers = case config.servers of
1540− Nothing -> withComponents
1541− 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)
15422007 in
15432008 unsafeCoerce $ withServers
15442009