2019-01-23 23:55:26 +03:00
|
|
|
{-# LANGUAGE DerivingStrategies #-}
|
|
|
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2019-01-24 23:58:22 +03:00
|
|
|
{-# LANGUAGE TupleSections #-}
|
|
|
|
{-# LANGUAGE ViewPatterns #-}
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-18 01:00:48 +03:00
|
|
|
-- TODO: qualified imports
|
|
|
|
-- TODO: format code
|
|
|
|
|
|
|
|
import Control.Monad
|
2019-01-27 01:39:38 +03:00
|
|
|
import Control.Monad.State
|
2019-01-23 23:55:26 +03:00
|
|
|
import Data.Aeson
|
|
|
|
import Data.Bifunctor
|
2019-01-24 23:58:22 +03:00
|
|
|
import Data.Char (toUpper)
|
2019-01-23 23:55:26 +03:00
|
|
|
import Data.Hashable (Hashable)
|
2019-01-27 01:39:38 +03:00
|
|
|
import Data.Maybe (mapMaybe)
|
2019-01-18 01:00:48 +03:00
|
|
|
import Data.Semigroup ((<>))
|
2019-01-27 01:39:38 +03:00
|
|
|
import Data.String
|
|
|
|
import GHC.Exts (toList)
|
2019-01-23 23:55:26 +03:00
|
|
|
import Options.Applicative
|
2019-01-18 01:00:48 +03:00
|
|
|
import System.Directory
|
|
|
|
import System.FilePath
|
2019-01-23 23:55:26 +03:00
|
|
|
import qualified Data.ByteString as B
|
|
|
|
import qualified Data.ByteString.Lazy as L
|
2019-01-24 23:58:22 +03:00
|
|
|
import qualified Data.HashMap.Strict as HMap
|
|
|
|
import qualified Data.List.NonEmpty as NE
|
2019-01-23 23:55:26 +03:00
|
|
|
import qualified Data.Text as T
|
2019-01-27 01:39:38 +03:00
|
|
|
import qualified GitHub as GH
|
|
|
|
import qualified GitHub.Data.Name as GH
|
2019-01-18 01:00:48 +03:00
|
|
|
|
|
|
|
fileFetchNix :: FilePath
|
|
|
|
fileFetchNix = "nix" </> "fetch.nix"
|
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
-- TODO: file "nix/default.nix"
|
|
|
|
|
2019-01-18 01:00:48 +03:00
|
|
|
fileFetchNixContent :: String
|
|
|
|
fileFetchNixContent = unlines
|
|
|
|
[
|
|
|
|
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
fileVersionsJson :: FilePath
|
|
|
|
fileVersionsJson = "nix" </> "versions.json"
|
|
|
|
|
|
|
|
fileVersionsJsonContent :: String
|
|
|
|
fileVersionsJsonContent = unlines
|
|
|
|
[
|
|
|
|
|
|
|
|
]
|
|
|
|
|
2019-01-24 23:58:22 +03:00
|
|
|
newtype VersionsSpec = VersionsSpec
|
|
|
|
{ unVersionsSpec :: HMap.HashMap PackageName PackageSpec }
|
|
|
|
deriving newtype (FromJSON, ToJSON)
|
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
getVersionsSpec :: IO VersionsSpec
|
|
|
|
getVersionsSpec = do
|
|
|
|
putStrLn $ "Reading versions file"
|
|
|
|
decodeFileStrict fileVersionsJson >>= \case
|
|
|
|
Just (Object v) ->
|
|
|
|
fmap (VersionsSpec . mconcat) $
|
|
|
|
forM (HMap.toList v) $ \(k, v) ->
|
|
|
|
case v of
|
|
|
|
Object v' ->
|
|
|
|
pure $ HMap.singleton (PackageName (T.unpack k)) (PackageSpec v')
|
|
|
|
_ -> error "baaaaz"
|
|
|
|
Just _ -> error "foo"
|
|
|
|
Nothing -> error "Cannot decode versions"
|
|
|
|
|
|
|
|
newtype PackageName = PackageName { unPackageName :: String }
|
2019-01-24 23:58:22 +03:00
|
|
|
deriving newtype (Eq, Hashable, FromJSONKey, ToJSONKey, Show)
|
|
|
|
|
|
|
|
parsePackageName :: Parser PackageName
|
|
|
|
parsePackageName = PackageName <$> argument str (metavar "PACKAGE")
|
2019-01-23 23:55:26 +03:00
|
|
|
|
|
|
|
newtype PackageSpec = PackageSpec { unPackageSpec :: Object }
|
2019-01-24 23:58:22 +03:00
|
|
|
deriving newtype (FromJSON, ToJSON, Show)
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-24 23:58:22 +03:00
|
|
|
parsePackageSpec :: Parser PackageSpec
|
|
|
|
parsePackageSpec =
|
|
|
|
(PackageSpec . HMap.fromList . fmap fixupAttributes) <$>
|
|
|
|
many parseAttribute
|
|
|
|
where
|
|
|
|
parseAttribute :: Parser (String, String)
|
|
|
|
parseAttribute = shortcutAttributes <|>
|
|
|
|
option (maybeReader parseKeyVal)
|
|
|
|
( long "attribute" <>
|
|
|
|
short 'a' <>
|
|
|
|
metavar "KEY=VAL"
|
|
|
|
)
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-24 23:58:22 +03:00
|
|
|
-- Parse "key=val" into ("key", "val")
|
|
|
|
parseKeyVal :: String -> Maybe (String, String)
|
|
|
|
parseKeyVal str = case span (/= '=') str of
|
|
|
|
(key, '=':val) -> Just (key, val)
|
|
|
|
_ -> Nothing
|
|
|
|
|
|
|
|
-- Shortcuts for common attributes
|
|
|
|
shortcutAttributes :: Parser (String, String)
|
|
|
|
shortcutAttributes = foldr (<|>) empty $ mkShortcutAttribute <$>
|
|
|
|
[ "branch", "name", "owner", "repo" ]
|
|
|
|
|
|
|
|
mkShortcutAttribute :: String -> Parser (String, String)
|
|
|
|
mkShortcutAttribute attr@(c:_) = (attr,) <$> strOption
|
|
|
|
( long attr <> short c <> metavar (toUpper <$> attr) )
|
|
|
|
|
|
|
|
fixupAttributes :: (String, String) -> (T.Text, Value)
|
|
|
|
fixupAttributes (k, v) = (T.pack k, String (T.pack v))
|
|
|
|
|
|
|
|
parsePackage :: Parser (PackageName, PackageSpec)
|
|
|
|
parsePackage = (,) <$> parsePackageName <*> parsePackageSpec
|
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- PackageSpec State helpers
|
|
|
|
-------------------------------------------------------------------------------
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
whenNotSet
|
|
|
|
:: T.Text
|
|
|
|
-> StateT (PackageName, PackageSpec) IO ()
|
|
|
|
-> StateT (PackageName, PackageSpec) IO ()
|
|
|
|
whenNotSet attrName act = getPackageSpecAttr attrName >>= \case
|
|
|
|
Just _ -> pure ()
|
|
|
|
Nothing -> act
|
|
|
|
|
|
|
|
withPackageSpecAttr
|
|
|
|
:: T.Text
|
|
|
|
-> (Value -> StateT (PackageName, PackageSpec) IO ())
|
|
|
|
-> StateT (PackageName, PackageSpec) IO ()
|
|
|
|
withPackageSpecAttr attrName act = getPackageSpecAttr attrName >>= \case
|
|
|
|
Just v -> act v
|
|
|
|
Nothing -> pure ()
|
|
|
|
|
|
|
|
getPackageSpecAttr
|
|
|
|
:: T.Text
|
|
|
|
-> StateT (PackageName, PackageSpec) IO (Maybe Value)
|
|
|
|
getPackageSpecAttr attrName = do
|
|
|
|
(_, PackageSpec obj) <- get
|
|
|
|
pure $ HMap.lookup attrName obj
|
|
|
|
|
|
|
|
setPackageSpecAttr
|
|
|
|
:: T.Text -> Value
|
|
|
|
-> StateT (PackageName, PackageSpec) IO ()
|
|
|
|
setPackageSpecAttr attrName attrValue = do
|
|
|
|
(packageName, PackageSpec obj) <- get
|
|
|
|
let obj' = HMap.insert attrName attrValue obj
|
|
|
|
put (packageName, PackageSpec obj')
|
|
|
|
|
|
|
|
setPackageName
|
|
|
|
:: String -> StateT (PackageName, PackageSpec) IO ()
|
|
|
|
setPackageName packageName = do
|
|
|
|
(_, spec) <- get
|
|
|
|
put (PackageName packageName, spec)
|
|
|
|
|
|
|
|
hasPackageSpecAttrs
|
|
|
|
:: [String]
|
|
|
|
-> StateT (PackageName, PackageSpec) IO Bool
|
|
|
|
hasPackageSpecAttrs attrNames = do
|
|
|
|
(_, PackageSpec obj) <- get
|
|
|
|
pure $ all (\k -> HMap.member (T.pack k) obj) attrNames
|
|
|
|
|
|
|
|
|
|
|
|
packageSpecStringValues :: PackageSpec -> [(String, String)]
|
|
|
|
packageSpecStringValues (PackageSpec m) = mapMaybe toVal (HMap.toList m)
|
|
|
|
where
|
|
|
|
toVal :: (T.Text, Value) -> Maybe (String, String)
|
|
|
|
toVal = \case
|
|
|
|
(key, String val) -> Just (T.unpack key, T.unpack val)
|
|
|
|
_ -> Nothing
|
2019-01-23 23:55:26 +03:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- INIT
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
parseCmdInit :: ParserInfo (IO ())
|
|
|
|
parseCmdInit = (info (pure cmdInit <**> helper)) fullDesc
|
|
|
|
|
2019-01-18 01:00:48 +03:00
|
|
|
cmdInit :: IO ()
|
|
|
|
cmdInit = do
|
|
|
|
putStrLn "Creating directory nix (if it doesn't exist)"
|
|
|
|
createDirectoryIfMissing True "nix"
|
|
|
|
|
|
|
|
putStrLn $ "Creating file " <> fileFetchNix <> " (if it doesn't exist)"
|
|
|
|
fileFetchNixExists <- doesFileExist fileFetchNix
|
|
|
|
|
|
|
|
if fileFetchNixExists
|
|
|
|
then do
|
|
|
|
putStrLn $ "Not writing " <> fileFetchNix
|
|
|
|
putStrLn "(file exists)"
|
|
|
|
else do
|
|
|
|
putStrLn $ "Writing " <> fileFetchNix
|
|
|
|
writeFile fileFetchNix fileFetchNixContent
|
|
|
|
|
|
|
|
putStrLn $ "Creating file " <> fileVersionsJson <> " (if it doesn't exist)"
|
|
|
|
fileVersionsJsonExists <- doesFileExist fileVersionsJson
|
|
|
|
|
|
|
|
if fileVersionsJsonExists
|
|
|
|
then do
|
|
|
|
putStrLn $ "Not writing " <> fileVersionsJson
|
|
|
|
putStrLn "(file exists)"
|
|
|
|
else do
|
|
|
|
putStrLn $ "Writing " <> fileVersionsJson
|
|
|
|
writeFile fileVersionsJson fileVersionsJsonContent
|
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- ADD
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
2019-01-24 23:58:22 +03:00
|
|
|
parseCmdAdd :: ParserInfo (IO ())
|
2019-01-27 01:39:38 +03:00
|
|
|
parseCmdAdd = (info ((cmdAdd <$> parsePackage) <**> helper)) fullDesc
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
cmdAdd :: (PackageName, PackageSpec) -> IO ()
|
|
|
|
cmdAdd package = do
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
(packageName, packageSpec) <- addCompletePackageSpec package
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
versionsSpec <- HMap.insert packageName packageSpec . unVersionsSpec <$>
|
|
|
|
getVersionsSpec
|
2019-01-23 23:55:26 +03:00
|
|
|
putStrLn $ "Writing new versions file"
|
2019-01-27 01:39:38 +03:00
|
|
|
print versionsSpec
|
|
|
|
-- encodeFile fileVersionsJson fileVersionsValue'
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-24 23:58:22 +03:00
|
|
|
addCompletePackageSpec
|
|
|
|
:: (PackageName, PackageSpec)
|
|
|
|
-> IO (PackageName, PackageSpec)
|
2019-01-27 01:39:38 +03:00
|
|
|
addCompletePackageSpec x@(PackageName str, _) = flip execStateT x $ do
|
|
|
|
|
|
|
|
-- Figures out the owner and repo
|
|
|
|
case span (/= '/') str of
|
|
|
|
(owner@(_:_), '/':repo@(_:_)) -> do
|
|
|
|
whenNotSet "owner" $
|
|
|
|
setPackageSpecAttr "owner" (String $ T.pack owner)
|
|
|
|
whenNotSet "repo" $ do
|
|
|
|
setPackageSpecAttr "repo" (String $ T.pack repo)
|
|
|
|
setPackageName repo
|
|
|
|
_ -> pure ()
|
|
|
|
|
|
|
|
-- In case we have @owner@ and @repo@, pull some data from GitHub
|
|
|
|
(,) <$> getPackageSpecAttr "owner" <*> getPackageSpecAttr "repo" >>= \case
|
|
|
|
(Just (String owner), Just (String repo)) -> do
|
|
|
|
liftIO (GH.executeRequest' $ GH.repositoryR (GH.N owner) (GH.N repo))
|
|
|
|
>>= \case
|
|
|
|
Right ghRepo -> do
|
|
|
|
|
|
|
|
-- Description
|
|
|
|
whenNotSet "description" $ case GH.repoDescription ghRepo of
|
|
|
|
Just descr -> setPackageSpecAttr "description" (String descr)
|
|
|
|
Nothing -> pure ()
|
|
|
|
|
|
|
|
-- Branch and rev
|
|
|
|
whenNotSet "branch" $ case GH.repoDefaultBranch ghRepo of
|
|
|
|
Just branch -> do
|
|
|
|
setPackageSpecAttr "branch" (String branch)
|
|
|
|
liftIO (GH.executeRequest' $
|
|
|
|
GH.commitsWithOptionsForR
|
|
|
|
(GH.N owner) (GH.N repo) (GH.FetchAtLeast 1)
|
|
|
|
[GH.CommitQuerySha branch]) >>= \case
|
|
|
|
Right (toList -> (commit:_)) -> do
|
|
|
|
let GH.N rev = GH.commitSha commit
|
|
|
|
setPackageSpecAttr "rev" (String rev)
|
|
|
|
_ -> pure ()
|
|
|
|
Nothing -> pure ()
|
|
|
|
|
|
|
|
-- Figures out the URL template
|
|
|
|
whenNotSet "url_template" $
|
|
|
|
setPackageSpecAttr "url_template" (String $ T.pack githubURLTemplate)
|
|
|
|
|
|
|
|
-- Figures out the URL from the template
|
|
|
|
withPackageSpecAttr "url_template" (\case
|
|
|
|
String (T.unpack -> template) -> do
|
|
|
|
(_, packageSpec) <- get
|
|
|
|
let stringValues = packageSpecStringValues packageSpec
|
|
|
|
case renderTemplate stringValues template of
|
|
|
|
Just renderedURL ->
|
|
|
|
setPackageSpecAttr "url" (String $ T.pack renderedURL)
|
|
|
|
Nothing -> pure ()
|
|
|
|
_ -> pure ()
|
|
|
|
)
|
|
|
|
where
|
|
|
|
githubURLTemplate :: String
|
|
|
|
githubURLTemplate =
|
|
|
|
"https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
2019-01-24 23:58:22 +03:00
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- SHOW
|
|
|
|
-------------------------------------------------------------------------------
|
2019-01-18 01:00:48 +03:00
|
|
|
|
2019-01-24 23:58:22 +03:00
|
|
|
parseCmdShow :: ParserInfo (IO ())
|
|
|
|
parseCmdShow = info (pure cmdShow <**> helper) fullDesc
|
|
|
|
|
2019-01-18 01:00:48 +03:00
|
|
|
cmdShow :: IO ()
|
2019-01-23 23:55:26 +03:00
|
|
|
cmdShow = do
|
|
|
|
putStrLn $ "Showing versions file"
|
|
|
|
|
|
|
|
VersionsSpec fileVersionsValue <- getVersionsSpec
|
|
|
|
|
|
|
|
forWithKeyM_ fileVersionsValue $ \key (PackageSpec spec) -> do
|
|
|
|
putStrLn $ "Package: " <> unPackageName key
|
|
|
|
forM_ (HMap.toList spec) $ \(attrName, attrValValue) -> do
|
|
|
|
let attrValue = case attrValValue of
|
|
|
|
String str -> str
|
|
|
|
_ -> "<barabajagal>"
|
|
|
|
putStrLn $ " " <> T.unpack attrName <> ": " <> T.unpack attrValue
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- UPDATE
|
|
|
|
-------------------------------------------------------------------------------
|
2019-01-18 01:00:48 +03:00
|
|
|
|
2019-01-24 23:58:22 +03:00
|
|
|
parseCmdUpdate :: ParserInfo (IO ())
|
2019-01-27 01:39:38 +03:00
|
|
|
parseCmdUpdate = info ((cmdUpdate <$> parsePackage) <**> helper) fullDesc
|
2019-01-24 23:58:22 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
cmdUpdate :: (PackageName, PackageSpec) -> IO ()
|
|
|
|
cmdUpdate pkgs = do
|
2019-01-23 23:55:26 +03:00
|
|
|
putStrLn $ "Updating versions file"
|
|
|
|
|
|
|
|
VersionsSpec fileVersionsValue <- getVersionsSpec
|
|
|
|
|
|
|
|
fileVersionsValue' <- forWithKeyM fileVersionsValue $ \key spec -> do
|
|
|
|
putStrLn $ "Package: " <> unPackageName key
|
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
-- TODO: use StateT
|
|
|
|
-- let packageUrl <- renderTemplate
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
-- putStrLn $ " URL: " <> packageUrl
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
-- sha256 <- nixPrefetchURL packageUrl
|
2019-01-23 23:55:26 +03:00
|
|
|
|
2019-01-27 01:39:38 +03:00
|
|
|
-- putStrLn $ " SHA256: " <> sha256
|
2019-01-23 23:55:26 +03:00
|
|
|
|
|
|
|
putStrLn $ "Writing new versions file"
|
|
|
|
encodeFile fileVersionsJson fileVersionsValue'
|
2019-01-18 01:00:48 +03:00
|
|
|
|
2019-01-23 23:55:26 +03:00
|
|
|
parseCommand :: Parser (IO ())
|
|
|
|
parseCommand = subparser (
|
|
|
|
command "init" parseCmdInit <>
|
2019-01-24 23:58:22 +03:00
|
|
|
command "add" parseCmdAdd <>
|
|
|
|
command "show" parseCmdShow <>
|
|
|
|
command "update" parseCmdUpdate )
|
2019-01-18 01:00:48 +03:00
|
|
|
|
|
|
|
main :: IO ()
|
2019-01-23 23:55:26 +03:00
|
|
|
main = join $ execParser opts
|
|
|
|
where
|
|
|
|
opts = info (parseCommand <**> helper)
|
|
|
|
( fullDesc
|
|
|
|
<> header "NIV - Nix Version manager" )
|
|
|
|
|
|
|
|
nixPrefetchURL :: String -> IO String
|
|
|
|
nixPrefetchURL = pure
|
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Aux
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
--- Aeson
|
|
|
|
|
|
|
|
-- | Efficiently deserialize a JSON value from a file.
|
|
|
|
-- If this fails due to incomplete or invalid input, 'Nothing' is
|
|
|
|
-- returned.
|
|
|
|
--
|
|
|
|
-- The input file's content must consist solely of a JSON document,
|
|
|
|
-- with no trailing data except for whitespace.
|
|
|
|
--
|
|
|
|
-- This function parses immediately, but defers conversion. See
|
|
|
|
-- 'json' for details.
|
|
|
|
decodeFileStrict :: (FromJSON a) => FilePath -> IO (Maybe a)
|
|
|
|
decodeFileStrict = fmap decodeStrict . B.readFile
|
|
|
|
|
|
|
|
-- | Efficiently serialize a JSON value as a lazy 'L.ByteString' and write it to a file.
|
|
|
|
encodeFile :: (ToJSON a) => FilePath -> a -> IO ()
|
|
|
|
encodeFile fp = L.writeFile fp . encode
|
|
|
|
|
|
|
|
--- HashMap
|
|
|
|
|
|
|
|
forWithKeyM
|
|
|
|
:: (Eq k, Hashable k, Monad m)
|
|
|
|
=> HMap.HashMap k v1
|
|
|
|
-> (k -> v1 -> m v2)
|
|
|
|
-> m (HMap.HashMap k v2)
|
|
|
|
forWithKeyM = flip mapWithKeyM
|
|
|
|
|
|
|
|
forWithKeyM_
|
|
|
|
:: (Eq k, Hashable k, Monad m)
|
|
|
|
=> HMap.HashMap k v1
|
|
|
|
-> (k -> v1 -> m ())
|
|
|
|
-> m ()
|
|
|
|
forWithKeyM_ = flip mapWithKeyM_
|
|
|
|
|
|
|
|
|
|
|
|
mapWithKeyM
|
|
|
|
:: (Eq k, Hashable k, Monad m)
|
|
|
|
=> (k -> v1 -> m v2)
|
|
|
|
-> HMap.HashMap k v1
|
|
|
|
-> m (HMap.HashMap k v2)
|
|
|
|
mapWithKeyM f m = do
|
|
|
|
fmap mconcat $ forM (HMap.toList m) $ \(k, v) ->
|
|
|
|
HMap.singleton k <$> f k v
|
|
|
|
|
|
|
|
mapWithKeyM_
|
|
|
|
:: (Eq k, Hashable k, Monad m)
|
|
|
|
=> (k -> v1 -> m ())
|
|
|
|
-> HMap.HashMap k v1
|
|
|
|
-> m ()
|
|
|
|
mapWithKeyM_ f m = do
|
|
|
|
forM_ (HMap.toList m) $ \(k, v) ->
|
|
|
|
HMap.singleton k <$> f k v
|
2019-01-27 01:39:38 +03:00
|
|
|
|
|
|
|
-- | Renders the template. Returns 'Nothing' if some of the attributes are
|
|
|
|
-- missing.
|
|
|
|
--
|
|
|
|
-- renderTemplate [("foo", "bar")] "<foo>" == Just "bar"
|
|
|
|
-- renderTemplate [("foo", "bar")] "<baz>" == Nothing
|
|
|
|
renderTemplate :: [(String, String)] -> String -> Maybe String
|
|
|
|
renderTemplate vals = \case
|
|
|
|
'<':str -> do
|
|
|
|
case span (/= '>') str of
|
|
|
|
(key, '>':rest) ->
|
|
|
|
liftA2 (<>) (lookup key vals) (renderTemplate vals rest)
|
|
|
|
|
|
|
|
c:str -> (c:) <$> renderTemplate vals str
|
|
|
|
[] -> Just []
|