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 +26/−24
- json-spec.cabal +2/−102
- src/Data/JsonSpec/Decode.hs +3/−1
- src/Data/JsonSpec/Encode.hs +3/−1
- test/jsonspec.hs +31/−16
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