173 lines
6.3 KiB
Haskell
173 lines
6.3 KiB
Haskell
module Utils.PathPiece
|
||
( nullaryToPathPiece', nullaryToPathPiece
|
||
, nullaryPathPiece', nullaryPathPiece, finitePathPiece
|
||
, splitCamel
|
||
, camelToPathPiece, camelToPathPiece'
|
||
, nameToPathPiece, nameToPathPiece'
|
||
, tuplePathPiece
|
||
, pathPieceJSON, pathPieceJSONKey
|
||
, pathPieceBinary
|
||
) where
|
||
|
||
import ClassyPrelude.Yesod
|
||
|
||
import Language.Haskell.TH
|
||
import qualified Language.Haskell.TH.Syntax as TH (Lift(..))
|
||
import Data.Universe
|
||
|
||
import qualified Data.Text as Text
|
||
import qualified Data.Char as Char
|
||
|
||
import Data.Map ((!))
|
||
import qualified Data.Map as Map
|
||
|
||
import qualified Data.HashMap.Strict as HashMap
|
||
|
||
import Numeric.Natural
|
||
|
||
import Data.List (foldl)
|
||
|
||
import Data.Aeson.Types
|
||
import qualified Data.Aeson.Types as Aeson
|
||
|
||
import Control.Monad.Fail
|
||
|
||
import Data.Binary (Binary)
|
||
import qualified Data.Binary as Binary
|
||
|
||
|
||
mkFiniteFromPathPiece :: Name -> Q ([Dec], Exp)
|
||
mkFiniteFromPathPiece finiteType = do
|
||
mapName <- newName $ "pathPieceMap" <> nameBase finiteType
|
||
let
|
||
decs = sequence
|
||
[ pragInlD mapName NoInline FunLike AllPhases
|
||
, sigD mapName [t|HashMap Text $(conT finiteType)|]
|
||
, funD mapName
|
||
[ clause [] (normalB [e|HashMap.fromList $ map (toPathPiece &&& id) universeF|]) [] ]
|
||
]
|
||
(,) <$> decs <*> [e|flip HashMap.lookup $(varE mapName)|]
|
||
|
||
nullaryToPathPiece' :: Name -> (Name -> Text) -> ExpQ
|
||
nullaryToPathPiece' nullaryType mangle = do
|
||
TyConI (DataD _ _ _ _ constructors _) <- reify nullaryType
|
||
helperName <- newName "helper"
|
||
let
|
||
toClause (NormalC cName []) = clause [conP cName []] (normalB . TH.lift $ mangle cName) []
|
||
toClause con = fail $ "Unsupported constructor: " ++ show con
|
||
helperDec = funD helperName $ map toClause constructors
|
||
letE [helperDec] $ varE helperName
|
||
|
||
nullaryToPathPiece :: Name -> (Text -> Text) -> ExpQ
|
||
nullaryToPathPiece nullaryType = nullaryToPathPiece' nullaryType . flip (.) (Text.pack . nameBase)
|
||
|
||
nullaryPathPiece' :: Name -> (Name -> Text) -> DecsQ
|
||
nullaryPathPiece' nullaryType mangle = do
|
||
(finDecs, finExp) <- mkFiniteFromPathPiece nullaryType
|
||
sequence . (map return finDecs ++) . pure $
|
||
instanceD (cxt []) [t|PathPiece $(conT nullaryType)|]
|
||
[ funD 'toPathPiece
|
||
[ clause [] (normalB $ nullaryToPathPiece' nullaryType mangle) [] ]
|
||
, funD 'fromPathPiece
|
||
[ clause [] (normalB $ return finExp) [] ]
|
||
]
|
||
|
||
nullaryPathPiece :: Name -> (Text -> Text) -> DecsQ
|
||
nullaryPathPiece nullaryType = nullaryPathPiece' nullaryType . flip (.) (Text.pack . nameBase)
|
||
|
||
finitePathPiece :: Name -> [Text] -> DecsQ
|
||
finitePathPiece finiteType verbs = do
|
||
(finDecs, finExp) <- mkFiniteFromPathPiece finiteType
|
||
sequence . (map return finDecs ++) . pure $
|
||
instanceD (cxt []) [t|PathPiece $(conT finiteType)|]
|
||
[ funD 'toPathPiece
|
||
[ clause [] (normalB [|(Map.fromList (zip universeF verbs) !)|]) [] ]
|
||
, funD 'fromPathPiece
|
||
[ clause [] (normalB $ return finExp) [] ]
|
||
]
|
||
|
||
|
||
splitCamel :: Textual t => t -> [t]
|
||
splitCamel = map fromList . reverse . helper (error "hasChange undefined at start of string") [] "" . otoList
|
||
where
|
||
helper _hadChange items thisWord [] = reverse thisWord : items
|
||
helper _hadChange items [] (c:cs) = helper True items [c] cs
|
||
helper hadChange items ws@(w:ws') (c:cs)
|
||
| sameCategory w c
|
||
, null ws' = helper (Char.isLower w) items (c:ws) cs
|
||
| sameCategory w c = helper hadChange items (c:ws) cs
|
||
| Char.isLower w
|
||
, Char.isUpper c = helper True (reverse ws :items) [c] cs
|
||
| null ws' = helper True items (c:ws) cs
|
||
| not hadChange = helper True (reverse ws':items) [c,w] cs
|
||
| otherwise = helper True (reverse ws :items) [c] cs
|
||
|
||
sameCategory = (==) `on` Char.generalCategory
|
||
|
||
camelToPathPiece' :: Textual t => Natural -> t -> t
|
||
camelToPathPiece' dropN = intercalate "-" . map toLower . drop (fromIntegral dropN) . splitCamel
|
||
|
||
camelToPathPiece :: Textual t => t -> t
|
||
camelToPathPiece = camelToPathPiece' 0
|
||
|
||
nameToPathPiece' :: Textual t => Natural -> Name -> t
|
||
nameToPathPiece' dropN = camelToPathPiece' dropN . repack . nameBase
|
||
|
||
nameToPathPiece :: Textual t => Name -> t
|
||
nameToPathPiece = nameToPathPiece' 0
|
||
|
||
|
||
tuplePathPiece :: Int -> DecQ
|
||
tuplePathPiece tupleDim = do
|
||
let
|
||
tupleSeparator :: Text
|
||
tupleSeparator = ","
|
||
|
||
xs <- replicateM tupleDim $ newName "x" :: Q [Name]
|
||
xs' <- replicateM tupleDim $ newName "x'" :: Q [Name]
|
||
|
||
let tupleType = foldl appT (tupleT tupleDim) $ map varT xs
|
||
tCxt = cxt
|
||
[ [t|PathPiece $(varT x)|] | x <- xs ]
|
||
|
||
t <- newName "t"
|
||
|
||
instanceD tCxt [t|PathPiece $(tupleType)|]
|
||
[ funD 'toPathPiece
|
||
[ clause [tupP $ map varP xs] (normalB [e|Text.intercalate tupleSeparator $(listE $ map (appE [e|toPathPiece|] . varE) xs)|]) []
|
||
]
|
||
, funD 'fromPathPiece
|
||
[ clause [varP t] (normalB . doE $ concat
|
||
[ pure $ bindS (listP $ map varP xs) [e|return $ Text.splitOn tupleSeparator $(varE t)|]
|
||
, [ bindS (varP x') [e|fromPathPiece $(varE x)|] | (x, x') <- zip xs xs' ]
|
||
, pure $ noBindS [e|return $(tupE $ map varE xs')|]
|
||
]) []
|
||
]
|
||
]
|
||
|
||
|
||
pathPieceJSONKey :: Name -> DecsQ
|
||
-- ^ Derive `ToJSONKey`- and `FromJSONKey`-Instances from a `PathPiece`-Instance
|
||
pathPieceJSONKey tName
|
||
= [d| instance ToJSONKey $(conT tName) where
|
||
toJSONKey = toJSONKeyText toPathPiece
|
||
instance FromJSONKey $(conT tName) where
|
||
fromJSONKey = FromJSONKeyTextParser $ \t -> maybe (fail $ "Could not parse ‘" <> unpack t <> "’ as value for " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return $ fromPathPiece t
|
||
|]
|
||
|
||
pathPieceJSON :: Name -> DecsQ
|
||
-- ^ Derive `ToJSON`- and `FromJSON`-Instances from a `PathPiece`-Instance
|
||
pathPieceJSON tName
|
||
= [d| instance ToJSON $(conT tName) where
|
||
toJSON = Aeson.String . toPathPiece
|
||
instance FromJSON $(conT tName) where
|
||
parseJSON = Aeson.withText $(TH.lift $ nameBase tName) $ \t -> maybe (fail $ "Could not parse ‘" <> unpack t <> "’ as value for " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return $ fromPathPiece t
|
||
|]
|
||
|
||
pathPieceBinary :: Name -> DecsQ
|
||
pathPieceBinary tName
|
||
= [d| instance Binary $(conT tName) where
|
||
get = Binary.get >>= maybe (fail $ "Could not parse value of " <> $(TH.lift $ nameBase tName) <> " via PathPiece") return . fromPathPiece
|
||
put = Binary.put . toPathPiece
|
||
|]
|