packages feed

domain-0.1: library/Domain.hs

{-|
This module contains the whole API of \"domain\".

Many functions come with collapsed example sections.
Do check them out for better understanding.
-}
module Domain
(
  -- * Declaration
  declare,
  -- * Schema
  Schema,
  schema,
  loadSchema,
  -- * Deriver
  Deriver.Deriver,
  stdDeriver,
  -- ** Common
  enumDeriver,
  boundedDeriver,
  showDeriver,
  eqDeriver,
  ordDeriver,
  genericDeriver,
  dataDeriver,
  typeableDeriver,
  hashableDeriver,
  liftDeriver,
  -- ** HasField
  hasFieldDeriver,
  -- ** IsLabel
  constructorIsLabelDeriver,
  accessorIsLabelDeriver,
  mapperIsLabelDeriver,
  -- * Clarifications
  -- ** Type Equality Constraint #type-equality-constraint#
  -- |
  -- You may have noticed that some instances (in particular of 'IsLabel')
  -- have some unusual tilde (@~@) constraint:
  -- 
  -- @
  -- instance a ~ TransportProtocol => IsLabel "protocol" (NetworkAddress -> a)
  -- @
  -- 
  -- This constraint states that types are equal.
  -- You might be wondering why do that instead of just
  -- 
  -- @
  -- instance IsLabel "protocol" (NetworkAddress -> TransportProtocol)
  -- @
  -- 
  -- The reason is that it helps the compiler pick up this instance having
  -- only the non-variable parts of the type signature,
  -- since type equality is verified after the instance match.
  -- This provides for better type inference and better error messages.
  -- 
  -- In case of our example we're ensuring that the compiler will pick
  -- up the instance for any function parameterised by @NetworkAddress@.
)
where

import Domain.Prelude hiding (liftEither, readFile, lift)
import Language.Haskell.TH.Syntax
import Language.Haskell.TH.Quote
import qualified Data.ByteString as ByteString
import qualified Data.Text.Encoding as Text
import qualified Domain.Resolvers.TypeCentricDoc as TypeCentricResolver
import qualified Domain.TH.TypeDec as TypeDec
import qualified Domain.TH.InstanceDecs as InstanceDecs
import qualified Domain.YamlUnscrambler.TypeCentricDoc as TypeCentricYaml
import qualified DomainCore.Deriver as Deriver
import qualified DomainCore.Model as Model
import qualified YamlUnscrambler


{-|
Declare datatypes and typeclass instances
from a schema definition according to the provided settings.

Use this function in combination with the 'schema' quasi-quoter or
the 'loadSchema' function.
__For examples__ refer to their documentation.

Call it on the top-level (where you declare your module members).
-}
declare ::
  {-|
  Field naming.
  When nothing, no fields will be generated.
  Otherwise the first wrapped boolean specifies,
  whether to prefix the names with underscore,
  and the second - whether to prefix with the type name.
  Please notice that when you choose not to prefix with the type name
  you need to have the @DuplicateRecords@ extension enabled.
  -}
  Maybe (Bool, Bool) ->
  {-|
  Which instances to derive and how.
  -}
  Deriver.Deriver ->
  {-|
  Schema definition.
  -}
  Schema ->
  {-|
  Template Haskell action splicing the generated code on declaration level.
  -}
  Q [Dec]
declare fieldNaming (Deriver.Deriver derive) (Schema schema) =
  do
    instanceDecs <- fmap (nub . concat) (traverse derive schema)
    return (fmap (TypeDec.typeDec fieldNaming) schema <> instanceDecs)


-- * Schema
-------------------------

{-|
Parsed and validated schema.

You can only produce it using the 'schema' quasi-quoter or
the 'loadSchema' function
and generate the code from it using 'declare'.
-}
newtype Schema =
  Schema [Model.TypeDec]
  deriving (Lift)

{-|
Quasi-quoter, which parses a YAML schema into a 'Schema' expression.

Use 'declare' to generate the code from it.

==== __Example__

@
{\-# LANGUAGE
  QuasiQuotes, TemplateHaskell,
  StandaloneDeriving, DeriveGeneric, DeriveDataTypeable, DeriveLift,
  FlexibleInstances, MultiParamTypeClasses,
  DataKinds, TypeFamilies
  #-\}
module Model where

import Data.Text (Text)
import Data.Word (Word16, Word32, Word64)
import Domain

'declare'
  (Just (False, True))
  'stdDeriver'
  ['schema'|

    Host:
      sum:
        ip: Ip
        name: Text

    Ip:
      sum:
        v4: Word32
        v6: Word128

    Word128:
      product:
        part1: Word64
        part2: Word64

    |]
@

-}
schema :: QuasiQuoter
schema =
  QuasiQuoter exp pat type_ dec
  where
    unsupported =
      const (fail "Quotation in this context is not supported")
    exp =
      lift <=< parseString
    pat =
      unsupported
    type_ =
      unsupported
    dec =
      unsupported

{-|
Load and parse a YAML file into a schema definition.

Use 'declare' to generate the code from it.

==== __Example__

@
{\-# LANGUAGE
  TemplateHaskell,
  StandaloneDeriving, DeriveGeneric, DeriveDataTypeable, DeriveLift,
  FlexibleInstances, MultiParamTypeClasses,
  DataKinds, TypeFamilies
  #-\}
module Model where

import Data.Text (Text)
import Data.Word (Word16, Word32, Word64)
import Domain

'declare'
  (Just (True, False))
  'stdDeriver'
  =<< 'loadSchema' "domain.yaml"
@
-}
loadSchema ::
  {-|
  Path to the schema file relative to the root of the project.
  -}
  FilePath ->
  {-|
  Template Haskell action producing a valid schema.
  -}
  Q Schema
loadSchema path =
  readFile path >>= parseByteString


-- * Helpers
-------------------------

readFile :: FilePath -> Q ByteString
readFile path =
  do
    addDependentFile path
    readRes <- liftIO (tryIOError (ByteString.readFile path))
    liftEither (first showAsText readRes)

parseString :: String -> Q Schema
parseString =
  parseText . fromString

parseText :: Text -> Q Schema
parseText =
  parseByteString . Text.encodeUtf8

parseByteString :: ByteString -> Q Schema
parseByteString input =
  liftEither $ do
    doc <- YamlUnscrambler.parseByteString TypeCentricYaml.doc input
    decs <- TypeCentricResolver.eliminateDoc doc
    return (Schema decs)

liftEither :: Either Text a -> Q a
liftEither =
  \ case
    Left err -> fail (toList err)
    Right a -> return a 


-- * Deriver
-------------------------

{-|
Combination of all derivers exported by this module.
-}
stdDeriver =
  mconcat [
    enumDeriver,
    boundedDeriver,
    showDeriver,
    eqDeriver,
    ordDeriver,
    genericDeriver,
    dataDeriver,
    typeableDeriver,
    hashableDeriver,
    liftDeriver,
    hasFieldDeriver,
    constructorIsLabelDeriver,
    mapperIsLabelDeriver,
    accessorIsLabelDeriver
    ]

{-|
Derives 'Enum' for enums or sums having no members in all variants.

Requires to have the @StandaloneDeriving@ compiler extension enabled.
-}
enumDeriver =
  Deriver.effectless InstanceDecs.enum

{-|
Derives 'Bounded' for enums.

Requires to have the @StandaloneDeriving@ compiler extension enabled.
-}
boundedDeriver =
  Deriver.effectless InstanceDecs.bounded

{-|
Derives 'Show'.

Requires to have the @StandaloneDeriving@ compiler extension enabled.
-}
showDeriver =
  Deriver.effectless InstanceDecs.show

{-|
Derives 'Eq'.

Requires to have the @StandaloneDeriving@ compiler extension enabled.
-}
eqDeriver =
  Deriver.effectless InstanceDecs.eq

{-|
Derives 'Ord'.

Requires to have the @StandaloneDeriving@ compiler extension enabled.
-}
ordDeriver =
  Deriver.effectless InstanceDecs.ord

{-|
Derives 'Generic'.

Requires to have the @StandaloneDeriving@ and @DeriveGeneric@ compiler extensions enabled.
-}
genericDeriver =
  Deriver.effectless InstanceDecs.generic

{-|
Derives 'Data'.

Requires to have the @StandaloneDeriving@ and @DeriveDataTypeable@ compiler extensions enabled.
-}
dataDeriver =
  Deriver.effectless InstanceDecs.data_

{-|
Derives 'Typeable'.

Requires to have the @StandaloneDeriving@ and @DeriveDataTypeable@ compiler extensions enabled.
-}
typeableDeriver =
  Deriver.effectless InstanceDecs.typeable

{-|
Generates 'Generic'-based instances of 'Hashable'.
-}
hashableDeriver =
  Deriver.effectless InstanceDecs.hashable

{-|
Derives 'Lift'.

Requires to have the @StandaloneDeriving@ and @DeriveLift@ compiler extensions enabled.
-}
liftDeriver =
  Deriver.effectless InstanceDecs.lift

-- ** HasField
-------------------------

{-|
Derives 'HasField' with unprefixed field names.

For each field of a product generates instances mapping to their values.

For each constructor of a sum maps to a 'Maybe' tuple of members of that constructor,
unless there\'s no members, in which case it maps to 'Bool'.

For each variant of an enum maps to 'Bool' signaling whether the value equals to it.

/Please notice that if you choose to generate unprefixed record field accessors, it will conflict with this deriver, since it\'s gonna generate duplicate instances./
-}
hasFieldDeriver =
  Deriver.effectless InstanceDecs.hasField


-- * IsLabel
-------------------------

{-|
Generates instances of 'IsLabel' for wrappers, enums and sums,
providing mappings from labels to constructors.

==== __Sum Example__

Having the following schema:

@
Host:
  sum:
    ip: Ip
    name: Text
@

The following instances will be generated:

@
instance a ~ Ip => IsLabel "ip" (a -> Host) where
  fromLabel = IpHost

instance a ~ Text => IsLabel "name" (a -> Host) where
  fromLabel = NameHost
@

In case you\'re wondering what this tilde (@~@) constraint business is about,
refer to the [Type Equality Constraint](#type-equality-constraint) section.

==== __Enum Example__

Having the following schema:

@
TransportProtocol:
  enum:
    - tcp
    - udp
@

The following instances will be generated:

@
instance IsLabel "tcp" TransportProtocol where
  fromLabel = TcpTransportProtocol

instance IsLabel "udp" TransportProtocol where
  fromLabel = UdpTransportProtocol
@
-}
constructorIsLabelDeriver =
  Deriver.effectless InstanceDecs.constructorIsLabel

{-|
Generates instances of 'IsLabel' for enums, sums and products,
providing accessors to their components.

==== __Product Example__

Having the following schema:

@
NetworkAddress:
  product:
    protocol: TransportProtocol
    host: Host
    port: Word16
@

The following instances will be generated:

@
instance a ~ TransportProtocol => IsLabel "protocol" (NetworkAddress -> a) where
  fromLabel (NetworkAddress a _ _) = a

instance a ~ Host => IsLabel "host" (NetworkAddress -> a) where
  fromLabel (NetworkAddress _ b _) = b

instance a ~ Word16 => IsLabel "port" (NetworkAddress -> a) where
  fromLabel (NetworkAddress _ _ c) = c
@

In case you\'re wondering what this tilde (@~@) constraint business is about,
refer to the [Type Equality Constraint](#type-equality-constraint) section.

==== __Sum Example__

Having the following schema:

@
Host:
  sum:
    ip: Ip
    name: Text
@

The following instances will be generated:

@
instance a ~ Maybe Ip => IsLabel "ip" (Host -> a) where
  fromLabel (IpHost a) = Just a
  fromLabel _ = Nothing

instance a ~ Maybe Text => IsLabel "name" (Host -> a) where
  fromLabel (NameHost a) = Just a
  fromLabel _ = Nothing
@

In case you\'re wondering what this tilde (@~@) constraint business is about,
refer to the [Type Equality Constraint](#type-equality-constraint) section.

==== __Enum Example__

Having the following schema:

@
TransportProtocol:
  enum:
    - tcp
    - udp
@

The following instances will be generated:

@
instance a ~ Bool => IsLabel "tcp" (TransportProtocol -> a) where
  fromLabel TcpTransportProtocol = True
  fromLabel _ = False

instance a ~ Bool => IsLabel "udp" (TransportProtocol -> a) where
  fromLabel UdpTransportProtocol = True
  fromLabel _ = False
@

In case you\'re wondering what this tilde (@~@) constraint business is about,
refer to the [Type Equality Constraint](#type-equality-constraint) section.
-}
accessorIsLabelDeriver =
  Deriver.effectless InstanceDecs.accessorIsLabel

{-|
Generates instances of 'IsLabel' for sums and products,
providing mappers over their components.

==== __Product Example__

Having the following schema:

@
NetworkAddress:
  product:
    protocol: TransportProtocol
    host: Host
    port: Word16
@

The following instances will be generated:

@
instance
  mapper ~ (TransportProtocol -> TransportProtocol) =>
  IsLabel "protocol" (mapper -> NetworkAddress -> NetworkAddress)
  where
    fromLabel mapper (NetworkAddress a b c) =
      NetworkAddress (mapper a) b c

instance
  mapper ~ (Host -> Host) =>
  IsLabel "host" (mapper -> NetworkAddress -> NetworkAddress)
  where
    fromLabel mapper (NetworkAddress a b c) = 
      NetworkAddress a (mapper b) c

instance
  mapper ~ (Word16 -> Word16) =>
  IsLabel "port" (mapper -> NetworkAddress -> NetworkAddress)
  where
    fromLabel mapper (NetworkAddress a b c) =
      NetworkAddress a b (mapper c)
@

In case you\'re wondering what this tilde (@~@) constraint business is about,
refer to the [Type Equality Constraint](#type-equality-constraint) section.

==== __Sum Example__

Having the following schema:

@
Host:
  sum:
    ip: Ip
    name: Text
@

The following instances will be generated:

@
instance
  mapper ~ (Ip -> Ip) =>
  IsLabel "ip" (mapper -> Host -> Host)
  where
    fromLabel fn (IpHost a) = IpHost (fn a)
    fromLabel _ a = a

instance
  mapper ~ (Text -> Text) =>
  IsLabel "name" (mapper -> Host -> Host)
  where
    fromLabel fn (NameHost a) = NameHost (fn a)
    fromLabel _ a = a
@

In case you\'re wondering what this tilde (@~@) constraint business is about,
refer to the [Type Equality Constraint](#type-equality-constraint) section.
-}
mapperIsLabelDeriver =
  Deriver.effectless InstanceDecs.mapperIsLabel