ocaml-export-0.1.0.0: src/OCaml/BuckleScript/Internal/Spec.hs
{-|
Module : OCaml.BuckleScript.Internal.Spec
Description : Build OCaml Modules from Haskell Types
Copyright : Plow Technologies, 2017
License : BSD3
Maintainer : mchaver@gmail.com
Stability : experimental
-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module OCaml.BuckleScript.Internal.Spec
(
mkOCamlSpecServer
, MkOCamlSpecAPI
, mkGoldenFiles
-- utility functions
, OCamlSpecAPI
, OCamlPackageTypeCount (..)
, OCamlModuleTypeCount (..)
) where
-- base
import Data.Proxy
import Data.Semigroup (Semigroup (..))
import GHC.Generics
import GHC.TypeLits
-- aeson
import Data.Aeson (ToJSON)
-- hspec-aeson-golden
import Test.Aeson.Internal.ADT.GoldenSpecs (mkGoldenFileForType)
-- ocaml-export
import OCaml.Internal.Common hiding ((</>))
import OCaml.BuckleScript.Internal.Module
import OCaml.BuckleScript.Internal.Package
-- quickcheck-arbitrary-adt
import Test.QuickCheck.Arbitrary.ADT
-- servant
import Servant.API
-- template-haskell
import Language.Haskell.TH
-- text
import Data.Text (Text)
mkOCamlSpecServer :: forall ocamlPackage. (OCamlPackageTypeCount ocamlPackage) => String -> Proxy ocamlPackage -> Q [Dec]
mkOCamlSpecServer typeName Proxy = do
let sizes = ocamlPackageTypeCount (Proxy :: Proxy ocamlPackage)
if (length sizes) < 1 || (not . and $ (> 0) <$> sizes)
then fail $ "sizes must have at least one element and each element must be greater than zero: " <> show sizes
else do
let argss = (\size -> foldr (\l r -> ParensE $ UInfixE l (ConE $ mkName ":<|>") r) (VarE $ mkName "pure") (replicate (size-1) (VarE $ mkName "pure"))) <$> sizes
let args = foldl (\l r -> UInfixE l (ConE $ mkName ":<|>") r) (head argss) (tail argss)
return $
[ SigD serverName (AppT (ConT $ mkName "Server") $ AppT (ConT $ mkName "MkOCamlSpecAPI") (ConT $ apiName))
, FunD serverName [Clause [] (NormalB args ) [] ]
, SigD apiProxy (AppT (ConT $ mkName "Proxy") $ AppT (ConT $ mkName "MkOCamlSpecAPI") (ConT $ apiName))
, FunD apiProxy [Clause [] (NormalB $ ConE $ mkName "Proxy") []]
, SigD appName (ConT $ mkName "Application")
, FunD appName [Clause [] (NormalB $ AppE (AppE (VarE $ mkName "serve") (VarE apiProxy)) (VarE serverName)) []]
]
where
serverName = mkName $ lowercaseFirst typeName ++ "Server"
apiName = mkName $ uppercaseFirst typeName
apiProxy = mkName $ lowercaseFirst typeName ++ "API"
appName = mkName $ lowercaseFirst typeName ++ "App"
-- | Make hspec-aeson-golden golden files for each type in an OCamlPackage
class HasMkGoldenFiles a where
mkGoldenFiles :: Proxy a -> Int -> FilePath -> IO ()
instance (HasMkGoldenFilesFlag a ~ flag, HasMkGoldenFiles' flag (a :: *)) => HasMkGoldenFiles a where
mkGoldenFiles = mkGoldenFiles' (Proxy :: Proxy flag)
type family (HasMkGoldenFilesFlag a) :: Nat where
HasMkGoldenFilesFlag (OCamlPackage a b :> c) = 7
HasMkGoldenFilesFlag ((OCamlModule a :> b) :<|> c) = 6
HasMkGoldenFilesFlag (OCamlModule a :> b) = 5
HasMkGoldenFilesFlag (OCamlSubModule a :> b) = 4
HasMkGoldenFilesFlag (OCamlTypeInFile a b) = 3
HasMkGoldenFilesFlag (a :> b) = 2
HasMkGoldenFilesFlag a = 1
class HasMkGoldenFiles' (flag :: Nat) a where
mkGoldenFiles' :: Proxy flag -> Proxy a -> Int -> FilePath -> IO ()
instance (HasMkGoldenFiles a) => HasMkGoldenFiles' 7 (OCamlPackage packageName deps :> a) where
mkGoldenFiles' _ Proxy size fp = mkGoldenFiles (Proxy :: Proxy a) size fp
instance (HasMkGoldenFiles a, HasMkGoldenFiles b) => HasMkGoldenFiles' 6 ((OCamlModule modules :> a) :<|> b) where
mkGoldenFiles' _ Proxy size fp = do
mkGoldenFiles (Proxy :: Proxy a) size fp
mkGoldenFiles (Proxy :: Proxy b) size fp
instance (HasMkGoldenFiles a) => HasMkGoldenFiles' 5 (OCamlModule modules :> a) where
mkGoldenFiles' _ Proxy size fp = mkGoldenFiles (Proxy :: Proxy a) size fp
instance (HasMkGoldenFiles a) => HasMkGoldenFiles' 4 (OCamlSubModule subModule :> a) where
mkGoldenFiles' _ Proxy size fp = mkGoldenFiles (Proxy :: Proxy a) size fp
instance (ToADTArbitrary a, ToJSON a) => HasMkGoldenFiles' 3 (OCamlTypeInFile a b) where
mkGoldenFiles' _ Proxy size fp = mkGoldenFileForType size (Proxy :: Proxy a) fp
instance (HasMkGoldenFiles a, HasMkGoldenFiles b) => HasMkGoldenFiles' 2 (a :> b) where
mkGoldenFiles' _ Proxy size fp = do
mkGoldenFiles (Proxy :: Proxy a) size fp
mkGoldenFiles (Proxy :: Proxy b) size fp
instance (ToADTArbitrary a, ToJSON a) => HasMkGoldenFiles' 1 a where
mkGoldenFiles' _ Proxy size fp = mkGoldenFileForType size (Proxy :: Proxy a) fp
-- | Convert an OCamlPackage into a servant API.
type family MkOCamlSpecAPI a :: * where
MkOCamlSpecAPI (OCamlPackage a deps :> rest) = MkOCamlSpecAPI rest
MkOCamlSpecAPI ((OCamlModule modules :> api) :<|> rest) = MkOCamlSpecAPI' modules '[] api :<|> MkOCamlSpecAPI rest
MkOCamlSpecAPI (OCamlModule modules :> api) = MkOCamlSpecAPI' modules '[] api
-- | Utility type level function.
type family MkOCamlSpecAPI' modules subModules api :: * where
MkOCamlSpecAPI' modules subModules ((OCamlSubModule restSubModules) :> a) = MkOCamlSpecAPI' modules (Append subModules '[restSubModules]) a
MkOCamlSpecAPI' modules subModules (a :> b) = MkOCamlSpecAPI' modules subModules a :<|> MkOCamlSpecAPI' modules subModules b
MkOCamlSpecAPI' modules subModules (OCamlTypeInFile api _typeFilePath) = OCamlSpecAPI modules subModules api
MkOCamlSpecAPI' modules subModules api = OCamlSpecAPI modules subModules api
-- | A servant route for a testing an OCaml type's encoder and decoder
type OCamlSpecAPI (modules :: [Symbol]) (subModules :: [Symbol]) typ =
ConcatSymbols (Insert (TypeName typ) (Append modules subModules)) (ReqBody '[JSON] [typ] :> Post '[JSON] [typ])
class OCamlModuleTypeCount api where
ocamlModuleTypeCount :: Proxy api -> Int
instance (OCamlModuleTypeCountFlag a ~ flag, OCamlModuleTypeCount' flag (a :: *)) => OCamlModuleTypeCount a where
ocamlModuleTypeCount = ocamlModuleTypeCount' (Proxy :: Proxy flag)
type family (OCamlModuleTypeCountFlag a) :: Bool where
OCamlModuleTypeCountFlag (a :> b) = 'True
OCamlModuleTypeCountFlag (OCamlModule a) = 'True
OCamlModuleTypeCountFlag (OCamlSubModule a) = 'True
OCamlModuleTypeCountFlag a = 'False
class OCamlModuleTypeCount' (flag :: Bool) a where
ocamlModuleTypeCount' :: Proxy flag -> Proxy a -> Int
instance (OCamlModuleTypeCount a, OCamlModuleTypeCount b) => OCamlModuleTypeCount' 'True (a :> b) where
ocamlModuleTypeCount' _ Proxy = (ocamlModuleTypeCount (Proxy :: Proxy a)) + (ocamlModuleTypeCount (Proxy :: Proxy b))
instance OCamlModuleTypeCount' 'True (OCamlModule modules) where
ocamlModuleTypeCount' _ Proxy = 0
instance OCamlModuleTypeCount' 'True (OCamlSubModule subModules) where
ocamlModuleTypeCount' _ Proxy = 0
instance OCamlModuleTypeCount' 'False a where
ocamlModuleTypeCount' _ Proxy = 1
class OCamlPackageTypeCount modules where
ocamlPackageTypeCount :: Proxy modules -> [Int]
instance (OCamlPackageTypeCountFlag a ~ flag, OCamlPackageTypeCount' flag (a :: *)) => OCamlPackageTypeCount a where
ocamlPackageTypeCount = ocamlPackageTypeCount' (Proxy :: Proxy flag)
type family (OCamlPackageTypeCountFlag a) :: Bool where
OCamlPackageTypeCountFlag (OCamlPackage a b :> c) = 'True
OCamlPackageTypeCountFlag (a :<|> b) = 'True
OCamlPackageTypeCountFlag a = 'False
class OCamlPackageTypeCount' (flag :: Bool) a where
ocamlPackageTypeCount' :: Proxy flag -> Proxy a -> [Int]
-- OCamlPackage does not increment
instance (OCamlPackageTypeCount b) => OCamlPackageTypeCount' 'True (OCamlPackage a deps :> b) where
ocamlPackageTypeCount' _ Proxy = ocamlPackageTypeCount (Proxy :: Proxy b)
-- Choice operator does not increment
instance (OCamlModuleTypeCount a, OCamlPackageTypeCount b) => OCamlPackageTypeCount' 'True (a :<|> b) where
ocamlPackageTypeCount' _ Proxy = (ocamlModuleTypeCount (Proxy :: Proxy a)) : (ocamlPackageTypeCount (Proxy :: Proxy b))
-- everything else should count as one
instance (OCamlModuleTypeCount a) => OCamlPackageTypeCount' 'False a where
ocamlPackageTypeCount' _ Proxy = [ocamlModuleTypeCount (Proxy :: Proxy a)]
-- | Convert a type into a Symbol at the type level.
type family TypeName a :: Symbol where
-- Types which don't have a Generic instance
TypeName Double = "Double"
TypeName Int = "Int"
TypeName String = "String"
TypeName Text = "Text"
-- Generic instances
TypeName (M1 D ('MetaData name _ _ _) f ()) = name
TypeName a = TypeName (Rep a ())
-- | Append two type level lists.
type family Append xy ys where
Append '[] ys = ys
Append (x ': xs) ys = x ': (Append xs ys)
-- | Get the length of a type level list.
type family Length xs :: Nat where
Length '[] = 0
Length (x ': xs) = 1 + Length xs
-- | Insert type into type level list
type family Insert a xs where
Insert a '[] = a ': '[]
Insert a (x ': xs) = x ': (Insert a xs)
-- | Concat a Symbol the end of a list of Symbols.
type family ConcatSymbols xs rhs :: * where
ConcatSymbols '[] rhs = rhs
ConcatSymbols (x ': xs) rhs = x :> ConcatSymbols xs rhs