{-# LANGUAGE DataKinds #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -Wall -Werror #-} module Trasa.TH ( Name , CaptureRep(..) , ParamRep(..) , QueryRep(..) , RouteRep(..) , RoutesRep(..) , routeDataType , enumRoutesInstance , metaInstance , trasa , parseTrasa )where import Data.Kind (Type) import qualified Data.List.NonEmpty as NE import Data.Maybe (listToMaybe, mapMaybe) import Language.Haskell.TH hiding (Type, match) import qualified Language.Haskell.TH as TH import Language.Haskell.TH.Quote import Trasa.Core import Trasa.Core.Implicit import Trasa.TH.Parse (parseRoutesRep) import Trasa.TH.Types genCodec :: Name -> Q CodecRep genCodec name = reify name >>= \case VarI fullName fullType _ -> case fullType of AppT codec typ -> return (CodecRep fullName codec typ) _ -> fail ("Codec: " ++ show name ++ " does not have a type like (Codec Type) but has the type: " ++ show fullType) _ -> fail ("Codec: " ++ show name ++ " is not a haskell value") genCodecs :: (RoutesRep CodecRep -> Q b) -> RoutesRep Name -> Q b genCodecs f routeRepNames = do routeReps <- traverse genCodec routeRepNames f routeReps typeList :: [TH.Type] -> TH.Type typeList = foldr (\typ rest -> PromotedConsT `AppT` typ `AppT` rest) PromotedNilT routeDataTypeCodec :: RoutesRep CodecRep -> Q Dec routeDataTypeCodec (RoutesRep routeStr routeReps) = do kind <- [t| [Type] -> [Param] -> Bodiedness -> Type -> Type |] return (DataD [] route [] (Just kind) (fmap (buildCons route) routeReps) []) where route = mkName routeStr buildCons :: Name -> RouteRep CodecRep -> Con buildCons rt (RouteRep name _ captures queries request response) = GadtC [mkName name] [] typ where typ = ConT rt `AppT` typeList (mapMaybe captureType captures) `AppT` typeList (fmap (paramType . queryRepParam) queries) `AppT` bodiednessType request `AppT` responseType response captureType :: CaptureRep CodecRep -> Maybe TH.Type captureType = \case MatchRep _ -> Nothing CaptureRep (CodecRep _ _ typ) -> Just typ paramType :: ParamRep CodecRep -> TH.Type paramType = \case FlagRep -> PromotedT 'Flag OptionalRep (CodecRep _ _ typ) -> PromotedT 'Optional `AppT` typ ListRep (CodecRep _ _ typ) -> PromotedT 'List `AppT` typ bodiednessType :: [CodecRep] -> TH.Type bodiednessType = \case [] -> PromotedT 'Bodyless (CodecRep _ _ typ:_) -> PromotedT 'Body `AppT` typ responseType = \case (CodecRep _ _ typ NE.:| _) -> typ routeDataType :: RoutesRep Name -> Q Dec routeDataType = genCodecs routeDataTypeCodec enumRoutesInstanceCodec :: RoutesRep CodecRep -> Dec enumRoutesInstanceCodec (RoutesRep routeStr routeReps) = InstanceD Nothing [] typ [FunD 'enumerateRoutes [Clause [] (NormalB (ListE expr)) []]] where route = mkName routeStr typ = ConT ''EnumerableRoute `AppT` ConT route buildCons name = ConE 'Constructed `AppE` ConE (mkName name) expr = fmap (buildCons . routeRepName) routeReps enumRoutesInstance :: RoutesRep Name -> Q Dec enumRoutesInstance = genCodecs (return . enumRoutesInstanceCodec) metaInstanceCodec :: RoutesRep CodecRep -> Q Dec metaInstanceCodec (RoutesRep routeStr routeReps) = do let route = mkName routeStr typ = ConT ''HasMeta `AppT` ConT route capStrat <- search routeRepCaptures capCodec [t| CaptureCodec |] qryStrat <- search routeRepQueries (paramCodec . queryRepParam) [t| CaptureCodec |] reqBodyStrat <- search routeReqRequest (Just . codecRepCodec) [t| BodyCodec |] respBodyStrat <- search (NE.toList . routeReqResponse) (Just . codecRepCodec) [t| BodyCodec |] many <- [t| Many |] let mkTypeFamily str strat = TySynInstD (mkName str) (TySynEqn [ConT route] strat) typeFamilies = [ mkTypeFamily "CaptureStrategy" capStrat , mkTypeFamily "QueryStrategy" qryStrat , mkTypeFamily "RequestBodyStrategy" (many `AppT` reqBodyStrat) , mkTypeFamily "ResponseBodyStrategy" (many `AppT` respBodyStrat) ] lam <- newName "route" let metaExp = LamE [VarP lam] (CaseE (VarE lam) (fmap routeRepToMetaPattern routeReps)) metaFunction = FunD 'meta [Clause [] (NormalB metaExp) []] return (InstanceD Nothing [] typ (typeFamilies ++ [metaFunction])) where search :: (RouteRep CodecRep -> [b]) -> (b -> Maybe TH.Type) -> Q TH.Type -> Q TH.Type search f g err = case listToMaybe (mapMaybe g (routeReps >>= f)) of Just t -> return t Nothing -> err capCodec :: CaptureRep CodecRep -> Maybe TH.Type capCodec = \case MatchRep _ -> Nothing CaptureRep (CodecRep _ codec _) -> Just codec paramCodec :: ParamRep CodecRep -> Maybe TH.Type paramCodec = \case FlagRep -> Nothing OptionalRep (CodecRep _ codec _) -> Just codec ListRep (CodecRep _ codec _) -> Just codec routeRepToMetaPattern :: RouteRep CodecRep -> Match routeRepToMetaPattern (RouteRep name method caps qrys req res) = Match (ConP (mkName name) []) (NormalB expr) [] where expr = ConE 'Meta `AppE` capsE `AppE` qrysE `AppE` reqE `AppE` respE `AppE` methodE capsE = foldr (\cp -> UInfixE (captureRepToExp cp) (VarE '(./))) (VarE 'end) caps captureRepToExp = \case MatchRep str -> VarE 'match `AppE` LitE (StringL str) CaptureRep (CodecRep n _ _) -> VarE 'capture `AppE` VarE n qrysE = foldr (\qp -> UInfixE (queryRepToExp qp) (VarE '(.&))) (VarE 'qend) qrys queryRepToExp (QueryRep idt param) = case param of FlagRep -> VarE 'flag `AppE` lit OptionalRep (CodecRep n _ _) -> VarE 'optional `AppE` lit `AppE` VarE n ListRep (CodecRep n _ _) -> VarE 'list `AppE` lit `AppE` VarE n where lit = LitE (StringL idt) reqE = case req of [] -> VarE 'bodyless (r : rs) -> VarE 'body `AppE` manyE (r NE.:| rs) respE = VarE 'resp `AppE` manyE res methodE = LitE (StringL method) manyE (CodecRep n _ _ NE.:| xs) = ConE 'Many `AppE` (UInfixE (VarE n) (ConE '(NE.:|)) (ListE (VarE . codecRepName <$> xs))) metaInstance :: RoutesRep Name -> Q Dec metaInstance = genCodecs metaInstanceCodec trasa :: RoutesRep Name -> Q [Dec] trasa routeRepNames = do routeReps <- traverse genCodec routeRepNames rt <- routeDataTypeCodec routeReps let cons = enumRoutesInstanceCodec routeReps m <- metaInstanceCodec routeReps return [rt, cons, m] parseTrasa :: QuasiQuoter parseTrasa = QuasiQuoter err err err quoter where err _ = fail "parseTrasa: This quasi quoter should only be used on the top level" quoter :: String -> Q [Dec] quoter str = case parseRoutesRep str of Left e -> fail e Right routeRepNames -> trasa routeRepNames