jsonlogic-hs-0.1.0.0: tutorial/Tutorial.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Every example in docs/TUTORIAL.md, with the answer the tutorial shows. The
-- run fails if any answer differs, so the tutorial cannot drift from the
-- library. Uses only the public "JsonLogic" module, as a reader would.
module Main (main) where
import Data.Aeson (Value (..), decode, object, (.=))
import qualified Data.ByteString.Lazy.Char8 as Char8
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import System.Exit (exitFailure)
import JsonLogic
json :: String -> Value
json = fromMaybe Null . decode . Char8.pack
startsWith :: CustomOperator
startsWith _ [String prefix, String s] = Right (Bool (prefix `Text.isPrefixOf` s))
startsWith _ _ = Left "starts_with takes two strings"
operators :: Either OperatorsError Operators
operators = operatorsFromList [(OperatorName "starts_with", startsWith)]
describe :: Either Error Value -> String
describe result = case result of
Right v -> "ok: " ++ show v
Left (ParseFailure at (ArityMismatch (OperatorName op) _ given)) ->
"rule error at " ++ show (renderLocation at) ++ ": " ++ show op ++ " given " ++ show given
Left (EvalFailure _ (LimitExceeded _)) -> "too expensive"
Left other -> "failed: " ++ show (renderError other)
-- | (section, what the tutorial shows, what the library says)
checks :: [(String, String, String)]
checks =
[ check "1 and" (Right (Bool True)) (apply (json "{\"and\": [{\">=\": [{\"var\": \"age\"}, 18]}, {\"var\": \"verified\"}]}") (json "{\"age\": 21, \"verified\": true}"))
, check "1 array" (Right (json "[1,2]")) (apply (json "[{\"var\": \"a\"}, 2]") (json "{\"a\": 1}"))
, check "1 path" (Right (String "admin")) (apply (json "{\"var\": \"user.roles.0\"}") (json "{\"user\": {\"roles\": [\"admin\"]}}"))
, check "1 default" (Right (String "default")) (apply (json "{\"var\": [\"missing.path\", \"default\"]}") (json "{}"))
, check "2 ==" (Right (Bool True)) (apply (json "{\"==\": [1, \"1\"]}") Null)
, check "2 ===" (Right (Bool False)) (apply (json "{\"===\": [1, \"1\"]}") Null)
, check "2 <" (Right (Bool True)) (apply (json "{\"<\": [\"10\", \"9\"]}") Null)
, check "2 +" (Right (Number 7)) (apply (json "{\"+\": [\"3\", 4]}") Null)
, check "2 /" (Right Null) (apply (json "{\"/\": [1, 0]}") Null)
, check "2 cat" (Right (String "0.1+0.2")) (apply (json "{\"cat\": [0.1, \"+\", 0.2]}") Null)
, check "2 if []" (Right (String "no")) (apply (json "{\"if\": [[], \"yes\", \"no\"]}") Null)
, check "2 !!" (Right (Bool True)) (apply (json "{\"!!\": \"0\"}") Null)
, check
"3 compile"
(Right [Right (Number 12), Right (Number 3)])
((\rule -> map (run rule) [json "{\"price\": 10}", json "{\"price\": 2.5}"]) <$> compile defaultConfig noOperators (json "{\"*\": [{\"var\": \"price\"}, 1.2]}"))
, check
"4 location"
(Left (EvalFailure (fromSegments [Key "if", Index 1]) (UnknownOperator (OperatorName "nope"))))
(apply (json "{\"if\": [true, {\"nope\": [1]}]}") Null)
, check "4 render" ("at /if/1: unknown operator \"nope\"" :: Text) (either renderError (const "") (apply (json "{\"if\": [true, {\"nope\": [1]}]}") Null))
, check "4 describe" "rule error at \"\": \"==\" given 1" (describe (apply (json "{\"==\": [1]}") Null))
, check "4 lazy" (Right (Bool True)) (apply (json "{\"or\": [true, {\"nope\": []}]}") Null)
, check
"5 strict"
(Left (ParseFailure (fromSegments []) (ArityMismatch (OperatorName "==") (Exactly 2) 1)))
(apply (json "{\"==\": [1]}") Null)
, check "5 lenient" (Right (Bool False)) (applyWithConfig defaultConfig{arityMode = Lenient} noOperators (json "{\"==\": [1]}") Null)
, check
"6 limits"
(Left (EvalFailure (fromSegments [Key "map", Index 1]) (LimitExceeded (ArrayLimit 1000))))
( applyWithConfig
defaultConfig{maxSteps = 10000, maxStringLength = 4096, maxArrayLength = 1000}
noOperators
(json "{\"map\": [{\"var\": \"xs\"}, {\"map\": [{\"var\": \"\"}, 1]}]}")
(object ["xs" .= [[1 .. 2000 :: Int]]])
)
, check
"7 custom"
(Right (Right (Bool True)))
((\ops -> applyWith ops (json "{\"starts_with\": [\"ad\", {\"var\": \"role\"}]}") (json "{\"role\": \"admin\"}")) <$> operators)
, check
"7 custom failure"
(Right (Left (EvalFailure (fromSegments []) (CustomOperatorFailed (OperatorName "starts_with") "starts_with takes two strings"))))
((\ops -> applyWith ops (json "{\"starts_with\": [1]}") Null) <$> operators)
, check
"8 dataUse"
(Right (DataUse ["orders", "user.age"] False))
( dataUse
<$> compile
defaultConfig
noOperators
(json "{\"and\": [{\"var\": \"user.age\"}, {\"some\": [{\"var\": \"orders\"}, {\">\": [{\"var\": \"total\"}, 100]}]}]}")
)
]
where
check :: (Eq a, Show a) => String -> a -> a -> (String, String, String)
check name shown actual = (name, show shown, if actual == shown then show shown else show actual)
main :: IO ()
main = do
let wrong = [(name, shown, actual) | (name, shown, actual) <- checks, shown /= actual]
mapM_ (\(name, shown, actual) -> putStrLn ("tutorial section " ++ name ++ ":\n shows " ++ shown ++ "\n gives " ++ actual)) wrong
if null wrong
then putStrLn ("tutorial: all " ++ show (length checks) ++ " examples give the answers shown")
else exitFailure