-- Hoogle documentation, generated by Haddock -- See Hoogle, http://www.haskell.org/hoogle/ -- | Type-level JSON specification -- -- See the README at: -- https://github.com/owensmurray/json-spec#json-spec @package json-spec @version 0.2.3.0 -- | This module provides a way to specify the shape of your JSON data at -- the type level. -- --
-- 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 }
--
--
-- -- --| 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. module Data.JsonSpec -- | Simple DSL for defining type level "specifications" for JSON data. -- Similar in spirit to (but not isomorphic with) JSON Schema. -- -- Intended to be used at the type level using -XDataKinds -- -- See JSONStructure for how these map into Haskell -- representations. data Specification -- | An object with the specified properties, each having its own -- specification. This does not yet support optional properties, although -- a property can be specified as "nullable" using JsonNullable JsonObject :: [(Symbol, Specification)] -> Specification -- | An arbitrary JSON string. JsonString :: Specification -- | An arbitrary (floating point) JSON number. JsonNum :: Specification -- | A JSON integer. JsonInt :: Specification -- | A JSON array of values which conform to the given spec. JsonArray :: Specification -> Specification -- | A JSON boolean value. JsonBool :: Specification -- | A value that can either be null, or else a value conforming to -- the specification. -- -- E.g.: -- --
-- type SpecWithNullableField =
-- JsonObject
-- '[ '("nullableProperty", JsonNullable JsonString)
-- ]
--
JsonNullable :: Specification -> Specification
-- | One of two different specifications. Corresponds to json-schema
-- "oneOf". Useful for encoding sum types. E.g:
--
--
-- data MyType
-- = Foo Text
-- | Bar Int
-- | Baz UTCTime
-- instance HasJsonEncodingSpec MyType where
-- type EncodingSpec MyType =
-- JsonEither
-- (
-- JsonObject
-- '[ '("tag", JsonTag "foo")
-- , '("content", JsonString)
-- ]
-- )
-- (
-- JsonEither
-- (
-- JsonObject
-- '[ '("tag", JsonTag "bar")
-- , '("content", JsonInt)
-- ]
-- )
-- (
-- JsonObject
-- '[ '("tag", JsonTag "baz")
-- , '("content", JsonDateTime)
-- ]
-- )
-- )
--
JsonEither :: Specification -> Specification -> Specification
-- | A constant string value
JsonTag :: Symbol -> Specification
-- | A JSON string formatted as an ISO-8601 string. In Haskell this
-- corresponds to UTCTime, and in json-schema it corresponds to
-- the "date-time" format.
JsonDateTime :: Specification
-- | A "let" expression. This is useful for giving names to types, which
-- can then be used in the generated code.
--
-- This is also useful to shorten repetitive type definitions. For
-- example, this repetitive definition:
--
--
-- type Triangle =
-- JsonObject
-- '[ '("vertex1",
-- JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
-- , '("vertex2",
-- JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
-- , '("vertex3",
-- JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
-- ]
--
--
-- Can be written more concisely as:
--
--
-- type Triangle =
-- JsonLet '[("Vertex",
-- JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
-- ]
-- (JsonObject
-- '[ '("vertex1", JsonRef "Vertex")
-- , '("vertex2", JsonRef "Vertex")
-- , '("vertex3", JsonRef "Vertex")
-- ])
--
--
-- Another use is to define recursive types:
--
--
-- type LabelledTree =
-- JsonLet '[ '("LabelledTree",
-- JsonObject
-- '[ '("label", JsonString)
-- , '("children", JsonArray (JsonRef "LabelledTree"))
-- ])
-- ]
-- (JsonRef "LabelledTree")
--
JsonLet :: [(Symbol, Specification)] -> Specification -> Specification
-- | A reference to a specification which has been defined in a surrounding
-- JsonLet.
JsonRef :: Symbol -> Specification
-- | Types of this class can be encoded to JSON according to a type-level
-- Specification.
class HasJsonEncodingSpec a where {
-- | The encoding specification.
type EncodingSpec a :: Specification;
}
-- | Encode the value into the structure appropriate for the specification.
toJSONStructure :: HasJsonEncodingSpec a => a -> JSONStructure (EncodingSpec a)
-- | Types of this class can be JSON decoded according to a type-level
-- Specification.
class HasJsonDecodingSpec a where {
-- | The decoding Specification.
type DecodingSpec a :: Specification;
}
-- | Given the structural encoding of the JSON data, parse the structure
-- into the final type. The reason this returns a Parser
-- a instead of just a plain a is because there may still
-- be some invariants of the JSON data that the Specification
-- language is not able to express, and so you may need to fail parsing
-- in those cases. For instance, Specification is not powerful
-- enough to express "this field must contain only prime numbers".
fromJSONStructure :: HasJsonDecodingSpec a => JSONStructure (DecodingSpec a) -> Parser a
-- | Helper for defining ToJSON and FromJSON instances based
-- on HasEncodingJsonSpec.
--
-- Use with -XDerivingVia like:
--
--
-- data MyObj = MyObj
-- { foo :: Int
-- , bar :: Text
-- }
-- deriving (ToJSON, FromJSON) via (SpecJSON MyObj)
-- instance HasEncodingSpec MyObj where ...
-- instance HasDecodingSpec MyObj where ...
--
newtype SpecJSON a
SpecJSON :: a -> SpecJSON a
[unSpecJson] :: SpecJSON a -> a
-- | Structural representation of JsonTag. (I.e. a constant string
-- value.)
data Tag (a :: Symbol)
Tag :: Tag (a :: Symbol)
-- | Structural representation of an object field.
newtype Field (key :: Symbol) t
Field :: t -> Field (key :: Symbol) t
-- | JSONStructure spec is the Haskell type used to contain
-- the JSON data that will be encoded or decoded according to the
-- provided spec.
--
-- Basically, we represent JSON objects as "list-like" nested tuples of
-- the form:
--
-- -- (Field @key1 valueType, -- (Field @key2 valueType, -- (Field @key3 valueType, -- ()))) ---- -- Arrays, booleans, numbers, and strings are just Lists, Bools, -- Scientifics, and Texts respectively. -- -- If the user can convert their normal business logic type to/from this -- tuple type, then they get a JSON encoding to/from their type that is -- guaranteed to be compliant with the Specification type family JSONStructure (spec :: Specification) -- | This allows for recursive specifications. -- -- Since the specification is at the type level, and type level haskell -- is strict, specifying a recursive definition the "naive" way would -- cause an infinitely sized type. -- -- For example this won't work: -- --
-- data Foo = Foo [Foo] -- instance HasJsonEncodingSpec Foo where -- type EncodingSpec Foo = JsonArray (EncodingSpec Foo) -- toJSONStructure = ... can't be written ---- -- Using JsonLet prevents the specification type from being -- infinitely sized, but what about "structure" type which holds real -- values corresponding to the spec? The structure type has to have some -- way to reference itself or else it too would be infinitely sized. -- -- In order to "reference itself" the structure type has to go through a -- newtype somewhere along the way, and that's what this type is for. -- Whenever the structure type for your spec requires a self-reference, -- it will require you to wrap the recursed upon values in this type. -- -- For example: -- --
-- data Foo = Foo [Foo]
-- instance HasJsonEncodingSpec Foo where
-- type EncodingSpec Foo =
-- JsonLet
-- '[ '("Foo", JsonArray (JsonRef "Foo")) ]
-- (JsonRef "Foo")
-- toJSONStructure (Foo fs) =
-- [ Rec (toJSONStructure f)
-- | f <- fs
-- ]
--
newtype Rec (env :: [(Symbol, Type)]) (name :: Symbol) (spec :: Specification)
Rec :: JStruct ('(name, Rec env name spec) ': env) spec -> Rec (env :: [(Symbol, Type)]) (name :: Symbol) (spec :: Specification)
[unRec] :: Rec (env :: [(Symbol, Type)]) (name :: Symbol) (spec :: Specification) -> JStruct ('(name, Rec env name spec) ': env) spec
-- | Directly decode some JSON accoring to a spec without going through any
-- To/FromJSON instances.
eitherDecode :: forall (spec :: Specification). StructureFromJSON (JSONStructure spec) => Proxy spec -> Value -> Either String (JSONStructure spec)
-- | Analog of FromJSON, but specialized for decoding our "json
-- representations", and closed to the user because the haskell
-- representation scheme is fixed and not extensible by the user.
--
-- We can't just use FromJSON because the types we are using to
-- represent "json data" (i.e. the JSONStructure type family)
-- already have ToJSON instances. Even if we were to make a
-- bunch of newtypes or whatever to act as the json representation (and
-- therefor also force the user to do a lot of wrapping and unwrapping),
-- that still wouldn't be sufficient because someone could always write
-- an overlapping (or incoherent) ToJSON instance of our
-- newtype! This way we don't have to worry about any of that, and the
-- types that the user must deal with when implementing
-- fromJSONRepr can be simple tuples and such.
class StructureFromJSON a
instance (Data.JsonSpec.Decode.StructureFromJSON (Data.JsonSpec.Spec.JSONStructure (Data.JsonSpec.Decode.DecodingSpec a)), Data.JsonSpec.Decode.HasJsonDecodingSpec a) => Data.Aeson.Types.FromJSON.FromJSON (Data.JsonSpec.SpecJSON a)
instance (Data.JsonSpec.Encode.StructureToJSON (Data.JsonSpec.Spec.JSONStructure (Data.JsonSpec.Encode.EncodingSpec a)), Data.JsonSpec.Encode.HasJsonEncodingSpec a) => Data.Aeson.Types.ToJSON.ToJSON (Data.JsonSpec.SpecJSON a)