Skip to content

Commit 542438a

Browse files
committed
Ditch attoparsec for megaparsec: great error msgs
1 parent cdadce7 commit 542438a

File tree

4 files changed

+98
-37
lines changed

4 files changed

+98
-37
lines changed

nri-redis/nri-redis.cabal

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ library
6565
build-depends:
6666
aeson >=1.4.6.0 && <2.2
6767
, async >=2.2.2 && <2.3
68-
, attoparsec >=0.13.0.0 && <0.15
6968
, base >=4.12.0.0 && <4.18
7069
, bytestring >=0.10.8.2 && <0.12
7170
, conduit >=1.3.0 && <1.4
7271
, containers >=0.6.0.1 && <0.7
7372
, hedis >=0.14.0 && <0.16
73+
, megaparsec >=9.2.2 && <9.3
7474
, modern-uri >=0.3.1.0 && <0.4
7575
, nri-env-parser >=0.1.0.0 && <0.2
7676
, nri-observability >=0.1.0 && <0.2
@@ -90,6 +90,7 @@ test-suite tests
9090
other-modules:
9191
Helpers
9292
Spec.Redis
93+
Spec.Redis.Script
9394
Spec.Settings
9495
NonEmptyDict
9596
Redis
@@ -127,12 +128,12 @@ test-suite tests
127128
build-depends:
128129
aeson >=1.4.6.0 && <2.2
129130
, async >=2.2.2 && <2.3
130-
, attoparsec >=0.13.0.0 && <0.15
131131
, base >=4.12.0.0 && <4.18
132132
, bytestring >=0.10.8.2 && <0.12
133133
, conduit >=1.3.0 && <1.4
134134
, containers >=0.6.0.1 && <0.7
135135
, hedis >=0.14.0 && <0.16
136+
, megaparsec >=9.2.2 && <9.3
136137
, modern-uri >=0.3.1.0 && <0.4
137138
, nri-env-parser >=0.1.0.0 && <0.2
138139
, nri-observability >=0.1.0 && <0.2

nri-redis/package.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ extra-doc-files:
1616
dependencies:
1717
- aeson >= 1.4.6.0 && < 2.2
1818
- async >=2.2.2 && <2.3
19-
- attoparsec >= 0.13.0.0 && < 0.15
2019
- base >= 4.12.0.0 && < 4.18
2120
- bytestring >= 0.10.8.2 && < 0.12
2221
- conduit >= 1.3.0 && < 1.4
2322
- containers >= 0.6.0.1 && < 0.7
2423
# hedis 14 introduces redis-cluster support
2524
- hedis >= 0.14.0 && < 0.16
25+
- megaparsec >= 9.2.2 && < 9.3
2626
- modern-uri >= 0.3.1.0 && < 0.4
2727
- nri-env-parser >= 0.1.0.0 && < 0.2
2828
- nri-observability >= 0.1.0 && < 0.2

nri-redis/src/Redis/Script.hs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
module Redis.Script (Script (..), script, evalString, mapKeys, keysTouchedByScript, paramNames, paramValues, parser, ScriptExpression (..)) where
1+
module Redis.Script (Script (..), script, evalString, mapKeys, keysTouchedByScript, paramNames, paramValues, parser, Tokens (..)) where
22

3-
import Data.Attoparsec.Text (Parser, char,choice, inClass, many1', skipSpace, takeWhile1, (<?>), endOfInput)
4-
import qualified Data.Attoparsec.Text as Attoparsec
3+
import Data.Void (Void)
54
import qualified Language.Haskell.TH as TH
65
import qualified Language.Haskell.TH.Quote as QQ
7-
-- import Control.Applicative ((<|>))
86
import qualified Set
9-
import Prelude (pure)
7+
import Text.Megaparsec ((<|>))
8+
import qualified Text.Megaparsec as P
9+
import qualified Text.Megaparsec.Char as PC
10+
import Prelude (notElem, pure, (<*))
1011
import qualified Prelude
1112

1213
data Script result = Script
@@ -41,37 +42,36 @@ script =
4142
qqScript :: Prelude.String -> TH.ExpQ
4243
qqScript scriptWithVars = do
4344
let str = Text.fromList scriptWithVars
44-
let _expr = Attoparsec.parseOnly parser str
45+
let _expr = P.parse parser "" str
4546
-- let parsedScript = case expr str of
4647
-- Left err -> Prelude.error <| "Failed to parse script: " ++ err
4748
-- Right parsed ->
4849
-- [|parsedScript|]
4950
Debug.todo "qqScript"
5051

51-
data ScriptExpression
52+
data Tokens
5253
= ScriptText Text
5354
| ScriptVariable Text
5455
deriving (Show, Eq)
5556

56-
parser :: Parser (List ScriptExpression)
57+
type Parser = P.Parsec Void Text
58+
59+
parser :: Parser (List Tokens)
5760
parser = do
58-
result <- many1' (choice [parseText, parseVariable]) <?> "Expected at least one"
59-
endOfInput
60-
pure <| result
61+
(P.some (parseText <|> parseVariable))
62+
<* P.eof
6163

62-
parseText :: Parser ScriptExpression
64+
parseText :: Parser Tokens
6365
parseText = do
64-
text <- takeWhile1 ('$' /=) <?> "Expected text"
66+
text <- P.takeWhile1P (Just "some plain text") (/= '$')
6567
pure <| ScriptText text
6668

67-
parseVariable :: Parser ScriptExpression
69+
parseVariable :: Parser Tokens
6870
parseVariable = do
69-
_ <- char '$' <?> "Expected '$'"
70-
_ <- char '{' <?> "Expected '{'"
71-
skipSpace <?> "Expected space after '{'"
72-
name <- (takeWhile1 (not << inClass "${}"))
73-
<?> "No '$', '{' or '}' allowed in interpolated expression. Note: I'm a simple parser and I don't support records inside ${}."
74-
_ <- char '}' <?> "Expected '}' after: ${" ++ Text.toList name
71+
_ <- PC.string "${"
72+
_ <- PC.space
73+
name <- P.takeWhile1P (Just "anything but '$', '{' or '}' (no records, sorry)") (\t -> t `notElem` ['$', '{', '}'])
74+
_ <- PC.char '}'
7575
pure <| ScriptVariable <| Text.trim name
7676

7777
-- | EVAL script numkeys [key [key ...]] [arg [arg ...]]

nri-redis/test/Spec/Redis/Script.hs

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module Spec.Redis.Script (tests) where
22

3-
import qualified Data.Attoparsec.Text as Attoparsec
43
import Data.Either (Either (..))
54
import qualified Expect
65
import Redis.Script
76
import qualified Test
7+
import qualified Text.Megaparsec as P
88

99
tests :: Test.Test
1010
tests =
@@ -16,19 +16,19 @@ tests =
1616
parserTests :: List Test.Test
1717
parserTests =
1818
[ Test.test "1 word" <| \_ ->
19-
(Attoparsec.parseOnly parser "Jabuticaba")
19+
P.runParser parser "" "Jabuticaba"
2020
|> Expect.equal (Right [ScriptText "Jabuticaba"]),
2121
Test.test "3 words" <| \_ ->
22-
(Attoparsec.parseOnly parser "Picolé de Jabuticaba")
22+
P.runParser parser "" "Picolé de Jabuticaba"
2323
|> Expect.equal (Right [ScriptText "Picolé de Jabuticaba"]),
2424
Test.test "1 value" <| \_ ->
25-
(Attoparsec.parseOnly parser "${value}")
25+
P.runParser parser "" "${value}"
2626
|> Expect.equal (Right [ScriptVariable "value"]),
2727
Test.test "function application" <| \_ ->
28-
(Attoparsec.parseOnly parser "${func arg1 arg2}")
28+
P.runParser parser "" "${func arg1 arg2}"
2929
|> Expect.equal (Right [ScriptVariable "func arg1 arg2"]),
3030
Test.test "text and variables" <| \_ ->
31-
(Attoparsec.parseOnly parser "some text ${value} some more text ${ anotherValue }")
31+
P.runParser parser "" "some text ${value} some more text ${ anotherValue }"
3232
|> Expect.equal
3333
( Right
3434
[ ScriptText "some text ",
@@ -37,13 +37,73 @@ parserTests =
3737
ScriptVariable "anotherValue"
3838
]
3939
),
40-
Test.only <| Test.test "ERROR: nested ${}" <| \_ -> do
41-
(Attoparsec.parseOnly parser "asdasd ${ ${ value } }")
42-
|> Expect.equal (Left "Expected at least one > No '$', '{' or '}' allowed in interpolated expression. Note: I'm a simple parser and I don't support records inside ${}.: Failed reading: takeWhile1"),
43-
Test.test "ERROR: misplaced ${ inside ${}" <| \_ -> do
44-
(Attoparsec.parseOnly parser "${ v$alue }")
45-
|> Expect.equal (Left "Expected at least one > Expected '}' after: ${v > '}': Failed reading: satisfy"),
40+
Test.test "ERROR: empty" <| \_ -> do
41+
P.runParser parser "" ""
42+
|> mapLeft P.errorBundlePretty
43+
|> Expect.equal
44+
( Left
45+
"1:1:\n\
46+
\ |\n\
47+
\1 | <empty line>\n\
48+
\ | ^\n\
49+
\unexpected end of input\n\
50+
\expecting \"${\" or some plain text\n\
51+
\"
52+
),
53+
Test.test "ERROR: empty variable" <| \_ -> do
54+
P.runParser parser "" "${}"
55+
|> mapLeft P.errorBundlePretty
56+
|> Expect.equal
57+
( Left
58+
"1:3:\n\
59+
\ |\n\
60+
\1 | ${}\n\
61+
\ | ^\n\
62+
\unexpected '}'\n\
63+
\expecting anything but '$', '{' or '}' (no records, sorry) or white space\n\
64+
\"
65+
),
66+
Test.test "ERROR: nested ${}" <| \_ -> do
67+
P.runParser parser "" "asdasd ${ ${ value } }"
68+
|> mapLeft P.errorBundlePretty
69+
|> Expect.equal
70+
( Left
71+
"1:11:\n\
72+
\ |\n\
73+
\1 | asdasd ${ ${ value } }\n\
74+
\ | ^\n\
75+
\unexpected '$'\n\
76+
\expecting anything but '$', '{' or '}' (no records, sorry) or white space\n\
77+
\"
78+
),
4679
Test.test "ERROR: misplaced ${ inside ${}" <| \_ -> do
47-
(Attoparsec.parseOnly parser "${ v{alue }")
48-
|> Expect.equal (Left "Expected at least one > Expected '}' after: ${v > '}': Failed reading: satisfy")
80+
P.runParser parser "" "${ v$alue }"
81+
|> mapLeft P.errorBundlePretty
82+
|> Expect.equal
83+
( Left
84+
"1:5:\n\
85+
\ |\n\
86+
\1 | ${ v$alue }\n\
87+
\ | ^\n\
88+
\unexpected '$'\n\
89+
\expecting '}' or anything but '$', '{' or '}' (no records, sorry)\n\
90+
\"
91+
),
92+
Test.test "ERROR: misplaced { inside ${}" <| \_ -> do
93+
P.runParser parser "" "${ v{alue }"
94+
|> mapLeft P.errorBundlePretty
95+
|> Expect.equal
96+
( Left
97+
"1:5:\n\
98+
\ |\n\
99+
\1 | ${ v{alue }\n\
100+
\ | ^\n\
101+
\unexpected '{'\n\
102+
\expecting '}' or anything but '$', '{' or '}' (no records, sorry)\n\
103+
\"
104+
)
49105
]
106+
107+
mapLeft :: (a -> c) -> Either a b -> Either c b
108+
mapLeft f (Left a) = Left (f a)
109+
mapLeft _ (Right b) = Right b

0 commit comments

Comments
 (0)