packages feed

json-spec-2.0.0.0: src/Data/JsonSpec/Spec.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

module Data.JsonSpec.Spec (
  Specification(..),
  Module(..),
  BindingSpec(..),
  FieldSpec(..),
  (:::),
  (::?),
  (:=),
  (::=),
  HasJsonEncodingSpec(..),
  HasJsonDecodingSpec(..),
) where

import GHC.TypeLits (Symbol)
import Prelude ()

{-|
  Type-level AST for JSON structure specifications.

  Use with @-XDataKinds@. Codecs such as 'Data.JsonSpec.Codec.Tuple'
  interpret these specs into concrete Haskell types and
  encode/decode strategies.

  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
    {-^
      Object with a fixed set of fields. Use 'Required' / 'Optional'
      (or '(:::)' / '(::?)') for each field.
    -}
  JsonString :: Specification
    {-^ Any JSON string. -}
  JsonNum :: Specification
    {-^ Any JSON number (floating point). -}
  JsonInt :: Specification
    {-^ A JSON integer. -}
  JsonArray :: Specification -> Specification
    {-^ Array whose elements all conform to the given spec. -}
  JsonDict :: Specification -> Specification
    {-^
      Object used as a string-keyed map: keys are unrestricted, and
      every value must conform to the given spec.

      Distinct from 'JsonObject', which has statically known field
      names.
    -}
  JsonBool :: Specification
    {-^ A JSON boolean. -}
  JsonNullable :: Specification -> Specification
    {-^
      Either JSON @null@, or a value conforming to the given spec.

      > type SpecWithNullableField =
      >   JsonObject '[
      >     Required "nullableProperty" (JsonNullable JsonString)
      >   ]
    -}
  JsonEither :: [Specification] -> Specification
    {-^
      Exactly one of the given alternatives (json-schema @oneOf@).
      Commonly used for sum types.

      > data MyType
      >   = Foo Text
      >   | Bar Int
      >   | Baz UTCTime
      > instance HasJsonEncodingSpec MyType where
      >   type EncodingSpec MyType =
      >     '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. -}
  JsonDateTime :: Specification
    {-^
      ISO-8601 date-time string. Maps to 'Data.Time.UTCTime' in
      Haskell and to the json-schema @"date-time"@ format.
    -}
  JsonLet :: [BindingSpec] -> Specification -> Specification
    {-^
      Bind names, then use them in the body via 'JsonRef'.

      '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.

      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"
      >     ])

      Recursive:

      > type LabelledTree =
      >   JsonLet
      >     '[
      >       "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
    {-^
      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
    {-^ An opaque JSON value; not further interpreted. -}
  JsonAnnotated :: forall k. [(Symbol, k)] -> Specification -> Specification
    {-^
      Attach documentation metadata to a specification. Has no effect
      on encoding or decoding.

      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
      >     '[ '("description", "A user record")
      >      , '("example", "...")
      >      ]
      >     (JsonObject '[
      >       Required "name" JsonString,
      >       Optional "last-login" JsonDateTime
      >      ])
      >
      > type ReadOnlyObject =
      >   JsonAnnotated '[ '("readOnly", 'True) ] (JsonObject '[])
    -}


{-|
  A closed specification: no free references to an outer
  environment.

  Corresponds to @module@ in the textual language. Also the return
  kind of 'EncodingSpec' / 'DecodingSpec', so associated codecs are
  closed by construction.
-}
data Module = Module Specification


{-|
  A named binding in a 'JsonLet'.

  'TypeBind' is open; 'ModuleBind' is closed. Neither introduces a
  namespace — there is no @M.N@ path syntax.
-}
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
    -}


{-| A field in a 'JsonObject'. -}
data FieldSpec
  = Required Symbol Specification {-^ Required field. -}
  | Optional Symbol Specification {-^ Optional field. -}


{-| Alias for 'Required'. -}
type (:::) = Required


{-| Alias for 'Optional'. -}
type (::?) = Optional


{-| Alias for 'TypeBind'. -}
type (:=) = TypeBind


{-| Alias for 'ModuleBind'. -}
type (::=) = ModuleBind


{-|
  Types that provide a closed encoding 'Module'.

  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'.
-}
class HasJsonEncodingSpec a where
  {-|
    The encoding specification.

    Kind 'Module' enforces closedness: no free references to an
    outer environment.
  -}
  type EncodingSpec a :: Module


{-|
  Types that provide a closed decoding 'Module'.

  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'.
-}
class HasJsonDecodingSpec a where
  {-|
    The decoding specification.

    Kind 'Module' enforces closedness: no free references to an
    outer environment.
  -}
  type DecodingSpec a :: Module