git.iko.soy/typeable/openapi3
on master help sign in
toEncoding via sopSwaggerGenericToEncoding
commit15541f1bb294e3cf05ab25b1238773a34ae2dfe6 authorOleg Grenrus <oleg.grenrus@iki.fi> dateTue 8 Mar 2016, 19:55 (11 years ago) parentf508aa2
7 files changed+120−16
src/Data/Swagger.hs+3−3 src/Data/Swagger/Internal.hs+18 src/Data/Swagger/Internal/AesonUtils.hs+83−4 src/Data/Swagger/Internal/Schema.hs+2−2 src/Data/Swagger/Operation.hs+4−4 swagger2.cabal+3−1 test/SpecCommon.hs+7−2
src/Data/Swagger.hs+3−3
@@ -186,7 +186,7 @@ import Data.Swagger.Internal
186186-- & at 200 ?~ ("OK" & _Inline.schema ?~ Ref (Reference "User"))
187187-- & at 404 ?~ "User info not found")) ]
188188-- :}
189−-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"definitions\":{\"User\":{\"type\":\"string\"}},\"paths\":{\"/user\":{\"get\":{\"responses\":{\"404\":{\"description\":\"User info not found\"},\"200\":{\"schema\":{\"$ref\":\"#/definitions/User\"},\"description\":\"OK\"}},\"produces\":[\"application/json\"]}}}}"
189+-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"paths\":{\"/user\":{\"get\":{\"produces\":[\"application/json\"],\"responses\":{\"404\":{\"description\":\"User info not found\"},\"200\":{\"schema\":{\"$ref\":\"#/definitions/User\"},\"description\":\"OK\"}}}}},\"definitions\":{\"User\":{\"type\":\"string\"}}}"
190190--
191191-- In the snippet above we declare an API with a single path @/user@. This path provides method @GET@
192192-- which produces @application/json@ output. It should respond with code @200@ and body specified
@@ -205,7 +205,7 @@ import Data.Swagger.Internal
205205-- & type_ .~ SwaggerBoolean
206206-- & description ?~ "To be or not to be"
207207-- :}
208−-- "{\"type\":\"boolean\",\"description\":\"To be or not to be\"}"
208+-- "{\"description\":\"To be or not to be\",\"type\":\"boolean\"}"
209209--
210210-- @'ParamSchema'@ is basically the /base schema specification/ and many types contain it (see @'HasParamSchema'@).
211211-- So for convenience, all @'ParamSchema'@ fields are transitively made fields of the type that has it.
@@ -271,7 +271,7 @@ import Data.Swagger.Internal
271271-- >>> encode (Person "David" 28)
272272-- "{\"age\":28,\"name\":\"David\"}"
273273-- >>> encode $ toSchema (Proxy :: Proxy Person)
274−-- "{\"required\":[\"name\",\"age\"],\"type\":\"object\",\"properties\":{\"age\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}}"
274+-- "{\"required\":[\"name\",\"age\"],\"properties\":{\"name\":{\"type\":\"string\"},\"age\":{\"type\":\"integer\"}},\"type\":\"object\"}"
275275
276276-- $manipulation
277277-- Sometimes you have to work with an imported or generated @'Swagger'@.
src/Data/Swagger/Internal.hs+18
@@ -58,6 +58,13 @@ import Data.Swagger.Internal.AesonUtils (sopSwaggerGenericToJSON
5858 ,saoSubObject)
5959import Data.Swagger.Internal.Utils
6060
61+#if MIN_VERSION_aeson(0,10,0)
62+import Data.Swagger.Internal.AesonUtils (sopSwaggerGenericToEncoding)
63+#define DEFINE_TOENCODING toEncoding = sopSwaggerGenericToEncoding
64+#else
65+#define DEFINE_TOENCODING
66+#endif
67+
6168-- | A list of definitions that can be used in references.
6269type Definitions = InsOrdHashMap Text
6370
@@ -982,6 +989,7 @@ instance ToJSON OAuth2Flow where
982989
983990instance ToJSON OAuth2Params where
984991 toJSON = sopSwaggerGenericToJSON
992+ DEFINE_TOENCODING
985993
986994instance ToJSON SecuritySchemeType where
987995 toJSON SecuritySchemeBasic
@@ -995,15 +1003,19 @@ instance ToJSON SecuritySchemeType where
9951003
9961004instance ToJSON Swagger where
9971005 toJSON = sopSwaggerGenericToJSON
1006+ DEFINE_TOENCODING
9981007
9991008instance ToJSON SecurityScheme where
10001009 toJSON = sopSwaggerGenericToJSON
1010+ DEFINE_TOENCODING
10011011
10021012instance ToJSON Schema where
10031013 toJSON = sopSwaggerGenericToJSON
1014+ DEFINE_TOENCODING
10041015
10051016instance ToJSON Header where
10061017 toJSON = sopSwaggerGenericToJSON
1018+ DEFINE_TOENCODING
10071019
10081020instance ToJSON (ParamSchema t) => ToJSON (SwaggerItems t) where
10091021 toJSON (SwaggerItemsPrimitive fmt schema) = object
@@ -1023,6 +1035,7 @@ instance ToJSON MimeList where
10231035
10241036instance ToJSON Param where
10251037 toJSON = sopSwaggerGenericToJSON
1038+ DEFINE_TOENCODING
10261039
10271040instance ToJSON ParamAnySchema where
10281041 toJSON (ParamBody s) = object [ "in" .= ("body" :: Text), "schema" .= s ]
@@ -1030,18 +1043,23 @@ instance ToJSON ParamAnySchema where
10301043
10311044instance ToJSON ParamOtherSchema where
10321045 toJSON = sopSwaggerGenericToJSON
1046+ DEFINE_TOENCODING
10331047
10341048instance ToJSON Responses where
10351049 toJSON = sopSwaggerGenericToJSON
1050+ DEFINE_TOENCODING
10361051
10371052instance ToJSON Response where
10381053 toJSON = sopSwaggerGenericToJSON
1054+ DEFINE_TOENCODING
10391055
10401056instance ToJSON Operation where
10411057 toJSON = sopSwaggerGenericToJSON
1058+ DEFINE_TOENCODING
10421059
10431060instance ToJSON PathItem where
10441061 toJSON = sopSwaggerGenericToJSON
1062+ DEFINE_TOENCODING
10451063
10461064instance ToJSON Example where
10471065 toJSON = toJSON . Map.mapKeys show . getExample
src/Data/Swagger/Internal/AesonUtils.hs+83−4
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE DataKinds #-}
23{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE GADTs #-}
@@ -8,6 +9,9 @@ module Data.Swagger.Internal.AesonUtils (
89 -- * Generic functions
910 AesonDefaultValue(..),
1011 sopSwaggerGenericToJSON,
12+#if MIN_VERSION_aeson(0,10,0)
13+ sopSwaggerGenericToEncoding,
14+#endif
1115 sopSwaggerGenericToJSONWithOpts,
1216 sopSwaggerGenericParseJSON,
1317 -- * Options
@@ -26,7 +30,7 @@ import Control.Applicative ((<|>))
2630import Control.Lens (makeLenses, (^.))
2731import Control.Monad (unless)
2832import Data.Aeson (ToJSON(..), FromJSON(..), Value(..), Object, object, (.:), (.:?), (.!=), withObject)
29−import Data.Aeson.Types (Parser)
33+import Data.Aeson.Types (Parser, Pair)
3034import Data.Char (toLower, isUpper)
3135import Data.Text (Text)
3236
@@ -37,6 +41,12 @@ import qualified Data.HashMap.Strict as HM
3741import qualified Data.Set as Set
3842import qualified Data.HashMap.Strict.InsOrd as InsOrd
3943
44+#if MIN_VERSION_aeson(0,10,0)
45+import Data.Aeson (Encoding, pairs, (.=), Series)
46+import Data.Monoid ((<>))
47+import Data.Foldable (foldMap)
48+#endif
49+
4050-------------------------------------------------------------------------------
4151-- SwaggerAesonOptions
4252-------------------------------------------------------------------------------
@@ -132,7 +142,7 @@ sopSwaggerGenericToJSON'
132142 -> SOP I '[xs]
133143 -> DatatypeInfo '[xs]
134144 -> POP Maybe '[xs]
135− -> [(Text, Value)]
145+ -> [Pair]
136146sopSwaggerGenericToJSON' opts (SOP (Z fields)) (ADT _ _ (Record _ fieldsInfo :* Nil)) (POP (defs :* Nil)) =
137147 sopSwaggerGenericToJSON'' opts fields fieldsInfo defs
138148
@@ -142,10 +152,10 @@ sopSwaggerGenericToJSON''
142152 -> NP I xs
143153 -> NP FieldInfo xs
144154 -> NP Maybe xs
145− -> [(Text, Value)]
155+ -> [Pair]
146156sopSwaggerGenericToJSON'' (SwaggerAesonOptions prefix _ sub) = go
147157 where
148− go :: (All ToJSON ys, All Eq ys) => NP I ys -> NP FieldInfo ys -> NP Maybe ys -> [(Text, Value)]
158+ go :: (All ToJSON ys, All Eq ys) => NP I ys -> NP FieldInfo ys -> NP Maybe ys -> [Pair]
149159 go Nil Nil Nil = []
150160 go (I x :* xs) (FieldInfo name :* names) (def :* defs)
151161 | Just name' == sub = case json of
@@ -240,3 +250,72 @@ sopSwaggerGenericParseJSON'' (SwaggerAesonOptions prefix _ sub) obj = go
240250 modifier = lowerFirstUppers . drop (length prefix)
241251 lowerFirstUppers s = map toLower x ++ y
242252 where (x, y) = span isUpper s
253+
254+-------------------------------------------------------------------------------
255+-- ToEncoding
256+-------------------------------------------------------------------------------
257+
258+#if MIN_VERSION_aeson(0,10,0)
259+
260+sopSwaggerGenericToEncoding
261+ :: forall a xs.
262+ ( Generic a
263+ , HasDatatypeInfo a
264+ , HasSwaggerAesonOptions a
265+ , All2 ToJSON (Code a)
266+ , All2 Eq (Code a)
267+ , Code a ~ '[xs]
268+ )
269+ => a
270+ -> Encoding
271+sopSwaggerGenericToEncoding x =
272+ let ps = sopSwaggerGenericToEncoding' opts (from x) (datatypeInfo proxy) (aesonDefaults proxy)
273+ in pairs (pairsToSeries (opts ^. saoAdditionalPairs) <> ps)
274+ where
275+ proxy = Proxy :: Proxy a
276+ opts = swaggerAesonOptions proxy
277+
278+pairsToSeries :: [Pair] -> Series
279+pairsToSeries = foldMap (\(k, v) -> (k .= v))
280+
281+sopSwaggerGenericToEncoding'
282+ :: (All2 ToJSON '[xs], All2 Eq '[xs])
283+ => SwaggerAesonOptions
284+ -> SOP I '[xs]
285+ -> DatatypeInfo '[xs]
286+ -> POP Maybe '[xs]
287+ -> Series
288+sopSwaggerGenericToEncoding' opts (SOP (Z fields)) (ADT _ _ (Record _ fieldsInfo :* Nil)) (POP (defs :* Nil)) =
289+ sopSwaggerGenericToEncoding'' opts fields fieldsInfo defs
290+
291+sopSwaggerGenericToEncoding''
292+ :: (All ToJSON xs, All Eq xs)
293+ => SwaggerAesonOptions
294+ -> NP I xs
295+ -> NP FieldInfo xs
296+ -> NP Maybe xs
297+ -> Series
298+sopSwaggerGenericToEncoding'' (SwaggerAesonOptions prefix _ sub) = go
299+ where
300+ go :: (All ToJSON ys, All Eq ys) => NP I ys -> NP FieldInfo ys -> NP Maybe ys -> Series
301+ go Nil Nil Nil = mempty
302+ go (I x :* xs) (FieldInfo name :* names) (def :* defs)
303+ | Just name' == sub = case toJSON x of
304+ Object m -> pairsToSeries (HM.toList m) <> rest
305+ Null -> rest
306+ _ -> error $ "sopSwaggerGenericToJSON: subjson is not an object: " ++ show (toJSON x)
307+ -- If default value: omit it.
308+ | Just x == def =
309+ rest
310+ | otherwise =
311+ (T.pack name' .= x) <> rest
312+ where
313+ name' = fieldNameModifier name
314+ rest = go xs names defs
315+
316+ fieldNameModifier = modifier . drop 1
317+ modifier = lowerFirstUppers . drop (length prefix)
318+ lowerFirstUppers s = map toLower x ++ y
319+ where (x, y) = span isUpper s
320+
321+#endif
src/Data/Swagger/Internal/Schema.hs+2−2
@@ -308,7 +308,7 @@ passwordSchema = mempty
308308-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
309309-- >>> instance ToJSON Person
310310-- >>> encode $ sketchSchema (Person "Jack" 25)
311−-- "{\"example\":{\"age\":25,\"name\":\"Jack\"},\"required\":[\"age\",\"name\"],\"type\":\"object\",\"properties\":{\"age\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"}}}"
311+-- "{\"required\":[\"age\",\"name\"],\"properties\":{\"age\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"}},\"example\":{\"age\":25,\"name\":\"Jack\"},\"type\":\"object\"}"
312312sketchSchema :: ToJSON a => a -> Schema
313313sketchSchema = sketch . toJSON
314314 where
@@ -352,7 +352,7 @@ sketchSchema = sketch . toJSON
352352-- >>> data Person = Person { name :: String, age :: Int } deriving (Generic)
353353-- >>> instance ToJSON Person
354354-- >>> encode $ sketchStrictSchema (Person "Jack" 25)
355−-- "{\"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\"]}}}"
355+-- "{\"required\":[\"age\",\"name\"],\"properties\":{\"age\":{\"maximum\":25,\"minimum\":25,\"multipleOf\":25,\"type\":\"number\",\"enum\":[25]},\"name\":{\"maxLength\":4,\"pattern\":\"Jack\",\"minLength\":4,\"type\":\"string\",\"enum\":[\"Jack\"]}},\"maxProperties\":2,\"minProperties\":2,\"type\":\"object\",\"enum\":[{\"age\":25,\"name\":\"Jack\"}]}"
356356sketchStrictSchema :: ToJSON a => a -> Schema
357357sketchStrictSchema = go . toJSON
358358 where
src/Data/Swagger/Operation.hs+4−4
@@ -81,9 +81,9 @@ allOperations = paths.traverse.template
8181-- >>> let api = (mempty :: Swagger) & paths .~ [("/user", mempty & get ?~ ok & post ?~ ok)]
8282-- >>> let sub = (mempty :: Swagger) & paths .~ [("/user", mempty & get ?~ mempty)]
8383-- >>> encode api
84−-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"paths\":{\"/user\":{\"post\":{\"responses\":{\"200\":{\"description\":\"OK\"}}},\"get\":{\"responses\":{\"200\":{\"description\":\"OK\"}}}}}}"
84+-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"paths\":{\"/user\":{\"get\":{\"responses\":{\"200\":{\"description\":\"OK\"}}},\"post\":{\"responses\":{\"200\":{\"description\":\"OK\"}}}}}}"
8585-- >>> encode $ api & operationsOf sub . at 404 ?~ "Not found"
86−-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"paths\":{\"/user\":{\"post\":{\"responses\":{\"200\":{\"description\":\"OK\"}}},\"get\":{\"responses\":{\"404\":{\"description\":\"Not found\"},\"200\":{\"description\":\"OK\"}}}}}}"
86+-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"paths\":{\"/user\":{\"get\":{\"responses\":{\"404\":{\"description\":\"Not found\"},\"200\":{\"description\":\"OK\"}}},\"post\":{\"responses\":{\"200\":{\"description\":\"OK\"}}}}}}"
8787operationsOf :: Swagger -> Traversal' Swagger Operation
8888operationsOf sub = paths.itraversed.withIndex.subops
8989 where
@@ -120,7 +120,7 @@ applyTagsFor ops ts swag = swag
120120-- necessary schema definitions.
121121--
122122-- >>> encode $ runDeclare (declareResponse (Proxy :: Proxy Day)) mempty
123−-- "[{\"Day\":{\"format\":\"date\",\"type\":\"string\"}},{\"schema\":{\"$ref\":\"#/definitions/Day\"},\"description\":\"\"}]"
123+-- "[{\"Day\":{\"format\":\"date\",\"type\":\"string\"}},{\"description\":\"\",\"schema\":{\"$ref\":\"#/definitions/Day\"}}]"
124124declareResponse :: ToSchema a => proxy a -> Declare (Definitions Schema) Response
125125declareResponse proxy = do
126126 s <- declareSchemaRef proxy
@@ -140,7 +140,7 @@ declareResponse proxy = do
140140-- >>> let api = (mempty :: Swagger) & paths .~ [("/user", mempty & get ?~ mempty)]
141141-- >>> let res = declareResponse (Proxy :: Proxy Day)
142142-- >>> encode $ api & setResponse 200 res
143−-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"definitions\":{\"Day\":{\"format\":\"date\",\"type\":\"string\"}},\"paths\":{\"/user\":{\"get\":{\"responses\":{\"200\":{\"schema\":{\"$ref\":\"#/definitions/Day\"},\"description\":\"\"}}}}}}"
143+-- "{\"swagger\":\"2.0\",\"info\":{\"version\":\"\",\"title\":\"\"},\"paths\":{\"/user\":{\"get\":{\"responses\":{\"200\":{\"schema\":{\"$ref\":\"#/definitions/Day\"},\"description\":\"\"}}}}},\"definitions\":{\"Day\":{\"format\":\"date\",\"type\":\"string\"}}}"
144144--
145145-- See also @'setResponseWith'@.
146146setResponse :: HttpStatusCode -> Declare (Definitions Schema) Response -> Swagger -> Swagger
swagger2.cabal+3−1
@@ -63,10 +63,12 @@ test-suite spec
6363 type: exitcode-stdio-1.0
6464 hs-source-dirs: test
6565 main-is: Spec.hs
66+ -- We need aeson's toEncoding for doctests too
6667 build-depends: base
6768 , base-compat
68− , aeson
69+ , aeson >=0.10.0.0
6970 , aeson-qq
71+ , bytestring
7072 , containers
7173 , hashable
7274 , hspec
test/SpecCommon.hs+7−2
@@ -1,6 +1,7 @@
11module SpecCommon where
22
33import Data.Aeson
4+import Data.ByteString.Builder (toLazyByteString)
45import qualified Data.Foldable as F
56import qualified Data.HashMap.Strict as HashMap
67import qualified Data.Vector as Vector
@@ -21,5 +22,9 @@ x <=> js = do
2122 toJSON x `shouldBe` js
2223 it "decodes correctly" $ do
2324 fromJSON js `shouldBe` Success x
24−
25−
25+ it "roundtrips: eitherDecode . encode" $ do
26+ eitherDecode (encode x) `shouldBe` Right x
27+ it "roundtrips with toJSON" $ do
28+ eitherDecode (encode $ toJSON x) `shouldBe` Right x
29+ it "roundtrips with toEncoding" $ do
30+ eitherDecode (toLazyByteString $ fromEncoding $ toEncoding x) `shouldBe` Right x