Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit abefc84

Browse files
committed
Add a randomInt provider
1 parent c209ca4 commit abefc84

5 files changed

Lines changed: 156 additions & 18 deletions

File tree

app/Main.hs

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,30 @@
22

33
module Main where
44

5-
import Control.Monad (forM_, forever)
5+
import Control.Monad (forever)
66
import Data.Aeson (Value (..), encode, object)
77
import qualified Data.ByteString.Lazy.Char8 as BL
8-
import qualified Data.List as L
9-
import Data.Maybe (mapMaybe)
8+
import Data.Either (isRight, lefts)
9+
import Data.Maybe (fromJust, mapMaybe)
10+
import qualified Data.Scientific as S
1011
import Data.Text (Text)
1112
import qualified Data.Text as T
1213
import qualified Data.Text.Read as T
1314
import qualified Data.UUID as UUID
14-
import qualified Data.UUID.V4 as UUID4
1515
import qualified Data.UUID.V1 as UUID1
16-
import Lib
16+
import qualified Data.UUID.V4 as UUID4
17+
import Expr (Expr (..), parseExpr)
1718
import System.Environment (getArgs)
19+
import System.Random (getStdGen, randomR, setStdGen)
1820

21+
-- $setup
22+
-- >>> :set -XOverloadedStrings
1923

2024
parseColumnDefinition :: String -> Maybe (Text, Text)
21-
parseColumnDefinition x =
25+
parseColumnDefinition x =
2226
case parts of
23-
[x, y] -> Just (x, y)
24-
_ -> Nothing
27+
[columnName, providerName] -> Just (columnName, providerName)
28+
_ -> Nothing
2529
where
2630
text = T.pack x
2731
parts = T.splitOn "=" text
@@ -35,22 +39,63 @@ uuid1 = do
3539
Nothing -> uuid1
3640

3741

38-
lookupProvider :: Text -> IO Value
39-
lookupProvider "uuid4" = String . UUID.toText <$> UUID4.nextRandom
40-
lookupProvider "uuid1" = String . UUID.toText <$> uuid1
41-
lookupProvider _ = pure $ String "foobar"
42+
-- | Try to extract an Int from Value
43+
--
44+
-- >>> asInt (Number 10)
45+
-- 10
46+
--
47+
-- >>> asInt (String "10")
48+
-- 10
49+
--
50+
-- >>> asInt (String "foo")
51+
-- *** Exception: Expected an integer, but received: foo
52+
-- ...
53+
asInt :: Value -> Int
54+
asInt (Number n) = fromJust $ S.toBoundedInteger n
55+
asInt (String s) =
56+
case T.decimal s of
57+
(Right (n, _)) -> n
58+
(Left _) -> error $ "Expected an integer, but received: " <> T.unpack s
59+
asInt o = error $ "Expected an integer but received: " <> show o
60+
61+
62+
-- | Create a value getter for an expression
63+
--
64+
-- >>> eval $ FunctionCall "randomInt" [IntLiteral 1, IntLiteral 1]
65+
-- Number 1.0
66+
eval :: Expr -> IO Value
67+
eval (IntLiteral x) = pure $ Number $ fromInteger x
68+
eval (StringLiteral x) = pure $ String x
69+
eval (FunctionCall "uuid4" []) = String . UUID.toText <$> UUID4.nextRandom
70+
eval (FunctionCall "uuid1" []) = String . UUID.toText <$> uuid1
71+
eval (FunctionCall "randomInt" [lower, upper]) = do
72+
lower' <- asInt <$> eval lower
73+
upper' <- asInt <$> eval upper
74+
stdGen <- getStdGen
75+
let
76+
(rndNumber, newStdGen) = randomR (lower', upper') stdGen
77+
setStdGen newStdGen
78+
pure $ Number $ fromIntegral rndNumber
79+
eval (FunctionCall name _) = pure $ String $ "No random generator for " <> name
4280

4381

4482
main :: IO ()
4583
main = do
4684
args <- getArgs
4785
let
4886
columns = mapMaybe parseColumnDefinition args
49-
providers = fmap (\(x, y) -> (x, lookupProvider y)) columns
50-
forever $ do
51-
obj <- encode . object <$> mapM runProvider providers
52-
BL.putStrLn obj
87+
allExpressions = fmap (\(x, y) -> (x, parseExpr y)) columns
88+
expressions = fmap unpackRight (filter (isRight . snd) allExpressions)
89+
errored = lefts $ fmap snd allExpressions
90+
providers = fmap (\(x, y) -> (x, eval y)) expressions
91+
if null errored
92+
then forever $ do
93+
obj <- encode . object <$> mapM runProvider providers
94+
BL.putStrLn obj
95+
else mapM_ print errored
5396
where
97+
unpackRight (x, Right y) = (x, y)
98+
unpackRight _ = error "Tuple must only contain Right eithers"
5499
runProvider (column, provider) = do
55100
val <- provider
56101
pure (column, val)

package.yaml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,20 @@ extra-source-files:
1414
description: |
1515
Please see the README on GitHub at <https://github.com/mfussenegger/fake-json#readme>
1616
17+
ghc-options:
18+
- -Wall
19+
- -fno-warn-unused-do-bind
20+
1721
dependencies:
1822
- base >= 4.7 && < 5
1923
- text
2024
- aeson
2125
- bytestring
2226
- scientific
2327
- uuid
28+
- parsec
29+
- random
30+
- scientific
2431

2532
library:
2633
source-dirs: src
@@ -36,11 +43,17 @@ executables:
3643

3744
tests:
3845
fake-json-test:
39-
main: Spec.hs
40-
source-dirs: test
46+
main: spec.hs
47+
source-dirs: tests
4148
ghc-options:
4249
- -threaded
4350
- -rtsopts
4451
- -with-rtsopts=-N
4552
dependencies:
4653
- fake-json
54+
fake-json-doctest:
55+
main: doctests.hs
56+
source-dirs: tests
57+
dependencies:
58+
- fake-json
59+
- doctest

src/Expr.hs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Expr where
4+
5+
import Data.Maybe (fromMaybe)
6+
import Data.Text (Text)
7+
import qualified Data.Text as T
8+
import Text.Parsec (many, many1, optionMaybe, parse, sepBy,
9+
(<|>))
10+
import Text.Parsec.Char (char, digit, letter, spaces)
11+
import Text.Parsec.Error (ParseError)
12+
import Text.Parsec.Text (Parser)
13+
14+
-- $setup
15+
-- >>> :set -XOverloadedStrings
16+
17+
data Expr = IntLiteral Integer
18+
| StringLiteral Text
19+
| FunctionCall { fcName :: Text, fcArgs :: [Expr] }
20+
deriving (Show, Eq)
21+
22+
expr :: Parser Expr
23+
expr = literal <|> functionCall
24+
25+
literal :: Parser Expr
26+
literal = number <|> stringLiteral
27+
28+
number :: Parser Expr
29+
number = IntLiteral . read <$> many1 digit
30+
31+
stringLiteral :: Parser Expr
32+
stringLiteral = do
33+
_ <- char '\''
34+
content <- many1 letter
35+
_ <- char '\''
36+
pure $ StringLiteral $ T.pack content
37+
38+
39+
functionCall :: Parser Expr
40+
functionCall = do
41+
name <- ident
42+
args <- fromMaybe [] <$> optionMaybe functionArgs
43+
pure $ FunctionCall name args
44+
where
45+
functionArgs = do
46+
_ <- char '('
47+
args <- expr `sepBy` (char ',' >> spaces)
48+
_ <- char ')'
49+
pure args
50+
51+
52+
ident :: Parser Text
53+
ident = do
54+
firstChar <- letter
55+
next <- many (digit <|> letter)
56+
pure $ T.pack (firstChar : next)
57+
58+
59+
60+
-- | Parse an expression
61+
--
62+
-- >>> parseExpr "20"
63+
-- Right (IntLiteral 20)
64+
--
65+
-- >>> parseExpr "uuid4"
66+
-- Right (FunctionCall {fcName = "uuid4", fcArgs = []})
67+
--
68+
-- >>> parseExpr "randomInt(0, 10)"
69+
-- Right (FunctionCall {fcName = "randomInt", fcArgs = [IntLiteral 0,IntLiteral 10]})
70+
parseExpr :: Text -> Either ParseError Expr
71+
parseExpr = parse expr "(unknown)"

tests/doctests.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
import Test.DocTest
3+
4+
main :: IO ()
5+
main = doctest
6+
[ "-isrc"
7+
, "src/Expr.hs"
8+
, "app/Main.hs"
9+
]
File renamed without changes.

0 commit comments

Comments
 (0)