packages feed

json-spec 0.2.2.0 → 0.2.3.0

raw patch · 5 files changed

+65/−144 lines, 5 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

README.md view
@@ -58,29 +58,31 @@  ## Example -> data User = User->   { name :: Text->   , lastLogin :: UTCTime->   }->   deriving stock (Show, Eq)->   deriving (ToJSON, FromJSON) via (SpecJSON User)-> instance HasJsonEncodingSpec User where->   type EncodingSpec User =->     JsonObject->       '[ '("name", JsonString)->        , '("last-login", JsonDateTime)->        ]->   toJSONStructure user =->     (Field @"name" (name user),->     (Field @"last-login" (lastLogin user),->     ()))-> instance HasJsonDecodingSpec User where->   type DecodingSpec User = EncodingSpec User->   fromJSONStructure->       (Field @"name" name,->       (Field @"last-login" lastLogin,->       ()))->     =->       pure User { name , lastLogin }+```haskell+data User = User+  { name :: Text+  , lastLogin :: UTCTime+  }+  deriving stock (Show, Eq)+  deriving (ToJSON, FromJSON) via (SpecJSON User)+instance HasJsonEncodingSpec User where+  type EncodingSpec User =+    JsonObject+      '[ '("name", JsonString)+       , '("last-login", JsonDateTime)+       ]+  toJSONStructure user =+    (Field @"name" (name user),+    (Field @"last-login" (lastLogin user),+    ()))+instance HasJsonDecodingSpec User where+  type DecodingSpec User = EncodingSpec User+  fromJSONStructure+      (Field @"name" name,+      (Field @"last-login" lastLogin,+      ()))+    =+      pure User { name , lastLogin }+```  For more examples, take a look at the test suite.
json-spec.cabal view
@@ -1,109 +1,9 @@ cabal-version:       3.0 name:                json-spec-version:             0.2.2.0+version:             0.2.3.0 synopsis:            Type-level JSON specification maintainer:          rick@owensmurray.com-description:         = Motivation-                     This package provides a way to specify the shape of-                     your JSON data at the type level. The particular use-                     cases we focus on are enabling (but not providing-                     in this package):--                     1. Auto-generating documentation to ensure it-                        is correct.-                     2. Auto-generating client code in front-end languages-                        to ensure it is correct.--                     There are already tools available to achieve this,-                     but they all have one major drawback: they rely on-                     generically derived Aeson instances. Some people-                     strongly object to using generically derived Aeson-                     instances for encoding/decoding http api data because-                     of how brittle it is. It can be surprisingly easy-                     accidentally break your API without noticing because-                     you don't realize that a small change to some type-                     somewhere affects the API representation. Avoiding-                     this requires very strict discipline about how you-                     organize and maintain your code. E.g. you will see-                     a lot of comments like--                     > --| BEWARE, Changing any of the types in this file will change the API-                     > -- representation!!-                     > module My.API (...) where--                     But then the types in this api might reference-                     types in in other modules where it isn't as obvious-                     that you might be changing the api when you make-                     an update.--                     I have even seen people go so far as to mandate-                     that /every/ type appearing on the API must be-                     in some similar \"API\" module. This usually ends-                     badly because you end up with a bunch of seemingly-                     spurious (and quite tedious) translations between-                     between \"business\" types and almost identical-                     \"API\" types.--                     The other option is to simply not use generically-                     derived instances and code all or some of your-                     'ToJSON'/'FromJSON' instances by hand. That-                     (sometimes) helps solve the problem of making it a-                     little more obvious when you are making a breaking-                     api change. And it definitely helps with the ability-                     to update the haskell type for some business purpose-                     while keeping the encoding backwards compatible.--                     The problem now though is that you can't take-                     advantage of any of the above tooling without-                     writing every instance by hand. Writing all the-                     individual instances by hand defeat's the purpose-                     because you are back to being unsure whether they-                     are all in sync!--                     The approach this library takes is to take a cue-                     from `servant` and provide a way to specify the-                     JSON encoding at the type level. You must manually-                     specify the encoding, but you only have to do so-                     once (at the type level). Other tools can then-                     inspect the type using either type families or-                     type classes to generate the appropriate artifacts-                     or behavior. Aeson integration (provided by this-                     package) works by using a type family to transform-                     the spec into a new Haskell type whose structure-                     is analogous to the specification. You are then-                     required to transform your regular business-                     value into a value of this \"structural type\"-                     (I strongly recommend using type holes to make this-                     easier). Values of the structural type will always-                     encode into specification-complient JSON.--                     = Example--                     > data User = User-                     >   { name :: Text-                     >   , lastLogin :: UTCTime-                     >   }-                     >   deriving stock (Show, Eq)-                     >   deriving (ToJSON, FromJSON) via (SpecJSON User)-                     > instance HasJsonEncodingSpec User where-                     >   type EncodingSpec User =-                     >     JsonObject-                     >       '[ '("name", JsonString)-                     >        , '("last-login", JsonDateTime)-                     >        ]-                     >   toJSONStructure user =-                     >     (Field @"name" (name user),-                     >     (Field @"last-login" (lastLogin user),-                     >     ()))-                     > instance HasJsonDecodingSpec User where-                     >   type DecodingSpec User = EncodingSpec User-                     >   fromJSONStructure-                     >       (Field @"name" name,-                     >       (Field @"last-login" lastLogin,-                     >       ()))-                     >     =-                     >       pure User { name , lastLogin }-+description:         See the README at: https://github.com/owensmurray/json-spec#json-spec homepage:            https://github.com/owensmurray/json-spec license:             MIT license-file:        LICENSE
src/Data/JsonSpec/Decode.hs view
@@ -27,7 +27,7 @@ import GHC.TypeLits (KnownSymbol) import Prelude (Applicative(pure), Either(Left, Right), Eq((==)),   Functor(fmap), Maybe(Just, Nothing), MonadFail(fail), Semigroup((<>)),-  Traversable(traverse), ($), (.), (<$>), Int, String)+  Traversable(traverse), ($), (.), (<$>), Bool, Int, String) import qualified Data.Aeson.KeyMap as KM import qualified Data.Vector as Vector @@ -78,6 +78,8 @@ instance StructureFromJSON () where   reprParseJSON =     withObject "empty object" $ \_ -> pure ()+instance StructureFromJSON Bool where+  reprParseJSON = parseJSON instance (KnownSymbol key, StructureFromJSON val, StructureFromJSON more) => StructureFromJSON (Field key val, more) where   reprParseJSON =     withObject "object" $ \o -> do
src/Data/JsonSpec/Encode.hs view
@@ -23,7 +23,7 @@ import Data.Time (UTCTime) import GHC.TypeLits (KnownSymbol) import Prelude (Either(Left, Right), Functor(fmap), Monoid(mempty),-  (.), Int, Maybe, maybe)+  (.), Bool, Int, Maybe, maybe) import qualified Data.Aeson as A import qualified Data.Aeson.KeyMap as KM import qualified Data.Set as Set@@ -57,6 +57,8 @@   reprToJSON :: a -> Value instance StructureToJSON () where   reprToJSON () = A.object []+instance StructureToJSON Bool where+  reprToJSON = toJSON instance StructureToJSON Text where   reprToJSON = toJSON instance StructureToJSON Scientific where
test/jsonspec.hs view
@@ -16,12 +16,18 @@  import Data.Aeson (FromJSON, ToJSON) import Data.ByteString.Lazy (ByteString)-import Data.JsonSpec-import Data.Proxy+import Data.JsonSpec (Field(Field), HasJsonDecodingSpec(DecodingSpec,+  fromJSONStructure), HasJsonEncodingSpec(EncodingSpec, toJSONStructure),+  Rec(Rec, unRec), SpecJSON(SpecJSON), Specification(JsonArray, JsonBool,+  JsonDateTime, JsonEither, JsonInt, JsonLet, JsonNullable, JsonNum,+  JsonObject, JsonRef, JsonString, JsonTag), Tag(Tag), eitherDecode)+import Data.Proxy (Proxy(Proxy)) import Data.Scientific (Scientific) import Data.Text (Text) import Data.Time (UTCTime(UTCTime))-import Prelude +import Prelude (Applicative(pure), Bool(False, True), Either(Left, Right),+  Enum(toEnum), Maybe(Just, Nothing), Monad((>>=)), Traversable(traverse),+  ($), (.), Eq, IO, Int, Show, String, realToFrac) import Test.Hspec (describe, hspec, it, shouldBe) import qualified Data.Aeson as A @@ -35,7 +41,7 @@           actual :: ByteString           actual = A.encode $ sampleTestObject           expected :: ByteString-          expected = "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":100}"+          expected = "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qoo\":true,\"qux\":100}"         in           actual `shouldBe` expected @@ -44,7 +50,7 @@           actual :: Either String TestObj           actual =             A.eitherDecode-              "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":100}"+              "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":100,\"qoo\":true}"           expected :: Either String TestObj           expected = Right sampleTestObject         in@@ -208,7 +214,7 @@             actual :: ByteString             actual = A.encode $ sampleTestObjectWithNull             expected :: ByteString-            expected = "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":null}"+            expected = "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qoo\":false,\"qux\":null}"           in             actual `shouldBe` expected @@ -217,7 +223,7 @@             actual :: Either String TestObj             actual =               A.eitherDecode-                "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":null}"+                "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":null,\"qoo\":false}"             expected :: Either String TestObj             expected = Right sampleTestObjectWithNull           in@@ -244,10 +250,11 @@                    (Field "bar" Int,                    ())),                  (Field "qux" (Maybe Int),-                 ()))))+                 (Field "qoo" Bool,+                 ())))))           actual =             A.eitherDecode-              "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":null}"+              "{\"bar\":1,\"baz\":{\"bar\":0,\"foo\":\"foo2\"},\"foo\":\"foo\",\"qux\":null,\"qoo\":false}"               >>= eitherDecode (Proxy @(EncodingSpec TestObj))           expected             :: Either@@ -259,7 +266,8 @@                  (Field "bar" Int,                  ())),                (Field "qux" (Maybe Int),-               ()))))+               (Field "qoo" Bool,+               ())))))           expected =             Right               (Field @"foo" "foo",@@ -269,7 +277,8 @@                 (Field @"bar" 0,                 ())),               (Field @"qux" Nothing,-              ()))))+              (Field @"qoo" False,+              ())))))         in           actual `shouldBe` expected @@ -284,6 +293,7 @@           , bar2 = 0           }     , qux = Just 100+    , qoo = True     }  @@ -299,6 +309,7 @@           }      , qux = Nothing+    , qoo = False     }  @@ -358,6 +369,7 @@   , bar :: Scientific   , baz :: TestSubObj   , qux :: Maybe Int+  , qoo :: Bool   }   deriving stock (Show, Eq)   deriving ToJSON via (SpecJSON TestObj)@@ -369,14 +381,16 @@         '("foo", JsonString),         '("bar", JsonNum),         '("baz", EncodingSpec TestSubObj),-        '("qux", JsonNullable JsonInt)+        '("qux", JsonNullable JsonInt),+        '("qoo", JsonBool)       ]-  toJSONStructure TestObj { foo , bar , baz, qux } =+  toJSONStructure TestObj { foo , bar , baz, qux, qoo } =     (Field @"foo" foo,     (Field @"bar" (realToFrac bar),     (Field @"baz" (toJSONStructure baz),     (Field @"qux" qux,-    ()))))+    (Field @"qoo" qoo,+    ()))))) instance HasJsonDecodingSpec TestObj where   type DecodingSpec TestObj = EncodingSpec TestObj   fromJSONStructure@@ -384,10 +398,11 @@       (Field @"bar" bar,       (Field @"baz" rawBaz,       (Field @"qux" qux,-      ()))))+      (Field @"qoo" qoo,+      ())))))     = do       baz <- fromJSONStructure rawBaz-      pure $ TestObj { foo, bar, baz, qux }+      pure $ TestObj { foo, bar, baz, qux, qoo }   data TestSubObj = TestSubObj