packages feed

json-spec 1.4.0.1 → 2.0.0.0

raw patch · 19 files changed

+2531/−912 lines, 19 filesdep +megaparsecdep +template-haskellPVP ok

version bump matches the API change (PVP)

Dependencies added: megaparsec, template-haskell

API changes (from Hackage documentation)

- Data.JsonSpec: Field :: t -> Field (key :: Symbol) t
- Data.JsonSpec: Ref :: JStruct env spec -> Ref (env :: Env) (spec :: Specification)
- Data.JsonSpec: SpecJSON :: a -> SpecJSON a
- Data.JsonSpec: Tag :: Tag (a :: Symbol)
- Data.JsonSpec: [unRef] :: Ref (env :: Env) (spec :: Specification) -> JStruct env spec
- Data.JsonSpec: [unSpecJson] :: SpecJSON a -> a
- Data.JsonSpec: class StructureFromJSON a
- Data.JsonSpec: class StructureToJSON a
- Data.JsonSpec: data Tag (a :: Symbol)
- Data.JsonSpec: eitherDecode :: forall (spec :: Specification). StructureFromJSON (JSONStructure spec) => Proxy spec -> Value -> Either String (JSONStructure spec)
- Data.JsonSpec: encode :: forall (spec :: Specification). StructureToJSON (JSONStructure spec) => Proxy spec -> JSONStructure spec -> Value
- Data.JsonSpec: fromJSONStructure :: HasJsonDecodingSpec a => JSONStructure (DecodingSpec a) -> Parser a
- Data.JsonSpec: 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)
- Data.JsonSpec: 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)
- Data.JsonSpec: newtype Field (key :: Symbol) t
- Data.JsonSpec: newtype Ref (env :: Env) (spec :: Specification)
- Data.JsonSpec: newtype SpecJSON a
- Data.JsonSpec: toJSONStructure :: HasJsonEncodingSpec a => a -> JSONStructure (EncodingSpec a)
- Data.JsonSpec: type family JSONStructure (spec :: Specification)
- Data.JsonSpec: unField :: forall (key :: Symbol) t. Field key t -> t
+ Data.JsonSpec: --
+ Data.JsonSpec: -- Kind <a>Module</a> enforces closedness: no free references to an outer
+ Data.JsonSpec: -- environment.
+ Data.JsonSpec: Module :: Specification -> Module
+ Data.JsonSpec: ModuleBind :: Symbol -> Module -> BindingSpec
+ Data.JsonSpec: TypeBind :: Symbol -> Specification -> BindingSpec
+ Data.JsonSpec: [JsonModule] :: Module -> Specification
+ Data.JsonSpec: data BindingSpec
+ Data.JsonSpec: data Module
+ Data.JsonSpec.Codec.Tuple: Field :: t -> Field (key :: Symbol) t
+ Data.JsonSpec.Codec.Tuple: Ref :: JStruct env spec -> Ref (env :: Env) (spec :: Specification)
+ Data.JsonSpec.Codec.Tuple: SpecJson :: a -> SpecJson a
+ Data.JsonSpec.Codec.Tuple: Tag :: Tag (a :: Symbol)
+ Data.JsonSpec.Codec.Tuple: [unRef] :: Ref (env :: Env) (spec :: Specification) -> JStruct env spec
+ Data.JsonSpec.Codec.Tuple: [unSpecJson] :: SpecJson a -> a
+ Data.JsonSpec.Codec.Tuple: class StructureFromJson a
+ Data.JsonSpec.Codec.Tuple: class StructureToJson a
+ Data.JsonSpec.Codec.Tuple: class HasJsonDecodingSpec a => TupleDecoding a
+ Data.JsonSpec.Codec.Tuple: class HasJsonEncodingSpec a => TupleEncoding a
+ Data.JsonSpec.Codec.Tuple: data Tag (a :: Symbol)
+ Data.JsonSpec.Codec.Tuple: eitherDecode :: forall (spec :: Module). StructureFromJson (JsonStructure spec) => Proxy spec -> Value -> Either String (JsonStructure spec)
+ Data.JsonSpec.Codec.Tuple: encode :: forall (spec :: Module). StructureToJson (JsonStructure spec) => Proxy spec -> JsonStructure spec -> Value
+ Data.JsonSpec.Codec.Tuple: fromJsonStructure :: TupleDecoding a => JsonStructure (DecodingSpec a) -> Parser a
+ Data.JsonSpec.Codec.Tuple: instance (Data.JsonSpec.Codec.Tuple.Decode.StructureFromJson (Data.JsonSpec.Codec.Tuple.Internal.JsonStructure (Data.JsonSpec.Spec.DecodingSpec a)), Data.JsonSpec.Codec.Tuple.Decode.TupleDecoding a) => Data.Aeson.Types.FromJSON.FromJSON (Data.JsonSpec.Codec.Tuple.SpecJson a)
+ Data.JsonSpec.Codec.Tuple: instance (Data.JsonSpec.Codec.Tuple.Encode.StructureToJson (Data.JsonSpec.Codec.Tuple.Internal.JsonStructure (Data.JsonSpec.Spec.EncodingSpec a)), Data.JsonSpec.Codec.Tuple.Encode.TupleEncoding a) => Data.Aeson.Types.ToJSON.ToJSON (Data.JsonSpec.Codec.Tuple.SpecJson a)
+ Data.JsonSpec.Codec.Tuple: newtype Field (key :: Symbol) t
+ Data.JsonSpec.Codec.Tuple: newtype Ref (env :: Env) (spec :: Specification)
+ Data.JsonSpec.Codec.Tuple: newtype SpecJson a
+ Data.JsonSpec.Codec.Tuple: toJsonStructure :: TupleEncoding a => a -> JsonStructure (EncodingSpec a)
+ Data.JsonSpec.Codec.Tuple: type family JsonStructure (spec :: Module)
+ Data.JsonSpec.Codec.Tuple: unField :: forall (key :: Symbol) t. Field key t -> t
+ Data.JsonSpec.Language.Parser: ArraySpec :: Spec -> Spec
+ Data.JsonSpec.Language.Parser: BoolSpec :: Spec
+ Data.JsonSpec.Language.Parser: DateTimeSpec :: Spec
+ Data.JsonSpec.Language.Parser: DictSpec :: Spec -> Spec
+ Data.JsonSpec.Language.Parser: EitherSpec :: [Spec] -> Spec
+ Data.JsonSpec.Language.Parser: Field :: Text -> Bool -> Spec -> Field
+ Data.JsonSpec.Language.Parser: IntSpec :: Spec
+ Data.JsonSpec.Language.Parser: LetSpec :: [Binding] -> Spec -> Spec
+ Data.JsonSpec.Language.Parser: ModuleBinding :: Text -> Spec -> Binding
+ Data.JsonSpec.Language.Parser: NullSpec :: Spec -> Spec
+ Data.JsonSpec.Language.Parser: NumberSpec :: Spec
+ Data.JsonSpec.Language.Parser: ObjectSpec :: [Field] -> Spec
+ Data.JsonSpec.Language.Parser: Program :: Text -> Spec -> Program
+ Data.JsonSpec.Language.Parser: RawSpec :: Spec
+ Data.JsonSpec.Language.Parser: RefSpec :: Text -> Spec
+ Data.JsonSpec.Language.Parser: StringSpec :: Spec
+ Data.JsonSpec.Language.Parser: TagSpec :: Text -> Spec
+ Data.JsonSpec.Language.Parser: TypeBinding :: Text -> Spec -> Binding
+ Data.JsonSpec.Language.Parser: [fieldName] :: Field -> Text
+ Data.JsonSpec.Language.Parser: [fieldOptional] :: Field -> Bool
+ Data.JsonSpec.Language.Parser: [fieldSpec] :: Field -> Spec
+ Data.JsonSpec.Language.Parser: [programName] :: Program -> Text
+ Data.JsonSpec.Language.Parser: [programSpec] :: Program -> Spec
+ Data.JsonSpec.Language.Parser: data Binding
+ Data.JsonSpec.Language.Parser: data Field
+ Data.JsonSpec.Language.Parser: data Program
+ Data.JsonSpec.Language.Parser: data Spec
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Classes.Eq Data.JsonSpec.Language.Parser.Binding
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Classes.Eq Data.JsonSpec.Language.Parser.Field
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Classes.Eq Data.JsonSpec.Language.Parser.Program
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Classes.Eq Data.JsonSpec.Language.Parser.Spec
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Show.Show Data.JsonSpec.Language.Parser.Binding
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Show.Show Data.JsonSpec.Language.Parser.Field
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Show.Show Data.JsonSpec.Language.Parser.Program
+ Data.JsonSpec.Language.Parser: instance GHC.Internal.Show.Show Data.JsonSpec.Language.Parser.Spec
+ Data.JsonSpec.Language.Parser: parseProgram :: String -> Text -> Either String Program
+ Data.JsonSpec.Language.Parser: parseSpec :: String -> Text -> Either String Spec
+ Data.JsonSpec.Language.Parser: program :: Parser Program
+ Data.JsonSpec.Language.Parser: spec :: Parser Spec
+ Data.JsonSpec.Language.QQ: jsonspec :: QuasiQuoter
- Data.JsonSpec: -- | The decoding <a>Specification</a>.
+ Data.JsonSpec: -- | The decoding specification.
- Data.JsonSpec: [JsonLet] :: [(Symbol, Specification)] -> Specification -> Specification
+ Data.JsonSpec: [JsonLet] :: [BindingSpec] -> Specification -> Specification
- Data.JsonSpec: class HasJsonDecodingSpec a where {
+ Data.JsonSpec: class HasJsonDecodingSpec (a :: k) where {
- Data.JsonSpec: class HasJsonEncodingSpec a where {
+ Data.JsonSpec: class HasJsonEncodingSpec (a :: k) where {
- Data.JsonSpec: type (::?) = 'Optional
+ Data.JsonSpec: type (::=) = 'ModuleBind
- Data.JsonSpec: type DecodingSpec a :: Specification;
+ Data.JsonSpec: type DecodingSpec (a :: k) :: Module;
- Data.JsonSpec: type EncodingSpec a :: Specification;
+ Data.JsonSpec: type EncodingSpec (a :: k) :: Module;

Files

CHANGELOG.md view
@@ -1,5 +1,25 @@ # Changelog +## 2.0.0.0++Breaking release: the specification DSL and tuple codec API were+reworked.++- Add a textual JsonSpec language with parser+  (`Data.JsonSpec.Language.Parser`), Template Haskell quasiquoter+  (`Data.JsonSpec.Language.QQ`), and language documentation+  (`docs/language-spec.md`). Specs can use open `type` and closed+  `module` bindings, `let` frames, and backtick-escaped identifiers+  when a name collides with a keyword.+- Split tuple-based encoding/decoding out of `Data.JsonSpec` into+  `Data.JsonSpec.Codec.Tuple` (`SpecJson`, `TupleEncoding`,+  `TupleDecoding`, `Field`, etc.).+- Rename JSON acronyms to camel case (for example `SpecJSON` →+  `SpecJson`, `toJSONStructure` → `toJsonStructure`,+  `JSONStructure` → `JsonStructure`).+- Replace the old encode/decode class surface with a `Module`-centered+  binding model (`Module`, `BindingSpec`, `JsonModule`, `:=`, `::=`).+ ## 1.4.0.1  - Relax the `aeson` upper bound to allow 2.3.x.
README.md view
@@ -41,26 +41,32 @@   , lastLogin :: UTCTime   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON User)+  deriving (ToJSON, FromJSON) via (SpecJson User) instance HasJsonEncodingSpec User where   type EncodingSpec User =     JsonObject '[       Required "name" JsonString,       Required "last-login" JsonDateTime     ]-  toJSONStructure user =+instance TupleEncoding User where+  toJsonStructure user =     (Field @"name" (name user),     (Field @"last-login" (lastLogin user),     ())) instance HasJsonDecodingSpec User where   type DecodingSpec User = EncodingSpec User-  fromJSONStructure+instance TupleDecoding User where+  fromJsonStructure       (Field @"name" name,       (Field @"last-login" lastLogin,       ()))     =       pure User { name , lastLogin } ```++Import `"Data.JsonSpec"` for the specification language and+`"Data.JsonSpec.Codec.Tuple"` for the tuple codec (`Field`, `TupleEncoding`,+`SpecJson`, etc.).  For more examples, take a look at the test suite. 
json-spec.cabal view
@@ -1,9 +1,24 @@ cabal-version:       3.0 name:                json-spec-version:             1.4.0.1+version:             2.0.0.0 synopsis:            Type-level JSON specification maintainer:          rick@owensmurray.com-description:         See the README at: https://github.com/owensmurray/json-spec#json-spec+description:+  See the README at: https://github.com/owensmurray/json-spec#json-spec++  = Modules++  ["Data.JsonSpec"]+    Type-level JSON specifications.++  ["Data.JsonSpec.Codec.Tuple"]+    Encode and decode via nested tuples.++  ["Data.JsonSpec.Language.Parser"]+    Parser for the JsonSpec textual language.++  ["Data.JsonSpec.Language.QQ"]+    Quasi-quoters for the JsonSpec language. homepage:            https://github.com/owensmurray/json-spec license:             MIT license-file:        LICENSE@@ -18,13 +33,15 @@  common dependencies   build-depends:-    , aeson      >= 2.2.1.0  && < 2.4-    , base       >= 4.19.0.0 && < 4.23-    , containers >= 0.6.8    && < 0.9-    , scientific >= 0.3.7.0  && < 0.4-    , text       >= 2.1      && < 2.2-    , time       >= 1.9.3    && < 1.16-    , vector     >= 0.13.0.0 && < 0.14+    , aeson            >= 2.2.1.0  && < 2.4+    , base             >= 4.19.0.0 && < 4.23+    , containers       >= 0.6.8    && < 0.9+    , megaparsec       >= 9.6.0    && < 9.9+    , scientific       >= 0.3.7.0  && < 0.4+    , template-haskell >= 2.21.0.0 && < 2.25+    , text             >= 2.1      && < 2.2+    , time             >= 1.9.3    && < 1.16+    , vector           >= 0.13.0.0 && < 0.14  common warnings   ghc-options:@@ -38,10 +55,14 @@   import: dependencies, warnings   exposed-modules:     Data.JsonSpec-  other-modules:       -    Data.JsonSpec.Encode-    Data.JsonSpec.Decode+    Data.JsonSpec.Language.Parser+    Data.JsonSpec.Language.QQ+    Data.JsonSpec.Codec.Tuple+  other-modules:     Data.JsonSpec.Spec+    Data.JsonSpec.Codec.Tuple.Internal+    Data.JsonSpec.Codec.Tuple.Decode+    Data.JsonSpec.Codec.Tuple.Encode   -- other-extensions:       hs-source-dirs:      src   default-language:    Haskell2010@@ -70,3 +91,41 @@     , bytestring >= 0.12.0.2 && < 0.13     , hspec      >= 2.11.0   && < 2.12     , om-show    >= 0.1.2.9  && < 0.2++test-suite language+  import: dependencies, warnings+  main-is: language.hs+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  default-language: Haskell2010+  build-depends:+    , json-spec+    , hspec >= 2.11.0 && < 2.12+++test-suite json-let+  import: dependencies, warnings+  main-is: jsonlet.hs+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  default-language: Haskell2010+  build-depends:+    , json-spec++test-suite json-let2+  import: dependencies, warnings+  main-is: jsonlet2.hs+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  default-language: Haskell2010+  build-depends:+    , json-spec++test-suite json-let3+  import: dependencies, warnings+  main-is: jsonlet3.hs+  type: exitcode-stdio-1.0+  hs-source-dirs: test+  default-language: Haskell2010+  build-depends:+    , json-spec
src/Data/JsonSpec.hs view
@@ -1,8 +1,8 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE ExplicitNamespaces #-}  {-|+  Description : Type-level JSON specifications+   This module provides a way to specify the shape of your JSON data at   the type level. @@ -13,20 +13,23 @@   >   , lastLogin :: UTCTime   >   }   >   deriving stock (Show, Eq)-  >   deriving (ToJSON, FromJSON) via (SpecJSON User)+  >   deriving (ToJSON, FromJSON) via (SpecJson User)   > instance HasJsonEncodingSpec User where   >   type EncodingSpec User =-  >     JsonObject '[-  >       Required "name" JsonString,-  >       Required "last-login" JsonDateTime-  >     ]-  >   toJSONStructure user =+  >     'Module+  >       (JsonObject '[+  >         Required "name" JsonString,+  >         Required "last-login" JsonDateTime+  >       ])+  > instance TupleEncoding User where+  >   toJsonStructure user =   >     (Field @"name" (name user),   >     (Field @"last-login" (lastLogin user),   >     ()))   > instance HasJsonDecodingSpec User where   >   type DecodingSpec User = EncodingSpec User-  >   fromJSONStructure+  > instance TupleDecoding User where+  >   fromJsonStructure   >       (Field @"name" name,   >       (Field @"last-login" lastLogin,   >       ()))@@ -66,78 +69,34 @@   is still pretty new, but it at least includes OpenApi compatibility   (i.e. ToSchema instances) and Elm code generation. +  For the tuple-based encoding/decoding interpretation of a+  'Specification', see "Data.JsonSpec.Codec.Tuple". -} module Data.JsonSpec (   -- * Writing specifications   Specification(..),+  Module(..),+  BindingSpec(..),   (:::),   (::?),+  (:=),+  (::=),   FieldSpec(..), -  -- * Encoding/decoding via a Specification+  -- * Associating a type with a Module   HasJsonEncodingSpec(..),   HasJsonDecodingSpec(..),-  SpecJSON(..),-  Tag(..),-  Field(..),-  unField,-  Ref(..),--  -- * Direct encoding/decoding-  eitherDecode,-  encode,--  -- * Other stuff-  {-|-    The items in this section are mainly exported because once in a-    while you might need to include them in a type signature, but they-    are not intended to be used directly.-  -}-  JSONStructure,-  StructureFromJSON,-  StructureToJSON, ) where -import Data.Aeson (FromJSON(parseJSON), ToJSON(toJSON))-import Data.JsonSpec.Decode-  ( HasJsonDecodingSpec(DecodingSpec, fromJSONStructure)-  , StructureFromJSON(reprParseJSON), eitherDecode-  )-import Data.JsonSpec.Encode-  ( HasJsonEncodingSpec(EncodingSpec, toJSONStructure)-  , StructureToJSON(reprToJSON), encode-  ) import Data.JsonSpec.Spec-  ( Field(Field), FieldSpec(Optional, Required), Ref(Ref, unRef)+  ( BindingSpec(ModuleBind, TypeBind), FieldSpec(Optional, Required)+  , HasJsonDecodingSpec(DecodingSpec), HasJsonEncodingSpec(EncodingSpec)+  , Module(Module)   , Specification     ( JsonAnnotated, JsonArray, JsonBool, JsonDateTime, JsonDict, JsonEither-    , JsonInt, JsonLet, JsonNullable, JsonNum, JsonObject, JsonRaw, JsonRef-    , JsonString, JsonTag+    , JsonInt, JsonLet, JsonModule, JsonNullable, JsonNum, JsonObject, JsonRaw+    , JsonRef, JsonString, JsonTag     )-  , Tag(Tag), (:::), (::?), JSONStructure, unField+  , type (:::), type (::=), type (::?), type (:=)   )-import Prelude ((.), (<$>), (=<<))--{- |-  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 {unSpecJson :: a}-instance (StructureToJSON (JSONStructure (EncodingSpec a)), HasJsonEncodingSpec a) => ToJSON (SpecJSON a) where-  toJSON = reprToJSON . toJSONStructure . unSpecJson-instance (StructureFromJSON (JSONStructure (DecodingSpec a)), HasJsonDecodingSpec a) => FromJSON (SpecJSON a) where-  parseJSON v =-    SpecJSON <$>-      (fromJSONStructure =<< reprParseJSON v)- 
+ src/Data/JsonSpec/Codec/Tuple.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE UndecidableInstances #-}++{-|+  Description : Encode and decode via nested tuples++  Tuple-based interpretation of 'Specification'. The purpose of this+  module is to encode and decode Haskell values to and from Aeson+  'Value's, using a nested tuple structure as the intermediate+  representation.++  A type-level 'Specification' is translated into a canonical nested+  tuple type—an ordinary, inhabited Haskell type whose values you can+  construct and pattern-match on. Encoding and decoding then amount to+  converting between your domain types and that tuple structure, and+  between the tuple structure and an Aeson 'Value'.+-}+module Data.JsonSpec.Codec.Tuple (+  -- * Direct encoding/decoding+  {-|+    Encode or decode a value directly to or from an Aeson 'Value',+    given a 'Specification' and the corresponding tuple conversion.+  -}+  eitherDecode,+  encode,++  -- * Interacting with Aeson+  {-|+    'SpecJson' is the main way to plug a 'Specification' into Aeson's+    'ToJSON' / 'FromJSON' ecosystem (typically via @DerivingVia@).+  -}+  SpecJson(..),++  -- * Tuple encoding and decoding+  {-|+    'TupleEncoding' and 'TupleDecoding' convert between your Haskell+    types and the nested tuple structure that backs 'SpecJson' (and+    the direct encode/decode helpers above).+  -}+  TupleEncoding(..),+  TupleDecoding(..),+  Tag(..),+  Field(..),+  unField,+  Ref(..),++  -- * Other stuff+  {-|+    The items in this section are mainly exported because once in a+    while you might need to include them in a type signature, but they+    are not intended to be used directly.+  -}+  JsonStructure,+  StructureFromJson,+  StructureToJson,+) where++import Data.Aeson (FromJSON(parseJSON), ToJSON(toJSON))+import Data.JsonSpec.Codec.Tuple.Decode+  ( StructureFromJson(reprParseJson), TupleDecoding(fromJsonStructure)+  , eitherDecode+  )+import Data.JsonSpec.Codec.Tuple.Encode+  ( StructureToJson(reprToJson), TupleEncoding(toJsonStructure), encode+  )+import Data.JsonSpec.Codec.Tuple.Internal+  ( Field(Field), Ref(Ref, unRef), Tag(Tag), JsonStructure, unField+  )+import Data.JsonSpec.Spec+  ( HasJsonDecodingSpec(DecodingSpec), HasJsonEncodingSpec(EncodingSpec)+  )+import Prelude ((.), (<$>), (=<<))++{- |+  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 {unSpecJson :: a}+instance (StructureToJson (JsonStructure (EncodingSpec a)), TupleEncoding a) => ToJSON (SpecJson a) where+  toJSON = reprToJson . toJsonStructure . unSpecJson+instance (StructureFromJson (JsonStructure (DecodingSpec a)), TupleDecoding a) => FromJSON (SpecJson a) where+  parseJSON v =+    SpecJson <$>+      (fromJsonStructure =<< reprParseJson v)
+ src/Data/JsonSpec/Codec/Tuple/Decode.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++{- | Tuple-structure decoding for specs. -}+module Data.JsonSpec.Codec.Tuple.Decode (+  StructureFromJson(..),+  TupleDecoding(..),+  eitherDecode,+) where++import Control.Applicative (Alternative((<|>)))+import Data.Aeson.Types+  ( FromJSON(parseJSON), Value(Null, Object), Parser, parseEither, withArray+  , withObject, withScientific, withText+  )+import Data.JsonSpec.Codec.Tuple.Internal+  ( Field(Field), Ref(Ref), Tag(Tag), JStruct, JsonStructure, sym+  )+import Data.JsonSpec.Spec (HasJsonDecodingSpec(DecodingSpec), Module)+import Data.Map (Map)+import Data.Proxy (Proxy)+import Data.Scientific (Scientific)+import Data.Text (Text)+import Data.Time (UTCTime)+import GHC.TypeLits (KnownSymbol)+import Prelude+  ( Applicative(pure), Either(Left, Right), Eq((==)), Functor(fmap)+  , Maybe(Just, Nothing), MonadFail(fail), Semigroup((<>))+  , Traversable(traverse), ($), (.), (<$>), Bool, Int, String+  )+import qualified Data.Aeson.Key as AK+import qualified Data.Aeson.KeyMap as KM+import qualified Data.Map as Map+import qualified Data.Vector as Vector++{- |+  Decode a value from the structure appropriate for its 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".+-}+class (HasJsonDecodingSpec a) => TupleDecoding a where+  fromJsonStructure :: JsonStructure (DecodingSpec a) -> Parser a++++{- |+  Analog of 'Data.Aeson.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 'Data.Aeson.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 where+  reprParseJson :: Value -> Parser a+instance StructureFromJson Value where+  reprParseJson = pure+instance StructureFromJson Text where+  reprParseJson = withText "string" pure+instance StructureFromJson Scientific where+  reprParseJson = withScientific "number" pure+instance StructureFromJson Int where+  reprParseJson = parseJSON+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+      more <- reprParseJson (Object o)+      case KM.lookup (sym @key) o of+        Nothing -> fail $ "could not find key: " <> sym @key+        Just rawVal -> do+          val <- reprParseJson rawVal+          pure (Field val, more)+instance (KnownSymbol key, StructureFromJson val, StructureFromJson more) => StructureFromJson (Maybe (Field key val), more) where+  reprParseJson =+    withObject "object" $ \o -> do+      more <- reprParseJson (Object o)+      case KM.lookup (sym @key) o of+        Nothing ->+          pure (Nothing, more)+        Just rawVal -> do+          val <- reprParseJson rawVal+          pure (Just (Field val), more)+instance (StructureFromJson left, StructureFromJson right) => StructureFromJson (Either left right) where+  reprParseJson v =+    (Left <$> reprParseJson v)+    <|> (Right <$> reprParseJson v)+instance (KnownSymbol const) => StructureFromJson (Tag const) where+  reprParseJson =+    withText "constant" $ \c ->+      if c == sym @const then pure Tag+      else fail "unexpected constant value"+instance (StructureFromJson a) => StructureFromJson [a] where+  reprParseJson =+    withArray+      "list"+      (fmap Vector.toList . traverse reprParseJson)+instance (StructureFromJson a) => StructureFromJson (Map Text a) where+  reprParseJson =+    withObject+      "dict"+      ( fmap Map.fromList+          . traverse+              ( \(key, val) ->+                  (\val_ -> (AK.toText key, val_)) <$> reprParseJson val+              )+          . KM.toList+      )+instance StructureFromJson UTCTime where+  reprParseJson = parseJSON+instance (StructureFromJson a) => StructureFromJson (Maybe a) where+  reprParseJson val = do+    case val of+      Null -> pure Nothing+      _ -> Just <$> reprParseJson val+instance+    (StructureFromJson (JStruct env spec))+  =>+    StructureFromJson (Ref env spec)+  where+  reprParseJson val =+    Ref <$> reprParseJson val+++{-|+  Directly decode some JSON accoring to a spec without going through+  any To/FromJSON instances.+-}+eitherDecode+  :: forall spec.+     (StructureFromJson (JsonStructure spec))+   => Proxy (spec :: Module)+  -> Value+  -> Either String (JsonStructure spec)+eitherDecode _spec =+  parseEither reprParseJson
+ src/Data/JsonSpec/Codec/Tuple/Encode.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Data.JsonSpec.Codec.Tuple.Encode (+  TupleEncoding(..),+  StructureToJson(..),+  encode,+) where++import Data.Aeson (ToJSON(toJSON), Value)+import Data.JsonSpec.Codec.Tuple.Internal+  ( Field(Field), Ref(unRef), JStruct, JsonStructure, Tag, sym+  )+import Data.JsonSpec.Spec (HasJsonEncodingSpec(EncodingSpec))+import Data.Map (Map)+import Data.Proxy (Proxy(Proxy))+import Data.Scientific (Scientific)+import Data.Text (Text)+import Data.Time (UTCTime)+import GHC.TypeLits (KnownSymbol)+import Prelude+  ( Either(Left, Right), Functor(fmap), Maybe(Just, Nothing), Monoid(mempty)+  , (.), Bool, Int, id, maybe+  )+import qualified Data.Aeson as A+import qualified Data.Aeson.Key as AK+import qualified Data.Aeson.KeyMap as KM+import qualified Data.Map as Map++{- |+  Encode a value into the structure appropriate for its specification.+-}+class (HasJsonEncodingSpec a) => TupleEncoding a where+  {- | Encode the value into the structure appropriate for the specification. -}+  toJsonStructure :: a -> JsonStructure (EncodingSpec a)+++{- |+  This is like 'ToJSON', but specialized for our custom "json+  representation" types (i.e. the 'JsonStructure' type family). It is+  also closed (i.e. not exported, so the user can't add instances),+  because our json representation is closed.++  see 'StructureFromJson' for an explaination about why we don't just use+  'ToJSON'.+-}+class StructureToJson a where+  reprToJson :: a -> Value+instance StructureToJson Value where+  reprToJson = id+instance StructureToJson () where+  reprToJson () = A.object []+instance StructureToJson Bool where+  reprToJson = toJSON+instance StructureToJson Text where+  reprToJson = toJSON+instance StructureToJson Scientific where+  reprToJson = toJSON+instance StructureToJson Int where+  reprToJson = toJSON+instance (ToJsonObject (a, b)) => StructureToJson (a, b) where+  reprToJson = A.Object . toJsonObject+instance (StructureToJson left, StructureToJson right) => StructureToJson (Either left right) where+  reprToJson = \case+    Left val -> reprToJson val+    Right val -> reprToJson val+instance (KnownSymbol const) => StructureToJson (Tag const) where+  reprToJson _proxy = toJSON (sym @const @Text)+instance (StructureToJson a) => StructureToJson [a] where+  reprToJson = toJSON . fmap reprToJson+instance (StructureToJson a) => StructureToJson (Map Text a) where+  reprToJson =+    A.Object+      . KM.fromList+      . fmap (\(key, val) -> (AK.fromText key, reprToJson val))+      . Map.toList+instance StructureToJson UTCTime where+  reprToJson = toJSON+instance (StructureToJson a) => StructureToJson (Maybe a) where+  reprToJson = maybe A.Null reprToJson+instance+    (StructureToJson (JStruct env spec))+  =>+    StructureToJson (Ref env spec)+  where+    reprToJson = reprToJson . unRef+++{- |+  This class is to help 'StructureToJson' recursively encode objects, and+  is mutually recursive with 'StructureToJson'. If we tried to "recurse+  on the rest of the object" directly in 'StructureToJson' we would end+  up with a partial function, because 'reprToJson' returns a 'Value'+  not an 'Object'. We would therefore have to pattern match on 'Value'+  to get the 'Object' back out, but we would have to call 'error' if the+  'Value' mysteriously somehow wasn't an 'Object' after all. Instead of+  calling error because "it can't ever happen", we use this helper so+  the compiler can prove it never happens.+-}+class ToJsonObject a where+  toJsonObject :: a -> A.Object+instance ToJsonObject () where+  toJsonObject _ = mempty+instance (KnownSymbol key, StructureToJson val, ToJsonObject more) => ToJsonObject (Field key val, more) where+  toJsonObject (Field val, more) =+    KM.insert+      (sym @key)+      (reprToJson val)+      (toJsonObject more)+instance (KnownSymbol key, StructureToJson val, ToJsonObject more) => ToJsonObject (Maybe (Field key val), more) where+  toJsonObject (mval, more) =+    case mval of+      Nothing -> toJsonObject more+      Just (Field val) ->+        KM.insert+          (sym @key)+          (reprToJson val)+          (toJsonObject more)+++{-|+  Given a raw Haskell structure, directly encode it directly into an+  aeson Value without having to go through any To/FromJSON instances.++  See also: `Data.JsonSpec.Codec.Tuple.eitherDecode`.+-}+encode :: StructureToJson (JsonStructure spec) => Proxy spec -> JsonStructure spec -> Value+encode Proxy = reprToJson
+ src/Data/JsonSpec/Codec/Tuple/Internal.hs view
@@ -0,0 +1,272 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++{- | Internal tuple-structure representation. Not part of the public API. -}+module Data.JsonSpec.Codec.Tuple.Internal (+  JsonStructure,+  JStruct,+  Tag(..),+  Field(..),+  unField,+  Ref(..),+  sym,+) where++import Data.Aeson (Value)+import Data.JsonSpec.Spec+  ( BindingSpec(ModuleBind, TypeBind), FieldSpec(Optional, Required)+  , Module(Module)+  , Specification+    ( JsonAnnotated, JsonArray, JsonBool, JsonDateTime, JsonDict, JsonEither+    , JsonInt, JsonLet, JsonModule, JsonNullable, JsonNum, JsonObject, JsonRaw+    , JsonRef, JsonString, JsonTag+    )+  )+import Data.Kind (Type)+import Data.Map (Map)+import Data.Proxy (Proxy(Proxy))+import Data.Scientific (Scientific)+import Data.String (IsString(fromString))+import Data.Text (Text)+import Data.Time (UTCTime)+import GHC.Records (HasField(getField))+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)+import Prelude (Maybe(Just, Nothing), ($), Bool, Either, Eq, Int, Show)+import qualified GHC.TypeError as GE++{- |+  @'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,+  > ())))++  Note! "Object structures" of this type have the appropriate 'HasField'+  instances, which allows you to use -XOverloadedRecordDot to extract+  values as an alternative to pattern matching the whole tuple structure+  when building your 'HasJsonDecodingSpec' instances. See @TestHasField@+  in the tests for an example++  Arrays, dicts, booleans, numbers, and strings are just Lists,+  @'Map' 'Text'@, 'Bool's, 'Scientific's, and 'Text's 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 :: Module) where+  JsonStructure ('Module s) = JStruct '[] s+++{-|+  Make the correct reference type by looking up the symbol, and providing+  the environment in which the symbol was _defined_. We mustn't use the+  environment in which the reference is _used_, or else 'Specification'+  would be a dynamically scoped language, instead of a statically scoped+  language.+-}+type family+    LookupRef+      (env :: Env)+      (search :: Env)+      (target :: Symbol)+    :: Type+  where+    LookupRef+        env+        ( ('(target, spec) : moreDefs) : moreStack )+        target+      =+        Ref env spec++    LookupRef+        env+        ( ('(miss, spec) : moreDefs) : moreStack)+        target+      =+        LookupRef env ( moreDefs : moreStack) target++    LookupRef+        env+        ( '[] : moreStack)+        target+      =+        LookupRef moreStack moreStack target+++type family PushAll (a :: [k]) (b :: [k]) :: [k] where+  PushAll '[] b = b+  PushAll (e : more) b = PushAll more (e : b)+++{-|+  Structural type for `JsonEither`: nested `Either` for two or more branches,+  or the lone branch type for a singleton list. Empty list is disallowed.+-}+type family EitherJStruct (env :: Env) (specs :: [Specification]) :: Type where+  EitherJStruct _env '[] =+    GE.TypeError (GE.Text "JsonEither requires at least one branch")+  EitherJStruct env '[spec] =+    JStruct env spec+  EitherJStruct env (a ': b ': more) =+    Either (JStruct env a) (EitherJStruct env (b ': more))+++type family+  JStruct+    (env :: Env)+    (spec :: Specification)+  :: Type+  where+    JStruct env (JsonObject '[]) = ()+    JStruct env (JsonObject ( Required key s : more )) =+      (+        Field key (JStruct env s),+        JStruct env (JsonObject more)+      )+    JStruct env (JsonObject ( Optional key s : more )) =+      (+        Maybe (Field key (JStruct env s)),+        JStruct env (JsonObject more)+      )+    JStruct env JsonString = Text+    JStruct env JsonNum = Scientific+    JStruct env JsonInt = Int+    JStruct env (JsonArray spec) = [JStruct env spec]+    JStruct env (JsonDict spec) = Map Text (JStruct env spec)+    JStruct env JsonBool = Bool+    JStruct env (JsonEither specs) =+      EitherJStruct env specs+    JStruct env (JsonTag tag) = Tag tag+    JStruct env JsonDateTime = UTCTime+    JStruct env (JsonNullable spec) = Maybe (JStruct env spec)+    JStruct env (JsonLet defs spec) =+      JStruct (BindingsToFrame defs : env) spec+    JStruct env (JsonRef ref) = LookupRef env env ref+    JStruct env (JsonModule m) =+      JsonStructure m+    JStruct env JsonRaw = Value+    JStruct env (JsonAnnotated _annotations spec) =+      JStruct env spec+++{-| Lower 'BindingSpec's to the env-frame representation. -}+type family BindingsToFrame (bs :: [BindingSpec]) :: [(Symbol, Specification)] where+  BindingsToFrame '[] = '[]+  BindingsToFrame (TypeBind n s : more) =+    '(n, s) : BindingsToFrame more+  BindingsToFrame (ModuleBind n s : more) =+    '(n, JsonModule s) : BindingsToFrame more+++{-|+  This is the "Haskell structure" type of 'JsonRef' references.++  The main reason why we need this is because of recursion, as explained+  below:++  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++  ... because @EncodingSpec Foo@ would expand strictly into an array of+  @EncodingSpec Foo@, which would expand strictly... to infinity.++  Using `JsonLet` prevents the specification type from being infinitely+  sized, but what about the "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 you use a 'JsonRef' in the spec, the corresponding+  structural type will have a 'Ref' newtype wrapper around the+  "dereferenced" structure type.++  For example:++  > data Foo = Foo [Foo]+  > instance HasJsonEncodingSpec Foo where+  >   type EncodingSpec Foo =+  >     JsonLet+  >       '[ "Foo" := JsonArray (JsonRef "Foo") ]+  >       (JsonRef "Foo")+  >   toJsonStructure (Foo fs) =+  >     Ref [ toJsonStructure <$> fs ]++  Strictly speaking, we wouldn't /necessarily/ have to translate every+  'JsonRef' into a 'Ref'. In principal we could get away with inserting a+  'Ref' somewhere in every mutually recursive cycle. But the type level+  programming to figure that out a) probably wouldn't do any favors to+  compilation times, b) is beyond what I'm willing to attempted right+  now, and c) requires some kind of deterministic and stable choice+  about where to insert the 'Ref' (which I'm not even certain exists)+  lest arbitrary 'HasJsonEncodingSpec' or 'HasJsonDecodingSpec' instances+  break when the members of the recursive cycle change, causing a new+  choice about where to place the 'Ref'.+-}+newtype Ref env spec = Ref+  { unRef :: JStruct env spec+  }+++{-| Structural representation of 'JsonTag'. (I.e. a constant string value.) -}+data Tag (a :: Symbol) = Tag+++{-| Structural representation of an object field. -}+newtype Field (key :: Symbol) t = Field t+  deriving stock (Show, Eq)+instance {-# overlappable #-} (HasField k more v) => HasField k (Field notIt x, more) v where+  getField (_, more) = getField @k @_ @v more+instance {-# overlappable #-} (HasField k more v) => HasField k (Maybe (Field notIt x), more) v where+  getField (_, more) = getField @k @_ @v more+instance HasField k (Maybe (Field k v), more) (Maybe v) where+  getField (mv, _) =+    case mv of+      Nothing -> Nothing+      Just (Field v) -> Just v+instance HasField k (Field k v, more) v where+  getField (Field v, _) = v+++unField :: Field key t -> t+unField (Field t) = t+++{- |+  Shorthand for demoting type-level strings.+  Use with -XTypeApplication, e.g.:++  > sym @var+-}+sym+  :: forall a b.+     ( IsString b+     , KnownSymbol a+     )+  => b+sym = fromString $ symbolVal (Proxy @a)+++type Env = [[(Symbol, Specification)]]
− src/Data/JsonSpec/Decode.hs
@@ -1,164 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--{- | Decoding using specs. -}-module Data.JsonSpec.Decode (-  StructureFromJSON(..),-  HasJsonDecodingSpec(..),-  eitherDecode,-) where--import Control.Applicative (Alternative((<|>)))-import Data.Aeson.Types-  ( FromJSON(parseJSON), Value(Null, Object), Parser, parseEither, withArray-  , withObject, withScientific, withText-  )-import Data.JsonSpec.Spec-  ( Field(Field), Ref(Ref), Tag(Tag), JSONStructure, JStruct, Specification, sym-  )-import Data.Map (Map)-import Data.Proxy (Proxy)-import Data.Scientific (Scientific)-import Data.Text (Text)-import Data.Time (UTCTime)-import GHC.TypeLits (KnownSymbol)-import Prelude-  ( Applicative(pure), Either(Left, Right), Eq((==)), Functor(fmap)-  , Maybe(Just, Nothing), MonadFail(fail), Semigroup((<>))-  , Traversable(traverse), ($), (.), (<$>), Bool, Int, String-  )-import qualified Data.Aeson.Key as AK-import qualified Data.Aeson.KeyMap as KM-import qualified Data.Map as Map-import qualified Data.Vector as Vector--{- |-  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 :: JSONStructure (DecodingSpec a) -> Parser a---{- |-  Analog of 'Data.Aeson.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 'Data.Aeson.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 where-  reprParseJSON :: Value -> Parser a-instance StructureFromJSON Value where-  reprParseJSON = pure-instance StructureFromJSON Text where-  reprParseJSON = withText "string" pure-instance StructureFromJSON Scientific where-  reprParseJSON = withScientific "number" pure-instance StructureFromJSON Int where-  reprParseJSON = parseJSON-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-      more <- reprParseJSON (Object o)-      case KM.lookup (sym @key) o of-        Nothing -> fail $ "could not find key: " <> sym @key-        Just rawVal -> do-          val <- reprParseJSON rawVal-          pure (Field val, more)-instance (KnownSymbol key, StructureFromJSON val, StructureFromJSON more) => StructureFromJSON (Maybe (Field key val), more) where-  reprParseJSON =-    withObject "object" $ \o -> do-      more <- reprParseJSON (Object o)-      case KM.lookup (sym @key) o of-        Nothing ->-          pure (Nothing, more)-        Just rawVal -> do-          val <- reprParseJSON rawVal-          pure (Just (Field val), more)-instance (StructureFromJSON left, StructureFromJSON right) => StructureFromJSON (Either left right) where-  reprParseJSON v =-    (Left <$> reprParseJSON v)-    <|> (Right <$> reprParseJSON v)-instance (KnownSymbol const) => StructureFromJSON (Tag const) where-  reprParseJSON =-    withText "constant" $ \c ->-      if c == sym @const then pure Tag-      else fail "unexpected constant value"-instance (StructureFromJSON a) => StructureFromJSON [a] where-  reprParseJSON =-    withArray-      "list"-      (fmap Vector.toList . traverse reprParseJSON)-instance (StructureFromJSON a) => StructureFromJSON (Map Text a) where-  reprParseJSON =-    withObject-      "dict"-      ( fmap Map.fromList-          . traverse-              ( \(key, val) ->-                  (\val_ -> (AK.toText key, val_)) <$> reprParseJSON val-              )-          . KM.toList-      )-instance StructureFromJSON UTCTime where-  reprParseJSON = parseJSON-instance (StructureFromJSON a) => StructureFromJSON (Maybe a) where-  reprParseJSON val = do-    case val of-      Null -> pure Nothing-      _ -> Just <$> reprParseJSON val-instance-    (StructureFromJSON (JStruct env spec))-  =>-    StructureFromJSON (Ref env spec)-  where-  reprParseJSON val =-    Ref <$> reprParseJSON val---{-|-  Directly decode some JSON accoring to a spec without going through-  any To/FromJSON instances.--}-eitherDecode-  :: forall spec.-     (StructureFromJSON (JSONStructure spec))-   => Proxy (spec :: Specification)-  -> Value-  -> Either String (JSONStructure spec)-eitherDecode _spec =-  parseEither reprParseJSON--
− src/Data/JsonSpec/Encode.hs
@@ -1,146 +0,0 @@-{-# LANGUAGE DataKinds #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeApplications #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--module Data.JsonSpec.Encode (-  HasJsonEncodingSpec(..),-  StructureToJSON(..),-  encode,-) where--import Data.Aeson (ToJSON(toJSON), Value)-import Data.JsonSpec.Spec-  ( Field(Field), Ref(unRef), Specification(JsonArray), JSONStructure, JStruct-  , Tag, sym-  )-import Data.Map (Map)-import Data.Proxy (Proxy(Proxy))-import Data.Scientific (Scientific)-import Data.Set (Set)-import Data.Text (Text)-import Data.Time (UTCTime)-import GHC.TypeLits (KnownSymbol)-import Prelude-  ( Either(Left, Right), Functor(fmap), Maybe(Just, Nothing), Monoid(mempty)-  , (.), Bool, Int, id, maybe-  )-import qualified Data.Aeson as A-import qualified Data.Aeson.Key as AK-import qualified Data.Aeson.KeyMap as KM-import qualified Data.Map as Map-import qualified Data.Set as Set--{- |-  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 :: a -> JSONStructure (EncodingSpec a)-instance (HasJsonEncodingSpec a) => HasJsonEncodingSpec (Set a) where-  type EncodingSpec (Set a) = JsonArray (EncodingSpec a)-  toJSONStructure = fmap toJSONStructure . Set.toList---{- |-  This is like 'ToJSON', but specialized for our custom "json-  representation" types (i.e. the 'JSONStructure' type family). It is-  also closed (i.e. not exported, so the user can't add instances),-  because our json representation is closed.--  see 'StructureFromJSON' for an explaination about why we don't just use-  'ToJSON'.--}-class StructureToJSON a where-  reprToJSON :: a -> Value-instance StructureToJSON Value where-  reprToJSON = id-instance StructureToJSON () where-  reprToJSON () = A.object []-instance StructureToJSON Bool where-  reprToJSON = toJSON-instance StructureToJSON Text where-  reprToJSON = toJSON-instance StructureToJSON Scientific where-  reprToJSON = toJSON-instance StructureToJSON Int where-  reprToJSON = toJSON-instance (ToJSONObject (a, b)) => StructureToJSON (a, b) where-  reprToJSON = A.Object . toJSONObject-instance (StructureToJSON left, StructureToJSON right) => StructureToJSON (Either left right) where-  reprToJSON = \case-    Left val -> reprToJSON val-    Right val -> reprToJSON val-instance (KnownSymbol const) => StructureToJSON (Tag const) where-  reprToJSON _proxy = toJSON (sym @const @Text)-instance (StructureToJSON a) => StructureToJSON [a] where-  reprToJSON = toJSON . fmap reprToJSON-instance (StructureToJSON a) => StructureToJSON (Map Text a) where-  reprToJSON =-    A.Object-      . KM.fromList-      . fmap (\(key, val) -> (AK.fromText key, reprToJSON val))-      . Map.toList-instance StructureToJSON UTCTime where-  reprToJSON = toJSON-instance (StructureToJSON a) => StructureToJSON (Maybe a) where-  reprToJSON = maybe A.Null reprToJSON-instance-    (StructureToJSON (JStruct env spec))-  =>-    StructureToJSON (Ref env spec)-  where-    reprToJSON = reprToJSON . unRef---{- |-  This class is to help 'StructureToJSON' recursively encode objects, and-  is mutually recursive with 'StructureToJSON'. If we tried to "recurse-  on the rest of the object" directly in 'StructureToJSON' we would end-  up with a partial function, because 'reprToJSON' returns a 'Value'-  not an 'Object'. We would therefore have to pattern match on 'Value'-  to get the 'Object' back out, but we would have to call 'error' if the-  'Value' mysteriously somehow wasn't an 'Object' after all. Instead of-  calling error because "it can't ever happen", we use this helper so-  the compiler can prove it never happens.--}-class ToJSONObject a where-  toJSONObject :: a -> A.Object-instance ToJSONObject () where-  toJSONObject _ = mempty-instance (KnownSymbol key, StructureToJSON val, ToJSONObject more) => ToJSONObject (Field key val, more) where-  toJSONObject (Field val, more) =-    KM.insert-      (sym @key)-      (reprToJSON val)-      (toJSONObject more)-instance (KnownSymbol key, StructureToJSON val, ToJSONObject more) => ToJSONObject (Maybe (Field key val), more) where-  toJSONObject (mval, more) =-    case mval of-      Nothing -> toJSONObject more-      Just (Field val) ->-        KM.insert-          (sym @key)-          (reprToJSON val)-          (toJSONObject more)---{-|-  Given a raw Haskell structure, directly encode it directly into an-  aeson Value without having to go through any To/FromJSON instances.--  See also: `Data.JsonSpec.eitherDecode`.--}-encode :: StructureToJSON (JSONStructure spec) => Proxy spec -> JSONStructure spec -> Value-encode Proxy = reprToJSON--
+ src/Data/JsonSpec/Language/Parser.hs view
@@ -0,0 +1,413 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++{-|+  Description : Parser for the JsonSpec textual language++  Megaparsec parser for the JsonSpec textual language.++  See @docs\/language-spec.md@. Trailing commas are allowed in+  objects (JSON-familiar). Identifiers accept letters matching+  'isAlpha'. Keyword names may be written as backtick-escaped+  identifiers (e.g. @`string`@).+-}+module Data.JsonSpec.Language.Parser (+  -- * AST+  Program(..),+  Binding(..),+  Spec(..),+  Field(..),++  -- * Parsing+  parseProgram,+  parseSpec,+  program,+  spec,+) where++import Control.Applicative+  ( Alternative((<|>), many), Applicative((<*), pure), (<$>), optional+  )+import Control.Monad (void)+import Data.Char (isAlpha, isAlphaNum)+import Data.Text (Text)+import Data.Void (Void)+import Prelude+  ( Bool(False, True), Either(Left, Right), Enum(fromEnum, toEnum)+  , Eq((/=), (==)), Functor(fmap), Maybe(Just, Nothing), Monad((>>))+  , MonadFail(fail), Num((*), (+), (-)), Ord((<=), (>=)), Semigroup((<>)), ($)+  , (&&), (.), (||), Char, Int, Show, String, otherwise+  )+import Text.Megaparsec+  ( MonadParsec(eof, notFollowedBy, takeWhile1P, try), Parsec, between, choice+  , errorBundlePretty, manyTill, parse, satisfy, sepEndBy+  )+import Text.Megaparsec.Char (char, space1, string)+import qualified Data.Set as Set+import qualified Data.Text as T+import qualified Text.Megaparsec.Char.Lexer as L++{-| A full program: one top-level closed @module@ binding. -}+data Program = Program+  { programName :: Text+  , programSpec :: Spec+  }+  deriving stock (Eq, Show)+++{-| A @type@ or @module@ binding inside a @let@. -}+data Binding+  = TypeBinding Text Spec+  | ModuleBinding Text Spec+  deriving stock (Eq, Show)+++{-| A specification expression. -}+data Spec+  = LetSpec [Binding] Spec+  | EitherSpec [Spec]+  | DictSpec Spec+  | NullSpec Spec+  | StringSpec+  | NumberSpec+  | IntSpec+  | BoolSpec+  | DateTimeSpec+  | RawSpec+  | TagSpec Text+  | RefSpec Text+  | ObjectSpec [Field]+  | ArraySpec Spec+  deriving stock (Eq, Show)+++{-| An object field. -}+data Field = Field+  { fieldName     :: Text+  , fieldOptional :: Bool+  , fieldSpec     :: Spec+  }+  deriving stock (Eq, Show)+++type Parser = Parsec Void Text+++{-| Parse a full program (@module Name = …@). -}+parseProgram+  :: String+  -> Text+  -> Either String Program+parseProgram name input =+  case parse (sc >> program <* eof) name input of+    Left err ->+      Left (errorBundlePretty err)+    Right p ->+      Right p+++{-| Parse a bare specification expression (not a full program). -}+parseSpec+  :: String+  -> Text+  -> Either String Spec+parseSpec name input =+  case parse (sc >> spec <* eof) name input of+    Left err ->+      Left (errorBundlePretty err)+    Right s ->+      Right s+++sc :: Parser ()+sc =+  L.space+    space1+    (L.skipLineComment "--")+    (L.skipBlockCommentNested "{-" "-}")+++lexeme :: Parser a -> Parser a+lexeme =+  L.lexeme sc+++symbol :: Text -> Parser Text+symbol =+  L.symbol sc+++{-| Top-level @module Name = spec@. -}+program :: Parser Program+program = do+  void (keyword "module")+  name <- ident+  void (symbol "=")+  body <- spec+  pure (Program name body)+++{-| Parse a specification. -}+spec :: Parser Spec+spec =+  choice+    [ letSpec+    , eitherSpec+    , dictSpec+    , nullSpec+    , primary+    ]+++letSpec :: Parser Spec+letSpec = do+  void (keyword "let")+  void (symbol "{")+  binds <- many binding+  void (keyword "in")+  body <- spec+  void (symbol "}")+  checkDuplicateBinds binds+  pure (LetSpec binds body)+++binding :: Parser Binding+binding =+  typeBind <|> moduleBind+++typeBind :: Parser Binding+typeBind = do+  void (keyword "type")+  name <- ident+  void (symbol "=")+  TypeBinding name <$> spec+++moduleBind :: Parser Binding+moduleBind = do+  void (keyword "module")+  name <- ident+  void (symbol "=")+  ModuleBinding name <$> spec+++eitherSpec :: Parser Spec+eitherSpec = do+  void (keyword "either")+  firstBranch <- eitherBranch+  rest <- many (try (symbol "|" >> primary))+  pure (EitherSpec (firstBranch : rest))+++eitherBranch :: Parser Spec+eitherBranch = do+  _ <- optional (symbol "|")+  primary+++dictSpec :: Parser Spec+dictSpec = do+  void (keyword "dict")+  DictSpec <$> primary+++nullSpec :: Parser Spec+nullSpec = do+  void (keyword "null")+  NullSpec <$> primary+++primary :: Parser Spec+primary =+  choice+    [ try (keyword "string")   >> pure StringSpec+    , try (keyword "number")   >> pure NumberSpec+    , try (keyword "int")      >> pure IntSpec+    , try (keyword "bool")     >> pure BoolSpec+    , try (keyword "datetime") >> pure DateTimeSpec+    , try (keyword "raw")      >> pure RawSpec+    , TagSpec <$> stringLit+    , RefSpec <$> ident+    , objectSpec+    , arraySpec+    , between (symbol "(") (symbol ")") spec+    ]+++objectSpec :: Parser Spec+objectSpec = do+  void (symbol "{")+  fields <- field `sepEndBy` symbol ","+  void (symbol "}")+  checkDuplicateFields fields+  pure (ObjectSpec fields)+++field :: Parser Field+field = do+  name <- stringLit+  opt <- optional (symbol "?")+  void (symbol ":")+  s <- spec+  pure Field+    { fieldName = name+    , fieldOptional = case opt of+        Just _ ->+          True+        Nothing ->+          False+    , fieldSpec = s+    }+++arraySpec :: Parser Spec+arraySpec =+  ArraySpec <$> between (symbol "[") (symbol "]") spec+++keywords :: Set.Set Text+keywords =+  Set.fromList+    [ "module", "type", "let", "in", "either", "dict", "null"+    , "string", "number", "int", "bool", "datetime", "raw"+    ]+++keyword :: Text -> Parser ()+keyword w = lexeme . try $ do+  void (string w)+  notFollowedBy (satisfy identChar)+++{-| Binding name or reference: bare non-keyword, or backtick-escaped. -}+ident :: Parser Text+ident =+  escapedIdent <|> bareIdent+++{-| @`name`@ — may be a keyword (e.g. @`string`@, @`type`@). -}+escapedIdent :: Parser Text+escapedIdent = lexeme . try $ do+  void (char '`')+  name <- identBody+  void (char '`')+  pure name+++{-| Bare identifier; keywords are rejected. -}+bareIdent :: Parser Text+bareIdent = lexeme . try $ do+  full <- identBody+  if Set.member full keywords then+    fail ("unexpected keyword " <> T.unpack full)+  else+    pure full+++identBody :: Parser Text+identBody = do+  first <- takeWhile1P (Just "identifier") identCharStart+  rest <- fmap T.pack (many (satisfy identChar))+  pure (first <> rest)+++identCharStart :: Char -> Bool+identCharStart c =+  isAlpha c || c == '_'+++identChar :: Char -> Bool+identChar c =+  isAlphaNum c || c == '_'+++stringLit :: Parser Text+stringLit = lexeme $ do+  void (char '"')+  chars <- manyTill stringChar (char '"')+  pure (T.pack chars)+++stringChar :: Parser Char+stringChar =+  satisfy (\c -> c /= '"' && c /= '\\')+  <|> (char '\\' >> escape)+++escape :: Parser Char+escape =+  choice+    [ char '"'  >> pure '"'+    , char '\\' >> pure '\\'+    , char '/'  >> pure '/'+    , char 'b'  >> pure '\b'+    , char 'f'  >> pure '\f'+    , char 'n'  >> pure '\n'+    , char 'r'  >> pure '\r'+    , char 't'  >> pure '\t'+    , char 'u'  >> unicodeEscape+    ]+++unicodeEscape :: Parser Char+unicodeEscape = do+  d1 <- hexDigit+  d2 <- hexDigit+  d3 <- hexDigit+  d4 <- hexDigit+  pure (toEnum (d1 * 4096 + d2 * 256 + d3 * 16 + d4))+++hexDigit :: Parser Int+hexDigit = do+  c <- satisfy isHex+  pure (hexVal c)+++isHex :: Char -> Bool+isHex c =+  (c >= '0' && c <= '9')+  || (c >= 'a' && c <= 'f')+  || (c >= 'A' && c <= 'F')+++hexVal :: Char -> Int+hexVal c+  | c >= '0' && c <= '9' =+      fromEnum c - fromEnum '0'+  | c >= 'a' && c <= 'f' =+      fromEnum c - fromEnum 'a' + 10+  | otherwise =+      fromEnum c - fromEnum 'A' + 10+++checkDuplicateBinds :: [Binding] -> Parser ()+checkDuplicateBinds binds =+  checkDups "duplicate binding" (fmap bindName binds)+++checkDuplicateFields :: [Field] -> Parser ()+checkDuplicateFields fields =+  checkDups "duplicate field" (fmap fieldName fields)+++bindName :: Binding -> Text+bindName (TypeBinding n _) =+  n+bindName (ModuleBinding n _) =+  n+++checkDups :: String -> [Text] -> Parser ()+checkDups msg names =+  go Set.empty names+  where+    go :: Set.Set Text -> [Text] -> Parser ()+    go _seen [] =+      pure ()+    go seen (n:ns)+      | Set.member n seen =+          fail (msg <> ": " <> T.unpack n)+      | otherwise =+          go (Set.insert n seen) ns
+ src/Data/JsonSpec/Language/QQ.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE TemplateHaskellQuotes #-}++{-|+  Description : Quasi-quoters for the JsonSpec language++  Quasi-quoters for the JsonSpec textual language.++  > type Person =+  >   [jsonspec|+  >     module Person = {+  >       "name": string,+  >       "age": int+  >     }+  >   |]++  produces a type of kind 'Module'.++  See @docs\/language-spec.md@.+-}+module Data.JsonSpec.Language.QQ (+  jsonspec,+) where++import Data.JsonSpec.Language.Parser+  ( Binding(ModuleBinding, TypeBinding)+  , Field(Field, fieldName, fieldOptional, fieldSpec)+  , Program(Program, programSpec)+  , Spec+    ( ArraySpec, BoolSpec, DateTimeSpec, DictSpec, EitherSpec, IntSpec, LetSpec+    , NullSpec, NumberSpec, ObjectSpec, RawSpec, RefSpec, StringSpec, TagSpec+    )+  , parseProgram+  )+import Data.JsonSpec.Spec+  ( BindingSpec(ModuleBind, TypeBind), FieldSpec(Optional, Required)+  , Module(Module)+  , Specification+    ( JsonArray, JsonBool, JsonDateTime, JsonDict, JsonEither, JsonInt, JsonLet+    , JsonNullable, JsonNum, JsonObject, JsonRaw, JsonRef, JsonString, JsonTag+    )+  )+import Data.Text (Text)+import Language.Haskell.TH (Q, Type, TypeQ, appT, litT, promotedT, strTyLit)+import Language.Haskell.TH.Quote+  ( QuasiQuoter(QuasiQuoter, quoteDec, quoteExp, quotePat, quoteType)+  )+import Prelude+  ( Bool(False, True), Either(Left, Right), Foldable(foldr), Functor(fmap)+  , MonadFail(fail), Semigroup((<>)), String+  )+import qualified Data.Text as T++{-|+  Quasi-quoter for a JsonSpec program.++  The quoted text must be a full program+  (@module Name = \<spec\>@). Use in type context; the result has+  kind 'Module'.+-}+jsonspec :: QuasiQuoter+jsonspec =+  QuasiQuoter+    { quoteExp  = unsupported "expression"+    , quotePat  = unsupported "pattern"+    , quoteType = quoteJsonSpecType+    , quoteDec  = unsupported "declaration"+    }+++unsupported :: String -> String -> Q a+unsupported kind _ =+  fail ("jsonspec: " <> kind <> " contexts are not supported; use as a type")+++quoteJsonSpecType :: String -> Q Type+quoteJsonSpecType input =+  case parseProgram "jsonspec" (T.pack input) of+    Left err ->+      fail err+    Right Program { programSpec = body } ->+      promotedT 'Module `appT` specType body+++specType :: Spec -> TypeQ+specType StringSpec =+  promotedT 'JsonString+specType NumberSpec =+  promotedT 'JsonNum+specType IntSpec =+  promotedT 'JsonInt+specType BoolSpec =+  promotedT 'JsonBool+specType DateTimeSpec =+  promotedT 'JsonDateTime+specType RawSpec =+  promotedT 'JsonRaw+specType (TagSpec t) =+  promotedT 'JsonTag `appT` symbolType t+specType (RefSpec n) =+  promotedT 'JsonRef `appT` symbolType n+specType (DictSpec s) =+  promotedT 'JsonDict `appT` specType s+specType (NullSpec s) =+  promotedT 'JsonNullable `appT` specType s+specType (ArraySpec s) =+  promotedT 'JsonArray `appT` specType s+specType (EitherSpec ss) =+  promotedT 'JsonEither `appT` listType (fmap specType ss)+specType (ObjectSpec fields) =+  promotedT 'JsonObject `appT` listType (fmap fieldType fields)+specType (LetSpec binds body) =+  (promotedT 'JsonLet `appT` listType (fmap bindingType binds))+    `appT` specType body+++fieldType :: Field -> TypeQ+fieldType Field { fieldName = name, fieldOptional = True, fieldSpec = s } =+  (promotedT 'Optional `appT` symbolType name) `appT` specType s+fieldType Field { fieldName = name, fieldOptional = False, fieldSpec = s } =+  (promotedT 'Required `appT` symbolType name) `appT` specType s+++bindingType :: Binding -> TypeQ+bindingType (TypeBinding name s) =+  (promotedT 'TypeBind `appT` symbolType name) `appT` specType s+bindingType (ModuleBinding name s) =+  (promotedT 'ModuleBind `appT` symbolType name)+    `appT` (promotedT 'Module `appT` specType s)+++listType :: [TypeQ] -> TypeQ+listType =+  foldr+    (\t acc -> promotedT '(:) `appT` t `appT` acc)+    (promotedT '[])+++symbolType :: Text -> TypeQ+symbolType t =+  litT (strTyLit (T.unpack t))
src/Data/JsonSpec/Spec.hs view
@@ -11,71 +11,58 @@  module Data.JsonSpec.Spec (   Specification(..),-  JSONStructure,-  sym,-  Tag(..),-  Field(..),-  unField,-  Ref(..),-  JStruct,+  Module(..),+  BindingSpec(..),   FieldSpec(..),   (:::),   (::?),+  (:=),+  (::=),+  HasJsonEncodingSpec(..),+  HasJsonDecodingSpec(..), ) where -import Data.Aeson (Value)-import Data.Kind (Type)-import Data.Map (Map)-import Data.Proxy (Proxy(Proxy))-import Data.Scientific (Scientific)-import Data.String (IsString(fromString))-import Data.Text (Text)-import Data.Time (UTCTime)-import GHC.Records (HasField(getField))-import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)-import Prelude (Maybe(Just, Nothing), ($), Bool, Either, Eq, Int, Show)-import qualified GHC.TypeError as GE+import GHC.TypeLits (Symbol)+import Prelude ()  {-|-  Simple DSL for defining type level "specifications" for JSON-  data. Similar in spirit to (but not isomorphic with) JSON Schema.+  Type-level AST for JSON structure specifications. -  Intended to be used at the type level using @-XDataKinds@+  Use with @-XDataKinds@. Codecs such as 'Data.JsonSpec.Codec.Tuple'+  interpret these specs into concrete Haskell types and+  encode/decode strategies. -  See 'JSONStructure' for how these map into Haskell representations.+  Similar in spirit to JSON Schema, but not isomorphic with it.+  The matching textual language is documented in+  @docs\/language-spec.md@. -} data Specification where   JsonObject :: [FieldSpec] -> 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`+      Object with a fixed set of fields. Use 'Required' / 'Optional'+      (or '(:::)' / '(::?)') for each field.     -}   JsonString :: Specification-    {-^ An arbitrary JSON string. -}+    {-^ Any JSON string. -}   JsonNum :: Specification-    {-^ An arbitrary (floating point) JSON number. -}+    {-^ Any JSON number (floating point). -}   JsonInt :: Specification-    {-^ A JSON integer.  -}+    {-^ A JSON integer. -}   JsonArray :: Specification -> Specification-    {-^ A JSON array of values which conform to the given spec. -}+    {-^ Array whose elements all conform to the given spec. -}   JsonDict :: Specification -> Specification     {-^-      A JSON object used as a dictionary: arbitrary string keys, with every-      value conforming to the given specification.+      Object used as a string-keyed map: keys are unrestricted, and+      every value must conform to the given spec. -      This is distinct from 'JsonObject', which represents a record with-      statically known fields.+      Distinct from 'JsonObject', which has statically known field+      names.     -}   JsonBool :: Specification-    {-^ A JSON boolean value. -}+    {-^ A JSON boolean. -}   JsonNullable :: Specification -> Specification     {-^-      A value that can either be `null`, or else a value conforming to-      the specification.--      E.g.:+      Either JSON @null@, or a value conforming to the given spec.        > type SpecWithNullableField =       >   JsonObject '[@@ -84,16 +71,8 @@     -}   JsonEither :: [Specification] -> Specification     {-^-      One of several different specifications. Corresponds to json-schema-      "oneOf". Useful for encoding sum types.--      Takes a type-level list of specs. In the structural representation-      ('JStruct'), `JsonEither` maps to nested `Either`: two or more-      branches become @Either (JStruct env a) (Either (JStruct env b) ...)@;-      a single branch maps to @JStruct env spec@ (no sum wrapper). Use-      `Left`/`Right` for construction and pattern matching.--      Example:+      Exactly one of the given alternatives (json-schema @oneOf@).+      Commonly used for sum types.        > data MyType       >   = Foo Text@@ -101,121 +80,119 @@       >   | Baz UTCTime       > instance HasJsonEncodingSpec MyType where       >   type EncodingSpec MyType =-      >     JsonEither-      >       '[-      >         JsonObject '[-      >           Required "tag" (JsonTag "foo"),-      >           Required "content" JsonString-      >         ],-      >         JsonObject '[-      >           Required "tag" (JsonTag "bar"),-      >           Required "content" JsonInt-      >         ],-      >         JsonObject '[-      >           Required "tag" (JsonTag "baz"),-      >           Required "content" JsonDateTime-      >         ]-      >       ]-      >-      >   toJSONStructure = \case-      >     Foo t ->-      >       Left-      >         ( Field @"tag" (Tag @"foo")-      >         , (Field @"content" t, ())-      >         )-      >     Bar i ->-      >       Right (Left-      >         ( Field @"tag" (Tag @"bar")-      >         , (Field @"content" i, ())-      >         )-      >     Baz dt ->-      >       Right (Right-      >         ( Field @"tag" (Tag @"baz")-      >         , (Field @"content" dt, ())-      >         )+      >     'Module+      >       (JsonEither+      >         '[+      >           JsonObject '[+      >             Required "tag" (JsonTag "foo"),+      >             Required "content" JsonString+      >           ],+      >           JsonObject '[+      >             Required "tag" (JsonTag "bar"),+      >             Required "content" JsonInt+      >           ],+      >           JsonObject '[+      >             Required "tag" (JsonTag "baz"),+      >             Required "content" JsonDateTime+      >           ]+      >         ])     -}   JsonTag :: Symbol -> Specification-    {-^ A constant string value -}+    {-^ A constant string value. -}   JsonDateTime :: Specification     {-^-      A JSON string formatted as an ISO-8601 string. In Haskell this-      corresponds to `Data.Time.UTCTime`, and in json-schema it corresponds-      to the "date-time" format.+      ISO-8601 date-time string. Maps to 'Data.Time.UTCTime' in+      Haskell and to the json-schema @"date-time"@ format.     -}-  JsonLet :: [(Symbol, Specification)] -> Specification -> Specification+  JsonLet :: [BindingSpec] -> Specification -> 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:+      Bind names, then use them in the body via 'JsonRef'. -      > type Triangle =-      >   JsonObject '[-      >     Required "vertex1" (JsonObject '[-      >       Required "x" JsonInt,-      >       Required "y" JsonInt,-      >       Required "z" JsonInt-      >     ]),-      >     Required "vertex2" (JsonObject '[-      >       Required "x" JsonInt,-      >       Required "y" JsonInt,-      >       Required "z" JsonInt-      >     ]),-      >     Required "vertex3" (JsonObject '[-      >       Required "x" JsonInt),-      >       Required "y" JsonInt),-      >       Required "z" JsonInt)-      >     ])-      >   ]+      'TypeBind' is open: the RHS can refer to sibling bindings and+      outer lets. 'ModuleBind' is closed: the RHS is a 'Module' and+      cannot see outer names. -      Can be written more concisely as:+      Bindings in the same let may refer to each other, including+      recursively.        > type Triangle =       >   JsonLet       >     '[-      >       '("Vertex", JsonObject '[-      >          ('x', JsonInt),-      >          ('y', JsonInt),-      >          ('z', JsonInt)-      >        ])-      >      ]-      >      (JsonObject '[-      >        "vertex1" ::: JsonRef "Vertex",-      >        "vertex2" ::: JsonRef "Vertex",-      >        "vertex3" ::: JsonRef "Vertex"-      >      ])+      >       "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:+      Recursive:        > type LabelledTree =       >   JsonLet       >     '[-      >       '("LabelledTree", JsonObject '[+      >       "LabelledTree" := JsonObject '[       >         "label" ::: JsonString,       >         "children" ::: JsonArray (JsonRef "LabelledTree")-      >        ])-      >      ]+      >       ]+      >     ]       >     (JsonRef "LabelledTree")++      Closed nested binding ('ModuleBind' / '(::=)'):++      > type Invoice =+      >   JsonLet+      >     '[+      >       "Id" := JsonString,+      >       "Tax" ::=+      >         'Module+      >           (JsonLet+      >             '[ "Rate" := JsonNum ]+      >             (JsonObject '[ "rate" ::: JsonRef "Rate" ]))+      >     ]+      >     (JsonObject '[+      >       "id" ::: JsonRef "Id",+      >       "tax" ::: JsonRef "Tax"+      >     ])     -}   JsonRef :: Symbol -> Specification     {-^-      A reference to a specification which has been defined in a surrounding-      'JsonLet'.+      Reference a name bound by an enclosing 'JsonLet'.++      Resolution uses the environment from the binding site, not+      from the reference site.     -}+  JsonModule :: Module -> Specification+    {-^+      Embed a closed 'Module' inside another specification. The+      embedded module cannot see names from any outer 'JsonLet'.++      Typical use: nest another type's 'EncodingSpec' (itself a+      'Module') without exposing the outer environment to it.++      > type EncodingSpec (Wrapper a) =+      >   'Module+      >     (JsonLet+      >       '[ "Unused" := JsonString ]+      >       (JsonObject '[+      >         "payload" ::: JsonModule (EncodingSpec a)+      >       ]))+    -}   JsonRaw :: Specification-    {-^ Some raw, uninterpreted JSON value -}+    {-^ An opaque JSON value; not further interpreted. -}   JsonAnnotated :: forall k. [(Symbol, k)] -> Specification -> Specification     {-^-      An annotation on a specification. This is purely for documentation-      purposes and has no effect on encoding or decoding. The annotations-      are a list of key-value pairs at the type level. Keys are always-      symbols (type-level strings). Values can be any kind @k@: strings-      ('Symbol'), booleans ('Bool'), natural numbers ('Nat'), or any-      custom promoted type the user defines. Within one list, all values-      must have the same kind.+      Attach documentation metadata to a specification. Has no effect+      on encoding or decoding. -      E.g.:+      Annotations are type-level key-value pairs. Keys are always+      'Symbol'. Values share a single kind @k@ within one list —+      commonly 'Symbol', 'Bool', 'Nat', or a user-defined promoted+      type.        > type AnnotatedUser =       >   JsonAnnotated@@ -232,240 +209,89 @@     -}  -{-| Specify a field in an object.  -}-data FieldSpec-  = Required Symbol Specification {-^ The field is required -}-  | Optional Symbol Specification {-^ The field is optionsl -}---{-| Alias for 'Required'. -}-type (:::) = Required---{-| Alias for 'Optional'. -}-type (::?) = Optional---{- |-  @'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,-  > ())))--  Note! "Object structures" of this type have the appropriate 'HasField'-  instances, which allows you to use -XOverloadedRecordDot to extract-  values as an alternative to pattern matching the whole tuple structure-  when building your 'HasJsonDecodingSpec' instances. See @TestHasField@-  in the tests for an example--  Arrays, dicts, booleans, numbers, and strings are just Lists,-  @'Map' 'Text'@, 'Bool's, 'Scientific's, and 'Text's respectively.+{-|+  A closed specification: no free references to an outer+  environment. -  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'+  Corresponds to @module@ in the textual language. Also the return+  kind of 'EncodingSpec' / 'DecodingSpec', so associated codecs are+  closed by construction. -}-type family JSONStructure (spec :: Specification) where-  JSONStructure spec = JStruct '[] spec+data Module = Module Specification   {-|-  Make the correct reference type by looking up the symbol, and providing-  the environment in which the symbol was _defined_. We mustn't use the-  environment in which the reference is _used_, or else 'Specification'-  would be a dynamically scoped language, instead of a statically scoped-  language.--}-type family-    LookupRef-      (env :: Env)-      (search :: Env)-      (target :: Symbol)-    :: Type-  where-    LookupRef-        env-        ( ('(target, spec) : moreDefs) : moreStack )-        target-      =-        Ref env spec--    LookupRef-        env-        ( ('(miss, spec) : moreDefs) : moreStack)-        target-      =-        LookupRef env ( moreDefs : moreStack) target--    LookupRef-        env-        ( '[] : moreStack)-        target-      =-        LookupRef moreStack moreStack target---type family PushAll (a :: [k]) (b :: [k]) :: [k] where-  PushAll '[] b = b-  PushAll (e : more) b = PushAll more (e : b)-+  A named binding in a 'JsonLet'. -{-|-  Structural type for `JsonEither`: nested `Either` for two or more branches,-  or the lone branch type for a singleton list. Empty list is disallowed.+  'TypeBind' is open; 'ModuleBind' is closed. Neither introduces a+  namespace — there is no @M.N@ path syntax. -}-type family EitherJStruct (env :: Env) (specs :: [Specification]) :: Type where-  EitherJStruct _env '[] =-    GE.TypeError (GE.Text "JsonEither requires at least one branch")-  EitherJStruct env '[spec] =-    JStruct env spec-  EitherJStruct env (a ': b ': more) =-    Either (JStruct env a) (EitherJStruct env (b ': more))+data BindingSpec+  = TypeBind Symbol Specification+    {-^+      Open binding (@type Name = …@). May refer to siblings in this+      let and to names from outer lets.+    -}+  | ModuleBind Symbol Module+    {-^+      Closed binding (@module Name = …@). The RHS is a 'Module' and+      cannot see outer names. Useful with 'EncodingSpec': +      > "Item" ::= EncodingSpec LineItem+    -} -type family-  JStruct-    (env :: Env)-    (spec :: Specification)-  :: Type-  where-    JStruct env (JsonObject '[]) = ()-    JStruct env (JsonObject ( Required key s : more )) =-      (-        Field key (JStruct env s),-        JStruct env (JsonObject more)-      )-    JStruct env (JsonObject ( Optional key s : more )) =-      (-        Maybe (Field key (JStruct env s)),-        JStruct env (JsonObject more)-      )-    JStruct env JsonString = Text-    JStruct env JsonNum = Scientific-    JStruct env JsonInt = Int-    JStruct env (JsonArray spec) = [JStruct env spec]-    JStruct env (JsonDict spec) = Map Text (JStruct env spec)-    JStruct env JsonBool = Bool-    JStruct env (JsonEither specs) =-      EitherJStruct env specs-    JStruct env (JsonTag tag) = Tag tag-    JStruct env JsonDateTime = UTCTime-    JStruct env (JsonNullable spec) = Maybe (JStruct env spec)-    JStruct env (JsonLet defs spec) =-      JStruct (defs : env) spec-    JStruct env (JsonRef ref) = LookupRef env env ref-    JStruct env JsonRaw = Value-    JStruct env (JsonAnnotated _annotations spec) =-      JStruct env spec +{-| A field in a 'JsonObject'. -}+data FieldSpec+  = Required Symbol Specification {-^ Required field. -}+  | Optional Symbol Specification {-^ Optional field. -} -{-|-  This is the "Haskell structure" type of 'JsonRef' references. -  The main reason why we need this is because of recursion, as explained-  below:+{-| Alias for 'Required'. -}+type (:::) = Required -  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:+{-| Alias for 'Optional'. -}+type (::?) = Optional -  > data Foo = Foo [Foo]-  > instance HasJsonEncodingSpec Foo where-  >   type EncodingSpec Foo = JsonArray (EncodingSpec Foo)-  >   toJSONStructure = ... can't be written -  ... because @EncodingSpec Foo@ would expand strictly into an array of-  @EncodingSpec Foo@, which would expand strictly... to infinity.+{-| Alias for 'TypeBind'. -}+type (:=) = TypeBind -  Using `JsonLet` prevents the specification type from being infinitely-  sized, but what about the "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 you use a 'JsonRef' in the spec, the corresponding-  structural type will have a 'Ref' newtype wrapper around the-  "dereferenced" structure type.+{-| Alias for 'ModuleBind'. -}+type (::=) = ModuleBind -  For example: -  > data Foo = Foo [Foo]-  > instance HasJsonEncodingSpec Foo where-  >   type EncodingSpec Foo =-  >     JsonLet-  >       '[ '("Foo", JsonArray (JsonRef "Foo")) ]-  >       (JsonRef "Foo")-  >   toJSONStructure (Foo fs) =-  >     Ref [ toJSONStructure <$> fs ]+{-|+  Types that provide a closed encoding 'Module'. -  Strictly speaking, we wouldn't /necessarily/ have to translate every-  'JsonRef' into a 'Ref'. In principal we could get away with inserting a-  'Ref' somewhere in every mutually recursive cycle. But the type level-  programming to figure that out a) probably wouldn't do any favors to-  compilation times, b) is beyond what I'm willing to attempted right-  now, and c) requires some kind of deterministic and stable choice-  about where to insert the 'Ref' (which I'm not even certain exists)-  lest arbitrary 'HasJsonEncodingSpec' or 'HasJsonDecodingSpec' instances-  break when the members of the recursive cycle change, causing a new-  choice about where to place the 'Ref'.+  Closed means the specification is self-contained: it cannot+  reference names from any outer 'JsonLet'. That is why the+  associated type has kind 'Module' rather than 'Specification'. -}-newtype Ref env spec = Ref-  { unRef :: JStruct env spec-  }---{-| Structural representation of 'JsonTag'. (I.e. a constant string value.) -}-data Tag (a :: Symbol) = Tag---{-| Structural representation of an object field. -}-newtype Field (key :: Symbol) t = Field t-  deriving stock (Show, Eq)-instance {-# overlappable #-} (HasField k more v) => HasField k (Field notIt x, more) v where-  getField (_, more) = getField @k @_ @v more-instance {-# overlappable #-} (HasField k more v) => HasField k (Maybe (Field notIt x), more) v where-  getField (_, more) = getField @k @_ @v more-instance HasField k (Maybe (Field k v), more) (Maybe v) where-  getField (mv, _) =-    case mv of-      Nothing -> Nothing-      Just (Field v) -> Just v-instance HasField k (Field k v, more) v where-  getField (Field v, _) = v-+class HasJsonEncodingSpec a where+  {-|+    The encoding specification. -unField :: Field key t -> t-unField (Field t) = t+    Kind 'Module' enforces closedness: no free references to an+    outer environment.+  -}+  type EncodingSpec a :: Module  -{- |-  Shorthand for demoting type-level strings.-  Use with -XTypeApplication, e.g.:--  This function doesn't really "go" in this module, it is only here because-  this module happens to be at the bottom of the dependency tree and so it is-  easy to stuff "reusable" things here, and I don't feel like creating a whole-  new module just for this function (although maybe I should).+{-|+  Types that provide a closed decoding 'Module'. -  > sym @var+  Closed means the specification is self-contained: it cannot+  reference names from any outer 'JsonLet'. That is why the+  associated type has kind 'Module' rather than 'Specification'. -}-sym-  :: forall a b.-     ( IsString b-     , KnownSymbol a-     )-  => b-sym = fromString $ symbolVal (Proxy @a)---type Env = [[(Symbol, Specification)]]+class HasJsonDecodingSpec a where+  {-|+    The decoding specification. +    Kind 'Module' enforces closedness: no free references to an+    outer environment.+  -}+  type DecodingSpec a :: Module
+ test/jsonlet.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Main (main) where++import Data.JsonSpec+  ( HasJsonEncodingSpec(EncodingSpec), Module(Module)+  , Specification+    ( JsonArray, JsonInt, JsonLet, JsonModule, JsonNum, JsonObject, JsonRef+    , JsonString+    )+  , type (:::), type (::=), type (:=)+  )+import Data.JsonSpec.Codec.Tuple+  ( Field(Field), Ref(Ref), TupleEncoding(toJsonStructure), encode+  )+import Data.Proxy (Proxy(Proxy))+import Data.Scientific (Scientific)+import Data.Text (Text, empty)+import Prelude ((.), (<$>), IO, Int, print)++data Money = Money+  { currency :: Text+  , amount :: Scientific+  }+instance HasJsonEncodingSpec Money where+  type EncodingSpec Money =+    'Module+      (JsonObject+      '[ "currency" ::: JsonString+       , "amount" ::: JsonNum+       ])+instance TupleEncoding Money where+  toJsonStructure money =+    ( Field money.currency+    , ( Field money.amount+      , ()))++data LineItem = LineItem+  { description :: Text+  , quantity :: Int+  , unitPrice :: Money+  , lineTotal :: Money+  }+instance HasJsonEncodingSpec LineItem where+  type EncodingSpec LineItem =+    'Module+      (JsonLet+      '[ "Money" ::= EncodingSpec Money ]+      ( JsonObject+          '[ "description" ::: JsonString+           , "quantity" ::: JsonInt+           , "unitPrice" ::: JsonRef "Money"+           , "lineTotal" ::: JsonRef "Money"+           ]+      ))+instance TupleEncoding LineItem where+  toJsonStructure li =+    ( Field li.description+    , ( Field li.quantity+      , ( Field (Ref (toJsonStructure li.unitPrice))+        , ( Field (Ref (toJsonStructure li.lineTotal))+          , ()))))++data Invoice = Invoice+  { invoiceNumber :: Text+  , items :: [LineItem]+  , featured :: [LineItem]+  }+instance HasJsonEncodingSpec Invoice where+  type EncodingSpec Invoice =+    'Module+      (JsonLet+      '[ "LineItem" := JsonModule (EncodingSpec LineItem) ]+      (JsonObject+        '[ "invoiceNumber" ::: JsonString+         , "items" ::: JsonArray (JsonRef "LineItem")+         , "featured" ::: JsonArray (JsonRef "LineItem")+         ]))+instance TupleEncoding Invoice where+  toJsonStructure inv =+    ( Field inv.invoiceNumber+    , ( Field @"items" (Ref . toJsonStructure <$> inv.items)+      , ( Field @"featured" (Ref . toJsonStructure <$> inv.featured)+        , ())))+++main :: IO ()+main =+  let+    money :: Money+    money =+      Money+        { currency = empty+        , amount = 0+        }++    lineItem :: LineItem+    lineItem =+      LineItem+        { description = empty+        , quantity = 0+        , unitPrice = money+        , lineTotal = money+        }++    invoice :: Invoice+    invoice =+      Invoice+        { invoiceNumber = empty+        , items = [lineItem]+        , featured = [lineItem]+        }+  in+    print+      (encode+        (Proxy @(EncodingSpec Invoice))+        (toJsonStructure invoice))
+ test/jsonlet2.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Main (main) where++import Data.JsonSpec+  ( HasJsonEncodingSpec(EncodingSpec), Module(Module)+  , Specification(JsonInt, JsonLet, JsonModule, JsonObject, JsonRef, JsonString)+  , type (:::), type (:=)+  )+import Data.JsonSpec.Codec.Tuple+  ( Field(Field), Ref(Ref), TupleEncoding(toJsonStructure), encode+  )+import Data.Proxy (Proxy(Proxy))+import Prelude (IO, Int, print)++newtype Wrapper a = Wrapper a++instance+    HasJsonEncodingSpec (Wrapper a)+  where+    type EncodingSpec (Wrapper a) =+      'Module+        (JsonLet+        '[ "Unused" := JsonString ]+        (JsonObject '[ "payload" ::: JsonModule (EncodingSpec a)] ))++instance+    (TupleEncoding a)+  =>+    TupleEncoding (Wrapper a)+  where+    toJsonStructure (Wrapper w) = (Field @"payload" (toJsonStructure w), ())++newtype MyInt = MyInt Int+instance HasJsonEncodingSpec MyInt where+  type EncodingSpec MyInt  = 'Module (JsonInt)+instance TupleEncoding MyInt where+  toJsonStructure (MyInt i) = i+++newtype MyInt2 = MyInt2 Int+instance HasJsonEncodingSpec MyInt2 where+  type EncodingSpec MyInt2 =+    'Module+      (JsonLet '[ "Int" := JsonInt ] (JsonRef "Int"))+instance TupleEncoding MyInt2 where+  toJsonStructure (MyInt2 i) = Ref i+++main :: IO ()+main = do+  print+    (+      encode+        (Proxy @(EncodingSpec (Wrapper MyInt)))+        (toJsonStructure (Wrapper (MyInt 1)))+    )+  print+    (+      encode+        (Proxy @(EncodingSpec (Wrapper MyInt2)))+        (toJsonStructure (Wrapper (MyInt2 1)))+    )
+ test/jsonlet3.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++module Main (main) where++import Data.JsonSpec+  ( HasJsonEncodingSpec(EncodingSpec), Module(Module)+  , Specification(JsonArray, JsonInt, JsonLet, JsonModule, JsonObject, JsonRef, JsonString)+  , type (:::), type (:=)+  )+import Data.JsonSpec.Codec.Tuple+  ( Field(Field), Ref(Ref), TupleEncoding(toJsonStructure), encode+  )+import Data.Proxy (Proxy(Proxy))+import Data.Text (Text, empty)+import Prelude ((.), (<$>), IO, Int, print)++-- A container polymorphic in its payload spec. Same shape as the library's+-- own Set instance. Compiles.+data LineItem a = LineItem+  { description :: Text+  , quantity :: Int+  , unitPrice :: a+  , lineTotal :: a+  }+instance HasJsonEncodingSpec (LineItem a) where+  type EncodingSpec (LineItem a) =+    'Module+      (JsonObject+      '[ "description" ::: JsonString+       , "quantity" ::: JsonInt+       , "unitPrice" ::: JsonModule (EncodingSpec a)+       , "lineTotal" ::: JsonModule (EncodingSpec a)+       ])+instance (TupleEncoding a) => TupleEncoding (LineItem a) where+  toJsonStructure li =+    ( Field li.description+    , ( Field li.quantity+      , ( Field (toJsonStructure li.unitPrice)+        , ( Field (toJsonStructure li.lineTotal)+          , ()))))++-- Name the LineItem spec in a JsonLet so we can share it within the invoice+-- spec, exactly as the Triangle/Vertex test names Vertex.+-- This will not compile.+data Invoice a = Invoice+  { invoiceNumber :: Text+  , items :: [LineItem a]+  }+instance HasJsonEncodingSpec (Invoice a) where+  type EncodingSpec (Invoice a) =+    'Module+      (JsonLet+      '[ "LineItem" := JsonModule (EncodingSpec (LineItem a)) ]+      (JsonObject+        '[ "invoiceNumber" ::: JsonString+         , "items" ::: JsonArray (JsonRef "LineItem")+         ]))+instance (TupleEncoding a) => TupleEncoding (Invoice a) where+  toJsonStructure inv =+    ( Field inv.invoiceNumber+    , ( Field (Ref . toJsonStructure <$> inv.items)+      , ()))+++newtype Money = Money Int+instance HasJsonEncodingSpec Money where+  type EncodingSpec Money  = 'Module (JsonInt)+instance TupleEncoding Money where+  toJsonStructure (Money i) = i+++main :: IO ()+main =+  let+    lineItem :: LineItem Money+    lineItem =+      LineItem+        { description = empty+        , quantity = 0+        , unitPrice = Money 0+        , lineTotal = Money 0+        }++    invoice :: Invoice Money+    invoice =+      Invoice+        { invoiceNumber = empty+        , items = [lineItem]+        }+  in+    print+      (encode+        (Proxy @(EncodingSpec (Invoice Money)))+        (toJsonStructure invoice))
test/jsonspec.hs view
@@ -28,17 +28,21 @@ import Data.ByteString.Lazy (ByteString) import Data.Either (isLeft) import Data.JsonSpec-  ( Field(Field), FieldSpec(Optional, Required)-  , HasJsonDecodingSpec(DecodingSpec, fromJSONStructure)-  , HasJsonEncodingSpec(EncodingSpec, toJSONStructure), Ref(Ref)-  , SpecJSON(SpecJSON)+  ( BindingSpec(ModuleBind, TypeBind), FieldSpec(Optional, Required)+  , HasJsonDecodingSpec(DecodingSpec), HasJsonEncodingSpec(EncodingSpec)+  , Module(Module)   , Specification     ( JsonAnnotated, JsonArray, JsonBool, JsonDateTime, JsonDict, JsonEither-    , JsonInt, JsonLet, JsonNullable, JsonNum, JsonObject, JsonRaw, JsonRef-    , JsonString, JsonTag+    , JsonInt, JsonLet, JsonModule, JsonNullable, JsonNum, JsonObject, JsonRaw+    , JsonRef, JsonString, JsonTag     )-  , Tag(Tag), (:::), (::?), eitherDecode, encode, unField+  , type (:::), type (::?)   )+import Data.JsonSpec.Codec.Tuple+  ( Field(Field), Ref(Ref), SpecJson(SpecJson), Tag(Tag)+  , TupleDecoding(fromJsonStructure), TupleEncoding(toJsonStructure)+  , eitherDecode, encode, unField+  ) import Data.Map (Map) import Data.Proxy (Proxy(Proxy)) import Data.Scientific (Scientific)@@ -376,7 +380,7 @@             actual :: A.Value             actual =               encode-                (Proxy @(JsonDict JsonInt))+                (Proxy @('Module (JsonDict JsonInt)))                 Map.empty              expected :: A.Value@@ -396,13 +400,13 @@             decoded :: Either String (Map Text Int)             decoded =               eitherDecode-                (Proxy @(JsonDict JsonInt))+                (Proxy @('Module (JsonDict JsonInt)))                 raw              encoded :: Either String A.Value             encoded =               fmap-                (encode (Proxy @(JsonDict JsonInt)))+                (encode (Proxy @('Module (JsonDict JsonInt))))                 decoded              expected :: Either String A.Value@@ -438,10 +442,10 @@                      ())))             actual =               eitherDecode-                (Proxy @(JsonDict (JsonObject+                (Proxy @('Module (JsonDict (JsonObject                   '[ "foo" ::: JsonString                    , "bar" ::: JsonInt-                   ])))+                   ]))))                 raw              expected@@ -480,7 +484,7 @@             actual :: Either String (Map Text (Maybe Text))             actual =               eitherDecode-                (Proxy @(JsonDict (JsonNullable JsonString)))+                (Proxy @('Module (JsonDict (JsonNullable JsonString))))                 raw              expected :: Either String (Map Text (Maybe Text))@@ -498,7 +502,7 @@             actual :: Either String (Map Text Int)             actual =               eitherDecode-                (Proxy @(JsonDict JsonInt))+                (Proxy @('Module (JsonDict JsonInt)))                 (A.object [("bad", A.String "not an int")])           in             actual `shouldSatisfy` isLeft@@ -508,7 +512,7 @@             actual :: Either String (Map Text Int)             actual =               eitherDecode-                (Proxy @(JsonDict JsonInt))+                (Proxy @('Module (JsonDict JsonInt)))                 (A.String "not an object")           in             actual `shouldSatisfy` isLeft@@ -521,7 +525,7 @@                    (Field "attrs" (Map Text Int), ())             actual =               eitherDecode-                (Proxy @(JsonObject '[ "attrs" ::: JsonDict JsonInt ]))+                (Proxy @('Module (JsonObject '[ "attrs" ::: JsonDict JsonInt ])))                 ( A.object                     [ ( "attrs"                       , A.object@@ -568,7 +572,7 @@               A.eitherDecode                 "{ \"foo\": { \"bar\": \"barval\", \"baz\": [ \"qux\", 1, false ] } }"               >>=-                eitherDecode (Proxy @( JsonObject '[ "foo" ::: JsonRaw ]))+                eitherDecode (Proxy @('Module (JsonObject '[ "foo" ::: JsonRaw ])))           in             actual `shouldBe` expected         it "encodes" $@@ -582,7 +586,7 @@             actual =               Just $                 encode-                  (Proxy @( JsonObject '[ Required "foo" JsonRaw ]))+                  (Proxy @('Module (JsonObject '[ Required "foo" JsonRaw ])))                   (Field @"foo"                     (                       A.object@@ -864,11 +868,12 @@   = TestA Int Text   | TestB   deriving stock (Eq, Show)-  deriving ToJSON via (SpecJSON TestSum)-  deriving FromJSON via (SpecJSON TestSum)+  deriving ToJSON via (SpecJson TestSum)+  deriving FromJSON via (SpecJson TestSum) instance HasJsonEncodingSpec TestSum where   type EncodingSpec TestSum =-    JsonEither+    'Module+      (JsonEither       '[         JsonObject '[           Required "tag" (JsonTag "a"),@@ -880,8 +885,9 @@         JsonObject '[           Required "tag" (JsonTag "b")         ]-      ]-  toJSONStructure = \case+      ])+instance TupleEncoding TestSum where+  toJsonStructure = \case     TestA i t ->       Left         (Field @"tag" (Tag @"a"),@@ -899,7 +905,8 @@         ) instance HasJsonDecodingSpec TestSum where   type DecodingSpec TestSum = EncodingSpec TestSum-  fromJSONStructure = \case+instance TupleDecoding TestSum where+  fromJsonStructure = \case     Left         (Field @"tag" Tag,         (Field @"content"@@ -918,14 +925,16 @@   , bar :: Maybe (Maybe Text)   }   deriving stock (Show, Eq)-  deriving FromJSON via (SpecJSON TestOptionalHasField)+  deriving FromJSON via (SpecJson TestOptionalHasField) instance HasJsonDecodingSpec TestOptionalHasField where   type DecodingSpec TestOptionalHasField =-    JsonObject-     '[ "foo" ::? JsonString-      , "bar" ::? JsonNullable JsonString-      ]-  fromJSONStructure v =+    'Module+      (JsonObject+        '[ "foo" ::? JsonString+         , "bar" ::? JsonNullable JsonString+         ])+instance TupleDecoding TestOptionalHasField where+  fromJsonStructure v =     pure       TestOptionalHasField         { foo = v.foo@@ -941,28 +950,31 @@   , qoo :: Bool   }   deriving stock (Show, Eq)-  deriving ToJSON via (SpecJSON TestObj)-  deriving FromJSON via (SpecJSON TestObj)+  deriving ToJSON via (SpecJson TestObj)+  deriving FromJSON via (SpecJson TestObj) instance HasJsonEncodingSpec TestObj where   type EncodingSpec TestObj =-    JsonObject+    'Module+      (JsonObject       '[         Required "foo" JsonString,         Optional "bar" JsonNum,-        Required "baz" (EncodingSpec TestSubObj),+        Required "baz" (JsonModule (EncodingSpec TestSubObj)),         Required "qux" (JsonNullable JsonInt),         Required "qoo" JsonBool-      ]-  toJSONStructure TestObj { foo , bar , baz, qux, qoo } =+      ])+instance TupleEncoding TestObj where+  toJsonStructure TestObj { foo , bar , baz, qux, qoo } =     (Field @"foo" foo,     (fmap (Field @"bar" . realToFrac) bar,-    (Field @"baz" (toJSONStructure baz),+    (Field @"baz" (toJsonStructure baz),     (Field @"qux" qux,     (Field @"qoo" qoo,     ()))))) instance HasJsonDecodingSpec TestObj where   type DecodingSpec TestObj = EncodingSpec TestObj-  fromJSONStructure+instance TupleDecoding TestObj where+  fromJsonStructure       (Field @"foo" foo,       (fmap (unField @"bar") -> bar,       (Field @"baz" rawBaz,@@ -970,7 +982,7 @@       (Field @"qoo" qoo,       ())))))     = do-      baz <- fromJSONStructure rawBaz+      baz <- fromJsonStructure rawBaz       pure TestObj { foo, bar, baz, qux, qoo }  @@ -981,17 +993,20 @@   deriving stock (Show, Eq) instance HasJsonEncodingSpec TestSubObj where   type EncodingSpec TestSubObj =-    JsonObject+    'Module+      (JsonObject       '[ Required "foo" JsonString        , Required "bar" JsonInt-       ]-  toJSONStructure TestSubObj { foo2 , bar2 } =+       ])+instance TupleEncoding TestSubObj where+  toJsonStructure TestSubObj { foo2 , bar2 } =     (Field @"foo" foo2,     (Field @"bar" bar2,     ())) instance HasJsonDecodingSpec TestSubObj where   type DecodingSpec TestSubObj = EncodingSpec TestSubObj-  fromJSONStructure+instance TupleDecoding TestSubObj where+  fromJsonStructure       (Field @"foo" foo2,       (Field @"bar" bar2,       ()))@@ -1004,20 +1019,23 @@   , lastLogin :: UTCTime   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON User)+  deriving (ToJSON, FromJSON) via (SpecJson User) instance HasJsonEncodingSpec User where   type EncodingSpec User =-    JsonObject+    'Module+      (JsonObject       '[ Required "name" JsonString        , Required "last-login" JsonDateTime-       ]-  toJSONStructure user =+       ])+instance TupleEncoding User where+  toJsonStructure user =     (Field @"name" (name user),     (Field @"last-login" (lastLogin user),     ())) instance HasJsonDecodingSpec User where   type DecodingSpec User = EncodingSpec User-  fromJSONStructure+instance TupleDecoding User where+  fromJsonStructure       (Field @"name" name,       (Field @"last-login" lastLogin,       ()))@@ -1031,22 +1049,25 @@   , z :: Int   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON Vertex)+  deriving (ToJSON, FromJSON) via (SpecJson Vertex) instance HasJsonEncodingSpec Vertex where   type EncodingSpec Vertex =-    JsonObject+    'Module+      (JsonObject       '[ Required "x" JsonInt        , Required "y" JsonInt        , Required "z" JsonInt-       ]-  toJSONStructure Vertex {x, y, z} =+       ])+instance TupleEncoding Vertex where+  toJsonStructure Vertex {x, y, z} =     (Field @"x" x,     (Field @"y" y,     (Field @"z" z,     ()))) instance HasJsonDecodingSpec Vertex where   type DecodingSpec Vertex = EncodingSpec Vertex-  fromJSONStructure+instance TupleDecoding Vertex where+  fromJsonStructure       (Field @"x" x,       (Field @"y" y,       (Field @"z" z,@@ -1061,32 +1082,35 @@   , vertex3 :: Vertex   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON Triangle)+  deriving (ToJSON, FromJSON) via (SpecJson Triangle) instance HasJsonEncodingSpec Triangle where   type EncodingSpec Triangle =-    JsonLet-      '[ '("Vertex", EncodingSpec Vertex) ]+    'Module+      (JsonLet+      '[ ModuleBind "Vertex" (EncodingSpec Vertex) ]       (JsonObject         '[ Required "vertex1" (JsonRef "Vertex")          , Required "vertex2" (JsonRef "Vertex")          , Required "vertex3" (JsonRef "Vertex")-         ])-  toJSONStructure Triangle {vertex1, vertex2, vertex3} =-    (Field @"vertex1" (Ref $ toJSONStructure vertex1),-    (Field @"vertex2" (Ref $ toJSONStructure vertex2),-    (Field @"vertex3" (Ref $ toJSONStructure vertex3),+         ]))+instance TupleEncoding Triangle where+  toJsonStructure Triangle {vertex1, vertex2, vertex3} =+    (Field @"vertex1" (Ref $ toJsonStructure vertex1),+    (Field @"vertex2" (Ref $ toJsonStructure vertex2),+    (Field @"vertex3" (Ref $ toJsonStructure vertex3),     ()))) instance HasJsonDecodingSpec Triangle where   type DecodingSpec Triangle = EncodingSpec Triangle-  fromJSONStructure+instance TupleDecoding Triangle where+  fromJsonStructure       (Field @"vertex1" (Ref rawVertex1),       (Field @"vertex2" (Ref rawVertex2),       (Field @"vertex3" (Ref rawVertex3),       ())))     = do-      vertex1 <- fromJSONStructure rawVertex1-      vertex2 <- fromJSONStructure rawVertex2-      vertex3 <- fromJSONStructure rawVertex3+      vertex1 <- fromJsonStructure rawVertex1+      vertex2 <- fromJsonStructure rawVertex2+      vertex3 <- fromJsonStructure rawVertex3       pure Triangle{vertex1, vertex2, vertex3}  @@ -1095,29 +1119,32 @@   , children :: [LabelledTree]   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON LabelledTree)+  deriving (ToJSON, FromJSON) via (SpecJson LabelledTree) instance HasJsonEncodingSpec LabelledTree where   type EncodingSpec LabelledTree =-      JsonLet-        '[ '("LabelledTree",-               JsonObject-                 '[ Required "label" JsonString-                  , Required "children" (JsonArray (JsonRef "LabelledTree"))-                  ]-            )+    'Module+      (JsonLet+        '[ TypeBind "LabelledTree"+             (JsonObject+               '[ Required "label" JsonString+                , Required "children" (JsonArray (JsonRef "LabelledTree"))+                ]+             )          ]-        (JsonRef "LabelledTree")-  toJSONStructure LabelledTree {label , children } =+        (JsonRef "LabelledTree"))+instance TupleEncoding LabelledTree where+  toJsonStructure LabelledTree {label , children } =     Ref       (Field @"label" label,       (Field @"children"-        [ toJSONStructure child+        [ toJsonStructure child         | child <- children         ],       ())) instance HasJsonDecodingSpec LabelledTree where   type DecodingSpec LabelledTree = EncodingSpec LabelledTree-  fromJSONStructure+instance TupleDecoding LabelledTree where+  fromJsonStructure       (         Ref           (Field @"label" label,@@ -1125,7 +1152,7 @@           ()))       )     = do-      children <- traverse fromJSONStructure children_+      children <- traverse fromJsonStructure children_       pure LabelledTree { label , children }  @@ -1135,19 +1162,20 @@   , toBaz :: Maybe Int   , toQux :: Int   }-  deriving (ToJSON, FromJSON) via (SpecJSON TestOptionality)+  deriving (ToJSON, FromJSON) via (SpecJson TestOptionality)   deriving (Show) via (ShowJ TestOptionality)   deriving stock (Eq) instance HasJsonEncodingSpec TestOptionality where   type EncodingSpec TestOptionality =-    JsonObject+    'Module+      (JsonObject       '[ "foo" ::? JsonInt        , Required "bar" (JsonNullable JsonInt)        , Optional "baz" (JsonNullable JsonInt)        , Required "qux" JsonInt-       ]--  toJSONStructure TestOptionality { toFoo , toBar , toBaz , toQux } =+       ])+instance TupleEncoding TestOptionality where+  toJsonStructure TestOptionality { toFoo , toBar , toBaz , toQux } =     (fmap (Field @"foo") toFoo,     (Field @"bar" toBar,     ((Just . Field @"baz") toBaz, -- when encoding, prefer explicit null for testing.@@ -1155,8 +1183,8 @@     ())))) instance HasJsonDecodingSpec TestOptionality where   type DecodingSpec TestOptionality = EncodingSpec TestOptionality--  fromJSONStructure+instance TupleDecoding TestOptionality where+  fromJsonStructure       (fmap (unField @"foo") -> toFoo,       (Field @"bar" toBar,       (join . fmap (unField @"baz") -> toBaz,@@ -1172,18 +1200,20 @@   , thfBaz :: TestSubObj   }   deriving stock (Show, Eq)-  deriving (FromJSON) via (SpecJSON TestHasField)+  deriving (FromJSON) via (SpecJson TestHasField) instance HasJsonDecodingSpec TestHasField where   type DecodingSpec TestHasField =-    JsonObject-      '[ "foo" ::: JsonString-       , "bar" ::: JsonInt-       , "baz" ::: JsonObject-                    '[ "a_string" ::: JsonString-                     ,   "an_int" ::: JsonInt-                     ]-       ]-  fromJSONStructure val =+    'Module+      (JsonObject+        '[ "foo" ::: JsonString+         , "bar" ::: JsonInt+         , "baz" ::: JsonObject+                       '[ "a_string" ::: JsonString+                        ,   "an_int" ::: JsonInt+                        ]+         ])+instance TupleDecoding TestHasField where+  fromJsonStructure val =     pure       TestHasField         { thfFoo = val.foo@@ -1201,31 +1231,32 @@ {- ========================================================================== -}  newtype MRec1 = MRec1 [MRec2]-  deriving (ToJSON, FromJSON) via (SpecJSON MRec1)+  deriving (ToJSON, FromJSON) via (SpecJson MRec1)   deriving stock (Show, Eq) newtype MRec2 = MRec2 [MRec1]   deriving stock (Show, Eq) instance HasJsonEncodingSpec MRec1 where   type EncodingSpec MRec1 =-    JsonLet-     '[ '("one", JsonArray (JsonRef "two"))-      , '("two", JsonArray (JsonRef "one"))+    'Module+      (JsonLet+     '[ TypeBind "one" (JsonArray (JsonRef "two"))+      , TypeBind "two" (JsonArray (JsonRef "one"))       ]-      (JsonRef "one")--  toJSONStructure (MRec1 m2s) =+      (JsonRef "one"))+instance TupleEncoding MRec1 where+  toJsonStructure (MRec1 m2s) =     Ref-      [ Ref (fmap toJSONStructure m1s)+      [ Ref (fmap toJsonStructure m1s)       | MRec2 m1s <- m2s       ] instance HasJsonDecodingSpec MRec1 where   type DecodingSpec MRec1 = EncodingSpec MRec1--  fromJSONStructure (Ref m2s_) = do+instance TupleDecoding MRec1 where+  fromJsonStructure (Ref m2s_) = do     m2s <-       traverse         (\(Ref m1s_) -> do-          m1s <- traverse fromJSONStructure m1s_+          m1s <- traverse fromJsonStructure m1s_           pure (MRec2 m1s)         )         m2s_@@ -1236,16 +1267,16 @@ {- ========================================================================== -}  type SharedRecSpecs =-  '[ '( "three"-      , JsonObject+  '[ TypeBind "three"+       (JsonObject          '[ "foo" ::: JsonNullable (JsonRef "four")           ]-      )-   , '( "four"-      , JsonObject+       )+   , TypeBind "four"+       (JsonObject          '[ "bar" ::: JsonRef "three"           ]-      )+       )    ]  @@ -1253,20 +1284,22 @@   { foo :: Maybe MRec4   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON MRec3)+  deriving (ToJSON, FromJSON) via (SpecJson MRec3) instance HasJsonEncodingSpec MRec3 where   type EncodingSpec MRec3 =-    JsonLet SharedRecSpecs (JsonRef "three")--  toJSONStructure MRec3 { foo } =+    'Module+      (JsonLet SharedRecSpecs (JsonRef "three"))+instance TupleEncoding MRec3 where+  toJsonStructure MRec3 { foo } =     Ref-      (Field @"foo" (fmap toJSONStructure foo),+      (Field @"foo" (fmap toJsonStructure foo),       ()) instance HasJsonDecodingSpec MRec3 where   type DecodingSpec MRec3 = EncodingSpec MRec3-  fromJSONStructure ( Ref (Field @"foo" rawFoo, ()))+instance TupleDecoding MRec3 where+  fromJsonStructure ( Ref (Field @"foo" rawFoo, ()))     = do-      foo <- traverse fromJSONStructure rawFoo+      foo <- traverse fromJsonStructure rawFoo       pure MRec3 { foo }  @@ -1276,16 +1309,19 @@   deriving stock (Show, Eq) instance HasJsonEncodingSpec MRec4 where   type EncodingSpec MRec4 =-    JsonLet SharedRecSpecs (JsonRef "four")-  toJSONStructure MRec4 { bar } =+    'Module+      (JsonLet SharedRecSpecs (JsonRef "four"))+instance TupleEncoding MRec4 where+  toJsonStructure MRec4 { bar } =     Ref-      (Field @"bar" (toJSONStructure bar),+      (Field @"bar" (toJsonStructure bar),       ()) instance HasJsonDecodingSpec MRec4 where   type DecodingSpec MRec4 = EncodingSpec MRec4-  fromJSONStructure ( Ref (Field @"bar" rawbar, ()))+instance TupleDecoding MRec4 where+  fromJsonStructure ( Ref (Field @"bar" rawbar, ()))     = do-      bar <- fromJSONStructure rawbar+      bar <- fromJsonStructure rawbar       pure MRec4 { bar }  {- ========================================================================== -}@@ -1299,24 +1335,27 @@   ,  auAge :: Int   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON AnnotatedUser)+  deriving (ToJSON, FromJSON) via (SpecJson AnnotatedUser) instance HasJsonEncodingSpec AnnotatedUser where   type EncodingSpec AnnotatedUser =-    JsonAnnotated+    'Module+      (JsonAnnotated       '[ '("description", "A user with a name and age")        , '("example", "{\"name\": \"alice\", \"age\": 30}")        ]       (JsonObject         '[ Required "name" JsonString          , Required "age" JsonInt-         ])-  toJSONStructure AnnotatedUser { auName, auAge } =+         ]))+instance TupleEncoding AnnotatedUser where+  toJsonStructure AnnotatedUser { auName, auAge } =     (Field @"name" auName,     (Field @"age" auAge,     ())) instance HasJsonDecodingSpec AnnotatedUser where   type DecodingSpec AnnotatedUser = EncodingSpec AnnotatedUser-  fromJSONStructure+instance TupleDecoding AnnotatedUser where+  fromJsonStructure       (Field @"name" auName,       (Field @"age" auAge,       ()))@@ -1330,24 +1369,27 @@   , avZ :: Int   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON AnnotatedVertex)+  deriving (ToJSON, FromJSON) via (SpecJson AnnotatedVertex) instance HasJsonEncodingSpec AnnotatedVertex where   type EncodingSpec AnnotatedVertex =-    JsonAnnotated+    'Module+      (JsonAnnotated       '[ '("description", "A 3D vertex") ]       (JsonObject         '[ Required "x" JsonInt          , Required "y" JsonInt          , Required "z" JsonInt-         ])-  toJSONStructure AnnotatedVertex { avX, avY, avZ } =+         ]))+instance TupleEncoding AnnotatedVertex where+  toJsonStructure AnnotatedVertex { avX, avY, avZ } =     (Field @"x" avX,     (Field @"y" avY,     (Field @"z" avZ,     ()))) instance HasJsonDecodingSpec AnnotatedVertex where   type DecodingSpec AnnotatedVertex = EncodingSpec AnnotatedVertex-  fromJSONStructure+instance TupleDecoding AnnotatedVertex where+  fromJsonStructure       (Field @"x" avX,       (Field @"y" avY,       (Field @"z" avZ,@@ -1362,18 +1404,19 @@   , atVertex3 :: AnnotatedVertex   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON AnnotatedTriangle)+  deriving (ToJSON, FromJSON) via (SpecJson AnnotatedTriangle) instance HasJsonEncodingSpec AnnotatedTriangle where   type EncodingSpec AnnotatedTriangle =-    JsonLet-      '[ '("Vertex",-             JsonAnnotated-               '[ '("description", "A 3D vertex used in shapes") ]-               (JsonObject-                 '[ Required "x" JsonInt-                  , Required "y" JsonInt-                  , Required "z" JsonInt-                  ]))+    'Module+      (JsonLet+      '[ TypeBind "Vertex"+           (JsonAnnotated+             '[ '("description", "A 3D vertex used in shapes") ]+             (JsonObject+               '[ Required "x" JsonInt+                , Required "y" JsonInt+                , Required "z" JsonInt+                ]))        ]       (JsonAnnotated         '[ '("description", "A triangle with three vertices") ]@@ -1381,23 +1424,25 @@           '[ Required "vertex1" (JsonRef "Vertex")            , Required "vertex2" (JsonRef "Vertex")            , Required "vertex3" (JsonRef "Vertex")-           ]))-  toJSONStructure AnnotatedTriangle { atVertex1, atVertex2, atVertex3 } =-    (Field @"vertex1" (Ref $ toJSONStructure atVertex1),-    (Field @"vertex2" (Ref $ toJSONStructure atVertex2),-    (Field @"vertex3" (Ref $ toJSONStructure atVertex3),+           ])))+instance TupleEncoding AnnotatedTriangle where+  toJsonStructure AnnotatedTriangle { atVertex1, atVertex2, atVertex3 } =+    (Field @"vertex1" (Ref $ toJsonStructure atVertex1),+    (Field @"vertex2" (Ref $ toJsonStructure atVertex2),+    (Field @"vertex3" (Ref $ toJsonStructure atVertex3),     ()))) instance HasJsonDecodingSpec AnnotatedTriangle where   type DecodingSpec AnnotatedTriangle = EncodingSpec AnnotatedTriangle-  fromJSONStructure+instance TupleDecoding AnnotatedTriangle where+  fromJsonStructure       (Field @"vertex1" (Ref rawVertex1),       (Field @"vertex2" (Ref rawVertex2),       (Field @"vertex3" (Ref rawVertex3),       ())))     = do-      atVertex1 <- fromJSONStructure rawVertex1-      atVertex2 <- fromJSONStructure rawVertex2-      atVertex3 <- fromJSONStructure rawVertex3+      atVertex1 <- fromJsonStructure rawVertex1+      atVertex2 <- fromJsonStructure rawVertex2+      atVertex3 <- fromJsonStructure rawVertex3       pure AnnotatedTriangle { atVertex1, atVertex2, atVertex3 }  @@ -1405,20 +1450,23 @@   { awbName :: Text   }   deriving stock (Show, Eq)-  deriving (ToJSON, FromJSON) via (SpecJSON AnnotatedWithBool)+  deriving (ToJSON, FromJSON) via (SpecJson AnnotatedWithBool) instance HasJsonEncodingSpec AnnotatedWithBool where   type EncodingSpec AnnotatedWithBool =-    JsonAnnotated+    'Module+      (JsonAnnotated       '[ '("readOnly", 'True)        , '("deprecated", 'False)        ]-      (JsonObject '[ Required "name" JsonString ])-  toJSONStructure AnnotatedWithBool { awbName } =+      (JsonObject '[ Required "name" JsonString ]))+instance TupleEncoding AnnotatedWithBool where+  toJsonStructure AnnotatedWithBool { awbName } =     (Field @"name" awbName,     ()) instance HasJsonDecodingSpec AnnotatedWithBool where   type DecodingSpec AnnotatedWithBool = EncodingSpec AnnotatedWithBool-  fromJSONStructure+instance TupleDecoding AnnotatedWithBool where+  fromJsonStructure       (Field @"name" awbName,       ())     =
+ test/language.hs view
@@ -0,0 +1,506 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++{-# OPTIONS_GHC -Werror=missing-import-lists #-}++module Main (main) where++import Data.Either (isLeft)+import Data.JsonSpec+  ( BindingSpec(ModuleBind, TypeBind), FieldSpec(Optional, Required)+  , Module(Module)+  , Specification+    ( JsonArray, JsonBool, JsonDateTime, JsonDict, JsonEither, JsonInt, JsonLet+    , JsonNullable, JsonNum, JsonObject, JsonRaw, JsonRef, JsonString, JsonTag+    )+  )+import Data.JsonSpec.Language.Parser+  ( Binding(ModuleBinding, TypeBinding), Field(Field), Program(Program)+  , Spec+    ( ArraySpec, BoolSpec, DateTimeSpec, DictSpec, EitherSpec, IntSpec, LetSpec+    , NullSpec, NumberSpec, ObjectSpec, RawSpec, RefSpec, StringSpec, TagSpec+    )+  , parseProgram+  )+import Data.JsonSpec.Language.QQ (jsonspec)+import Data.Text (Text)+import Prelude (Applicative(pure), Bool(False, True), Either(Right), ($), IO)+import Test.Hspec (describe, hspec, it, shouldBe, shouldSatisfy)+import qualified Test.Hspec as Hspec++{-| Proxy for types of kind 'Module'. -}+data Mod (m :: Module) = Mod+++main :: IO ()+main =+  hspec suite+++suite :: Hspec.Spec+suite = do+  describe "parser" parserTests+  describe "quasi-quoter" qqTests+++parserTests :: Hspec.Spec+parserTests = do+  it "parses a trivial module" $+    parseProgram "trivial" "module Id = string"+      `shouldBe`+        Right (Program "Id" StringSpec)++  it "parses Person from the language spec" $+    parseProgram "Person" personSrc+      `shouldBe`+        Right (Program "Person" personAst)++  it "parses Graphs mutual recursion" $+    parseProgram "Graphs" graphsSrc+      `shouldBe`+        Right (Program "Graphs" graphsAst)++  it "parses Demo with nested closed module" $+    parseProgram "Demo" demoSrc+      `shouldBe`+        Right (Program "Demo" demoAst)++  it "parses either, dict, null, tags, and primitives" $+    parseProgram "Kitchen" kitchenSrc+      `shouldBe`+        Right (Program "Kitchen" kitchenAst)++  it "allows comments and trailing commas" $+    parseProgram "Comments" commentsSrc+      `shouldBe`+        Right+          (Program "Comments"+            (ObjectSpec+              [ Field "a" False IntSpec+              , Field "b" True StringSpec+              ]))++  it "rejects duplicate field names" $+    parseProgram "DupField"+      "module X = { \"a\": int, \"a\": string }"+      `shouldSatisfy` isLeft++  it "rejects duplicate binding names" $+    parseProgram "DupBind"+      "module X = let { type A = int type A = string in A }"+      `shouldSatisfy` isLeft++  it "rejects keyword used as bare identifier" $+    parseProgram "Keyword"+      "module type = string"+      `shouldSatisfy` isLeft++  it "allows backtick-escaped keyword as module name" $+    parseProgram "EscapedMod"+      "module `type` = string"+      `shouldBe`+        Right (Program "type" StringSpec)++  it "allows backtick-escaped keyword bind and ref" $+    parseProgram "EscapedBind"+      "module X = let { type `string` = int in `string` }"+      `shouldBe`+        Right+          (Program "X"+            (LetSpec+              [TypeBinding "string" IntSpec]+              (RefSpec "string")))++  it "keeps bare string as the primitive, not a ref" $+    parseProgram "BarePrim"+      "module X = let { type `string` = int in string }"+      `shouldBe`+        Right+          (Program "X"+            (LetSpec+              [TypeBinding "string" IntSpec]+              StringSpec))+++qqTests :: Hspec.Spec+qqTests = do+  it "quotes a trivial module" $+    sameModule+      (Mod @TrivialQuoted)+      (Mod @('Module 'JsonString))++  it "quotes Person" $+    sameModule+      (Mod @PersonQuoted)+      (Mod @PersonExpected)++  it "quotes the exhaustive Demo program" $+    sameModule+      (Mod @DemoQuoted)+      (Mod @DemoExpected)++  it "quotes either / dict / tags / datetime / raw" $+    sameModule+      (Mod @KitchenQuoted)+      (Mod @KitchenExpected)++  it "quotes backtick-escaped keyword bind and ref" $+    sameModule+      (Mod @EscapedQuoted)+      (Mod @EscapedExpected)+++sameModule+  :: Mod a+  -> Mod a+  -> IO ()+sameModule _ _ =+  pure ()+++type TrivialQuoted =+  [jsonspec| module Id = string |]+++type EscapedQuoted =+  [jsonspec|+    module X = let {+      module `let` = let {+        type `module` = int+        in `module`+      }+      type `string` = int+      in `string`+    }+  |]+++type EscapedExpected =+  'Module+    (JsonLet+      '[ ModuleBind "let" (+           'Module (+             JsonLet+               '[ TypeBind "module" JsonInt ]+               (JsonRef "module")+           )+         )+       , TypeBind "string" JsonInt+       ]+       (JsonRef "string"))+++type PersonQuoted =+  [jsonspec|+    module Person = let {+      type Person = {+        "name": string,+        "age": int,+        "email"?: null string+      }+      in Person+    }+  |]+++type PersonExpected =+  'Module+    (JsonLet+      '[ TypeBind "Person"+          (JsonObject+            '[ Required "name" JsonString+             , Required "age" JsonInt+             , Optional "email" (JsonNullable JsonString)+             ])+       ]+      (JsonRef "Person"))+++type DemoQuoted =+  [jsonspec|+    module Demo = let {+      type Id = string++      type Money = {+        "amount": number,+        "currency": string+      }++      -- Closed: cannot see Id or Money; define what it needs locally.+      module Tax = let {+        type Rate = number+        type Line = {+          "sku": string,+          "qty": int,+          "price": {+            "amount": number,+            "currency": string+          }+        }+        in {+          "rate": Rate,+          "lines": [Line]+        }+      }++      -- Open let: may use Id, Money, Tax from the enclosing frame.+      type Invoice = let {+        type Line = {+          "sku": string,+          "qty": int,+          "price": Money+        }+        in {+          "id": Id,+          "items": [Line],+          "tax": Tax,+          "notes"?: null string+        }+      }++      in Invoice+    }+  |]+++type DemoExpected =+  'Module+    (JsonLet+      '[ TypeBind "Id" JsonString+       , TypeBind "Money"+          (JsonObject+            '[ Required "amount" JsonNum+             , Required "currency" JsonString+             ])+       , ModuleBind "Tax"+          ('Module+            (JsonLet+              '[ TypeBind "Rate" JsonNum+               , TypeBind "Line"+                  (JsonObject+                    '[ Required "sku" JsonString+                     , Required "qty" JsonInt+                     , Required "price"+                        (JsonObject+                          '[ Required "amount" JsonNum+                           , Required "currency" JsonString+                           ])+                     ])+               ]+              (JsonObject+                '[ Required "rate" (JsonRef "Rate")+                 , Required "lines" (JsonArray (JsonRef "Line"))+                 ])))+       , TypeBind "Invoice"+          (JsonLet+            '[ TypeBind "Line"+                (JsonObject+                  '[ Required "sku" JsonString+                   , Required "qty" JsonInt+                   , Required "price" (JsonRef "Money")+                   ])+             ]+            (JsonObject+              '[ Required "id" (JsonRef "Id")+               , Required "items" (JsonArray (JsonRef "Line"))+               , Required "tax" (JsonRef "Tax")+               , Optional "notes" (JsonNullable JsonString)+               ]))+       ]+      (JsonRef "Invoice"))+++type KitchenQuoted =+  [jsonspec|+    module Kitchen = {+      "tag": "ok",+      "choice": either int | string | bool,+      "meta": dict datetime,+      "payload": null raw+    }+  |]+++type KitchenExpected =+  'Module+    (JsonObject+      '[ Required "tag" (JsonTag "ok")+       , Required "choice" (JsonEither '[JsonInt, JsonString, JsonBool])+       , Required "meta" (JsonDict JsonDateTime)+       , Required "payload" (JsonNullable JsonRaw)+       ])+++personSrc :: Text+personSrc =+  "module Person = let {\n\+  \  type Person = {\n\+  \    \"name\": string,\n\+  \    \"age\": int,\n\+  \    \"email\"?: null string\n\+  \  }\n\+  \  in Person\n\+  \}"+++personAst :: Spec+personAst =+  LetSpec+    [ TypeBinding "Person"+        (ObjectSpec+          [ Field "name" False StringSpec+          , Field "age" False IntSpec+          , Field "email" True (NullSpec StringSpec)+          ])+    ]+    (RefSpec "Person")+++graphsSrc :: Text+graphsSrc =+  "module Graphs = let {\n\+  \  type Node = {\n\+  \    \"id\": string,\n\+  \    \"edges\": [Edge]\n\+  \  }\n\+  \  type Edge = {\n\+  \    \"from\": Node,\n\+  \    \"to\": Node\n\+  \  }\n\+  \  in Node\n\+  \}"+++graphsAst :: Spec+graphsAst =+  LetSpec+    [ TypeBinding "Node"+        (ObjectSpec+          [ Field "id" False StringSpec+          , Field "edges" False (ArraySpec (RefSpec "Edge"))+          ])+    , TypeBinding "Edge"+        (ObjectSpec+          [ Field "from" False (RefSpec "Node")+          , Field "to" False (RefSpec "Node")+          ])+    ]+    (RefSpec "Node")+++demoSrc :: Text+demoSrc =+  "module Demo = let {\n\+  \  type Id = string\n\+  \  type Money = {\n\+  \    \"amount\": number,\n\+  \    \"currency\": string\n\+  \  }\n\+  \  module Tax = let {\n\+  \    type Rate = number\n\+  \    type Line = {\n\+  \      \"sku\": string,\n\+  \      \"qty\": int,\n\+  \      \"price\": {\n\+  \        \"amount\": number,\n\+  \        \"currency\": string\n\+  \      }\n\+  \    }\n\+  \    in {\n\+  \      \"rate\": Rate,\n\+  \      \"lines\": [Line]\n\+  \    }\n\+  \  }\n\+  \  type Invoice = let {\n\+  \    type Line = {\n\+  \      \"sku\": string,\n\+  \      \"qty\": int,\n\+  \      \"price\": Money\n\+  \    }\n\+  \    in {\n\+  \      \"id\": Id,\n\+  \      \"items\": [Line],\n\+  \      \"tax\": Tax,\n\+  \      \"notes\"?: null string\n\+  \    }\n\+  \  }\n\+  \  in Invoice\n\+  \}"+++demoAst :: Spec+demoAst =+  LetSpec+    [ TypeBinding "Id" StringSpec+    , TypeBinding "Money"+        (ObjectSpec+          [ Field "amount" False NumberSpec+          , Field "currency" False StringSpec+          ])+    , ModuleBinding "Tax"+        (LetSpec+          [ TypeBinding "Rate" NumberSpec+          , TypeBinding "Line"+              (ObjectSpec+                [ Field "sku" False StringSpec+                , Field "qty" False IntSpec+                , Field "price" False+                    (ObjectSpec+                      [ Field "amount" False NumberSpec+                      , Field "currency" False StringSpec+                      ])+                ])+          ]+          (ObjectSpec+            [ Field "rate" False (RefSpec "Rate")+            , Field "lines" False (ArraySpec (RefSpec "Line"))+            ]))+    , TypeBinding "Invoice"+        (LetSpec+          [ TypeBinding "Line"+              (ObjectSpec+                [ Field "sku" False StringSpec+                , Field "qty" False IntSpec+                , Field "price" False (RefSpec "Money")+                ])+          ]+          (ObjectSpec+            [ Field "id" False (RefSpec "Id")+            , Field "items" False (ArraySpec (RefSpec "Line"))+            , Field "tax" False (RefSpec "Tax")+            , Field "notes" True (NullSpec StringSpec)+            ]))+    ]+    (RefSpec "Invoice")+++kitchenSrc :: Text+kitchenSrc =+  "module Kitchen = {\n\+  \  \"tag\": \"ok\",\n\+  \  \"choice\": either int | string | bool,\n\+  \  \"meta\": dict datetime,\n\+  \  \"payload\": null raw\n\+  \}"+++kitchenAst :: Spec+kitchenAst =+  ObjectSpec+    [ Field "tag" False (TagSpec "ok")+    , Field "choice" False+        (EitherSpec [IntSpec, StringSpec, BoolSpec])+    , Field "meta" False (DictSpec DateTimeSpec)+    , Field "payload" False (NullSpec RawSpec)+    ]+++commentsSrc :: Text+commentsSrc =+  "module Comments = {- block -} {\n\+  \  \"a\": int, -- line comment\n\+  \  \"b\"?: string,\n\+  \}"
test/stable-environment.hs view
@@ -17,9 +17,10 @@ module Main (main) where  import Data.JsonSpec-  ( HasJsonEncodingSpec(EncodingSpec, toJSONStructure), Ref(Ref)-  , Specification(JsonLet, JsonRef, JsonString)+  ( HasJsonEncodingSpec(EncodingSpec), Module(Module)+  , Specification(JsonLet, JsonRef, JsonString), type (:=)   )+import Data.JsonSpec.Codec.Tuple (Ref(Ref), TupleEncoding(toJsonStructure)) import Data.Text (Text) import Prelude (Applicative(pure), ($), (.), Eq, IO, Ord) @@ -46,22 +47,23 @@  {-| Shared specification definitions. -} type TestShared a =-  JsonLet-    '[ '( "Foo"-        , JsonLet '[ '("bar", JsonRef "Baz") ] (JsonRef "bar")-        )-     , '( "Baz" , JsonString)-     ]-    (JsonRef a)+    JsonLet+      '[ "Foo" :=+           (JsonLet '[ "bar" := JsonRef "Baz" ] (JsonRef "bar"))+       , "Baz" := JsonString+       ]+      (JsonRef a)   newtype Foo = Foo Baz   deriving stock (Eq, Ord) instance HasJsonEncodingSpec Foo where   type EncodingSpec Foo =-    TestShared "Foo"-  toJSONStructure (Foo val) =-    Ref . Ref . toJSONStructure $ val+    'Module+      (TestShared "Foo")+instance TupleEncoding Foo where+  toJsonStructure (Foo val) =+    Ref . Ref . toJsonStructure $ val   newtype Baz = Baz Text@@ -71,7 +73,7 @@     ) instance HasJsonEncodingSpec Baz where   type EncodingSpec Baz =-    TestShared "Baz"-  toJSONStructure (Baz val) = Ref val--+    'Module+      (TestShared "Baz")+instance TupleEncoding Baz where+  toJsonStructure (Baz val) = Ref val