packages feed

aeson-schemas-1.4.1.0: src/Data/Aeson/Schema/TH/Unwrap.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}

{-|
Module      :  Data.Aeson.Schema.TH.Unwrap
Maintainer  :  Brandon Chinn <brandonchinn178@gmail.com>
Stability   :  experimental
Portability :  portable

The 'unwrap' quasiquoter.
-}
module Data.Aeson.Schema.TH.Unwrap where

import Control.Monad ((<=<), (>=>))
import Data.Bifunctor (first)
import qualified Data.List.NonEmpty as NonEmpty
import Language.Haskell.TH
import Language.Haskell.TH.Quote (QuasiQuoter (..))

import Data.Aeson.Schema.Internal (Object, SchemaResult)
import Data.Aeson.Schema.Key (fromSchemaKeyV)
import Data.Aeson.Schema.TH.Parse (
  GetterOperation (..),
  GetterOps,
  UnwrapSchema (..),
  parseUnwrapSchema,
 )
import Data.Aeson.Schema.TH.Utils (
  reifySchema,
  resolveSchemaType,
  schemaTypeVToTypeQ,
  schemaVToTypeQ,
 )
import Data.Aeson.Schema.Type (
  Schema' (..),
  SchemaType' (..),
  SchemaTypeV,
  SchemaV,
  showSchemaTypeV,
  toSchemaObjectV,
 )

-- | Defines a QuasiQuoter to extract a schema within the given schema.
--
--  The base schema needs to be defined in a separate module.
--
--  For example:
--
--  > -- | MyFoo ~ Object [schema| { b: Maybe Bool } |]
--  > type MyFoo = [unwrap| MySchema.foo.nodes[] |]
--
--  If the schema is imported qualified, you can use parentheses to distinguish it from the
--  expression:
--
--  > type MyFoo = [unwrap| (MyModule.Schema).foo.nodes[] |]
--
--  You can then use the type alias as usual:
--
--  > parseBar :: MyFoo -> String
--  > parseBar = maybe "null" show . [get| .b |]
--  >
--  > foo = map parseBar [get| result.foo.nodes[] |]
--
--  The syntax is mostly the same as 'Data.Aeson.Schema.TH.get', except the operations run on the
--  type itself, instead of the values. Differences from 'Data.Aeson.Schema.TH.get':
--
--  * @x!@ is only valid if @x@ is a @Maybe a@ type. Returns @a@, the type wrapped in the 'Maybe'.
--
--  * @x?@ is the same as @x!@.
--
--  * @x[]@ is only valid if @x@ is a @[a]@ type. Returns @a@, the type contained in the list.
--
--  * @x\@#@ is only valid if @x@ is a @SumType@. Returns the type at that branch in the sum type.
unwrap :: QuasiQuoter
unwrap =
  QuasiQuoter
    { quoteExp = error "Cannot use `unwrap` for Exp"
    , quoteDec = error "Cannot use `unwrap` for Dec"
    , quoteType = parseUnwrapSchema >=> generateUnwrapSchema
    , quotePat = error "Cannot use `unwrap` for Pat"
    }

generateUnwrapSchema :: UnwrapSchema -> TypeQ
generateUnwrapSchema UnwrapSchema{..} = reifySchema startSchema >>= unwrapSchema getterOps

-- | Unwrap the given schema by applying the given operations, stripping out functors.
unwrapSchema :: GetterOps -> SchemaV -> TypeQ
unwrapSchema = unwrapSchemaUsing StripFunctors

-- | Unwrap the given schema by applying the given operations, using the given 'FunctorHandler'.
unwrapSchemaUsing :: FunctorHandler -> GetterOps -> SchemaV -> TypeQ
unwrapSchemaUsing functorHandler getterOps = toResultTypeQ <=< flip go (NonEmpty.toList getterOps) . toSchemaObjectV
  where
    toResultTypeQ :: UnwrapSchemaResult -> TypeQ
    toResultTypeQ = \case
      -- special case SchemaObject to make it further inspectable
      SchemaResult (SchemaObject pairs) -> [t|Object $(schemaVToTypeQ (Schema pairs))|]
      SchemaResult schemaType -> [t|SchemaResult $(schemaTypeVToTypeQ schemaType)|]
      SchemaResultList schemaResult -> appT listT (toResultTypeQ schemaResult)
      SchemaResultTuple schemaResults -> foldl appT (tupleT $ length schemaResults) $ map toResultTypeQ schemaResults
      SchemaResultWrapped functorTy schemaResult ->
        let handleFunctor ty =
              case functorHandler of
                ApplyFunctors -> AppT functorTy ty
                StripFunctors -> ty
         in handleFunctor <$> toResultTypeQ schemaResult

    go :: SchemaTypeV -> [GetterOperation] -> Q UnwrapSchemaResult
    go schemaType [] = pure $ SchemaResult schemaType
    go schemaType' (op : ops) = do
      schemaType <- resolveSchemaType schemaType'

      let invalid message = fail $ message ++ ": " ++ showSchemaTypeV schemaType
          wrapMaybe = SchemaResultWrapped (ConT ''Maybe)
          wrapList = SchemaResultWrapped ListT

      case op of
        GetterKey key ->
          case schemaType of
            SchemaObject pairs ->
              case lookup key $ map (first fromSchemaKeyV) pairs of
                Just inner -> go inner ops
                Nothing -> invalid $ "Key '" ++ key ++ "' does not exist in schema"
            _ -> invalid $ "Cannot get key '" ++ key ++ "' in schema"
        GetterBang ->
          case schemaType of
            SchemaMaybe inner -> go inner ops
            SchemaTry inner -> go inner ops
            _ -> invalid "Cannot use `!` operator on schema"
        GetterMapMaybe ->
          case schemaType of
            SchemaMaybe inner -> wrapMaybe <$> go inner ops
            SchemaTry inner -> wrapMaybe <$> go inner ops
            _ -> invalid "Cannot use `?` operator on schema"
        GetterMapList ->
          case schemaType of
            SchemaList inner -> wrapList <$> go inner ops
            _ -> invalid "Cannot use `[]` operator on schema"
        GetterBranch branch ->
          case schemaType of
            SchemaUnion schemas ->
              if branch < length schemas
                then go (schemas !! branch) ops
                else invalid "Branch out of bounds for schema"
            _ -> invalid "Cannot use `@` operator on schema"
        -- suffixes; ops should be empty

        GetterList elemOps ->
          case schemaType of
            SchemaObject _ -> do
              elemSchemas <- traverse (go schemaType . NonEmpty.toList) elemOps
              let elemSchema = NonEmpty.head elemSchemas
              if all (== elemSchema) elemSchemas
                then pure $ SchemaResultList elemSchema
                else invalid "List contains different types in schema"
            _ -> invalid "Cannot get keys in schema"
        GetterTuple elemOps ->
          case schemaType of
            SchemaObject _ -> SchemaResultTuple <$> mapM (go schemaType . NonEmpty.toList) (NonEmpty.toList elemOps)
            _ -> invalid "Cannot get keys in schema"

data UnwrapSchemaResult
  = SchemaResult SchemaTypeV
  | SchemaResultList UnwrapSchemaResult
  | SchemaResultTuple [UnwrapSchemaResult]
  | -- | Type should be of kind `* -> *`
    SchemaResultWrapped Type UnwrapSchemaResult
  deriving (Eq)

-- | A data type that indicates how to handle functors when unwrapping a schema.
data FunctorHandler
  = -- | handleFunctor Maybe Int ==> Maybe Int
    ApplyFunctors
  | -- | handleFunctor Maybe Int ==> Int
    StripFunctors