interp (empty) → 1.0.0.0
raw patch · 16 files changed
+736/−0 lines, 16 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, containers, hspec, interp, megaparsec, mtl, optparse-applicative, parser-combinators, random-fu, rvar, semigroups, text, transformers, unordered-containers, vector
Files
- ChangeLog.md +3/−0
- LICENSE +30/−0
- README.md +91/−0
- Setup.hs +2/−0
- app/Main.hs +19/−0
- interp.cabal +117/−0
- src/Data/Text/Interp.hs +34/−0
- src/Data/Text/Interp/CLI.hs +37/−0
- src/Data/Text/Interp/Get.hs +34/−0
- src/Data/Text/Interp/Interpolate.hs +49/−0
- src/Data/Text/Interp/Parse.hs +58/−0
- src/Data/Text/Interp/Types.hs +66/−0
- test/GetSpec.hs +63/−0
- test/InterpolateSpec.hs +69/−0
- test/ParseSpec.hs +63/−0
- test/Spec.hs +1/−0
+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Changelog for interp++## Unreleased changes
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright sam raker (c) 2019++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 sam raker 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.
+ README.md view
@@ -0,0 +1,91 @@+# interp++A randomized text-interpolation tool inspired by [Tracery](https://tracery.io), in Haskell.++### Installation+#### Prerequisites+ * Haskell+ * [Stack](https://www.haskellstack.org), unless you can work Cabal yourself++Clone this repo, `cd` into it, and then:+ * if you want to use the CLI, run `stack install`+ * if you want to use it as a library, just run `stack build`++#### Development+Fork this repo, make some changes, run `stack test` to make sure everything's ok, then (if you want) submit a PR. Thank you!++### Use+#### CLI+```+Usage: interp (--substitutions ARG | SUBST) (--interpolations ARG | INTERP)+ Randomly interpolate values into a template++Available options:+ --substitutions ARG JSON file containing map of substitutions+ SUBST JSON file containing map of substitutions+ --interpolations ARG file containing text to interpolate+ INTERP file containing text to interpolate+ -h,--help Show this help text+```++`substitutions` is the (path to the) file containing the JSON map of substitutions. `interpolations` is (path to the) file containing the text to interpolate. See below for more on both.++#### Library+`Data.Text.Interp.Interpolate.interp` is probably what you'll want to use. It takes a `Subst` object (usually parsed from JSON, but you can make one yourself) and a `NonEmpty` list of `IText`s. Check out `Data.Text.Interp.Types` for more on those types.++### Syntax+In both substitution and interpolation files, keys can be comprised of any letter or number, or the characters "\_" or "-". Spaces and other punctuation are not allowed and will give you Problems.+#### Substitutions+```+{"coolThings": [+ {"name": "computers",+ "reason": "you can do haskell on them"+ },+ {"name": "dogs",+ "reason": "they're good"+ }+ ],+ "badThings": [+ {"name": "flat tires",+ "reason": "they make you late"+ },+ {"name": "jobs",+ "reason": "they make you tired"+ }+ ],+ "sam": {+ "likes": "computers"+}+```+The substitutions file is a JSON file. JSON maps are treated how one would expect. The values in a JSON array are chosen among randomly (but see also below on binding). Regular values are treated regularly, but note:+ * because of how JSON is parsed, _all_ numeric literals are treated as floating-point, so a `1` will be interpolated as `"1.0"`. If this is an issue, just wrap your numbers in quotes and they'll be treated as strings.+ * `null` values are explicitly forbidden; empty lists and maps should be avoided.++#### Interpolations+`{{ ... }}` sets off a thing to interpolate. Keys, corresponding to keys in the substitutions file, can be period-separated to 'dig into' nested maps. Thus, in the example above, `sam.likes` will be have the value `"computers"`.++##### Binding+Within braces, values can be bound to variables using the syntax `(varToBind#keys.to.bind).other.keys`. When a variable is bound, references to that variable will be interpolated with that same value. Thus, given the interpolation template+```+{{ (cool#coolThings).name }} are cool because {{ cool.reason }}. {{ (bad#badThings).name }} are bad because {{ bad.reason }}.+```+`cool` in the first interpolation will be the same as `cool` in the second. Note that keys within the parentheses are part of the binding, while those after the closing parentheses are interpolated normally--`{{ (cool#coolThings).name }}` will be either `"computers"` or `"dogs"`, while `{{ cool.reason }}` will be `"you can do haskell on them"` or `"they're good"`, depending on which sub-map was randomly chosen to be bound to `cool`.++Binding is primarily useful for map values. It's possible to bind arrays, but since array elements are chosen randomly evey time, there's not much of a point. Similarly, there's no real difference between binding a simple value and just using the full path to it, unless you're trying to save characters.+++### Etc.+#### To-Do+ - [ ] Haddock documentation/comments+ - [x] Add positional args in CLI+ - [ ] Other formats (e.g. YAML) for substitutions file+ - [ ] More features+ - [ ] Preprocessing (prepending "a(n)", title casing, etc.)+ - [ ] Other stuff??+ - [ ] Check Tracery documentation for stuff to steal+ - [ ] Web interface??+ - [ ] Come up with a better name+ - [ ] Publish to Hackage++#### Contributing+PRs, issues, feature requests, etc. are always more than welcome! Feel free to hit me up on [twitter](https://twitter.com/swizzard) or email me at sam dot raker at gmail dot com. Let me know if you like this, hate this, wish it were better, wish it were worse, whatever.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,19 @@+{-# LANGUAGE LambdaCase #-}+module Main where++import Control.Monad.Except+import Data.Text (unpack, Text)+import Options.Applicative+import System.IO++import Data.Text.Interp (doInterp, opts, Input(..))+++run :: Input -> IO (Either Text Text)+run (Input sf tf) = runExceptT $ doInterp sf tf+++main :: IO ()+main = execParser opts >>= run >>= \case+ (Left e) -> hPutStrLn stderr $ unpack e+ (Right v) -> hPutStrLn stdout $ unpack v
+ interp.cabal view
@@ -0,0 +1,117 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: 2c5ae1e8028bb10c6d701cd2ccc86981da63658513595f95c99c55d09c26074e++name: interp+version: 1.0.0.0+synopsis: Tracery-like randomized text interpolation+description: Please see the README on GitHub at <https://github.com/swizzard/interp#readme>+category: Text, Interpolation+homepage: https://github.com/swizzard/interp#readme+bug-reports: https://github.com/swizzard/interp/issues+author: sam raker+maintainer: sam.raker@gmail.com+copyright: (c) 2019 sam raker+license: BSD3+license-file: LICENSE+build-type: Simple+extra-source-files:+ README.md+ ChangeLog.md++source-repository head+ type: git+ location: https://github.com/swizzard/interp++library+ exposed-modules:+ Data.Text.Interp+ Data.Text.Interp.CLI+ Data.Text.Interp.Get+ Data.Text.Interp.Interpolate+ Data.Text.Interp.Parse+ Data.Text.Interp.Types+ other-modules:+ Paths_interp+ hs-source-dirs:+ src+ build-depends:+ aeson+ , base >=4.7 && <5+ , bytestring+ , containers+ , hspec+ , megaparsec+ , mtl+ , optparse-applicative+ , parser-combinators+ , random-fu+ , rvar+ , semigroups+ , text+ , transformers+ , unordered-containers+ , vector+ default-language: Haskell2010++executable interp+ main-is: Main.hs+ other-modules:+ Paths_interp+ hs-source-dirs:+ app+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ aeson+ , base >=4.7 && <5+ , bytestring+ , containers+ , hspec+ , interp+ , megaparsec+ , mtl+ , optparse-applicative+ , parser-combinators+ , random-fu+ , rvar+ , semigroups+ , text+ , transformers+ , unordered-containers+ , vector+ default-language: Haskell2010++test-suite interp-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ GetSpec+ InterpolateSpec+ ParseSpec+ Paths_interp+ hs-source-dirs:+ test+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ aeson+ , base >=4.7 && <5+ , bytestring+ , containers+ , hspec+ , interp+ , megaparsec+ , mtl+ , optparse-applicative+ , parser-combinators+ , random-fu+ , rvar+ , semigroups+ , text+ , transformers+ , unordered-containers+ , vector+ default-language: Haskell2010
+ src/Data/Text/Interp.hs view
@@ -0,0 +1,34 @@+module Data.Text.Interp+ ( opts+ , doInterp+ , Input(..)+ ) where++import Control.Monad.Except+import Control.Monad.Trans (lift)+import Data.Aeson+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T+import System.IO (FilePath)+import Text.Megaparsec++import Data.Text.Interp.CLI (Input(..), opts)+import Data.Text.Interp.Interpolate (interp)+import Data.Text.Interp.Parse (itP)+import Data.Text.Interp.Types (I)+++-- | Generate interpolated text within a `Data.Text.Interp.Types.I` monad+doInterp :: FilePath -- ^ path to substitutions file+ -> FilePath -- ^ path to interpolations file+ -> I Text -- ^ resulting interpolated text+doInterp sf tf = do+ i <- parse itP "" <$> T.strip <$> (lift $ T.readFile tf)+ case i of+ (Left e) -> throwError . T.pack $ errorBundlePretty e+ (Right i') -> do+ s <- lift $ eitherDecodeFileStrict sf+ case s of+ (Left e) -> throwError $ T.pack e+ (Right s') -> interp s' i'
+ src/Data/Text/Interp/CLI.hs view
@@ -0,0 +1,37 @@+module Data.Text.Interp.CLI+ ( Input(..)+ , opts+ ) where++import Data.Semigroup ((<>))+import Options.Applicative+import System.IO (FilePath)+++-- | Record to hold CLI options+data Input = Input {+ substF :: FilePath -- ^ path to the substitutions file+ , interpF :: FilePath -- ^ path to the interpolations file+ }++-- | CLI parser for `Input`+input :: Parser Input+input = Input <$> (+ (strOption $ long "substitutions"+ <> help "JSON file containing map of substitutions")+ <|>+ (strArgument $ metavar "SUBST"+ <> help "JSON file containing map of substitutions")+ ) <*> (+ (strOption $ long "interpolations"+ <> help "file containing text to interpolate")+ <|>+ (strArgument $ metavar "INTERP"+ <> help "file containing text to interpolate"))++-- | ParserInfo for `Input`+opts :: ParserInfo Input+opts = info (input <**> helper)+ (fullDesc+ <> progDesc "Randomly interpolate values into a template"+ <> header "interp")
+ src/Data/Text/Interp/Get.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}+module Data.Text.Interp.Get+ ( get+ ) where++import Control.Monad.Except+import qualified Data.Map.Strict as M+import Data.Random (MonadRandom, randomElement)+import Data.RVar (sampleRVar)+import Data.Text (Text, append)++import Data.Text.Interp.Types+++-- | Randomly retrieve a value out of a `Data.Text.Interp.Types.Subst` mapping+get :: Subst -- ^ source mapping+ -> [Key] -- ^ a list of `Data.Text.Interp.Types.Key`s leading to the value+ -> BindingMap -- ^ the `Data.Text.Interp.Types.BindingMap` to check for bound values in+ -> I Subst -- ^ retrieved value+get v@(SubstV _) [] m = return v+get (SubstV _) xs _ = throwError "too many keys"+get m@(SubstM _) [] _ = return $ m+get (SubstM m) (k:ks) bm = case M.lookup k bm of+ Nothing -> do+ res <- g k m+ get res ks bm+ (Just s') -> get s' ks bm++-- | Helper function that digs one layer down in a `Data.Text.Interp.Types.Subst` mapping+g :: Key -> (M.Map Key Subst) -> I Subst+g k m = case M.lookup k m of+ Nothing -> throwError $ "bad key: " `append` (unKey k)+ (Just (SubstL xs)) -> lift $ sampleRVar (randomElement xs)+ (Just v) -> return v
+ src/Data/Text/Interp/Interpolate.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}+module Data.Text.Interp.Interpolate where++import Control.Monad (foldM)+import Control.Monad.Except+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NE+import qualified Data.Map.Strict as M+import Data.Text (Text, append)++import Data.Text.Interp.Get+import Data.Text.Interp.Types+++-- | Record holding intermediary interpolation state+data IState = IState {+ acc :: Text -- ^ interpolated text so far+ , bm :: BindingMap -- ^ mapping of bound variables+ }++newIState :: IState+newIState = IState "" M.empty++-- | Build interpolated text out of a `Data.Text.Interp.Types.Subst` mapping and+-- a list of segments to interpolate+interp :: Subst -- ^ mapping to get substitutions out of+ -> NonEmpty IText -- ^ lits of segments to interpolate+ -> I Text -- ^ final interpolation+interp s its = acc <$> foldM (interp' s) newIState its++-- | Interpolate a single segment+interp' :: Subst -- ^ mapping+ -> IState -- ^ intermediary state+ -> IText -- ^ segment to interpolate+ -> I IState -- ^ updated state, after the segment's been interpolated+interp' _ (IState a m) (RawText t) = return $ IState (a `append` t) m+interp' s (IState a m) (ToInterpolate Nothing p) = do+ res <- get s p m+ case res of+ (SubstM _) -> throwError "too few keys"+ (SubstL _) -> throwError "too few keys"+ (SubstV x) -> return $ IState (a `append` x) m+interp' s (IState a m) (ToInterpolate (Just (Binding n bp)) p) = do+ toBind <- get s (NE.toList bp) m+ res <- get toBind p m+ case res of+ (SubstM _) -> throwError "too few keys"+ (SubstL _) -> throwError "too few keys"+ (SubstV x) -> return $ IState (a `append` x) $ M.insert n toBind m
+ src/Data/Text/Interp/Parse.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE OverloadedStrings #-}++module Data.Text.Interp.Parse where++import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.NonEmpty as NE+import Data.Text (Text, pack)+import Data.Void (Void)+import Text.Megaparsec+import Text.Megaparsec.Char++import Data.Text.Interp.Types++type Parser = Parsec Void Text++-- | Parse `Data.Text.Interp.Types.IText`s+itP :: Parser (NonEmpty IText)+itP = NE.fromList <$> (some $ try $ toInterpolateP <|> rawTextP)++interpStart = "{{"+interpStop = "}}"++-- | parse raw text (i.e., text without interpolations)+rawTextP :: Parser IText+rawTextP = RawText <$> takeWhile1P (Just "raw text") (\c -> not (c `elem` ['}', '{']))++-- | parse text that needs interpolation+toInterpolateP :: Parser IText+toInterpolateP = between (string interpStart >> space)+ (space >> string interpStop)+ interP++-- | parse interpolation instructions (with or without binding)+interP :: Parser IText+interP = try $ bindP <|> keyP++-- | parse interpolation that needs binding+bindP :: Parser IText+bindP = do+ char '('+ bk <- key <* char '#'+ bp <- NE.fromList <$> keys <* string ")"+ path <- (optional (char '.')) *> keys+ let b = Just $ Binding bk bp+ return $ ToInterpolate b path++-- | parse interpolation that doesn't need binding+keyP :: Parser IText+keyP = ToInterpolate Nothing <$> keys++-- | parse a single key+key :: Parser Key+key = Key <$> pack <$> some (alphaNumChar <|> satisfy (`elem` ['_', '-'])) <?>+ "keys can only contain letters, numbers, _, and -"++-- | parse a list of keys+keys :: Parser [Key]+keys = sepBy key (char '.')
+ src/Data/Text/Interp/Types.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Data.Text.Interp.Types+ ( Binding(..)+ , BindingMap+ , I+ , IText(..)+ , Key(..)+ , Subst(..)+ , substm+ ) where++import Control.Monad.Except+import Data.Aeson+import Data.Aeson.Types (Parser)+import qualified Data.HashMap.Strict as HM+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.Map.Strict as M+import Data.Map.Strict (Map)+import Data.String (IsString, fromString)+import Data.Text (Text, pack)+import qualified Data.Vector as V+++newtype Key = Key { unKey :: Text } deriving (Eq, IsString, Monoid, Ord, Semigroup, Show)++-- | The main substitution type, either a list, mapping, or single value+data Subst = SubstL [Subst] -- ^ a list of values, one of which will be randomly selected+ | SubstM (Map Key Subst) -- ^ a mapping of `Key`s to values+ | SubstV Text -- ^ a single value+ deriving (Eq, Show)++instance FromJSON Subst where+ parseJSON (String s) = return $ SubstV s+ parseJSON (Number n) = return $ SubstV . pack . show $ n+ parseJSON (Bool b) = return $ SubstV . pack . show $ b+ parseJSON Null = fail "null not allowed"+ parseJSON (Array a) = SubstL <$> (sequence $ V.toList $ parseJSON <$> a)+ parseJSON (Object o) = SubstM <$> HM.foldlWithKey' g (return M.empty) o where+ g :: Parser (Map Key Subst) -> Text -> Value -> Parser (Map Key Subst)+ -- considered liftM2 but it was hard to read imo+ g p k v = do+ acc <- p+ x <- parseJSON v+ return $ M.insert (Key k) x acc++-- | Helper function to build a `Subst` mapping+substm :: [(Key, Subst)] -> Subst+substm = SubstM . M.fromList++-- | A binding of a `Key` to a path of `Key`s+data Binding = Binding {+ name :: Key+ , pathToBind :: NonEmpty Key+ } deriving (Eq, Show)++-- | A segment of text to interpolate (or not)+data IText = RawText Text -- ^ 'Raw' text that doesn't need interpolation+ -- | Text that requires interpolation+ | ToInterpolate {+ binding :: Maybe Binding -- ^ Optional binding+ , path :: [Key] -- ^ Path of `Key`s to the value to interpolate+ } deriving (Eq, Show)++type I = ExceptT Text IO++type BindingMap = M.Map Key Subst
+ test/GetSpec.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}+module GetSpec where++import Control.Monad.Except+import qualified Data.Map.Strict as M+import Test.Hspec++import Data.Text.Interp.Get+import Data.Text.Interp.Types++samFedoraColors :: Subst+samFedoraColors = SubstL $ SubstV <$> ["green", "red", "blue"]++samBallcapColors :: Subst+samBallcapColors = SubstL $ SubstV <$> ["white", "black", "orange"]++testSubst :: Subst+testSubst = substm [+ ("people", SubstL [+ substm [+ ("sam", substm [+ ("hats", SubstL [+ substm [+ ("type", SubstV "fedora"),+ ("color", samFedoraColors)+ ],+ substm [+ ("type", SubstV "ballcap"),+ ("color", samBallcapColors)+ ]+ ]),+ ("shoes", SubstL $ SubstV <$> ["sneakers", "boots"])+ ])+ ]+ ])+ ]++expectedColors = let (SubstL fcs) = samFedoraColors+ (SubstL bcs) = samBallcapColors+ in fcs ++ bcs++spec :: Spec+spec = do+ describe "get" $ do+ it "should get a color" $ do+ (Right c) <- runExceptT $+ get testSubst ["people", "sam", "hats", "color"] M.empty+ c `shouldSatisfy` (`elem` expectedColors)++ it "should error on bad key" $ do+ (Left e) <- runExceptT $ get testSubst ["people", "sam", "shirts"] M.empty+ e `shouldBe` "bad key: shirts"++ it "should error on too many keys" $ do+ (Left e) <- runExceptT $+ get testSubst ["people", "sam", "hats", "color", "hue"] M.empty+ e `shouldBe` "too many keys"++ it "should re-use bound results" $ do+ let bm = M.singleton "hats" $+ substm [("type", SubstV "fedora"), ("color", samFedoraColors)]+ (Right c) <- runExceptT $ get testSubst ["people", "sam", "hats", "type"] bm+ c `shouldBe` SubstV "fedora"
+ test/InterpolateSpec.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}+module InterpolateSpec where++import Control.Monad.Except+import qualified Data.Map.Strict as M+import Data.List.NonEmpty (NonEmpty(..))+import Data.Text (append)+import Test.Hspec++import Data.Text.Interp.Interpolate+import Data.Text.Interp.Types++fcs = ["green", "red"]+bcs = ["white", "black"]+++samFedoraColors :: Subst+samFedoraColors = SubstL $ SubstV <$> fcs++samBallcapColors :: Subst+samBallcapColors = SubstL $ SubstV <$> bcs++testSubst :: Subst+testSubst = substm [+ ("people", SubstL [+ substm [+ ("sam", substm [+ ("hats", SubstL [+ substm [+ ("type", SubstV "fedora"),+ ("color", samFedoraColors)+ ],+ substm [+ ("type", SubstV "ballcap"),+ ("color", samBallcapColors)+ ]+ ]),+ ("shoes", SubstL $ SubstV <$> ["sneakers", "boots"])+ ])+ ]+ ])+ ]++spec :: Spec+spec = do+ describe "interp" $ do+ it "should interpolate without binding" $ do+ let its = RawText "sam was wearing a " :|+ [ToInterpolate Nothing ["people", "sam", "hats", "type"]]+ (Right res) <- runExceptT $ interp testSubst its+ let expected = ["sam was wearing a fedora", "sam was wearing a ballcap"]+ res `shouldSatisfy` (`elem` expected)++ it "should interpolate with binding" $ do+ let its = RawText "sam was wearing a " :|+ [ToInterpolate (Just (Binding "h" $ "people" :|+ ["sam", "hats"])) ["color"],+ RawText " ",+ ToInterpolate Nothing ["h", "type"]]+ (Right res) <- runExceptT $ interp testSubst its+ let expected = ["sam was wearing a " `append` fc `append` " fedora" | fc <- fcs] +++ ["sam was wearing a " `append` bc `append` " ballcap" | bc <- bcs]+ res `shouldSatisfy` (`elem` expected)++ it "should throw an error with too few keys" $ do+ let its = RawText "sam was wearing a " :|+ [ToInterpolate Nothing ["people", "sam"]]+ (Left e) <- runExceptT $ interp testSubst its+ e `shouldBe` "too few keys"
+ test/ParseSpec.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings #-}+module ParseSpec where++import Data.Either+import Data.Text (Text)+import Data.List.NonEmpty (NonEmpty(..))+import Test.Hspec+import Text.Megaparsec++import Data.Text.Interp.Parse+import Data.Text.Interp.Types+++spec :: Spec+spec = do+ describe "parsing" $ do+ it "should parse a single key" $ do+ let (Right k) = parse key "" "key"+ k `shouldBe` (Key "key")++ it "should parse multiple keys separated by periods" $ do+ let (Right ks) = parse keys "" "foo.bar.baz"+ ks `shouldBe` [Key "foo", Key "bar", Key "baz"]++ it "should create a ToInterpolate without any binding" $ do+ let (Right ti) = parse keyP "" "foo.bar.baz"+ ti `shouldBe` ToInterpolate Nothing [Key "foo", Key "bar", Key "baz"]++ it "should create a ToInterpolate with a binding" $ do+ let (Right ti) = parse bindP "" "(b#foo.bar).baz.quux"+ ti `shouldBe` ToInterpolate (Just $ Binding (Key "b")+ (Key "foo" :| [Key "bar"]))+ [Key "baz", Key "quux"]++ it "should parse raw text" $ do+ let (Right txt) = parse rawTextP "" "raw text"+ txt `shouldBe` RawText "raw text"++ it "shouldn't permit an empty binding key" $ do+ let res = parse bindP "" "(#foo).bar.baz"+ res `shouldSatisfy` isLeft++ it "shouldn't permit parens without binding stuff" $ do+ let res = parse bindP "" "(foo).bar.baz"+ res `shouldSatisfy` isLeft++ it "should deal properly with errant periods" $ do+ let res = parse bindP "" ".foo.bar"+ res `shouldSatisfy` isLeft+ let res' = parse bindP "" "foo.bar."+ res' `shouldSatisfy` isLeft++ it "should parse a mixture properly" $ do+ let s = "there is some {{ (text#to.parse) }} {{ for.us }} here"+ let (Right res) = parse itP "" s+ let expected = (RawText "there is some ") :|+ [+ ToInterpolate (Just $ Binding (Key "text")+ (Key "to" :| [Key "parse"])) [],+ RawText " ",+ ToInterpolate Nothing [Key "for", Key "us"],+ RawText " here" ]+ res `shouldBe` expected
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}