Squashed 'miscellaneous/suckless-conf/' content from commit ff6f1a2e0

git-subtree-dir: miscellaneous/suckless-conf
git-subtree-split: ff6f1a2e053005a52af5c7375fb66e8bb89bce2d
This commit is contained in:
voidlizard 2024-10-07 05:03:16 +03:00
commit e1cbd3eb64
27 changed files with 2492 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

78
.fixme/config Normal file
View File

@ -0,0 +1,78 @@
fixme-comments "--"
fixme-prefix FIXME: bugs issues
fixme-prefix TODO: bugs issues
fixme-prefix REVIEW: review
fixme-prefix PR: pr
fixme-files **/*.hs docs/devlog.md
fixme-files docs/pep*.txt
fixme-files docs/drafts/**/*.txt
fixme-files docs/pr/**/*.txt
fixme-files docs/todo/**/*.txt
fixme-files-ignore .direnv/** dist-newstyle/**
fixme-id-show-len 10
fixme-attribs assigned workflow resolution cat scope
fixme-value-set workflow new backlog wip test fixed done
fixme-value-set cat bug feat refactor
fixme-value-set scope mvp-0 mvp-1 backlog
[ fixme-report all json
(render builtin:microstache report-wip.tpl)
(post builtin:columns | 10 8 8 10 12 _)
; (query ~workflow:backlog)
]
[ fixme-report open json
(render builtin:microstache report-wip.tpl)
(post builtin:columns | 10 8 8 10 12 _)
(query ~workflow:backlog)
(query ~workflow:done)
(query ~workflow:test)
]
[ fixme-report backlog json
(render builtin:microstache report-wip.tpl)
(post builtin:columns | 10 8 8 10 12 _)
(query workflow:backlog)
]
[ fixme-report wip json
(render builtin:microstache report-wip.tpl)
(post builtin:columns | 10 8 8 10 12 _)
(query ?workflow:test)
(query ?workflow:wip)
(query ?workflow:fixed)
]
[ fixme-report review json
(render builtin:microstache report-wip.tpl)
(post builtin:columns | 10 8 8 10 12 _)
(query tag:REVIEW:)
]
[ fixme-report wip-json json
]
fixme-log-macro backlog (fixme-set "workflow" "backlog" "$1")
fixme-log-macro test (fixme-set "workflow" "test" "$1")
fixme-log-macro wip (fixme-set "workflow" "wip" "$1")
fixme-log-macro done (fixme-set "workflow" "done" "$1")
fixme-log-macro assign (fixme-set "assigned" "$1" "$2")
fixme-log-macro scope (fixme-set "scope" "$1" "$2")

5
.fixme/log Normal file
View File

@ -0,0 +1,5 @@
;; This is a log file. All fixmies status updates go here
fixme-set "workflow" "done" "9QfPgLHLSw"
(fixme-set "assigned" "voidlizard" "9XmyXek1Y6")

3
.fixme/report-wip.tpl Normal file
View File

@ -0,0 +1,3 @@
{{#items}}
{{&id}}|{{&tag}}|{{&scope}}|[{{&workflow}}]|{{&assigned}}|{{&title}}
{{/items}}

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
dist-newstyle/
.direnv/
.fixme/state.db

7
CHANGELOG.md Normal file
View File

@ -0,0 +1,7 @@
# Revision history for suckless-conf
## 0.1.0.0 -- YYYY-mm-dd
* Test
* First version. Released on an unsuspecting world.

30
LICENSE Normal file
View File

@ -0,0 +1,30 @@
Copyright (c) 2023, Dmitry Zuikov
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Dmitry Zuikov nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

77
doc/devlog Normal file
View File

@ -0,0 +1,77 @@
## 2023-07-12
PR: key-value-utilities
branch: key-value-utilities
commit: cb4ee37f455b8e001fd5688106b2da1b31885dc4
Добавлены утилиты для работы с ключами и значениями.
## 2023-02-09
TODO: implement-regression-tests
54377068aac95cbfd8c69177a101c434feecff41
## 2023-02-07
Для fixme нам нужны конфиги. Стандартые конфиги это, в основном, треш,
поэтому будем делать новые. Как всегда.
Конфиги будут sexp, при этом, будут уметь притворяться не sexp.
поэтому:
```
atom term* eol
```
единичная инструкция.
эквивалентна
```
( atom term* )
```
выражение конфига:
```
(atom term* )
```
### term
```
term ::= string | number | atom | bool
```
Про bool это не точно.
### Пример конфига:
```
fixme-comments // # ; --
; FIXME могут быть в блоках комментариев,
; а могут и нет.
fixme-prefix FIXME: bug issue
; комментарий. ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
; Префикс Категории для префикса
```
Как биндить термы на целевой язык? В нашем случае Haskell.
FIXME: хорошо бы тут сразу поддержать wisp.
Но что бы его поддержать, надо его понять.

130
flake.lock Normal file
View File

@ -0,0 +1,130 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1644229661,
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"fuzzy": {
"inputs": {
"haskell-flake-utils": "haskell-flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1727197542,
"narHash": "sha256-BF9Xd2fa8L5Xju9NTaoUjmzUEJfrRMMKULYQieBjbKo=",
"ref": "refs/heads/master",
"rev": "a834b152e29d632c816eefe117036e5d9330bd03",
"revCount": 43,
"type": "git",
"url": "http://git.hbs2.net/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA"
},
"original": {
"type": "git",
"url": "http://git.hbs2.net/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA"
}
},
"haskell-flake-utils": {
"inputs": {
"flake-utils": "flake-utils"
},
"locked": {
"lastModified": 1707809372,
"narHash": "sha256-wfTL9PlCSOqSSyU4eenFFI7pHrV21gba4GEILnI4nAU=",
"owner": "ivanovs-4",
"repo": "haskell-flake-utils",
"rev": "3cbdc5d6093e8b4464ae64097e0c8c61e4414ff2",
"type": "github"
},
"original": {
"owner": "ivanovs-4",
"repo": "haskell-flake-utils",
"type": "github"
}
},
"haskell-flake-utils_2": {
"inputs": {
"flake-utils": "flake-utils_2"
},
"locked": {
"lastModified": 1707809372,
"narHash": "sha256-wfTL9PlCSOqSSyU4eenFFI7pHrV21gba4GEILnI4nAU=",
"owner": "ivanovs-4",
"repo": "haskell-flake-utils",
"rev": "3cbdc5d6093e8b4464ae64097e0c8c61e4414ff2",
"type": "github"
},
"original": {
"owner": "ivanovs-4",
"repo": "haskell-flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1727089097,
"narHash": "sha256-ZMHMThPsthhUREwDebXw7GX45bJnBCVbfnH1g5iuSPc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "568bfef547c14ca438c56a0bece08b8bb2b71a9c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1727089097,
"narHash": "sha256-ZMHMThPsthhUREwDebXw7GX45bJnBCVbfnH1g5iuSPc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "568bfef547c14ca438c56a0bece08b8bb2b71a9c",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fuzzy": "fuzzy",
"haskell-flake-utils": "haskell-flake-utils_2",
"nixpkgs": "nixpkgs_2"
}
}
},
"root": "root",
"version": 7
}

69
flake.nix Normal file
View File

@ -0,0 +1,69 @@
{
description = "suckless-cong: sexp based configs";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
haskell-flake-utils.url = "github:ivanovs-4/haskell-flake-utils";
fuzzy.url =
# "git+http://git.hbs2/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA?ref=sexp-parser&rev=bd3a38904864d5cc333974e7b029412607b46871";
"git+http://git.hbs2.net/GmcLB9gEPT4tbx9eyQiECwsu8oPyEh6qKEpQDtyBWVPA";
};
outputs = { self, fuzzy, nixpkgs, haskell-flake-utils, ... }@inputs:
haskell-flake-utils.lib.simpleCabal2flake {
inherit self nixpkgs;
# systems = [ "x86_64-linux" ];
# wtf = import fetcher-flake.out.outPath;
# project-b = import fuzzy.out.outPath;
name = "suckless-conf";
## Optional parameters follow
# nixpkgs config
# config = { };
# Add another haskell flakes as requirements
# haskellFlakes = [ inputs.another-simple-haskell-flake ];
# Use this to load other flakes overlays to supplement nixpkgs
# preOverlays = [ ];
# Pass either a function or a file
# preOverlay = ./overlay.nix;
# Override haskell packages
# hpPreOverrides = { pkgs }: new: old:
# with pkgs.haskell.lib; with haskell-flake-utils.lib;
# tunePackages pkgs old {
# some-haskellPackages-package = [ dontHaddock ];
# } // {
# some-cabal-pkg = ((jailbreakUnbreak pkgs) (dontCheck (old.callCabal2nix "some-cabal-pkg" inputs.some-cabal-pkg {})));
# };
# Arguments for callCabal2nix
# cabal2nixArgs = {pkgs}: {
# };
# Maps to the devShell output. Pass in a shell.nix file or function
haskellFlakes = with inputs; [
fuzzy
];
# Additional build intputs of the default shell
shellExtBuildInputs = {pkgs}: with pkgs; [
haskellPackages.haskell-language-server
];
# Wether to build hoogle in the default shell
# shellWithHoogle = true;
};
}

View File

@ -0,0 +1,12 @@
module Data.Config.Suckless
( module Data.Config.Suckless.Syntax
, module Data.Config.Suckless.Parse
, module Data.Config.Suckless.KeyValue
)
where
import Data.Config.Suckless.Syntax
import Data.Config.Suckless.Parse
import Data.Config.Suckless.KeyValue

View File

@ -0,0 +1,111 @@
{-# Language PatternSynonyms #-}
{-# Language UndecidableInstances #-}
{-# Language AllowAmbiguousTypes #-}
module Data.Config.Suckless.KeyValue where
import Data.Config.Suckless.Syntax
import Data.String (IsString(..))
import Data.Set qualified as Set
import Data.Set (Set)
import Data.Maybe
import Data.Scientific
import Data.Aeson
import Prettyprinter
import Control.Monad.Reader
import Control.Monad.Identity
import Safe
import Debug.Trace
class HasCfgKey a b where
-- type family CfgValue a :: Type
key :: Id
class Monad m => HasCfgValue a b m where
cfgValue :: m b
class Monad m => HasConf m where
getConf :: m [Syntax C]
pattern Key :: forall {c}. Id -> [Syntax c] -> [Syntax c]
pattern Key n ns <- SymbolVal n : ns
instance {-# OVERLAPPABLE #-} (Monad m) => HasConf (ReaderT [Syntax C] m) where
getConf = ask
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Integer)) => HasCfgValue a (Maybe Integer) m where
cfgValue = lastMay . val <$> getConf
where
val syn = [ e
| ListVal (Key s [LitIntVal e]) <- syn, s == key @a @(Maybe Integer)
]
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Int)) => HasCfgValue a (Maybe Int) m where
cfgValue = lastMay . val <$> getConf @m
where
val syn = [ fromIntegral e
| ListVal (Key s [LitIntVal e]) <- syn, s == key @a @(Maybe Int)
]
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Scientific)) => HasCfgValue a (Maybe Scientific) m where
cfgValue = lastMay . val <$> getConf
where
val syn = [ e
| ListVal (Key s [LitScientificVal e]) <- syn, s == key @a @(Maybe Scientific)
]
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Bool)) => HasCfgValue a (Maybe Bool) m where
cfgValue = lastMay . val <$> getConf
where
val syn = [ e
| ListVal (Key s [LitBoolVal e]) <- syn, s == key @a @(Maybe Bool)
]
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Maybe Value)) => HasCfgValue a (Maybe Value) m where
cfgValue = lastMay . val <$> getConf
where
val syn = [ toJSON v
| ListVal (Key s [v@ListVal{}]) <- syn, s == key @a @(Maybe Value)
]
instance {-# OVERLAPPABLE #-} (HasConf m, IsString b, HasCfgKey a (Maybe b)) => HasCfgValue a (Maybe b) m where
cfgValue = lastMay . val <$> getConf
where
val syn = [ fromString (show $ pretty e)
| ListVal (Key s [LitStrVal e]) <- syn, s == key @a @(Maybe b)
]
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Set Integer)) => HasCfgValue a (Set Integer) m where
cfgValue = Set.fromList . val <$> getConf
where
val syn = [ e
| ListVal (Key s [LitIntVal e]) <- syn, s == key @a @(Set Integer)
]
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Set Scientific)) => HasCfgValue a (Set Scientific) m where
cfgValue = Set.fromList . val <$> getConf
where
val syn = [ e
| ListVal (Key s [LitScientificVal e]) <- syn, s == key @a @(Set Scientific)
]
instance {-# OVERLAPPABLE #-} (HasConf m, HasCfgKey a (Set Value)) => HasCfgValue a (Set Value) m where
cfgValue = Set.fromList . val <$> getConf
where
val syn = [ toJSON v
| ListVal (Key s [v@ListVal{}]) <- syn, s == key @a @(Set Value)
]
instance {-# OVERLAPPABLE #-} (HasConf m, Ord b, IsString b, HasCfgKey a (Set b)) => HasCfgValue a (Set b) m where
cfgValue = Set.fromList . val <$> getConf
where
val syn = [ fromString (show $ pretty e)
| ListVal (Key s [LitStrVal e]) <- syn, s == key @a @(Set b)
]

View File

@ -0,0 +1,8 @@
module Data.Config.Suckless.Parse
( module Data.Config.Suckless.Parse.Fuzzy
) where
import Data.Config.Suckless.Parse.Fuzzy

View File

@ -0,0 +1,40 @@
module Data.Config.Suckless.Parse.Fuzzy
( ParseSExp(..)
) where
import Data.Config.Suckless.Syntax
import Data.Text.Fuzzy.SExp qualified as P
import Data.Text.Fuzzy.SExp (C0(..),SExpParseError,ForMicroSexp(..))
import Data.Functor
import Data.Text as Text
import Control.Monad.Except
import Control.Monad.Identity
class ParseSExp what where
parseTop :: what -> Either SExpParseError [Syntax C]
parseSyntax :: what -> Either SExpParseError (Syntax C)
instance ParseSExp Text where
parseTop what = runIdentity (runExceptT (P.parseTop what)) <&> fmap toSyntax
parseSyntax txt = runIdentity (runExceptT (P.parseSexp txt)) <&> toSyntax
instance ParseSExp String where
parseTop what = runIdentity (runExceptT (P.parseTop (Text.pack what))) <&> fmap toSyntax
parseSyntax txt = runIdentity (runExceptT (P.parseSexp (Text.pack txt))) <&> toSyntax
toSyntax :: P.MicroSexp C0 -> Syntax C
toSyntax = \case
P.List_ co a -> List (toContext co) (fmap toSyntax a)
P.Symbol_ co a -> Symbol (toContext co) (Id a)
P.String_ co a -> Literal (toContext co) (LitStr a)
P.Boolean_ co a -> Literal (toContext co) (LitBool a)
P.Number_ co v -> case v of
P.NumInteger n -> Literal (toContext co) (LitInt n)
P.NumDouble n -> Literal (toContext co) (LitScientific (realToFrac n))
toContext :: C0 -> Context C
toContext (C0 what) = SimpleContext (fromIntegral <$> what)

View File

@ -0,0 +1,50 @@
{-# Language UndecidableInstances #-}
{-# Language PatternSynonyms #-}
module Data.Config.Suckless.Script
( module Exported
, module Data.Config.Suckless.Script
) where
import Data.Config.Suckless as Exported
import Data.Config.Suckless.Script.Internal as Exported
import Control.Monad
import Control.Monad.Reader
import Data.HashMap.Strict qualified as HM
import Prettyprinter
import Prettyprinter.Render.Terminal
import Data.List qualified as List
import Data.Text qualified as Text
import UnliftIO
{- HLINT ignore "Functor law" -}
helpList :: MonadUnliftIO m => Bool -> Maybe String -> RunM c m ()
helpList hasDoc p = do
let match = maybe (const True) (Text.isPrefixOf . Text.pack) p
d <- ask >>= readTVarIO
let ks = [k | Id k <- List.sort (HM.keys d)
, match k
, not hasDoc || docDefined (HM.lookup (Id k) d)
]
display_ $ vcat (fmap pretty ks)
where
docDefined (Just (Bind (Just w) _)) = True
docDefined _ = False
helpEntry :: MonadUnliftIO m => Id -> RunM c m ()
helpEntry what = do
man <- ask >>= readTVarIO
<&> HM.lookup what
<&> maybe mzero bindMan
liftIO $ hPutDoc stdout (pretty man)
pattern HelpEntryBound :: forall {c}. Id -> [Syntax c]
pattern HelpEntryBound what <- [ListVal (SymbolVal "builtin:lambda" : SymbolVal what : _ )]

View File

@ -0,0 +1,85 @@
{-# Language MultiWayIf #-}
module Data.Config.Suckless.Script.File where
import Data.Config.Suckless
import Data.Config.Suckless.Script.Internal
import Control.Monad
import Control.Monad.Trans.Class
import Control.Monad.Trans.Cont
import Data.Maybe
import Data.Either
import Data.Foldable
import System.Directory
import System.FilePath
import System.FilePattern
import Data.HashSet qualified as HS
import Lens.Micro.Platform
import UnliftIO
import Control.Concurrent.STM qualified as STM
import Streaming.Prelude qualified as S
-- FIXME: skip-symlink
glob :: forall m . MonadIO m
=> [FilePattern] -- ^ search patterns
-> [FilePattern] -- ^ ignore patterns
-> FilePath -- ^ directory
-> (FilePath -> m Bool) -- ^ file action
-> m ()
glob pat ignore dir action = do
q <- newTQueueIO
void $ liftIO (async $ go q dir >> atomically (writeTQueue q Nothing))
fix $ \next -> do
atomically (readTQueue q) >>= \case
Nothing -> pure ()
Just x -> do
r <- action x
when r next
where
matches p f = or [ i ?== f | i <- p ]
skip p = or [ i ?== p | i <- ignore ]
go q f = do
isD <- doesDirectoryExist f
if not isD then do
isF <- doesFileExist f
when (isF && matches pat f && not (skip f)) do
atomically $ writeTQueue q (Just f)
else do
co' <- (try @_ @IOError $ listDirectory f)
<&> fromRight mempty
forConcurrently_ co' $ \x -> do
let p = normalise (f </> x)
unless (skip p) (go q p)
entries :: forall c m . ( IsContext c
, Exception (BadFormException c)
, MonadUnliftIO m)
=> MakeDictM c m ()
entries = do
entry $ bindMatch "glob" $ \syn -> do
(p,i,d) <- case syn of
[] -> pure (["*"], [], ".")
[StringLike d, StringLike i, StringLike e] -> do
pure ([i], [e], d)
[StringLike d, ListVal (StringLikeList i), ListVal (StringLikeList e)] -> do
pure (i, e, d)
_ -> throwIO (BadFormException @c nil)
r <- S.toList_ $ glob p i d $ \fn -> do
S.yield (mkStr @c fn) -- do
pure True
pure (mkList r)

View File

@ -0,0 +1,969 @@
{-# Language AllowAmbiguousTypes #-}
{-# Language UndecidableInstances #-}
{-# Language PatternSynonyms #-}
{-# Language ViewPatterns #-}
module Data.Config.Suckless.Script.Internal
( module Data.Config.Suckless.Script.Internal
, module Export
) where
import Data.Config.Suckless
import Control.Applicative
import Control.Monad
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.Writer
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as BS8
import Data.Data
import Data.Function as Export
import Data.Functor as Export
import Data.Hashable
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HM
import Data.Kind
import Data.List (isPrefixOf)
import Data.List qualified as List
import Data.Maybe
import Data.String
import Data.Text.IO qualified as TIO
import Data.Text qualified as Text
import Data.Text (Text)
import Data.Time.Clock.POSIX
import GHC.Generics hiding (C)
import Prettyprinter
import Prettyprinter.Render.Terminal
import Safe
import Streaming.Prelude qualified as S
import System.Environment
import Text.InterpolatedString.Perl6 (qc)
import UnliftIO
-- TODO: move-to-suckless-conf
data ManApplyArg = ManApplyArg Text Text
deriving stock (Eq,Show,Data,Generic)
newtype ManApply = ManApply [ ManApplyArg ]
deriving stock (Eq,Show,Data,Generic)
deriving newtype (Semigroup,Monoid)
data ManSynopsis =
ManSynopsis ManApply
deriving stock (Eq,Show,Data,Generic)
data ManDesc = ManDescRaw Text
deriving stock (Eq,Show,Data,Generic)
data ManRetVal = ManRetVal
deriving stock (Eq,Show,Data,Generic)
newtype ManName a = ManName Id
deriving stock (Eq,Show,Data,Generic)
deriving newtype (IsString,Pretty)
newtype ManBrief = ManBrief Text
deriving stock (Eq,Show,Data,Generic)
deriving newtype (Pretty,IsString)
data ManReturns = ManReturns Text Text
deriving stock (Eq,Show,Data,Generic)
newtype ManExamples =
ManExamples Text
deriving stock (Eq,Show,Data,Generic)
deriving newtype (Pretty,IsString,Monoid,Semigroup)
class ManNameOf a ann where
manNameOf :: a -> ManName ann
data Man a =
Man
{ manName :: Maybe (ManName a)
, manHidden :: Bool
, manBrief :: Maybe ManBrief
, manSynopsis :: [ManSynopsis]
, manDesc :: Maybe ManDesc
, manReturns :: Maybe ManReturns
, manExamples :: [ManExamples]
}
deriving stock (Eq,Show,Generic)
instance Monoid (Man a) where
mempty = Man Nothing False Nothing mempty Nothing Nothing mempty
instance Semigroup (Man a) where
(<>) a b = Man (manName b <|> manName a)
(manHidden b || manHidden a)
(manBrief b <|> manBrief a)
(manSynopsis a <> manSynopsis b)
(manDesc b <|> manDesc a)
(manReturns b <|> manReturns a)
(manExamples a <> manExamples b)
instance ManNameOf Id a where
manNameOf = ManName
instance Pretty ManDesc where
pretty = \case
ManDescRaw t -> pretty t
instance IsString ManDesc where
fromString s = ManDescRaw (Text.pack s)
instance Pretty (Man a) where
pretty e = "NAME"
<> line
<> indent 8 (pretty (manName e) <> fmtBrief e)
<> line
<> fmtSynopsis
<> fmtDescription
<> retval
<> fmtExamples
where
fmtBrief a = case manBrief a of
Nothing -> mempty
Just x -> " - " <> pretty x
retval = case manReturns e of
Nothing -> mempty
Just (ManReturns t s) ->
line <> "RETURN VALUE" <> line
<> indent 8 (
if not (Text.null s) then
(pretty t <> hsep ["","-",""] <> pretty s) <> line
else pretty t )
fmtDescription = line
<> "DESCRIPTION" <> line
<> indent 8 ( case manDesc e of
Nothing -> pretty (manBrief e)
Just x -> pretty x)
<> line
fmtSynopsis = case manSynopsis e of
[] -> mempty
_ ->
line
<> "SYNOPSIS"
<> line
<> vcat (fmap synEntry (manSynopsis e))
<> line
fmtExamples = case manExamples e of
[] -> mempty
es -> line
<> "EXAMPLES"
<> line
<> indent 8 ( vcat (fmap pretty es) )
synEntry (ManSynopsis (ManApply [])) =
indent 8 ( parens (pretty (manName e)) ) <> line
synEntry (ManSynopsis (ManApply xs)) = do
indent 8 do
parens (pretty (manName e) <+>
hsep [ pretty n | ManApplyArg t n <- xs ] )
<> line
<> line
<> vcat [ pretty n <+> ":" <+> pretty t | ManApplyArg t n <- xs ]
stringLike :: Syntax c -> Maybe String
stringLike = \case
LitStrVal s -> Just $ Text.unpack s
SymbolVal (Id s) -> Just $ Text.unpack s
_ -> Nothing
stringLikeList :: [Syntax c] -> [String]
stringLikeList syn = [ stringLike s | s <- syn ] & takeWhile isJust & catMaybes
blobLike :: Syntax c -> Maybe ByteString
blobLike = \case
LitStrVal s -> Just $ BS8.pack (Text.unpack s)
ListVal [SymbolVal "blob", LitStrVal s] -> Just $ BS8.pack (Text.unpack s)
_ -> Nothing
pattern BlobLike :: forall {c} . ByteString -> Syntax c
pattern BlobLike s <- (blobLike -> Just s)
class Display a where
display :: MonadIO m => a -> m ()
instance {-# OVERLAPPABLE #-} Pretty w => Display w where
display = liftIO . print . pretty
instance IsContext c => Display (Syntax c) where
display = \case
LitStrVal s -> liftIO $ TIO.putStr s
-- ListVal [SymbolVal "small-encrypted-block", LitStrVal txt] -> do
-- let s = Text.unpack txt & BS8.pack & toBase58 & AsBase58 & pretty
-- liftIO $ print $ parens $ "small-encrypted-block" <+> parens ("blob" <+> dquotes s)
-- ListVal [SymbolVal "blob", LitStrVal txt] -> do
-- let s = Text.unpack txt & BS8.pack & toBase58 & AsBase58 & pretty
-- liftIO $ print $ parens $ "blob:base58" <+> dquotes s
x -> liftIO $ putStr (show $ pretty x)
instance Display Text where
display = liftIO . TIO.putStr
instance Display String where
display = liftIO . putStr
display_ :: (MonadIO m, Show a) => a -> m ()
display_ = liftIO . print
{- HLINT ignore "Functor law" -}
class IsContext c => MkSym c a where
mkSym :: a -> Syntax c
instance IsContext c => MkSym c String where
mkSym s = Symbol noContext (Id $ Text.pack s)
instance IsContext c => MkSym c Text where
mkSym s = Symbol noContext (Id s)
instance IsContext c => MkSym c Id where
mkSym = Symbol noContext
class IsContext c => MkStr c s where
mkStr :: s -> Syntax c
instance IsContext c => MkStr c String where
mkStr s = Literal noContext $ LitStr (Text.pack s)
instance IsContext c => MkStr c Text where
mkStr s = Literal noContext $ LitStr s
mkBool :: forall c . IsContext c => Bool -> Syntax c
mkBool v = Literal noContext (LitBool v)
class IsContext c => MkForm c a where
mkForm :: a-> [Syntax c] -> Syntax c
instance (IsContext c, MkSym c s) => MkForm c s where
mkForm s sy = List noContext ( mkSym @c s : sy )
mkList :: forall c. IsContext c => [Syntax c] -> Syntax c
mkList = List noContext
isFalse :: forall c . IsContext c => Syntax c -> Bool
isFalse = \case
Literal _ (LitBool False) -> True
ListVal [] -> True
_ -> False
eatNil :: Monad m => (Syntax c -> m a) -> Syntax c -> m ()
eatNil f = \case
Nil -> pure ()
x -> void $ f x
class IsContext c => MkInt c s where
mkInt :: s -> Syntax c
instance (Integral i, IsContext c) => MkInt c i where
mkInt n = Literal noContext $ LitInt (fromIntegral n)
class OptionalVal c b where
optional :: b -> Syntax c -> b
instance IsContext c => OptionalVal c Int where
optional d = \case
LitIntVal x -> fromIntegral x
_ -> d
hasKey :: IsContext c => Id -> [Syntax c] -> Maybe (Syntax c)
hasKey k ss = headMay [ e | ListVal [SymbolVal z, e] <- ss, z == k]
pattern Lambda :: forall {c}. [Id] -> Syntax c -> Syntax c
pattern Lambda a e <- ListVal [SymbolVal "lambda", LambdaArgs a, e]
pattern LambdaArgs :: [Id] -> Syntax c
pattern LambdaArgs a <- (lambdaArgList -> Just a)
lambdaArgList :: Syntax c -> Maybe [Id]
lambdaArgList (ListVal a) = sequence argz
where
argz = flip fmap a \case
(SymbolVal x) -> Just x
_ -> Nothing
lambdaArgList _ = Nothing
pattern PairList :: [Syntax c] -> [Syntax c]
pattern PairList es <- (pairList -> es)
pairList :: [Syntax c ] -> [Syntax c]
pairList syn = [ isPair s | s <- syn ] & takeWhile isJust & catMaybes
optlist :: IsContext c => [Syntax c] -> [(Id, Syntax c)]
optlist = reverse . go []
where
go acc ( SymbolVal i : b : rest ) = go ((i, b) : acc) rest
go acc [ SymbolVal i ] = (i, nil) : acc
go acc _ = acc
isPair :: Syntax c -> Maybe (Syntax c)
isPair = \case
e@(ListVal [_,_]) -> Just e
_ -> Nothing
data BindAction c ( m :: Type -> Type) =
BindLambda { fromLambda :: [Syntax c] -> RunM c m (Syntax c) }
| BindValue (Syntax c)
data Bind c ( m :: Type -> Type) = Bind
{ bindMan :: Maybe (Man AnsiStyle)
, bindAction :: BindAction c m
} deriving (Generic)
deriving newtype instance Hashable Id
newtype NameNotBoundException =
NameNotBound Id
deriving stock Show
deriving newtype (Generic,Typeable)
newtype NotLambda = NotLambda Id
deriving stock Show
deriving newtype (Generic,Typeable)
instance Exception NotLambda
data BadFormException c = BadFormException (Syntax c)
| ArityMismatch (Syntax c)
newtype TypeCheckError c = TypeCheckError (Syntax c)
instance Exception (TypeCheckError C)
newtype BadValueException = BadValueException String
deriving stock Show
deriving newtype (Generic,Typeable)
instance Exception NameNotBoundException
instance IsContext c => Show (BadFormException c) where
show (BadFormException sy) = show $ "BadFormException" <+> pretty sy
show (ArityMismatch sy) = show $ "ArityMismatch" <+> pretty sy
instance IsContext c => Show (TypeCheckError c) where
show (TypeCheckError sy) = show $ "TypeCheckError" <+> pretty sy
instance Exception (BadFormException C)
instance Exception BadValueException
type Dict c m = HashMap Id (Bind c m)
newtype RunM c m a = RunM { fromRunM :: ReaderT (TVar (Dict c m)) m a }
deriving newtype ( Applicative
, Functor
, Monad
, MonadIO
, MonadUnliftIO
, MonadReader (TVar (Dict c m))
)
instance MonadTrans (RunM c) where
lift = RunM . lift
newtype MakeDictM c m a = MakeDictM { fromMakeDict :: Writer (Dict c m) a }
deriving newtype ( Applicative
, Functor
, Monad
, MonadWriter (Dict c m)
)
makeDict :: (IsContext c, Monad m) => MakeDictM c m () -> Dict c m
makeDict w = execWriter ( fromMakeDict w )
entry :: Dict c m -> MakeDictM c m ()
entry = tell
hide :: MakeDictM c m ()
hide = pure ()
desc :: Doc ann -> MakeDictM c m () -> MakeDictM c m ()
desc txt = censor (HM.map setDesc)
where
w0 = mempty { manDesc = Just (ManDescRaw $ Text.pack $ show txt) }
setDesc (Bind w x) = Bind (Just (maybe w0 (<> w0) w)) x
brief :: ManBrief -> MakeDictM c m () -> MakeDictM c m ()
brief txt = censor (HM.map setBrief)
where
w0 = mempty { manBrief = Just txt }
setBrief (Bind w x) = Bind (Just (maybe w0 (<> w0) w)) x
returns :: Text -> Text -> MakeDictM c m () -> MakeDictM c m ()
returns tp txt = censor (HM.map setReturns)
where
w0 = mempty { manReturns = Just (ManReturns tp txt) }
setReturns (Bind w x) = Bind (Just (maybe w0 (<>w0) w)) x
addSynopsis :: ManSynopsis -> Bind c m -> Bind c m
addSynopsis synopsis (Bind w x) = Bind (Just updatedMan) x
where
updatedMan = case w of
Nothing -> mempty { manSynopsis = [synopsis] }
Just man -> man { manSynopsis = manSynopsis man <> [synopsis] }
noArgs :: MakeDictM c m () -> MakeDictM c m ()
noArgs = censor (HM.map (addSynopsis (ManSynopsis (ManApply []))))
arg :: Text -> Text -> ManApplyArg
arg = ManApplyArg
args :: [ManApplyArg] -> MakeDictM c m () -> MakeDictM c m ()
args argList = censor (HM.map (addSynopsis (ManSynopsis (ManApply argList))))
opt :: Doc a -> Doc a -> Doc a
opt n d = n <+> "-" <+> d
examples :: ManExamples -> MakeDictM c m () -> MakeDictM c m ()
examples (ManExamples s) = censor (HM.map setExamples )
where
ex = ManExamples (Text.unlines $ Text.strip <$> Text.lines (Text.strip s))
ex0 = mempty { manExamples = [ex] }
setExamples (Bind w x) = Bind (Just (maybe ex0 (<>ex0) w)) x
splitForms :: [String] -> [[String]]
splitForms s0 = runIdentity $ S.toList_ (go mempty s0)
where
go acc ( "then" : rest ) = emit acc >> go mempty rest
go acc ( "and" : rest ) = emit acc >> go mempty rest
go acc ( x : rest ) | isPrefixOf "-" x = go ( x : acc ) rest
go acc ( x : rest ) | isPrefixOf "--" x = go ( x : acc ) rest
go acc ( x : rest ) = go ( x : acc ) rest
go acc [] = emit acc
emit = S.yield . reverse
applyLambda :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
)
=> [Id]
-> Syntax c
-> [Syntax c]
-> RunM c m (Syntax c)
applyLambda decl body args = do
when (length decl /= length args) do
throwIO (ArityMismatch @c nil)
ev <- mapM eval args
tv <- ask
d0 <- readTVarIO tv
forM_ (zip decl ev) $ \(n,v) -> do
bind n v
e <- eval body
atomically $ writeTVar tv d0
pure e
apply_ :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
)
=> Syntax c
-> [Syntax c]
-> RunM c m (Syntax c)
apply_ s args = case s of
ListVal [SymbolVal "builtin:lambda", SymbolVal n] -> apply n args
SymbolVal what -> apply what args
Lambda d body -> applyLambda d body args
e -> throwIO $ BadFormException @c s
apply :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
)
=> Id
-> [Syntax c]
-> RunM c m (Syntax c)
apply name args' = do
-- notice $ red "APPLY" <+> pretty name
what <- ask >>= readTVarIO <&> HM.lookup name
case bindAction <$> what of
Just (BindLambda e) -> mapM eval args' >>= e
Just (BindValue (Lambda argz body) ) -> do
applyLambda argz body args'
Just (BindValue _) -> do
throwIO (NotLambda name)
Nothing -> throwIO (NameNotBound name)
bind :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
)
=> Id
-> Syntax c
-> RunM c m ()
bind name expr = do
t <- ask
what <- case expr of
ListVal [SymbolVal "builtin:lambda", SymbolVal n] -> do
m <- readTVarIO t
HM.lookup n m & maybe (throwIO (NameNotBound n)) pure
e -> pure $ Bind mzero (BindValue e)
atomically do
modifyTVar t (HM.insert name what)
bindBuiltins :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
)
=> Dict c m
-> RunM c m ()
bindBuiltins dict = do
t <- ask
atomically do
modifyTVar t (<> dict)
eval :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
) => Syntax c -> RunM c m (Syntax c)
eval syn = handle (handleForm syn) $ do
dict <- ask >>= readTVarIO
case syn of
ListVal [ w, SymbolVal ".", b] -> do
pure $ mkList [w, b]
ListVal [ SymbolVal "quot", ListVal b] -> do
pure $ mkList b
ListVal [SymbolVal "define", SymbolVal what, e] -> do
ev <- eval e
bind what ev>> pure nil
ListVal [SymbolVal "lambda", arglist, body] -> do
pure $ mkForm @c "lambda" [ arglist, body ]
ListVal [SymbolVal "define", LambdaArgs (name : args), e] -> do
bind name ( mkForm @c "lambda" [ mkList [ mkSym s | s <- args], e ] )
pure nil
ListVal [SymbolVal "false?", e'] -> do
e <- eval e'
pure $ if isFalse e then mkBool True else mkBool False
ListVal [SymbolVal "if", w, e1, e2] -> do
what <- eval w
if isFalse what then eval e2 else eval e1
ListVal (SymbolVal "begin" : what) -> do
evalTop what
e@(ListVal (SymbolVal "blob" : what)) -> do
pure e
-- evalTop what
lc@(ListVal (Lambda decl body : args)) -> do
applyLambda decl body args
ListVal (SymbolVal name : args') -> do
apply name args'
SymbolVal (Id s) | Text.isPrefixOf ":" s -> do
pure (mkSym @c (Text.drop 1 s))
SymbolVal name | HM.member name dict -> do
let what = HM.lookup name dict
& maybe (BindValue (mkSym name)) bindAction
case what of
BindValue e -> pure e
BindLambda e -> pure $ mkForm "builtin:lambda" [mkSym name]
e@(SymbolVal name) | not (HM.member name dict) -> do
pure e
e@Literal{} -> pure e
e -> throwIO $ BadFormException @c e
where
handleForm syn = \case
(BadFormException _ :: BadFormException c) -> do
throwIO (BadFormException syn)
(ArityMismatch s :: BadFormException c) -> do
throwIO (ArityMismatch syn)
runM :: forall c m a. ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
) => Dict c m -> RunM c m a -> m a
runM d m = do
tvd <- newTVarIO d
runReaderT (fromRunM m) tvd
run :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c)
) => Dict c m -> [Syntax c] -> m (Syntax c)
run d sy = do
tvd <- newTVarIO d
lastDef nil <$> runReaderT (fromRunM (mapM eval sy)) tvd
evalTop :: forall c m . ( IsContext c
, MonadUnliftIO m
, Exception (BadFormException c))
=> [Syntax c]
-> RunM c m (Syntax c)
evalTop syn = lastDef nil <$> mapM eval syn
bindMatch :: Id -> ([Syntax c] -> RunM c m (Syntax c)) -> Dict c m
bindMatch n fn = HM.singleton n (Bind man (BindLambda fn))
where
man = Just $ mempty { manName = Just (manNameOf n) }
bindValue :: Id -> Syntax c -> Dict c m
bindValue n e = HM.singleton n (Bind mzero (BindValue e))
nil :: forall c . IsContext c => Syntax c
nil = List noContext []
nil_ :: (IsContext c, MonadIO m) => (a -> RunM c m b) -> a -> RunM c m (Syntax c)
nil_ m w = m w >> pure (List noContext [])
fixContext :: (IsContext c1, IsContext c2) => Syntax c1 -> Syntax c2
fixContext = go
where
go = \case
List _ xs -> List noContext (fmap go xs)
Symbol _ w -> Symbol noContext w
Literal _ l -> Literal noContext l
fmt :: Syntax c -> Doc ann
fmt = \case
LitStrVal x -> pretty $ Text.unpack x
x -> pretty x
internalEntries :: forall c m . (IsContext c, Exception (BadFormException c), MonadUnliftIO m) => MakeDictM c m ()
internalEntries = do
entry $ bindValue "false" (mkBool False)
entry $ bindValue "true" (mkBool True)
entry $ bindValue "chr:semi" (mkStr ";")
entry $ bindValue "chr:tilda" (mkStr "~")
entry $ bindValue "chr:colon" (mkStr ":")
entry $ bindValue "chr:comma" (mkStr ",")
entry $ bindValue "chr:q" (mkStr "'")
entry $ bindValue "chr:minus" (mkStr "-")
entry $ bindValue "chr:dq" (mkStr "\"")
entry $ bindValue "chr:lf" (mkStr "\n")
entry $ bindValue "chr:cr" (mkStr "\r")
entry $ bindValue "chr:tab" (mkStr "\t")
entry $ bindValue "chr:space" (mkStr " ")
brief "concatenates list of string-like elements into a string"
$ args [arg "list" "(list ...)"]
$ args [arg "..." "..."]
$ returns "string" ""
$ examples [qc|
(concat a b c d)
abcd|]
$ examples [qc|
(concat 1 2 3 4 5)
12345|]
$ entry $ bindMatch "concat" $ \syn -> do
case syn of
[ListVal xs] -> do
pure $ mkStr ( show $ hcat (fmap fmt xs) )
xs -> do
pure $ mkStr ( show $ hcat (fmap fmt xs) )
brief "creates a list of elements"
$ args [arg "..." "..."]
$ returns "list" ""
$ examples [qc|
(list 1 2 3 fuu bar "baz")
(1 2 3 fuu bar "baz")
|]
$ entry $ bindMatch "list" $ \case
es -> do
pure $ mkList es
entry $ bindMatch "dict" $ \case
(pairList -> es@(_:_)) -> do
pure $ mkForm "dict" es
[a, b] -> do
pure $ mkForm "dict" [ mkList [a, b] ]
_ -> throwIO (BadFormException @C nil)
brief "creates a dict from a linear list of string-like items"
$ args [arg "list-of-terms" "..."]
$ desc ( "macro; syntax sugar" <> line
<> "useful for creating function args" <> line
<> "leftover records are skipped"
)
$ returns "dict" ""
$ examples [qc|
[kw a 1 b 2 c 3]
(dict (a 1) (b 2) (c 3))
[kw a]
(dict (a ()))
[kw a b]
(dict (a b))
[kw 1 2 3]
(dict)
[kw a b c]
(dict (a b) (c ()))
|]
$ entry $ bindMatch "kw" $ \syn -> do
let wat = [ mkList @c [mkSym i, e] | (i,e) <- optlist syn ]
pure $ mkForm "dict" wat
entry $ bindMatch "iterate" $ nil_ $ \syn -> do
case syn of
[ListVal (SymbolVal "builtin:lambda" : SymbolVal fn : _), ListVal rs] -> do
mapM_ (apply @c fn . List.singleton) rs
[Lambda decl body, ListVal args] -> do
mapM_ (applyLambda decl body . List.singleton) args
_ -> do
throwIO (BadFormException @C nil)
entry $ bindMatch "repeat" $ nil_ $ \case
[LitIntVal n, Lambda [] b] -> do
replicateM_ (fromIntegral n) (applyLambda [] b [])
[LitIntVal n, e@(ListVal _)] -> do
replicateM_ (fromIntegral n) (eval e)
z ->
throwIO (BadFormException @C nil)
entry $ bindMatch "map" $ \syn -> do
case syn of
[ListVal (SymbolVal "builtin:lambda" : SymbolVal fn : _), ListVal rs] -> do
mapM (apply @c fn . List.singleton) rs
<&> mkList
[Lambda decl body, ListVal args] -> do
mapM (applyLambda decl body . List.singleton) args
<&> mkList
_ -> do
throwIO (BadFormException @C nil)
entry $ bindMatch "head" $ \case
[ ListVal es ] -> pure (head es)
_ -> throwIO (TypeCheckError @C nil)
brief "get tail of list"
$ args [arg "list" "list"]
$ desc "nil if the list is empty; error if not list"
$ examples [qc|
(tail [list 1 2 3])
(2 3)
(tail [list])
|]
$ entry $ bindMatch "tail" $ \case
[] -> pure nil
[ListVal []] -> pure nil
[ListVal es] -> pure $ mkList (tail es)
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "lookup" $ \case
[s, ListVal (SymbolVal "dict" : es) ] -> do
let val = headDef nil [ v | ListVal [k, v] <- es, k == s ]
pure val
[StringLike s, ListVal [] ] -> do
pure nil
_ -> throwIO (BadFormException @c nil)
brief "returns current unix time"
$ returns "int" "current unix time in seconds"
$ noArgs
$ entry $ bindMatch "now" $ \case
[] -> mkInt . round <$> liftIO getPOSIXTime
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "display" $ nil_ \case
[ sy ] -> display sy
ss -> display (mkList ss)
brief "prints new line character to stdout"
$ entry $ bindMatch "newline" $ nil_ $ \case
[] -> liftIO (putStrLn "")
_ -> throwIO (BadFormException @c nil)
brief "prints a list of terms to stdout"
$ entry $ bindMatch "print" $ nil_ $ \case
[ sy ] -> display sy
ss -> mapM_ display ss
entry $ bindMatch "println" $ nil_ $ \case
[ sy ] -> display sy >> liftIO (putStrLn "")
ss -> mapM_ display ss >> liftIO (putStrLn "")
entry $ bindMatch "str:read-stdin" $ \case
[] -> liftIO getContents <&> mkStr @c
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "str:put" $ nil_ $ \case
[LitStrVal s] -> liftIO $ TIO.putStr s
_ -> throwIO (BadFormException @c nil)
brief "reads file as a string" do
entry $ bindMatch "str:read-file" $ \case
[StringLike fn] -> liftIO (TIO.readFile fn) <&> mkStr
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "str:save" $ nil_ \case
[StringLike fn, StringLike what] ->
liftIO (writeFile fn what)
_ -> throwIO (BadFormException @c nil)
entry $ bindValue "space" $ mkStr " "
entry $ bindMatch "parse-top" $ \case
[SymbolVal w, LitStrVal s] -> do
pure $ parseTop s & either (const nil) (mkForm w . fmap fixContext)
[LitStrVal s] -> do
pure $ parseTop s & either (const nil) (mkList . fmap fixContext)
_ -> throwIO (BadFormException @c nil)
let atomFrom = \case
[StringLike s] -> pure (mkSym s)
[e] -> pure (mkSym $ show $ pretty e)
_ -> throwIO (BadFormException @c nil)
brief "type of argument"
$ args [arg "term" "term"]
$ returns "symbol" "type"
$ entry $ bindMatch "type" \case
[ListVal _] -> pure $ mkSym "list"
[SymbolVal _] -> pure $ mkSym "symbol"
[LitStrVal _] -> pure $ mkSym "string"
[LitIntVal _] -> pure $ mkSym "int"
[LitScientificVal _] -> pure $ mkSym "float"
[LitBoolVal _] -> pure $ mkSym "bool"
_ -> throwIO (BadFormException @c nil)
brief "creates a symbol from argument"
$ args [arg "any-term" "term"]
$ returns "symbol" ""
do
entry $ bindMatch "sym" atomFrom
entry $ bindMatch "atom" atomFrom
brief "compares two terms" $
args [arg "term" "a", arg "term" "b"] $
returns "boolean" "#t if terms are equal, otherwise #f" $
entry $ bindMatch "eq?" $ \case
[a, b] -> do
pure $ if a == b then mkBool True else mkBool False
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "length" $ \case
[ListVal es] -> pure $ mkInt (length es)
[StringLike es] -> pure $ mkInt (length es)
_ -> pure $ mkInt 0
entry $ bindMatch "nil?" $ \case
[ListVal []] -> pure $ mkBool True
_ -> pure $ mkBool False
entry $ bindMatch "not" $ \case
[w] -> do
pure $ if isFalse w then mkBool True else mkBool False
_ -> throwIO (BadFormException @c nil)
brief "get system environment"
$ args []
$ args [ arg "string" "string" ]
$ returns "env" "single var or dict of all vars"
$ examples [qc|
(env HOME)
/home/user
(env)
(dict
(HOME "/home/user") ... (CC "gcc") ...)
|]
$ entry $ bindMatch "env" $ \case
[] -> do
s <- liftIO getEnvironment
pure $ mkForm "dict" [ mkList [mkSym @c a, mkStr b] | (a,b) <- s ]
[StringLike s] -> do
liftIO (lookupEnv s)
<&> maybe nil mkStr
_ -> throwIO (BadFormException @c nil)
-- FIXME: we-need-opaque-type
entry $ bindMatch "blob:read-stdin" $ \case
[] -> do
blob <- liftIO BS8.getContents <&> BS8.unpack
pure (mkForm "blob" [mkStr @c blob])
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "blob:read-file" $ \case
[StringLike fn] -> do
blob <- liftIO (BS8.readFile fn) <&> BS8.unpack
pure (mkForm "blob" [mkStr @c blob])
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "blob:save" $ nil_ $ \case
[StringLike fn, ListVal [SymbolVal "blob", LitStrVal t]] -> do
let s = Text.unpack t & BS8.pack
liftIO $ BS8.writeFile fn s
_ -> throwIO (BadFormException @c nil)
entry $ bindMatch "blob:put" $ nil_ $ \case
[ListVal [SymbolVal "blob", LitStrVal t]] -> do
let s = Text.unpack t & BS8.pack
liftIO $ BS8.putStr s
_ -> throwIO (BadFormException @c nil)

View File

@ -0,0 +1,211 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE PatternSynonyms #-}
{-# Language ViewPatterns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Data.Config.Suckless.Syntax
( Syntax(..)
, Id(..)
, Literal(..)
, HasContext
, C(..)
, Context(..)
, IsContext(..)
, IsLiteral(..)
, pattern SymbolVal
, pattern ListVal
, pattern LitIntVal
, pattern LitStrVal
, pattern LitBoolVal
, pattern LitScientificVal
, pattern StringLike
, pattern StringLikeList
, pattern Nil
)
where
import Data.Data
import Data.Kind
import Data.String
import Data.Text (Text)
import Data.Scientific
import GHC.Generics (Generic(..))
import Data.Maybe
-- import GHC.Generics( Fixity(..) )
-- import Data.Data as Data
import Data.Aeson
import Data.Aeson.Key
import Data.Aeson.KeyMap qualified as Aeson
import Data.Vector qualified as V
import Data.Traversable (forM)
import Data.Text qualified as Text
import Data.Function
import Prettyprinter
pattern SymbolVal :: Id -> Syntax c
pattern SymbolVal v <- Symbol _ v
-- pattern LitVal :: forall {c}. Id -> Li
pattern LitIntVal :: Integer -> Syntax c
pattern LitIntVal v <- Literal _ (LitInt v)
pattern LitScientificVal :: Scientific -> Syntax c
pattern LitScientificVal v <- Literal _ (LitScientific v)
pattern LitStrVal :: Text -> Syntax c
pattern LitStrVal v <- Literal _ (LitStr v)
pattern LitBoolVal :: Bool -> Syntax c
pattern LitBoolVal v <- Literal _ (LitBool v)
pattern ListVal :: [Syntax c] -> Syntax c
pattern ListVal v <- List _ v
stringLike :: Syntax c -> Maybe String
stringLike = \case
LitStrVal s -> Just $ Text.unpack s
SymbolVal (Id s) -> Just $ Text.unpack s
_ -> Nothing
stringLikeList :: [Syntax c] -> [String]
stringLikeList syn = [ stringLike s | s <- syn ] & takeWhile isJust & catMaybes
pattern StringLike :: forall {c} . String -> Syntax c
pattern StringLike e <- (stringLike -> Just e)
pattern StringLikeList :: forall {c} . [String] -> [Syntax c]
pattern StringLikeList e <- (stringLikeList -> e)
pattern Nil :: forall {c} . Syntax c
pattern Nil <- ListVal []
data family Context c :: Type
class IsContext c where
noContext :: Context c
data instance Context () = EmptyContext
instance IsContext () where
noContext = EmptyContext
class HasContext c a where
class IsLiteral a where
mkLit :: a -> Literal
newtype Id =
Id Text
deriving newtype (IsString,Pretty)
deriving stock (Data,Generic,Show,Eq,Ord)
data Literal =
LitStr Text
| LitInt Integer
| LitScientific Scientific
| LitBool Bool
deriving stock (Eq,Ord,Data,Generic,Show)
instance IsLiteral Text where
mkLit = LitStr
instance IsLiteral Bool where
mkLit = LitBool
instance IsLiteral Integer where
mkLit = LitInt
data C = C
deriving stock (Eq,Ord,Show,Data,Typeable,Generic)
-- simple, yet sufficient context
-- Integer may be offset, maybe line number,
-- token number, whatever
-- it's up to parser to use this context for
-- error printing, etc
newtype instance (Context C) =
SimpleContext { fromSimpleContext :: Maybe Integer }
deriving stock (Eq,Ord,Show,Data,Typeable,Generic)
instance IsContext C where
noContext = SimpleContext Nothing
data Syntax c
= List (Context c) [Syntax c]
| Symbol (Context c) Id
| Literal (Context c) Literal
deriving stock (Generic,Typeable)
instance Eq (Syntax c) where
(==) (Literal _ a) (Literal _ b) = a == b
(==) (Symbol _ a) (Symbol _ b) = a == b
(==) (List _ a) (List _ b) = a == b
(==) _ _ = False
deriving instance (Data c, Data (Context c)) => Data (Syntax c)
instance Pretty (Syntax c) where
pretty (Literal _ ast) = pretty ast
pretty (Symbol _ s) = pretty s
pretty (List _ (x:xs)) = parens $ align $ sep ( fmap pretty (x:xs) )
pretty (List _ []) = parens mempty
instance Pretty Literal where
pretty = \case
LitStr s -> dquotes (pretty s)
LitInt i -> pretty i
LitScientific v -> viaShow v
LitBool b | b -> "#t"
| otherwise -> "#f"
instance ToJSON Literal where
toJSON (LitStr s) = String s
toJSON (LitInt i) = Number (fromInteger i)
toJSON (LitScientific s) = Number s
toJSON (LitBool b) = Bool b
instance ToJSON (Syntax c) where
toJSON (Symbol _ (Id "#nil")) = Null
toJSON (Symbol _ (Id s)) = String s
toJSON (Literal _ l) = toJSON l
toJSON (List _ items) =
case items of
(Symbol _ "object" : rest) ->
object $ mapMaybe pairToKeyValue rest
_ -> Array . V.fromList $ fmap toJSON items
where
pairToKeyValue :: Syntax c -> Maybe (Key, Value)
pairToKeyValue (List _ [SymbolVal (Id k), SymbolVal ":", v]) = Just (fromText k .= toJSON v)
pairToKeyValue (List _ [LitStrVal k, SymbolVal ":", v]) = Just (fromText k .= toJSON v)
pairToKeyValue _ = Nothing
instance IsContext c => FromJSON (Syntax c) where
parseJSON (String t) = pure $ Literal noContext (LitStr t)
parseJSON (Number n)
| isInteger n = pure $ Literal noContext (LitInt (floor n))
| otherwise = pure $ Literal noContext (LitScientific n)
parseJSON (Bool b) = pure $ Literal noContext (LitBool b)
parseJSON (Array a) = List noContext <$> mapM parseJSON (V.toList a)
parseJSON (Object o) = do
pairs <- forM (Aeson.toList o) $ \(key, value) -> do
valueSyntax <- parseJSON value
pure $ List noContext [ Symbol noContext (Id (toText key))
, Symbol noContext ":"
, valueSyntax
]
pure $ List noContext (Symbol noContext (Id "object") : pairs)
parseJSON _ = fail "Cannot parse JSON to Syntax"

View File

@ -0,0 +1 @@
module Data.Config.Suckless.Types where

143
suckless-conf.cabal Normal file
View File

@ -0,0 +1,143 @@
cabal-version: 3.0
name: suckless-conf
version: 0.1.2.9
-- synopsis:
-- description:
license: BSD-3-Clause
license-file: LICENSE
author: Dmitry Zuikov
maintainer: dzuikov@gmail.com
-- copyright:
category: Text
build-type: Simple
extra-doc-files: CHANGELOG.md
-- extra-source-files:
common shared-properties
ghc-options:
-Wall
-- -fno-warn-unused-matches
-- -fno-warn-unused-do-bind
-- -Werror=missing-methods
-- -Werror=incomplete-patterns
-- -fno-warn-unused-binds
-- -threaded
-- -rtsopts
-- "-with-rtsopts=-N4 -A64m -AL256m -I0"
default-language: Haskell2010
default-extensions:
ApplicativeDo
, BangPatterns
, BlockArguments
, ConstraintKinds
, DataKinds
, DeriveDataTypeable
, DeriveGeneric
, DerivingStrategies
, DerivingVia
, ExtendedDefaultRules
, FlexibleContexts
, FlexibleInstances
, GADTs
, GeneralizedNewtypeDeriving
, ImportQualifiedPost
, LambdaCase
, MultiParamTypeClasses
, OverloadedStrings
, QuasiQuotes
, ScopedTypeVariables
, StandaloneDeriving
, TupleSections
, TypeApplications
, TypeFamilies
, ImportQualifiedPost
library
import: shared-properties
exposed-modules:
Data.Config.Suckless
, Data.Config.Suckless.Syntax
, Data.Config.Suckless.Parse
, Data.Config.Suckless.KeyValue
, Data.Config.Suckless.Script
, Data.Config.Suckless.Script.File
other-modules:
Data.Config.Suckless.Types
, Data.Config.Suckless.Parse.Fuzzy
, Data.Config.Suckless.Script.Internal
-- other-extensions:
build-depends: base
, aeson
, bytestring
, containers
, directory
, filepath
, filepattern
, fuzzy-parse >= 0.1.3.1
, hashable
, interpolatedstring-perl6
, microlens-platform
, mtl
, prettyprinter
, prettyprinter-ansi-terminal
, safe
, scientific
, streaming
, stm
, text
, time
, transformers
, unliftio
, unordered-containers
, vector
hs-source-dirs: lib
default-language: Haskell2010
test-suite spec
import: shared-properties
type: exitcode-stdio-1.0
main-is: Spec.hs
other-modules:
Data.Config.Suckless.KeyValueSpec
Data.Config.Suckless.AesonSpec
hs-source-dirs:
test
ghc-options:
-Wall
-threaded
-rtsopts
-with-rtsopts=-N
build-tool-depends:
hspec-discover:hspec-discover
build-depends: base
, hspec
, aeson
, scientific
, suckless-conf
, fuzzy-parse >= 0.1.3.1
, containers
, mtl
, text
, prettyprinter
, interpolatedstring-perl6
, tasty-hunit
default-language: Haskell2010
default-extensions:
DerivingStrategies
, FlexibleInstances
, MultiParamTypeClasses
, OverloadedStrings
, ScopedTypeVariables
, TypeApplications

23
t/fixme-config-1 Normal file
View File

@ -0,0 +1,23 @@
;; fixme config file
fixme-comments // # --
fixme-prefix FIXME: bugs issues
fixme-prefix TODO: bugs issues
fixme-files **/*.hs
fixme-files doc/devlog
fixme-files-ignore .direnv/** dist-newstyle/**
fixme-id-show-len 10
; fixme-tag-prefix #
fixme-list-full-row-pref "## "
fixme-list-full-row-suff "\n\n;;;"

4
t/fixme-log Normal file
View File

@ -0,0 +1,4 @@
; fixme log file
fixme-merged BbjfCj H4epFBNr2i

1
t/just-comment Normal file
View File

@ -0,0 +1 @@
; blah-blah

34
t/key-value-test-config Normal file
View File

@ -0,0 +1,34 @@
; comment
foo "a"
bar "a"
bar "b"
int1 122
int2 0
int3 -22
int4 0xFAFA
int5 0b11111111
int6 -0xFAFA
(jopa-kita)
(sci1 1e9)
(sci2 0.003)
(sci3 -0.001)
(sci4 -2e11)
(sci5 -2e-3)
(wtf1 .001)
some-object {object ( key : 42) }
{another-object
(object
( key1 : 42 )
( key2 : #f )
( key3 : [ 1 2 3 4 ] )
)
}

View File

@ -0,0 +1,212 @@
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Data.Config.Suckless.AesonSpec (spec) where
import Data.Config.Suckless.KeyValue
import Data.Config.Suckless.Parse
import Data.Config.Suckless.Syntax
import Data.Functor
import Data.Function
import Data.Scientific
import Data.Text (Text)
import Data.Text.IO qualified as Text
import GHC.Generics hiding (C)
import Text.InterpolatedString.Perl6 (qc,q)
import Data.Aeson
import Data.Maybe
import Control.Monad.Reader
import Test.Hspec
import Test.Tasty.HUnit
import Prettyprinter
readConfig :: Text -> IO [Syntax C]
readConfig s = do
pure $ parseTop s & either mempty id
-- print $ pretty f
-- pure f
data SomeData =
SomeData
{ someDataKey1 :: Int
, someDataKey2 :: String
, someDataKey3 :: [Scientific]
}
deriving stock (Generic,Show,Eq)
instance ToJSON SomeData
instance FromJSON SomeData
data Port
data Users
instance HasCfgKey Port (Maybe Int)
where key = "port"
instance HasCfgKey Users [Value]
where key = "basic-users"
spec :: Spec
spec = do
describe "toJSON" $ do
it "reads int" $ do
c <- readConfig [qc|1|] <&> toJSON
c `shouldBe` toJSON [[1::Int]]
it "reads scientific" $ do
c <- readConfig [qc|1.00|] <&> toJSON
c `shouldBe` toJSON [[1.00 :: Scientific]]
it "reads bool" $ do
t <- readConfig [qc|#t|] <&> toJSON . head
t `shouldBe` toJSON [Bool True]
f <- readConfig [qc|#f|] <&> toJSON . head
f `shouldBe` toJSON [Bool False]
it "reads string" $ do
s <- readConfig [qc|"somestring"|] <&> toJSON
s `shouldBe` toJSON [["somestring" :: String]]
it "reads array" $ do
s <- readConfig [qc|(1 2 3 4)|] <&> toJSON . head
print s
s `shouldBe` toJSON [1::Int,2,3,4]
it "reads simple object" $ do
s <- readConfig [qc|
(object
(key1 : 22)
(key2 : #f)
(key3 : [1 2 3 4])
(key4 : (object (o1 : "bebe")) )
("fafa" : "fifa")
(none : #nil)
)
|] <&> toJSON . head
let s1 = decode @Value [q|
{
"key1": 22,
"key2": false,
"key3": [1, 2, 3, 4],
"key4": {
"o1": "bebe"
},
"fafa" : "fifa",
"none" : null
}
|]
print s
print s1
Just s `shouldBe` s1
it "serializes object to syntax" $ do
let some = SomeData 1 "some-data" [1, 2, 3, 4, 5, 10]
let someSyn = case fromJSON @(Syntax ()) (toJSON some) of
Success syn -> Just syn
_ -> Nothing
print $ pretty someSyn
let json = fromJust $ someSyn <&> toJSON
let someObject = fromJSON @SomeData json
print someObject
someObject `shouldBe` Success some
it "read-real-config" do
let cfg = [q|
port 3000
hbs2-url "http://localhost:5001"
default-token-name "LCOIN"
hbs2-keyring "/home/hbs2/lcoin-adapter/secrets/hbs2.key"
; old test thermoland reflog
hbs2-keyring "/home/hbs2/lcoin-adapter/secrets/termoland-reflog-GX8gmPi2cAxxgnaKmLmR5iViup1BNkwpdCCub3snLT1y.key"
; new test thermoland reflog
hbs2-keyring "/home/hbs2/lcoin-adapter/secrets/termoland-reflog-AdowWzo4iW1JejHFRnPnxQWNot8uL5sciFup6RHx2gZG.key"
hbs2-keyring "/home/hbs2/keys/lcoin-belorusskaya-JAiAjKzfWfTGXjuSf4GXaj44cWfDQ8vifxoQU3tq5hn7.key"
hbs2-keyring "/home/hbs2/keys/lcoin-krymskaya-CEDBX2niVK3YL7WxzLR3xj8iUNHa9GU2EfXUqDU7fSGK.key"
hbs2-keyring "/home/hbs2/keys/lcoin-ushakova-GyTXGiCUJu81CMXYZzs7RhHu4vxJnLYgT3n2neXG5uaY.key"
hbs2-keyring "/home/hbs2/keys/lcoin-zelenopark-4fFvFGzQRp2WSXtDHepeJvMtCfQdSASq9qmsELWRJDgv.key"
jwk-path "/home/hbs2/lcoin-adapter/secrets/jwk/public_key.jwk"
jwk-path "/home/hbs2/lcoin-adapter/secrets/jwk/public-key-2023-11-03.jwk"
lcoin-rate 5
db-path "/home/hbs2/.local/share/lcoin-adapter/state.db"
registration-bonus 500
log-file "/home/hbs2/lcoin-adapter/log.txt"
; qblf-socket "/tmp/qblf.socket"
qblf-treasure "64zvWqGUf57WmGCTFWrVaNEqXikUocGyKFtg5mhyWCiB"
reports-ignore-key "DyKWNLvpRSsTsJfVxTciqxnCJ6UhF4Mf6WoMw5qkftG4"
reports-ignore-key "3MjGvpffawUijHxbbsaF9J6wt4YReRdArUCTfHo1RhSm"
;; v2
db-journal "/tmp/lcoin-adapter-journal.sqlite"
db-cache "/tmp/lcoin-adapter-cache-db.sqlite"
hbs2-store "/tmp/hbs2-store"
treasure "64zvWqGUf57WmGCTFWrVaNEqXikUocGyKFtg5mhyWCiB"
keybox "http://localhost:8034/"
dev-env false
(jwk-keys (
"/home/hbs2/lcoin-adapter/secrets/jwk/public_key.jwk"
"/home/hbs2/lcoin-adapter/secrets/jwk/public-key-2023-11-03.jwk"
))
(basic-users (
(object (name "mobile") (pass "mobile-pass"))
(object (name "termo") (pass "termo-pass"))
))
(client-creator "BYVqWJdn18Q3AjmJBPw2yusZ5ouNmgiRydWQgBEh684J")
(client-creator-keyring "/home/hbs2/keys/journal/client-creator_BYVqWJdn18Q3AjmJBPw2yusZ5ouNmgiRydWQgBEh684J.key")
(coin-minter "4Gnno5yXUbT5dwfphKtDW7dWeq4uBvassSdbVvB3y67p")
(coin-minter-keyring "/home/hbs2/keys/journal/coin-minter_4Gnno5yXUbT5dwfphKtDW7dWeq4uBvassSdbVvB3y67p.key")
|] :: Text
let what = parseTop cfg & either (error.show) id
let pno = runReader (cfgValue @Port @(Maybe Int)) what
-- what
assertEqual "pno" pno (Just 3000)

View File

@ -0,0 +1,184 @@
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Data.Config.Suckless.KeyValueSpec (spec) where
import Data.Function
import Control.Monad.IO.Class
import Control.Monad.Reader
import Data.Config.Suckless.KeyValue
import Data.Config.Suckless.Parse
import Data.Config.Suckless.Syntax
import Data.Functor
import Data.Scientific
import Data.Text.IO qualified as Text
import Data.Set (Set)
import qualified Data.Set as Set
import Prettyprinter
import Data.Aeson
import Text.InterpolatedString.Perl6 (qc,q)
import Test.Hspec
data FirstKey
data SecondKey
data ThirdKey
data Int1
data Int2
data Int3
data Int4
data Int5
data Int6
data Sci1
data Sci2
data Sci3
data Sci4
data Sci5
data O1
data O2
instance HasCfgKey FirstKey (Maybe String) where
key = "foo"
instance HasCfgKey SecondKey (Set String) where
key = "bar"
instance HasCfgKey ThirdKey (Maybe String) where
key = "baz"
instance HasCfgKey Int1 b where
key = "int1"
instance HasCfgKey Int2 b where
key = "int2"
instance HasCfgKey Int3 b where
key = "int3"
instance HasCfgKey Int4 b where
key = "int4"
instance HasCfgKey Int5 b where
key = "int5"
instance HasCfgKey Int6 b where
key = "int6"
instance HasCfgKey Sci1 b where
key = "sci1"
instance HasCfgKey Sci2 b where
key = "sci2"
instance HasCfgKey Sci3 b where
key = "sci3"
instance HasCfgKey Sci4 b where
key = "sci4"
instance HasCfgKey Sci5 b where
key = "sci5"
instance HasCfgKey O1 b where
key = "some-object"
instance HasCfgKey O2 b where
key = "another-object"
instance HasConf IO where
getConf = liftIO readConfig
readConfig :: IO [Syntax C]
readConfig = do
let configFilePath = "t/key-value-test-config"
f <- Text.readFile configFilePath <&> parseTop <&> either mempty id
print $ pretty f
pure f
spec :: Spec
spec = do
describe "config parsing" $ do
it "reads string" $ do
firstValue <- cfgValue @FirstKey @(Maybe String)
firstValue `shouldBe` Just "a"
it "reads a set of strings" $ do
secondValue <- cfgValue @SecondKey @(Set String)
secondValue `shouldBe` Set.fromList ["a", "b"]
it "reads nothing" $ do
thridValue <- cfgValue @ThirdKey @(Maybe String)
thridValue `shouldBe` Nothing
it "reads ints" $ do
x1 <- cfgValue @Int1 @(Maybe Integer)
x1 `shouldBe` Just 122
x2 <- cfgValue @Int2
x2 `shouldBe` Just (0 :: Integer)
x3 <- cfgValue @Int3
x3 `shouldBe` Just (-22 :: Integer)
x4 <- cfgValue @Int4 @(Maybe Integer)
x4 `shouldBe` Just 0xFAFA
x5 <- cfgValue @Int5 @(Maybe Integer)
x5 `shouldBe` Just 255
x6 <- cfgValue @Int6 @(Maybe Integer)
x6 `shouldBe` Just (-0xFAFA)
it "reads scientifics" $ do
x1 <- cfgValue @Sci1 @(Maybe Scientific)
x1 `shouldBe` Just 1e9
x2 <- cfgValue @Sci2 @(Maybe Scientific)
x2 `shouldBe` Just 0.003
-- x3 <- cfgValue @Sci3 @(Maybe Scientific)
-- x3 `shouldBe` Just (-0.001)
x4 <- cfgValue @Sci4 @(Maybe Scientific)
x4 `shouldBe` Just (-2e11)
x5 <- cfgValue @Sci5 @(Maybe Scientific)
x5 `shouldBe` Just (-2e-3)
it "reads objects" $ do
o1 <- cfgValue @O1 @(Maybe Value)
let wtf1 = [q|{ "key" : 42 }|]
o1 `shouldBe` decode wtf1
let wtf2 = [q|
{ "key1" : 42
, "key2" : false
, "key3" : [ 1, 2, 3, 4]
}
|]
o2 <- cfgValue @O2 @(Maybe Value)
o2 `shouldBe` decode wtf2
it "works in Reader" $ do
let cfg = [qc|
int1 123
|]
let conf = parseTop cfg & either mempty id
let v = runReader (cfgValue @Int1 @(Maybe Int)) conf
v `shouldBe` Just 123

1
test/Spec.hs Normal file
View File

@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}