git.iko.soy/rowtype-yoga/purescript-yoga-http-api
on feat/custom-type-errors help sign in
v0.3.0: Support response types without body and/or headers
commitabbe5221f6abfb817552bb0524577d173a1f080b authorMark <mark.eibes@gmail.com> dateMon 2 Mar 2026, 21:36 (7 months ago) parent3b940ab
3 files changed+61−30
spago.yaml+1−1 src/Yoga/HTTP/API/Route/OpenAPI.purs+33−24 src/Yoga/HTTP/API/Route/Response.purs+27−5
spago.yaml+1−1
@@ -9,7 +9,7 @@ workspace:
99package:
1010 name: yoga-http-api
1111 publish:
12− version: 0.2.0
12+ version: 0.3.0
1313 license: MIT
1414 location:
1515 githubOwner: rowtype-yoga
src/Yoga/HTTP/API/Route/OpenAPI.purs+33−24
@@ -29,6 +29,8 @@ module Yoga.HTTP.API.Route.OpenAPI
2929 , renderVariantResponseSchema
3030 , class RenderVariantResponseSchemaRL
3131 , renderVariantResponseSchemaRL
32+ , class RenderResponseContent
33+ , renderResponseContent
3234 , class RenderJSONSchema
3335 , renderJSONSchema
3436 , class RenderRecordSchemaRL
@@ -103,7 +105,9 @@ import Yoga.HTTP.API.Route.Encoding (JSON, FormData, MultipartFormData, PlainTex
103105import Yoga.HTTP.API.Route.HeaderValue (class HeaderValueType, headerValueType)
104106import 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)
105107import Yoga.HTTP.API.Route.RenderMethod (class RenderMethod, renderMethod)
106−import Yoga.HTTP.API.Route.Response (class ToResponse)
108+import Prim.Boolean (True, False)
109+import Data.Unit (Unit)
110+import Yoga.HTTP.API.Route.Response (class ToResponse, class IsUnitBody)
107111import Yoga.HTTP.API.Route.StatusCode (class StatusCodeMap, statusCodeFor, statusCodeToString)
108112import Yoga.JSON (class WriteForeign, writeImpl, unsafeStringify)
109113import Yoga.JSON as Yoga.JSON
@@ -1412,10 +1416,9 @@ instance renderVariantResponseSchemaRLCons ::
14121416 ( IsSymbol label
14131417 , StatusCodeMap label
14141418 , ToResponse recordType headers body
1419+ , IsUnitBody body isUnit
1420+ , RenderResponseContent isUnit headers body
14151421 , RenderResponseHeadersSchema headers
1416− , RenderJSONSchema body
1417− , GetContentType body
1418− -- , HasLinks recordType
14191422 , RenderVariantResponseSchemaRL tail
14201423 ) =>
14211424 RenderVariantResponseSchemaRL (Cons label recordType tail) where
@@ -1423,30 +1426,36 @@ instance renderVariantResponseSchemaRLCons ::
14231426 let
14241427 statusCode = statusCodeFor (Proxy :: Proxy label)
14251428 statusCodeStr = statusCodeToString statusCode
1426− headersObj = renderResponseHeadersSchema (Proxy :: Proxy headers)
1427− bodySchema = renderJSONSchema (Proxy :: Proxy body)
1428− contentType = getContentType (Proxy :: Proxy body)
1429− contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
1430− -- linksArray = links (Proxy :: Proxy recordType)
1431− -- linksObj =
1432− -- if Array.null linksArray then FObject.empty
1433− -- else FObject.fromFoldable $ linksArray <#> \link ->
1434− -- Tuple link.name $ unsafeCoerce $ FObject.fromFoldable
1435− -- [ Tuple "operationId" (unsafeCoerce link.operationId)
1436− -- , Tuple "parameters" link.parameters
1437− -- ]
1438− responseObjBase =
1439− { description: "Successful response"
1440− , headers: headersObj
1441− , content: unsafeCoerce contentObj
1442− }
1443− responseObj = unsafeCoerce responseObjBase
1444− -- if FObject.isEmpty linksObj then unsafeCoerce responseObjBase
1445− -- else unsafeCoerce $ FObject.insert "links" (unsafeCoerce linksObj) (unsafeCoerce responseObjBase :: FObject.Object Foreign)
1429+ responseObj = renderResponseContent (Proxy :: Proxy isUnit) (Proxy :: Proxy headers) (Proxy :: Proxy body)
14461430 rest = renderVariantResponseSchemaRL (Proxy :: Proxy tail)
14471431 in
14481432 FObject.insert statusCodeStr responseObj rest
14491433
1434+class RenderResponseContent (isUnit :: Boolean) (headers :: Row Type) (body :: Type) where
1435+ renderResponseContent :: Proxy isUnit -> Proxy headers -> Proxy body -> ResponseObject
1436+
1437+-- Unit body: no content field
1438+instance RenderResponseHeadersSchema headers =>
1439+ RenderResponseContent True headers Unit where
1440+ renderResponseContent _ _ _ =
1441+ { description: "No content"
1442+ , headers: renderResponseHeadersSchema (Proxy :: Proxy headers)
1443+ , content: unsafeCoerce (FObject.empty :: FObject.Object Foreign)
1444+ }
1445+
1446+-- Non-Unit body: render content schema
1447+else instance (RenderResponseHeadersSchema headers, RenderJSONSchema body, GetContentType body) =>
1448+ RenderResponseContent False headers body where
1449+ renderResponseContent _ _ _ = do
1450+ let headersObj = renderResponseHeadersSchema (Proxy :: Proxy headers)
1451+ let bodySchema = renderJSONSchema (Proxy :: Proxy body)
1452+ let contentType = getContentType (Proxy :: Proxy body)
1453+ let contentObj = FObject.singleton contentType (unsafeCoerce { schema: bodySchema })
1454+ { description: "Successful response"
1455+ , headers: headersObj
1456+ , content: unsafeCoerce contentObj
1457+ }
1458+
14501459-- --------------------------------------------------------------------------------
14511460-- -- RenderCallbacks: Render full callback objects for OpenAPI
14521461-- --------------------------------------------------------------------------------
src/Yoga/HTTP/API/Route/Response.purs+27−5
@@ -6,6 +6,7 @@ module Yoga.HTTP.API.Route.Response
66 , EmptyResponse
77 , class ToResponse
88 , class ToResponseRL
9+ , class IsUnitBody
910 , respond
1011 , respondWith
1112 , respondNoHeaders
@@ -147,6 +148,7 @@ import Data.Symbol (class IsSymbol)
147148import Data.Unit (Unit, unit)
148149import Data.Variant (Variant)
149150import Data.Variant as Variant
151+import Prim.Boolean (True, False)
150152import Prim.Row (class Cons)
151153import Prim.RowList as RL
152154import Prim.TypeError (class Fail, Text, Above)
@@ -208,8 +210,14 @@ instance toResponseRLBodyHeaders ::
208210else instance toResponseRLBodyOnly ::
209211 ToResponseRL (RL.Cons "body" body RL.Nil) () body
210212
211−-- Invalid record structure: has fields other than "body" and "headers"
212−else instance toResponseRLInvalid ::
213+-- { headers :: Record h } (no body)
214+else instance ToResponseRL (RL.Cons "headers" (Record headers) RL.Nil) headers Unit
215+
216+-- {} (empty record: no body, no headers)
217+else instance ToResponseRL RL.Nil () Unit
218+
219+-- Invalid record structure
220+else instance
213221 ( Fail
214222 ( Above
215223 (Text "Invalid response record structure.")
@@ -218,7 +226,7 @@ else instance toResponseRLInvalid ::
218226 ( Above
219227 (Text "Response records must contain ONLY:")
220228 ( Above
221− (Text " • body :: YourBodyType (required)")
229+ (Text " • body :: YourBodyType (optional)")
222230 ( Above
223231 (Text " • headers :: Record (...) (optional)")
224232 ( Above
@@ -230,8 +238,14 @@ else instance toResponseRLInvalid ::
230238 ( Above
231239 (Text " { body: user, headers: { \"Location\": \"/users/123\" } }")
232240 ( Above
233− (Text "")
234− (Text "Do not include 'status', 'statusCode', or other fields.")
241+ (Text " { headers: { \"X-Exists\": \"true\" } }")
242+ ( Above
243+ (Text " {}")
244+ ( Above
245+ (Text "")
246+ (Text "Do not include 'status', 'statusCode', or other fields.")
247+ )
248+ )
235249 )
236250 )
237251 )
@@ -245,6 +259,14 @@ else instance toResponseRLInvalid ::
245259 ) =>
246260 ToResponseRL rl headers body
247261
262+--------------------------------------------------------------------------------
263+-- IsUnitBody: Type-level Boolean dispatch for body presence
264+--------------------------------------------------------------------------------
265+
266+class IsUnitBody (body :: Type) (isUnit :: Boolean) | body -> isUnit
267+instance IsUnitBody Unit True
268+else instance IsUnitBody body False
269+
248270--------------------------------------------------------------------------------
249271-- Response Construction Helpers
250272--------------------------------------------------------------------------------