@@ -42,6 +42,7 @@ import qualified Data.HashMap.Strict.InsOrd as InsOrdHashMap
4242import Data.Int
4343import Data.IntSet (IntSet)
4444import Data.IntMap (IntMap)
45+import Data.List (sort)
4546import Data.List.NonEmpty.Compat (NonEmpty)
4647import Data.Map (Map)
4748import Data.Maybe (fromMaybe)
@@ -156,13 +157,19 @@ declareSchema = fmap _namedSchemaSchema . declareNamedSchema
156157--
157158-- >>> toNamedSchema (Proxy :: Proxy String) ^. name
158159-- Nothing
159−-- >>> BSL.putStrLn $ encode (toNamedSchema (Proxy :: Proxy String) ^. schema)
160−-- {"type":"string"}
160+-- >>> BSL.putStrLn $ encodePretty (toNamedSchema (Proxy :: Proxy String) ^. schema)
161+-- {
162+-- "type": "string"
163+-- }
161164--
162165-- >>> toNamedSchema (Proxy :: Proxy Day) ^. name
163166-- Just "Day"
164−-- >>> BSL.putStrLn $ encode (toNamedSchema (Proxy :: Proxy Day) ^. schema)
165−-- {"example":"2016-07-22","format":"date","type":"string"}
167+-- >>> BSL.putStrLn $ encodePretty (toNamedSchema (Proxy :: Proxy Day) ^. schema)
168+-- {
169+-- "example": "2016-07-22",
170+-- "format": "date",
171+-- "type": "string"
172+-- }
166173toNamedSchema :: ToSchema a => Proxy a -> NamedSchema
167174toNamedSchema = undeclare . declareNamedSchema
168175
@@ -178,22 +185,35 @@ schemaName = _namedSchemaName . toNamedSchema
178185
179186-- | Convert a type into a schema.
180187--
181−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy Int8)
182−-- {"maximum":127,"minimum":-128,"type":"integer"}
188+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy Int8)
189+-- {
190+-- "maximum": 127,
191+-- "minimum": -128,
192+-- "type": "integer"
193+-- }
183194--
184−-- >>> BSL.putStrLn $ encode $ toSchema (Proxy :: Proxy [Day])
185−-- {"items":{"$ref":"#/components/schemas/Day"},"type":"array"}
195+-- >>> BSL.putStrLn $ encodePretty $ toSchema (Proxy :: Proxy [Day])
196+-- {
197+-- "items": {
198+-- "$ref": "#/components/schemas/Day"
199+-- },
200+-- "type": "array"
201+-- }
186202toSchema :: ToSchema a => Proxy a -> Schema
187203toSchema = _namedSchemaSchema . toNamedSchema
188204
189205-- | Convert a type into a referenced schema if possible.
190206-- Only named schemas can be referenced, nameless schemas are inlined.
191207--
192−-- >>> BSL.putStrLn $ encode $ toSchemaRef (Proxy :: Proxy Integer)
193−-- {"type":"integer"}
208+-- >>> BSL.putStrLn $ encodePretty $ toSchemaRef (Proxy :: Proxy Integer)
209+-- {
210+-- "type": "integer"
211+-- }
194212--
195−-- >>> BSL.putStrLn $ encode $ toSchemaRef (Proxy :: Proxy Day)
196−-- {"$ref":"#/components/schemas/Day"}
213+-- >>> BSL.putStrLn $ encodePretty $ toSchemaRef (Proxy :: Proxy Day)
214+-- {
215+-- "$ref": "#/components/schemas/Day"
216+-- }
197217toSchemaRef :: ToSchema a => Proxy a -> Referenced Schema
198218toSchemaRef = undeclare . declareSchemaRef
199219
@@ -261,8 +281,15 @@ inlineAllSchemas = inlineSchemasWhen (const True)
261281
262282-- | Convert a type into a schema without references.
263283--
264−-- >>> BSL.putStrLn $ encode $ toInlinedSchema (Proxy :: Proxy [Day])
265−-- {"items":{"example":"2016-07-22","format":"date","type":"string"},"type":"array"}
284+-- >>> BSL.putStrLn $ encodePretty $ toInlinedSchema (Proxy :: Proxy [Day])
285+-- {
286+-- "items": {
287+-- "example": "2016-07-22",
288+-- "format": "date",
289+-- "type": "string"
290+-- },
291+-- "type": "array"
292+-- }
266293--
267294-- __WARNING:__ @'toInlinedSchema'@ will produce infinite schema
268295-- when inlining recursive schemas.
@@ -295,19 +322,64 @@ inlineNonRecursiveSchemas defs = inlineSchemasWhen nonRecursive defs
295322-- | Make an unrestrictive sketch of a @'Schema'@ based on a @'ToJSON'@ instance.
296323-- Produced schema can be used for further refinement.
297324--
298−-- >>> BSL.putStrLn $ encode $ sketchSchema "hello"
299−-- {"example":"hello","type":"string"}
325+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema "hello"
326+-- {
327+-- "example": "hello",
328+-- "type": "string"
329+-- }
300330--
301−-- >>> BSL.putStrLn $ encode $ sketchSchema (1, 2, 3)
302−-- {"example":[1,2,3],"items":{"type":"number"},"type":"array"}
331+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema (1, 2, 3)
332+-- {
333+-- "example": [
334+-- 1,
335+-- 2,
336+-- 3
337+-- ],
338+-- "items": {
339+-- "type": "number"
340+-- },
341+-- "type": "array"
342+-- }
303343--
304−-- >>> BSL.putStrLn $ encode $ sketchSchema ("Jack", 25)
305−-- {"example":["Jack",25],"items":[{"type":"string"},{"type":"number"}],"type":"array"}
344+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema ("Jack", 25)
345+-- {
346+-- "example": [
347+-- "Jack",
348+-- 25
349+-- ],
350+-- "items": [
351+-- {
352+-- "type": "string"
353+-- },
354+-- {
355+-- "type": "number"
356+-- }
357+-- ],
358+-- "type": "array"
359+-- }
306360--
307361-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
308362-- >>> instance ToJSON Person
309−-- >>> BSL.putStrLn $ encode $ sketchSchema (Person "Jack" 25)
310−-- {"example":{"age":25,"name":"Jack"},"required":["age","name"],"type":"object","properties":{"age":{"type":"number"},"name":{"type":"string"}}}
363+-- >>> BSL.putStrLn $ encodePretty $ sketchSchema (Person "Jack" 25)
364+-- {
365+-- "example": {
366+-- "age": 25,
367+-- "name": "Jack"
368+-- },
369+-- "properties": {
370+-- "age": {
371+-- "type": "number"
372+-- },
373+-- "name": {
374+-- "type": "string"
375+-- }
376+-- },
377+-- "required": [
378+-- "age",
379+-- "name"
380+-- ],
381+-- "type": "object"
382+-- }
311383sketchSchema :: ToJSON a => a -> Schema
312384sketchSchema = sketch . toJSON
313385 where
@@ -333,25 +405,139 @@ sketchSchema = sketch . toJSON
333405 _ -> Nothing
334406 go (Object o) = mempty
335407 & type_ ?~ OpenApiObject
336− & required .~ HashMap.keys o
408+ & required .~ sort (HashMap.keys o)
337409 & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
338410
339411-- | Make a restrictive sketch of a @'Schema'@ based on a @'ToJSON'@ instance.
340412-- Produced schema uses as much constraints as possible.
341413--
342−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema "hello"
343−-- {"maxLength":5,"pattern":"hello","minLength":5,"type":"string","enum":["hello"]}
414+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema "hello"
415+-- {
416+-- "enum": [
417+-- "hello"
418+-- ],
419+-- "maxLength": 5,
420+-- "minLength": 5,
421+-- "pattern": "hello",
422+-- "type": "string"
423+-- }
344424--
345−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema (1, 2, 3)
346−-- {"minItems":3,"uniqueItems":true,"items":[{"maximum":1,"minimum":1,"multipleOf":1,"type":"number","enum":[1]},{"maximum":2,"minimum":2,"multipleOf":2,"type":"number","enum":[2]},{"maximum":3,"minimum":3,"multipleOf":3,"type":"number","enum":[3]}],"maxItems":3,"type":"array","enum":[[1,2,3]]}
425+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema (1, 2, 3)
426+-- {
427+-- "enum": [
428+-- [
429+-- 1,
430+-- 2,
431+-- 3
432+-- ]
433+-- ],
434+-- "items": [
435+-- {
436+-- "enum": [
437+-- 1
438+-- ],
439+-- "maximum": 1,
440+-- "minimum": 1,
441+-- "multipleOf": 1,
442+-- "type": "number"
443+-- },
444+-- {
445+-- "enum": [
446+-- 2
447+-- ],
448+-- "maximum": 2,
449+-- "minimum": 2,
450+-- "multipleOf": 2,
451+-- "type": "number"
452+-- },
453+-- {
454+-- "enum": [
455+-- 3
456+-- ],
457+-- "maximum": 3,
458+-- "minimum": 3,
459+-- "multipleOf": 3,
460+-- "type": "number"
461+-- }
462+-- ],
463+-- "maxItems": 3,
464+-- "minItems": 3,
465+-- "type": "array",
466+-- "uniqueItems": true
467+-- }
347468--
348−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema ("Jack", 25)
349−-- {"minItems":2,"uniqueItems":true,"items":[{"maxLength":4,"pattern":"Jack","minLength":4,"type":"string","enum":["Jack"]},{"maximum":25,"minimum":25,"multipleOf":25,"type":"number","enum":[25]}],"maxItems":2,"type":"array","enum":[["Jack",25]]}
469+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema ("Jack", 25)
470+-- {
471+-- "enum": [
472+-- [
473+-- "Jack",
474+-- 25
475+-- ]
476+-- ],
477+-- "items": [
478+-- {
479+-- "enum": [
480+-- "Jack"
481+-- ],
482+-- "maxLength": 4,
483+-- "minLength": 4,
484+-- "pattern": "Jack",
485+-- "type": "string"
486+-- },
487+-- {
488+-- "enum": [
489+-- 25
490+-- ],
491+-- "maximum": 25,
492+-- "minimum": 25,
493+-- "multipleOf": 25,
494+-- "type": "number"
495+-- }
496+-- ],
497+-- "maxItems": 2,
498+-- "minItems": 2,
499+-- "type": "array",
500+-- "uniqueItems": true
501+-- }
350502--
351503-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
352504-- >>> instance ToJSON Person
353−-- >>> BSL.putStrLn $ encode $ sketchStrictSchema (Person "Jack" 25)
354−-- {"minProperties":2,"required":["age","name"],"maxProperties":2,"type":"object","enum":[{"age":25,"name":"Jack"}],"properties":{"age":{"maximum":25,"minimum":25,"multipleOf":25,"type":"number","enum":[25]},"name":{"maxLength":4,"pattern":"Jack","minLength":4,"type":"string","enum":["Jack"]}}}
505+-- >>> BSL.putStrLn $ encodePretty $ sketchStrictSchema (Person "Jack" 25)
506+-- {
507+-- "enum": [
508+-- {
509+-- "age": 25,
510+-- "name": "Jack"
511+-- }
512+-- ],
513+-- "maxProperties": 2,
514+-- "minProperties": 2,
515+-- "properties": {
516+-- "age": {
517+-- "enum": [
518+-- 25
519+-- ],
520+-- "maximum": 25,
521+-- "minimum": 25,
522+-- "multipleOf": 25,
523+-- "type": "number"
524+-- },
525+-- "name": {
526+-- "enum": [
527+-- "Jack"
528+-- ],
529+-- "maxLength": 4,
530+-- "minLength": 4,
531+-- "pattern": "Jack",
532+-- "type": "string"
533+-- }
534+-- },
535+-- "required": [
536+-- "age",
537+-- "name"
538+-- ],
539+-- "type": "object"
540+-- }
355541sketchStrictSchema :: ToJSON a => a -> Schema
356542sketchStrictSchema = go . toJSON
357543 where
@@ -383,7 +569,7 @@ sketchStrictSchema = go . toJSON
383569 allUnique = sz == HashSet.size (HashSet.fromList (V.toList xs))
384570 go js@(Object o) = mempty
385571 & type_ ?~ OpenApiObject
386− & required .~ names
572+ & required .~ sort names
387573 & properties .~ fmap (Inline . go) (InsOrdHashMap.fromHashMap o)
388574 & maxProperties ?~ fromIntegral (length names)
389575 & minProperties ?~ fromIntegral (length names)
@@ -553,8 +739,12 @@ instance ToSchema a => ToSchema (Identity a) where declareNamedSchema _ = declar
553739
554740-- | Default schema for @'Bounded'@, @'Integral'@ types.
555741--
556−-- >>> BSL.putStrLn $ encode $ toSchemaBoundedIntegral (Proxy :: Proxy Int16)
557−-- {"maximum":32767,"minimum":-32768,"type":"integer"}
742+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedIntegral (Proxy :: Proxy Int16)
743+-- {
744+-- "maximum": 32767,
745+-- "minimum": -32768,
746+-- "type": "integer"
747+-- }
558748toSchemaBoundedIntegral :: forall a. (Bounded a, Integral a) => Proxy a -> Schema
559749toSchemaBoundedIntegral _ = mempty
560750 & type_ ?~ OpenApiInteger
@@ -586,8 +776,27 @@ genericDeclareNamedSchemaNewtype opts f proxy = genericNameSchema opts proxy <$>
586776-- >>> instance ToSchema ButtonState
587777-- >>> instance ToJSONKey ButtonState where toJSONKey = toJSONKeyText (T.pack . show)
588778-- >>> type ImageUrl = T.Text
589−-- >>> BSL.putStrLn $ encode $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
590−-- {"type":"object","properties":{"Focus":{"type":"string"},"Disabled":{"type":"string"},"Active":{"type":"string"},"Neutral":{"type":"string"},"Hover":{"type":"string"}}}
779+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
780+-- {
781+-- "properties": {
782+-- "Active": {
783+-- "type": "string"
784+-- },
785+-- "Disabled": {
786+-- "type": "string"
787+-- },
788+-- "Focus": {
789+-- "type": "string"
790+-- },
791+-- "Hover": {
792+-- "type": "string"
793+-- },
794+-- "Neutral": {
795+-- "type": "string"
796+-- }
797+-- },
798+-- "type": "object"
799+-- }
591800--
592801-- Note: this is only useful when @key@ is encoded with 'ToJSONKeyText'.
593802-- If it is encoded with 'ToJSONKeyValue' then a regular schema for @[(key, value)]@ is used.
@@ -614,8 +823,27 @@ declareSchemaBoundedEnumKeyMapping _ = case toJSONKey :: ToJSONKeyFunction key o
614823-- >>> instance ToSchema ButtonState
615824-- >>> instance ToJSONKey ButtonState where toJSONKey = toJSONKeyText (T.pack . show)
616825-- >>> type ImageUrl = T.Text
617−-- >>> BSL.putStrLn $ encode $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
618−-- {"type":"object","properties":{"Focus":{"type":"string"},"Disabled":{"type":"string"},"Active":{"type":"string"},"Neutral":{"type":"string"},"Hover":{"type":"string"}}}
826+-- >>> BSL.putStrLn $ encodePretty $ toSchemaBoundedEnumKeyMapping (Proxy :: Proxy (Map ButtonState ImageUrl))
827+-- {
828+-- "properties": {
829+-- "Active": {
830+-- "type": "string"
831+-- },
832+-- "Disabled": {
833+-- "type": "string"
834+-- },
835+-- "Focus": {
836+-- "type": "string"
837+-- },
838+-- "Hover": {
839+-- "type": "string"
840+-- },
841+-- "Neutral": {
842+-- "type": "string"
843+-- }
844+-- },
845+-- "type": "object"
846+-- }
619847--
620848-- Note: this is only useful when @key@ is encoded with 'ToJSONKeyText'.
621849-- If it is encoded with 'ToJSONKeyValue' then a regular schema for @[(key, value)]@ is used.
@@ -908,6 +1136,7 @@ instance (ToSchema1 f, Generic (f a), GToSchema (Rep (f a)), Typeable (f a), ToS
9081136>>> import Data.OpenApi
9091137>>> import Data.Aeson (encode)
9101138>>> import Data.Aeson.Types (toJSONKeyText)
1139+>>> import Data.OpenApi.Internal.Utils
9111140>>> :set -XScopedTypeVariables
9121141>>> :set -XDeriveAnyClass
9131142>>> :set -XStandaloneDeriving