packages feed

keid-core-0.1.11.1: src/Resource/Static.hs

module Resource.Static where

import RIO

import Data.Char (isDigit, isLower, isUpper, toUpper)
import Language.Haskell.TH (Q, Dec)
import Language.Haskell.TH.Lib
import Language.Haskell.TH.Syntax (qRunIO)
import Language.Haskell.TH.Syntax qualified as TH
import RIO.Directory (doesDirectoryExist, doesFileExist, getDirectoryContents)
import RIO.FilePath (combine, joinPath)
import RIO.List qualified as List
import RIO.Map qualified as Map
import RIO.State (StateT, evalStateT, get, put)

data Scope
  = Files
  | Dirs
  deriving (Eq, Ord, Show, Enum, Bounded, Generic)

filePaths :: Scope -> FilePath -> Q [Dec]
filePaths = mkDeclsWith mkPattern
  where
    mkPattern fp fs = do
      let name = TH.mkName "paths"

      sigType <- [t| [FilePath] |]

      let
        body = TH.ListE do
          segments <- List.sort fs
          pure . TH.LitE . TH.StringL $
            joinPath (fp : segments)

      pure
        [ TH.SigD name sigType
        , TH.FunD name [TH.Clause [] (TH.NormalB body) []]
        ]

filePatterns :: Scope -> FilePath -> Q [Dec]
filePatterns = mkDeclsWith mkPattern
  where
    mkPattern fp fs =
      fmap concat $ for fs \segments -> do
        let name = TH.mkName $ patternName segments
        patType <- [t| FilePath |]
        let pat = TH.LitP . TH.StringL $ joinPath (fp : segments)

        pure
          [ TH.PatSynSigD name patType
          , TH.PatSynD name (TH.PrefixPatSyn []) TH.ImplBidir pat
          ]

replace :: Char -> Char
replace c =
  if isLower c || isUpper c || isDigit c then
    c
  else
    '_'

fieldName :: [[Char]] -> String
fieldName =
  map replace . List.takeWhile (/= '.') . List.intercalate "_"

patternName :: [[Char]] -> String
patternName =
  map (replace . toUpper) . List.intercalate "_"

collection :: [(String, TH.Name)] -> Scope -> FilePath -> Q [Dec]
collection prologue scope fp = do
  pattDecs <- filePatterns scope fp
  recDecs <- collectionRec prologue scope fp
  sourcesDecs <- sourcesVal prologue scope fp
  pure $ mconcat [recDecs, pattDecs, sourcesDecs]

collectionRec :: [(String, TH.Name)] -> Scope -> FilePath -> Q [Dec]
collectionRec prologue = mkDeclsWith mkRecord
  where
    mkRecord _fp fs = do
      let
        mkConstr = recC collName $
          map mkInclude prologue ++ map mkField (List.sort fs)

      appViaGen1 <- viaStrategy (conT (TH.mkName "Generically1") `appT` conT collName)
      let
        derivs =
          [ derivClause Nothing $ map (conT . TH.mkName) ["Show", "Functor", "Foldable", "Traversable", "Generic1"]
          , derivClause (Just appViaGen1) [conT $ TH.mkName "Applicative"]
          ]
      collectionData <- dataD mempty collName [plainTV a] Nothing [mkConstr] derivs
      pure [collectionData]

    mkField segments =
      varBangType (TH.mkName $ fieldName segments) (a')

    mkInclude (f, t) =
      varBangType (TH.mkName f) . bangType_ $
        conT t `appT` varT a

    a = TH.mkName "a"
    a' = bangType_ $ varT a

    bangType_ = bangType (bang noSourceUnpackedness noSourceStrictness)

collName :: TH.Name
collName = TH.mkName "Collection"

sourcesVal :: [(String, TH.Name)] -> Scope -> FilePath -> Q [Dec]
sourcesVal prologue = mkDeclsWith mkVal
  where
    sources = TH.mkName "sources"
    mkVal _fp fs = do
      sig <- sigD sources $ conT collName `appT`conT (TH.mkName "Source")
      let body = recConE collName $ map prologueSource prologue <> map sourceVal fs
      fun <- funD sources [clause [] (normalB body) []]
      pure [sig, fun]
    prologueSource (fn, cn) =
      pure (TH.mkName fn, TH.VarE $ samePkg cn "sources")

    sourceVal segments = (TH.mkName $ fieldName segments,) <$> mkSrc
      where
        mkSrc = [| File Nothing $val |]
        val = conE . TH.mkName $ patternName segments

samePkg :: TH.Name -> String -> TH.Name
samePkg (TH.Name _occ nf) identifier =
  TH.Name (TH.mkOccName identifier) $
    case nf of
      TH.NameG _ns _pkg mn -> TH.NameQ mn
      _ -> nf

mkDeclsWith
  :: (FilePath -> [[String]] -> Q [Dec])
  -> Scope
  -> FilePath
  -> Q [Dec]
mkDeclsWith mkDecl scope fp =
  qRunIO (getFileListPieces scope fp) >>= mkDecl fp

-- XXX: Initially sourced from yesod-static
getFileListPieces :: Scope -> FilePath -> IO [[String]]
getFileListPieces scope rootPath = evalStateT (go id rootPath) mempty
  where
    go
      :: ([String] -> [String])
      -> String
      -> StateT (Map.Map String String) IO [[String]]
    go prefixF parentPath = do
      let expandPath = combine parentPath
      rawContents <- liftIO $ getDirectoryContents parentPath
      (dirs, files) <- foldM (partitionContents expandPath) (mempty, mempty) $
        filter notHidden rawContents

      inner <- for dirs \(path, fullPath) ->
        go (prefixF . (:) path) fullPath

      let collect = traverse $ traverse dedupe . prefixF . pure
      current <- case scope of
        Dirs ->
          collect $ map snd dirs
        Files -> do
          collect files

      pure $ concat (current : inner)

    partitionContents expandPath acc@(accDirs, accFiles) path = do
      let fullPath = expandPath path
      isDir <- doesDirectoryExist fullPath
      if isDir then
        pure
          ( (path, fullPath) : accDirs
          , accFiles
          )
      else do
        isFile <- doesFileExist fullPath
        if isFile then
          pure
            ( accDirs
            , path : accFiles
            )
        else
          -- XXX: skip weird stuff
          pure acc

    -- | Reuse data buffers for identical strings
    dedupe :: String -> StateT (Map String String) IO String
    dedupe s = do
      m <- get
      case Map.lookup s m of
        Just seen ->
          pure seen
        Nothing -> do
          put $ Map.insert s s m
          pure s

    notHidden :: FilePath -> Bool
    notHidden = \case
      "tmp"   -> False
      '.' : _ -> False
      _       -> True