replace parser example with URL package

This commit is contained in:
Luke Boswell 2024-01-23 13:06:25 +11:00
parent e2dac4f022
commit 07ad6bb1f7
No known key found for this signature in database
GPG Key ID: F6DB3C9DB47377B0
13 changed files with 74 additions and 2129 deletions

View File

@ -1,196 +0,0 @@
interface Parser.CSV
exposes [
CSV,
CSVRecord,
file,
record,
parseStr,
parseCSV,
parseStrToCSVRecord,
field,
string,
nat,
f64,
]
imports [
Parser.Core.{ Parser, parse, buildPrimitiveParser, alt, map, many, sepBy1, between, ignore, flatten, sepBy },
Parser.Str.{ RawStr, oneOf, codeunit, codeunitSatisfies, strFromRaw },
]
## This is a CSV parser which follows RFC4180
##
## For simplicity's sake, the following things are not yet supported:
## - CSV files with headings
##
## The following however *is* supported
## - A simple LF ("\n") instead of CRLF ("\r\n") to separate records.
CSV : List CSVRecord
CSVRecord : List CSVField
CSVField : RawStr
## Attempts to parse an `a` from a `Str` that is encoded in CSV format.
parseStr : Parser CSVRecord a, Str -> Result (List a) [ParsingFailure Str, SyntaxError Str, ParsingIncomplete CSVRecord]
parseStr = \csvParser, input ->
when parseStrToCSV input is
Err (ParsingIncomplete rest) ->
restStr = Parser.Str.strFromRaw rest
Err (SyntaxError restStr)
Err (ParsingFailure str) ->
Err (ParsingFailure str)
Ok csvData ->
when parseCSV csvParser csvData is
Err (ParsingFailure str) ->
Err (ParsingFailure str)
Err (ParsingIncomplete problem) ->
Err (ParsingIncomplete problem)
Ok vals ->
Ok vals
## Attempts to parse an `a` from a `CSV` datastructure (a list of lists of bytestring-fields).
parseCSV : Parser CSVRecord a, CSV -> Result (List a) [ParsingFailure Str, ParsingIncomplete CSVRecord]
parseCSV = \csvParser, csvData ->
csvData
|> List.mapWithIndex (\recordFieldsList, index -> { record: recordFieldsList, index: index })
|> List.walkUntil (Ok []) \state, { record: recordFieldsList, index: index } ->
when parseCSVRecord csvParser recordFieldsList is
Err (ParsingFailure problem) ->
indexStr = Num.toStr (index + 1)
recordStr = recordFieldsList |> List.map strFromRaw |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", "
problemStr = "\(problem)\nWhile parsing record no. \(indexStr): `\(recordStr)`"
Break (Err (ParsingFailure problemStr))
Err (ParsingIncomplete problem) ->
Break (Err (ParsingIncomplete problem))
Ok val ->
state
|> Result.map (\vals -> List.append vals val)
|> Continue
## Attempts to parse an `a` from a `CSVRecord` datastructure (a list of bytestring-fields)
##
## This parser succeeds when all fields of the CSVRecord are consumed by the parser.
parseCSVRecord : Parser CSVRecord a, CSVRecord -> Result a [ParsingFailure Str, ParsingIncomplete CSVRecord]
parseCSVRecord = \csvParser, recordFieldsList ->
parse csvParser recordFieldsList (\leftover -> leftover == [])
## Wrapper function to combine a set of fields into your desired `a`
##
## ## Usage example
##
## >>> record (\firstName -> \lastName -> \age -> User {firstName, lastName, age})
## >>> |> field string
## >>> |> field string
## >>> |> field nat
##
record : a -> Parser CSVRecord a
record = Parser.Core.const
## Turns a parser for a `List U8` into a parser that parses part of a `CSVRecord`.
field : Parser RawStr a -> Parser CSVRecord a
field = \fieldParser ->
buildPrimitiveParser \fieldsList ->
when List.get fieldsList 0 is
Err OutOfBounds ->
Err (ParsingFailure "expected another CSV field but there are no more fields in this record")
Ok rawStr ->
when Parser.Str.parseRawStr fieldParser rawStr is
Ok val ->
Ok { val: val, input: List.dropFirst fieldsList 1 }
Err (ParsingFailure reason) ->
fieldStr = rawStr |> strFromRaw
Err (ParsingFailure "Field `\(fieldStr)` could not be parsed. \(reason)")
Err (ParsingIncomplete reason) ->
reasonStr = strFromRaw reason
fieldsStr = fieldsList |> List.map strFromRaw |> Str.joinWith ", "
Err (ParsingFailure "The field parser was unable to read the whole field: `\(reasonStr)` while parsing the first field of leftover \(fieldsStr))")
## Parser for a field containing a UTF8-encoded string
string : Parser CSVField Str
string = Parser.Str.anyString
## Parse a natural number from a CSV field
nat : Parser CSVField Nat
nat =
string
|> map
(\val ->
when Str.toNat val is
Ok num ->
Ok num
Err _ ->
Err "\(val) is not a Nat."
)
|> flatten
## Parse a 64-bit float from a CSV field
f64 : Parser CSVField F64
f64 =
string
|> map
(\val ->
when Str.toF64 val is
Ok num ->
Ok num
Err _ ->
Err "\(val) is not a F64."
)
|> flatten
## Attempts to parse a Str into the internal `CSV` datastructure (A list of lists of bytestring-fields).
parseStrToCSV : Str -> Result CSV [ParsingFailure Str, ParsingIncomplete RawStr]
parseStrToCSV = \input ->
parse file (Str.toUtf8 input) (\leftover -> leftover == [])
## Attempts to parse a Str into the internal `CSVRecord` datastructure (A list of bytestring-fields).
parseStrToCSVRecord : Str -> Result CSVRecord [ParsingFailure Str, ParsingIncomplete RawStr]
parseStrToCSVRecord = \input ->
parse csvRecord (Str.toUtf8 input) (\leftover -> leftover == [])
# The following are parsers to turn strings into CSV structures
file : Parser RawStr CSV
file = sepBy csvRecord endOfLine
csvRecord : Parser RawStr CSVRecord
csvRecord = sepBy1 csvField comma
csvField : Parser RawStr CSVField
csvField = alt escapedCsvField nonescapedCsvField
escapedCsvField : Parser RawStr CSVField
escapedCsvField = between escapedContents dquote dquote
escapedContents = many
(
oneOf [
twodquotes |> map (\_ -> '"'),
comma,
cr,
lf,
textdata,
]
)
twodquotes = Parser.Str.string "\"\""
nonescapedCsvField : Parser RawStr CSVField
nonescapedCsvField = many textdata
comma = codeunit ','
dquote = codeunit '"'
endOfLine = alt (ignore crlf) (ignore lf)
cr = codeunit '\r'
lf = codeunit '\n'
crlf = Parser.Str.string "\r\n"
textdata = codeunitSatisfies (\x -> (x >= 32 && x <= 33) || (x >= 35 && x <= 43) || (x >= 45 && x <= 126)) # Any printable char except " (34) and , (44)

View File

@ -1,354 +0,0 @@
interface Parser.Core
exposes [
Parser,
ParseResult,
parse,
parsePartial,
fail,
const,
alt,
keep,
skip,
oneOf,
map,
map2,
map3,
lazy,
maybe,
oneOrMore,
many,
between,
sepBy,
sepBy1,
ignore,
buildPrimitiveParser,
flatten,
]
imports []
## Opaque type for a parser that will try to parse an `a` from an `input`.
##
## As a simple example, you might consider a parser that tries to parse a `U32` from a `Str`.
## Such a process might succeed or fail, depending on the current value of `input`.
##
## As such, a parser can be considered a recipe
## for a function of the type `input -> Result {val: a, input: input} [ParsingFailure Str]`.
##
## How a parser is _actually_ implemented internally is not important
## and this might change between versions;
## for instance to improve efficiency or error messages on parsing failures.
Parser input a := input -> ParseResult input a
ParseResult input a : Result { val : a, input : input } [ParsingFailure Str]
buildPrimitiveParser : (input -> ParseResult input a) -> Parser input a
buildPrimitiveParser = \fun ->
@Parser fun
# -- Generic parsers:
## Most general way of running a parser.
##
## Can be tought of turning the recipe of a parser into its actual parsing function
## and running this function on the given input.
##
## Many (but not all!) parsers consume part of `input` when they succeed.
## This allows you to string parsers together that run one after the other:
## The part of the input that the first parser did not consume, is used by the next parser.
## This is why a parser returns on success both the resulting value and the leftover part of the input.
##
## Of course, this is mostly useful when creating your own internal parsing building blocks.
## `run` or `Parser.Str.runStr` etc. are more useful in daily usage.
parsePartial : Parser input a, input -> ParseResult input a
parsePartial = \@Parser parser, input ->
parser input
## Runs a parser on the given input, expecting it to fully consume the input
##
## The `input -> Bool` parameter is used to check whether parsing has 'completed',
## (in other words: Whether all of the input has been consumed.)
##
## For most (but not all!) input types, a parsing run that leaves some unparsed input behind
## should be considered an error.
parse : Parser input a, input, (input -> Bool) -> Result a [ParsingFailure Str, ParsingIncomplete input]
parse = \parser, input, isParsingCompleted ->
when parsePartial parser input is
Ok { val: val, input: leftover } ->
if isParsingCompleted leftover then
Ok val
else
Err (ParsingIncomplete leftover)
Err (ParsingFailure msg) ->
Err (ParsingFailure msg)
## Parser that can never succeed, regardless of the given input.
## It will always fail with the given error message.
##
## This is mostly useful as 'base case' if all other parsers
## in a `oneOf` or `alt` have failed, to provide some more descriptive error message.
fail : Str -> Parser * *
fail = \msg ->
@Parser \_input -> Err (ParsingFailure msg)
## Parser that will always produce the given `val`, without looking at the actual input.
## This is useful as basic building block, especially in combination with
## `map` and `keep`.
const : a -> Parser * a
const = \val ->
@Parser \input ->
Ok { val: val, input: input }
## Try the `first` parser and (only) if it fails, try the `second` parser as fallback.
alt : Parser input a, Parser input a -> Parser input a
alt = \first, second ->
buildPrimitiveParser \input ->
when parsePartial first input is
Ok { val: val, input: rest } -> Ok { val: val, input: rest }
Err (ParsingFailure firstErr) ->
when parsePartial second input is
Ok { val: val, input: rest } -> Ok { val: val, input: rest }
Err (ParsingFailure secondErr) ->
Err (ParsingFailure ("\(firstErr) or \(secondErr)"))
## Runs a parser building a function, then a parser building a value,
## and finally returns the result of calling the function with the value.
##
## This is useful if you are building up a structure that requires more parameters
## than there are variants of `map`, `map2`, `map3` etc. for.
##
## For instance, the following two are the same:
##
## >>> const (\x, y, z -> Triple x y z)
## >>> |> map3 Parser.Str.nat Parser.Str.nat Parser.Str.nat
##
## >>> const (\x -> \y -> \z -> Triple x y z)
## >>> |> keep Parser.Str.nat
## >>> |> keep Parser.Str.nat
## >>> |> keep Parser.Str.nat
##
## (And indeed, this is how `map`, `map2`, `map3` etc. are implemented under the hood.)
##
## # Currying
## Be aware that when using `keep`, you need to explicitly 'curry' the parameters to the construction function.
## This means that instead of writing `\x, y, z -> ...`
## you'll need to write `\x -> \y -> \z -> ...`.
## This is because the parameters to the function will be applied one-by-one as parsing continues.
keep : Parser input (a -> b), Parser input a -> Parser input b
keep = \funParser, valParser ->
@Parser \input ->
when parsePartial funParser input is
Ok { val: funVal, input: rest } ->
when parsePartial valParser rest is
Ok { val: val, input: rest2 } ->
Ok { val: funVal val, input: rest2 }
Err e ->
Err e
Err e ->
Err e
## Skip over a parsed item as part of a pipeline
##
## This is useful if you are using a pipeline of parsers with `keep` but
## some parsed items are not part of the final result
##
## >>> const (\x -> \y -> \z -> Triple x y z)
## >>> |> keep Parser.Str.nat
## >>> |> skip (codeunit ',')
## >>> |> keep Parser.Str.nat
## >>> |> skip (codeunit ',')
## >>> |> keep Parser.Str.nat
##
skip : Parser input kept, Parser input skipped -> Parser input kept
skip = \kept, skipped ->
@Parser \input ->
when parsePartial kept input is
Ok step1 ->
when parsePartial skipped step1.input is
Ok step2 ->
Ok { val: step1.val, input: step2.input }
Err e ->
Err e
Err e ->
Err e
# Internal utility function. Not exposed to users, since usage is discouraged!
#
# Runs `firstParser` and (only) if it succeeds,
# runs the function `buildNextParser` on its result value.
# This function returns a new parser, which is finally run.
#
# `andThen` is usually more flexible than necessary, and less efficient
# than using `const` with `map` and/or `keep`.
# Consider using those functions first.
andThen : Parser input a, (a -> Parser input b) -> Parser input b
andThen = \@Parser firstParser, buildNextParser ->
@Parser \input ->
when firstParser input is
Ok step ->
(@Parser nextParser) = buildNextParser step.val
nextParser step.input
Err e ->
Err e
## Try a list of parsers in turn, until one of them succeeds
oneOf : List (Parser input a) -> Parser input a
oneOf = \parsers ->
List.walkBackwards parsers (fail "oneOf: The list of parsers was empty") (\laterParser, earlierParser -> alt earlierParser laterParser)
## Transforms the result of parsing into something else,
## using the given transformation function.
map : Parser input a, (a -> b) -> Parser input b
map = \@Parser simpleParser, transform ->
@Parser \input ->
when simpleParser input is
Ok step ->
Ok { val: transform step.val, input: step.input }
Err e ->
Err e
## Transforms the result of parsing into something else,
## using the given two-parameter transformation function.
map2 : Parser input a, Parser input b, (a, b -> c) -> Parser input c
map2 = \@Parser parserA, @Parser parserB, transform ->
@Parser \input ->
when parserA input is
Ok step1 ->
when parserB step1.input is
Ok step2 ->
Ok { val: transform step1.val step2.val, input: step2.input }
Err e ->
Err e
Err e ->
Err e
## Transforms the result of parsing into something else,
## using the given three-parameter transformation function.
##
## If you need transformations with more inputs,
## take a look at `keep`.
map3 : Parser input a, Parser input b, Parser input c, (a, b, c -> d) -> Parser input d
map3 = \@Parser parserA, @Parser parserB, @Parser parserC, transform ->
@Parser \input ->
when parserA input is
Ok step1 ->
when parserB step1.input is
Ok step2 ->
when parserC step2.input is
Ok step3 ->
Ok { val: transform step1.val step2.val step3.val, input: step3.input }
Err e ->
Err e
Err e ->
Err e
Err e ->
Err e
# ^ And this could be repeated for as high as we want, of course.
# Removes a layer of 'result' from running the parser.
#
# This allows for instance to map functions that return a result over the parser,
# where errors are turned into `ParsingFailure` s.
flatten : Parser input (Result a Str) -> Parser input a
flatten = \parser ->
buildPrimitiveParser \input ->
result = parsePartial parser input
when result is
Err problem ->
Err problem
Ok { val: Ok val, input: inputRest } ->
Ok { val: val, input: inputRest }
Ok { val: Err problem, input: _inputRest } ->
Err (ParsingFailure problem)
## Runs a parser lazily
##
## This is (only) useful when dealing with a recursive structure.
## For instance, consider a type `Comment : { message: String, responses: List Comment }`.
## Without `lazy`, you would ask the compiler to build an infinitely deep parser.
## (Resulting in a compiler error.)
##
lazy : ({} -> Parser input a) -> Parser input a
lazy = \thunk ->
const {}
|> andThen thunk
maybe : Parser input a -> Parser input (Result a [Nothing])
maybe = \parser ->
alt (parser |> map (\val -> Ok val)) (const (Err Nothing))
manyImpl : Parser input a, List a, input -> ParseResult input (List a)
manyImpl = \@Parser parser, vals, input ->
result = parser input
when result is
Err _ ->
Ok { val: vals, input: input }
Ok { val: val, input: inputRest } ->
manyImpl (@Parser parser) (List.append vals val) inputRest
## A parser which runs the element parser *zero* or more times on the input,
## returning a list containing all the parsed elements.
##
## Also see `oneOrMore`.
many : Parser input a -> Parser input (List a)
many = \parser ->
@Parser \input ->
manyImpl parser [] input
## A parser which runs the element parser *one* or more times on the input,
## returning a list containing all the parsed elements.
##
## Also see `many`.
oneOrMore : Parser input a -> Parser input (List a)
oneOrMore = \@Parser parser ->
@Parser \input ->
when parser input is
Ok step ->
manyImpl (@Parser parser) [step.val] step.input
Err e ->
Err e
## Runs a parser for an 'opening' delimiter, then your main parser, then the 'closing' delimiter,
## and only returns the result of your main parser.
##
## Useful to recognize structures surrounded by delimiters (like braces, parentheses, quotes, etc.)
##
## >>> betweenBraces = \parser -> parser |> between (scalar '[') (scalar ']')
between : Parser input a, Parser input open, Parser input close -> Parser input a
between = \parser, open, close ->
map3 open parser close (\_, val, _ -> val)
sepBy1 : Parser input a, Parser input sep -> Parser input (List a)
sepBy1 = \parser, separator ->
parserFollowedBySep =
const (\_ -> \val -> val)
|> keep separator
|> keep parser
const (\val -> \vals -> List.prepend vals val)
|> keep parser
|> keep (many parserFollowedBySep)
sepBy : Parser input a, Parser input sep -> Parser input (List a)
sepBy = \parser, separator ->
alt (sepBy1 parser separator) (const [])
ignore : Parser input a -> Parser input {}
ignore = \parser ->
map parser (\_ -> {})

View File

@ -1,261 +0,0 @@
interface Parser.Http
exposes [
Request,
Response,
request,
response,
]
imports [
Parser.Core.{ Parser, ParseResult, map, keep, skip, const, oneOrMore, many },
Parser.Str.{
RawStr,
oneOf,
string,
codeunit,
parseStr,
codeunitSatisfies,
strFromRaw,
anyRawString,
digits,
},
]
# https://www.ietf.org/rfc/rfc2616.txt
Method : [Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch]
HttpVersion : { major : U8, minor : U8 }
Request : {
method : Method,
uri : Str,
httpVersion : HttpVersion,
headers : List [Header Str Str],
body : List U8,
}
Response : {
httpVersion : HttpVersion,
statusCode : U16,
status : Str,
headers : List [Header Str Str],
body : List U8,
}
method : Parser RawStr Method
method =
oneOf [
string "OPTIONS" |> map \_ -> Options,
string "GET" |> map \_ -> Get,
string "POST" |> map \_ -> Post,
string "PUT" |> map \_ -> Put,
string "DELETE" |> map \_ -> Delete,
string "HEAD" |> map \_ -> Head,
string "TRACE" |> map \_ -> Trace,
string "CONNECT" |> map \_ -> Connect,
string "PATCH" |> map \_ -> Patch,
]
expect parseStr method "GET" == Ok Get
expect parseStr method "DELETE" == Ok Delete
# TODO: do we want more structure in the URI, or is Str actually what programs want anyway?
# This is not a full URL!
# Request-URI = "*" | absoluteURI | abs_path | authority
RequestUri : Str
requestUri : Parser RawStr RequestUri
requestUri =
codeunitSatisfies \c -> c != ' '
|> oneOrMore
|> map strFromRaw
sp = codeunit ' '
crlf = string "\r\n"
httpVersion : Parser RawStr HttpVersion
httpVersion =
const (\major -> \minor -> { major, minor })
|> skip (string "HTTP/")
|> keep digits
|> skip (codeunit '.')
|> keep digits
expect
actual = parseStr httpVersion "HTTP/1.1"
expected = Ok { major: 1, minor: 1 }
actual == expected
Header : [Header Str Str]
stringWithoutColon : Parser RawStr Str
stringWithoutColon =
codeunitSatisfies \c -> c != ':'
|> oneOrMore
|> map strFromRaw
stringWithoutCr : Parser RawStr Str
stringWithoutCr =
codeunitSatisfies \c -> c != '\r'
|> oneOrMore
|> map strFromRaw
header : Parser RawStr Header
header =
const (\k -> \v -> Header k v)
|> keep stringWithoutColon
|> skip (string ": ")
|> keep stringWithoutCr
|> skip crlf
expect
actual = parseStr header "Accept-Encoding: gzip, deflate\r\n"
expected = Ok (Header "Accept-Encoding" "gzip, deflate")
actual == expected
request : Parser RawStr Request
request =
const (\m -> \u -> \hv -> \hs -> \b -> { method: m, uri: u, httpVersion: hv, headers: hs, body: b })
|> keep method
|> skip sp
|> keep requestUri
|> skip sp
|> keep httpVersion
|> skip crlf
|> keep (many header)
|> skip crlf
|> keep anyRawString
expect
requestText =
"""
GET /things?id=1 HTTP/1.1\r
Host: bar.example\r
Accept-Encoding: gzip, deflate\r
\r
Hello, world!
"""
actual =
parseStr request requestText
expected : Result Request [ParsingFailure Str, ParsingIncomplete Str]
expected = Ok {
method: Get,
uri: "/things?id=1",
httpVersion: { major: 1, minor: 1 },
headers: [
Header "Host" "bar.example",
Header "Accept-Encoding" "gzip, deflate",
],
body: "Hello, world!" |> Str.toUtf8,
}
actual == expected
expect
requestText =
"""
OPTIONS /resources/post-here/ HTTP/1.1\r
Host: bar.example\r
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r
Accept-Language: en-us,en;q=0.5\r
Accept-Encoding: gzip,deflate\r
Connection: keep-alive\r
Origin: https://foo.example\r
Access-Control-Request-Method: POST\r
Access-Control-Request-Headers: X-PINGOTHER, Content-Type\r
\r\n
"""
actual =
parseStr request requestText
expected = Ok {
method: Options,
uri: "/resources/post-here/",
httpVersion: { major: 1, minor: 1 },
headers: [
Header "Host" "bar.example",
Header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Header "Accept-Language" "en-us,en;q=0.5",
Header "Accept-Encoding" "gzip,deflate",
Header "Connection" "keep-alive",
Header "Origin" "https://foo.example",
Header "Access-Control-Request-Method" "POST",
Header "Access-Control-Request-Headers" "X-PINGOTHER, Content-Type",
],
body: [],
}
actual == expected
response : Parser RawStr Response
response =
const (\hv -> \sc -> \s -> \hs -> \b -> { httpVersion: hv, statusCode: sc, status: s, headers: hs, body: b })
|> keep httpVersion
|> skip sp
|> keep digits
|> skip sp
|> keep stringWithoutCr
|> skip crlf
|> keep (many header)
|> skip crlf
|> keep anyRawString
expect
body =
"""
<!DOCTYPE html>\r
<html lang="en">\r
<head>\r
<meta charset="utf-8">\r
<title>A simple webpage</title>\r
</head>\r
<body>\r
<h1>Simple HTML webpage</h1>\r
<p>Hello, world!</p>\r
</body>\r
</html>\r\n
"""
responseText =
"""
HTTP/1.1 200 OK\r
Content-Type: text/html; charset=utf-8\r
Content-Length: 55743\r
Connection: keep-alive\r
Cache-Control: s-maxage=300, public, max-age=0\r
Content-Language: en-US\r
Date: Thu, 06 Dec 2018 17:37:18 GMT\r
ETag: "2e77ad1dc6ab0b53a2996dfd4653c1c3"\r
Server: meinheld/0.6.1\r
Strict-Transport-Security: max-age=63072000\r
X-Content-Type-Options: nosniff\r
X-Frame-Options: DENY\r
X-XSS-Protection: 1; mode=block\r
Vary: Accept-Encoding,Cookie\r
Age: 7\r
\r
\(body)
"""
actual =
parseStr response responseText
expected =
Ok {
httpVersion: { major: 1, minor: 1 },
statusCode: 200,
status: "OK",
headers: [
Header "Content-Type" "text/html; charset=utf-8",
Header "Content-Length" "55743",
Header "Connection" "keep-alive",
Header "Cache-Control" "s-maxage=300, public, max-age=0",
Header "Content-Language" "en-US",
Header "Date" "Thu, 06 Dec 2018 17:37:18 GMT",
Header "ETag" "\"2e77ad1dc6ab0b53a2996dfd4653c1c3\"",
Header "Server" "meinheld/0.6.1",
Header "Strict-Transport-Security" "max-age=63072000",
Header "X-Content-Type-Options" "nosniff",
Header "X-Frame-Options" "DENY",
Header "X-XSS-Protection" "1; mode=block",
Header "Vary" "Accept-Encoding,Cookie",
Header "Age" "7",
],
body: Str.toUtf8 body,
}
actual == expected

View File

@ -1,216 +0,0 @@
interface Parser.Str
exposes [
RawStr,
parseStr,
parseStrPartial,
parseRawStr,
parseRawStrPartial,
string,
stringRaw,
codeunit,
codeunitSatisfies,
anyString,
anyRawString,
anyCodeunit,
scalar,
oneOf,
digit,
positiveInt,
strFromRaw,
]
imports [Parser.Core.{ Parser, ParseResult, map, oneOrMore, parse, parsePartial, buildPrimitiveParser }]
# Specific string-based parsers:
RawStr : List U8
strFromRaw : RawStr -> Str
strFromRaw = \rawStr ->
rawStr
|> Str.fromUtf8
|> Result.withDefault "Unexpected problem while turning a List U8 (that was originally a Str) back into a Str. This should never happen!"
strToRaw : Str -> RawStr
strToRaw = \str ->
str |> Str.toUtf8
strFromAscii : U8 -> Str
strFromAscii = \asciiNum ->
when Str.fromUtf8 [asciiNum] is
Ok answer -> answer
Err _ -> crash "The number $(Num.toStr asciiNum) is not a valid ASCII constant!"
strFromCodeunit : U8 -> Str
strFromCodeunit = \cu ->
strFromRaw [cu]
## Runs a parser against the start of a list of scalars, allowing the parser to consume it only partially.
parseRawStrPartial : Parser RawStr a, RawStr -> ParseResult RawStr a
parseRawStrPartial = \parser, input ->
parsePartial parser input
## Runs a parser against the start of a string, allowing the parser to consume it only partially.
##
## - If the parser succeeds, returns the resulting value as well as the leftover input.
## - If the parser fails, returns `Err (ParsingFailure msg)`
parseStrPartial : Parser RawStr a, Str -> ParseResult Str a
parseStrPartial = \parser, input ->
parser
|> parseRawStrPartial (strToRaw input)
|> Result.map \{ val: val, input: restRaw } ->
{ val: val, input: strFromRaw restRaw }
## Runs a parser against a string, requiring the parser to consume it fully.
##
## - If the parser succeeds, returns `Ok val`
## - If the parser fails, returns `Err (ParsingFailure msg)`
## - If the parser succeeds but does not consume the full string, returns `Err (ParsingIncomplete leftover)`
parseRawStr : Parser RawStr a, RawStr -> Result a [ParsingFailure Str, ParsingIncomplete RawStr]
parseRawStr = \parser, input ->
parse parser input (\leftover -> List.len leftover == 0)
parseStr : Parser RawStr a, Str -> Result a [ParsingFailure Str, ParsingIncomplete Str]
parseStr = \parser, input ->
parser
|> parseRawStr (strToRaw input)
|> Result.mapErr \problem ->
when problem is
ParsingFailure msg ->
ParsingFailure msg
ParsingIncomplete leftoverRaw ->
ParsingIncomplete (strFromRaw leftoverRaw)
codeunitSatisfies : (U8 -> Bool) -> Parser RawStr U8
codeunitSatisfies = \check ->
buildPrimitiveParser \input ->
{ before: start, others: inputRest } = List.split input 1
when List.get start 0 is
Err OutOfBounds ->
Err (ParsingFailure "expected a codeunit satisfying a condition, but input was empty.")
Ok startCodeunit ->
if check startCodeunit then
Ok { val: startCodeunit, input: inputRest }
else
otherChar = strFromCodeunit startCodeunit
inputStr = strFromRaw input
Err (ParsingFailure "expected a codeunit satisfying a condition but found `\(otherChar)`.\n While reading: `\(inputStr)`")
# Implemented manually instead of on top of codeunitSatisfies
# because of better error messages
codeunit : U8 -> Parser RawStr U8
codeunit = \expectedCodeUnit ->
buildPrimitiveParser \input ->
{ before: start, others: inputRest } = List.split input 1
when List.get start 0 is
Err OutOfBounds ->
errorChar = strFromCodeunit expectedCodeUnit
Err (ParsingFailure "expected char `\(errorChar)` but input was empty.")
Ok startCodeunit ->
if startCodeunit == expectedCodeUnit then
Ok { val: expectedCodeUnit, input: inputRest }
else
errorChar = strFromCodeunit expectedCodeUnit
otherChar = strFromRaw start
inputStr = strFromRaw input
Err (ParsingFailure "expected char `\(errorChar)` but found `\(otherChar)`.\n While reading: `\(inputStr)`")
# Implemented manually instead of a sequence of codeunits
# because of efficiency and better error messages
stringRaw : List U8 -> Parser RawStr (List U8)
stringRaw = \expectedString ->
buildPrimitiveParser \input ->
{ before: start, others: inputRest } = List.split input (List.len expectedString)
if start == expectedString then
Ok { val: expectedString, input: inputRest }
else
errorString = strFromRaw expectedString
otherString = strFromRaw start
inputString = strFromRaw input
Err (ParsingFailure "expected string `\(errorString)` but found `\(otherString)`.\nWhile reading: \(inputString)")
string : Str -> Parser RawStr Str
string = \expectedString ->
strToRaw expectedString
|> stringRaw
|> map \_val -> expectedString
scalar : U8 -> Parser RawStr U8
scalar = \expectedAscii ->
expectedAscii
|> strFromAscii
|> string
|> map \_ -> expectedAscii
# Matches any codeunit
anyCodeunit : Parser RawStr U8
anyCodeunit = codeunitSatisfies (\_ -> Bool.true)
# Matches any bytestring
# and consumes all of it.
# Does not fail.
anyRawString : Parser RawStr RawStr
anyRawString = buildPrimitiveParser \rawStringValue ->
Ok { val: rawStringValue, input: [] }
# Matches any string
# as long as it is valid UTF8.
anyString : Parser RawStr Str
anyString = buildPrimitiveParser \fieldRawString ->
when Str.fromUtf8 fieldRawString is
Ok stringVal ->
Ok { val: stringVal, input: [] }
Err (BadUtf8 _ _) ->
Err (ParsingFailure "Expected a string field, but its contents cannot be parsed as UTF8.")
# betweenBraces : Parser RawStr a -> Parser RawStr a
# betweenBraces = \parser ->
# between parser (scalar '[') (scalar ']')
digit : Parser RawStr U8
digit =
digitParsers =
List.range { start: At '0', end: At '9' }
|> List.map \digitCodeUnit ->
digitCodeUnit
|> codeunit
|> map \_ -> digitCodeUnit - '0'
oneOf digitParsers
# NOTE: Currently happily accepts leading zeroes
positiveInt : Parser RawStr (Int *)
positiveInt =
oneOrMore digit
|> map \digitsList ->
digitsList
|> List.map \char -> Num.intCast char - '0'
|> List.walk 0 \sum, digitVal -> 10 * sum + digitVal
## Try a bunch of different parsers.
##
## The first parser which is tried is the one at the front of the list,
## and the next one is tried until one succeeds or the end of the list was reached.
##
## >>> boolParser : Parser RawStr Bool
## >>> boolParser = oneOf [string "true", string "false"] |> map (\x -> if x == "true" then True else False)
# NOTE: This implementation works, but is limited to parsing strings.
# Blocked until issue #3444 is fixed.
oneOf : List (Parser RawStr a) -> Parser RawStr a
oneOf = \parsers ->
buildPrimitiveParser \input ->
List.walkUntil parsers (Err (ParsingFailure "(no possibilities)")) \_, parser ->
when parseRawStrPartial parser input is
Ok val ->
Break (Ok val)
Err problem ->
Continue (Err problem)

View File

@ -0,0 +1,3 @@
The parser package example has been moved to [https://github.com/lukewilliamboswell/roc-parser](https://github.com/lukewilliamboswell/roc-parser).
The examples here use a release of this package.

View File

@ -1,65 +0,0 @@
app "example"
packages {
pf: "../../platform-switching/zig-platform/main.roc",
parser: "../package/main.roc",
}
imports [
parser.ParserCore.{ Parser, map, keep },
parser.ParserStr.{ RawStr, strFromRaw },
parser.ParserCSV.{ CSV, record, field, string, nat, parseStr },
]
provides [main] to pf
input : Str
input = "Airplane!,1980,\"Robert Hays,Julie Hagerty\"\r\nCaddyshack,1980,\"Chevy Chase,Rodney Dangerfield,Ted Knight,Michael O'Keefe,Bill Murray\""
main : Str
main =
when parseStr movieInfoParser input is
Ok movies ->
moviesString =
movies
|> List.map movieInfoExplanation
|> Str.joinWith ("\n")
nMovies = List.len movies |> Num.toStr
"\(nMovies) movies were found:\n\n\(moviesString)\n\nParse success!\n"
Err problem ->
when problem is
ParsingFailure failure ->
"Parsing failure: \(failure)\n"
ParsingIncomplete leftover ->
leftoverStr = leftover |> List.map strFromRaw |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", "
"Parsing incomplete. Following leftover fields while parsing a record: \(leftoverStr)\n"
SyntaxError error ->
"Parsing failure. Syntax error in the CSV: \(error)"
MovieInfo := { title : Str, releaseYear : Nat, actors : List Str }
movieInfoParser =
record (\title -> \releaseYear -> \actors -> @MovieInfo { title, releaseYear, actors })
|> keep (field string)
|> keep (field nat)
|> keep (field actorsParser)
actorsParser =
string
|> map (\val -> Str.split val ",")
movieInfoExplanation = \@MovieInfo { title, releaseYear, actors } ->
enumeratedActors = enumerate actors
releaseYearStr = Num.toStr releaseYear
"The movie '\(title)' was released in \(releaseYearStr) and stars \(enumeratedActors)"
enumerate : List Str -> Str
enumerate = \elements ->
{ before: inits, others: last } = List.split elements (List.len elements - 1)
last
|> List.prepend (inits |> Str.joinWith ", ")
|> Str.joinWith " and "

View File

@ -1,13 +1,13 @@
app "example"
packages {
cli: "https://github.com/roc-lang/basic-cli/releases/download/0.7.1/Icc3xJoIixF3hCcfXrDwLCu4wQHtNdPyoJkEbkgIElA.tar.br",
parser: "../package/main.roc",
parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/0.5/KB-TITJ4DfunB88sFBWjCtCGV7LRRDdTH5JUXp4gIb8.tar.br",
}
imports [
cli.Stdout,
cli.Stderr,
parser.ParserCore.{ Parser, buildPrimitiveParser, many },
parser.ParserStr.{ parseStr },
parser.Core.{ Parser, buildPrimitiveParser, many },
parser.String.{ parseStr },
]
provides [main] to cli

View File

@ -1,196 +0,0 @@
interface ParserCSV
exposes [
CSV,
CSVRecord,
file,
record,
parseStr,
parseCSV,
parseStrToCSVRecord,
field,
string,
nat,
f64,
]
imports [
ParserCore.{ Parser, parse, buildPrimitiveParser, alt, map, many, sepBy1, between, ignore, flatten, sepBy },
ParserStr.{ RawStr, oneOf, codeunit, codeunitSatisfies, strFromRaw },
]
## This is a CSV parser which follows RFC4180
##
## For simplicity's sake, the following things are not yet supported:
## - CSV files with headings
##
## The following however *is* supported
## - A simple LF ("\n") instead of CRLF ("\r\n") to separate records.
CSV : List CSVRecord
CSVRecord : List CSVField
CSVField : RawStr
## Attempts to parse an `a` from a `Str` that is encoded in CSV format.
parseStr : Parser CSVRecord a, Str -> Result (List a) [ParsingFailure Str, SyntaxError Str, ParsingIncomplete CSVRecord]
parseStr = \csvParser, input ->
when parseStrToCSV input is
Err (ParsingIncomplete rest) ->
restStr = ParserStr.strFromRaw rest
Err (SyntaxError restStr)
Err (ParsingFailure str) ->
Err (ParsingFailure str)
Ok csvData ->
when parseCSV csvParser csvData is
Err (ParsingFailure str) ->
Err (ParsingFailure str)
Err (ParsingIncomplete problem) ->
Err (ParsingIncomplete problem)
Ok vals ->
Ok vals
## Attempts to parse an `a` from a `CSV` datastructure (a list of lists of bytestring-fields).
parseCSV : Parser CSVRecord a, CSV -> Result (List a) [ParsingFailure Str, ParsingIncomplete CSVRecord]
parseCSV = \csvParser, csvData ->
csvData
|> List.mapWithIndex (\recordFieldsList, index -> { record: recordFieldsList, index: index })
|> List.walkUntil (Ok []) \state, { record: recordFieldsList, index: index } ->
when parseCSVRecord csvParser recordFieldsList is
Err (ParsingFailure problem) ->
indexStr = Num.toStr (index + 1)
recordStr = recordFieldsList |> List.map strFromRaw |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", "
problemStr = "\(problem)\nWhile parsing record no. \(indexStr): `\(recordStr)`"
Break (Err (ParsingFailure problemStr))
Err (ParsingIncomplete problem) ->
Break (Err (ParsingIncomplete problem))
Ok val ->
state
|> Result.map (\vals -> List.append vals val)
|> Continue
## Attempts to parse an `a` from a `CSVRecord` datastructure (a list of bytestring-fields)
##
## This parser succeeds when all fields of the CSVRecord are consumed by the parser.
parseCSVRecord : Parser CSVRecord a, CSVRecord -> Result a [ParsingFailure Str, ParsingIncomplete CSVRecord]
parseCSVRecord = \csvParser, recordFieldsList ->
parse csvParser recordFieldsList (\leftover -> leftover == [])
## Wrapper function to combine a set of fields into your desired `a`
##
## ## Usage example
##
## >>> record (\firstName -> \lastName -> \age -> User {firstName, lastName, age})
## >>> |> field string
## >>> |> field string
## >>> |> field nat
##
record : a -> Parser CSVRecord a
record = ParserCore.const
## Turns a parser for a `List U8` into a parser that parses part of a `CSVRecord`.
field : Parser RawStr a -> Parser CSVRecord a
field = \fieldParser ->
buildPrimitiveParser \fieldsList ->
when List.get fieldsList 0 is
Err OutOfBounds ->
Err (ParsingFailure "expected another CSV field but there are no more fields in this record")
Ok rawStr ->
when ParserStr.parseRawStr fieldParser rawStr is
Ok val ->
Ok { val: val, input: List.dropFirst fieldsList 1 }
Err (ParsingFailure reason) ->
fieldStr = rawStr |> strFromRaw
Err (ParsingFailure "Field `\(fieldStr)` could not be parsed. \(reason)")
Err (ParsingIncomplete reason) ->
reasonStr = strFromRaw reason
fieldsStr = fieldsList |> List.map strFromRaw |> Str.joinWith ", "
Err (ParsingFailure "The field parser was unable to read the whole field: `\(reasonStr)` while parsing the first field of leftover \(fieldsStr))")
## Parser for a field containing a UTF8-encoded string
string : Parser CSVField Str
string = ParserStr.anyString
## Parse a natural number from a CSV field
nat : Parser CSVField Nat
nat =
string
|> map
(\val ->
when Str.toNat val is
Ok num ->
Ok num
Err _ ->
Err "\(val) is not a Nat."
)
|> flatten
## Parse a 64-bit float from a CSV field
f64 : Parser CSVField F64
f64 =
string
|> map
(\val ->
when Str.toF64 val is
Ok num ->
Ok num
Err _ ->
Err "\(val) is not a F64."
)
|> flatten
## Attempts to parse a Str into the internal `CSV` datastructure (A list of lists of bytestring-fields).
parseStrToCSV : Str -> Result CSV [ParsingFailure Str, ParsingIncomplete RawStr]
parseStrToCSV = \input ->
parse file (Str.toUtf8 input) (\leftover -> leftover == [])
## Attempts to parse a Str into the internal `CSVRecord` datastructure (A list of bytestring-fields).
parseStrToCSVRecord : Str -> Result CSVRecord [ParsingFailure Str, ParsingIncomplete RawStr]
parseStrToCSVRecord = \input ->
parse csvRecord (Str.toUtf8 input) (\leftover -> leftover == [])
# The following are parsers to turn strings into CSV structures
file : Parser RawStr CSV
file = sepBy csvRecord endOfLine
csvRecord : Parser RawStr CSVRecord
csvRecord = sepBy1 csvField comma
csvField : Parser RawStr CSVField
csvField = alt escapedCsvField nonescapedCsvField
escapedCsvField : Parser RawStr CSVField
escapedCsvField = between escapedContents dquote dquote
escapedContents = many
(
oneOf [
twodquotes |> map (\_ -> '"'),
comma,
cr,
lf,
textdata,
]
)
twodquotes = ParserStr.string "\"\""
nonescapedCsvField : Parser RawStr CSVField
nonescapedCsvField = many textdata
comma = codeunit ','
dquote = codeunit '"'
endOfLine = alt (ignore crlf) (ignore lf)
cr = codeunit '\r'
lf = codeunit '\n'
crlf = ParserStr.string "\r\n"
textdata = codeunitSatisfies (\x -> (x >= 32 && x <= 33) || (x >= 35 && x <= 43) || (x >= 45 && x <= 126)) # Any printable char except " (34) and , (44)

View File

@ -1,354 +0,0 @@
interface ParserCore
exposes [
Parser,
ParseResult,
parse,
parsePartial,
fail,
const,
alt,
keep,
skip,
oneOf,
map,
map2,
map3,
lazy,
maybe,
oneOrMore,
many,
between,
sepBy,
sepBy1,
ignore,
buildPrimitiveParser,
flatten,
]
imports []
## Opaque type for a parser that will try to parse an `a` from an `input`.
##
## As a simple example, you might consider a parser that tries to parse a `U32` from a `Str`.
## Such a process might succeed or fail, depending on the current value of `input`.
##
## As such, a parser can be considered a recipe
## for a function of the type `input -> Result {val: a, input: input} [ParsingFailure Str]`.
##
## How a parser is _actually_ implemented internally is not important
## and this might change between versions;
## for instance to improve efficiency or error messages on parsing failures.
Parser input a := input -> ParseResult input a
ParseResult input a : Result { val : a, input : input } [ParsingFailure Str]
buildPrimitiveParser : (input -> ParseResult input a) -> Parser input a
buildPrimitiveParser = \fun ->
@Parser fun
# -- Generic parsers:
## Most general way of running a parser.
##
## Can be tought of turning the recipe of a parser into its actual parsing function
## and running this function on the given input.
##
## Many (but not all!) parsers consume part of `input` when they succeed.
## This allows you to string parsers together that run one after the other:
## The part of the input that the first parser did not consume, is used by the next parser.
## This is why a parser returns on success both the resulting value and the leftover part of the input.
##
## Of course, this is mostly useful when creating your own internal parsing building blocks.
## `run` or `Parser.Str.runStr` etc. are more useful in daily usage.
parsePartial : Parser input a, input -> ParseResult input a
parsePartial = \@Parser parser, input ->
parser input
## Runs a parser on the given input, expecting it to fully consume the input
##
## The `input -> Bool` parameter is used to check whether parsing has 'completed',
## (in other words: Whether all of the input has been consumed.)
##
## For most (but not all!) input types, a parsing run that leaves some unparsed input behind
## should be considered an error.
parse : Parser input a, input, (input -> Bool) -> Result a [ParsingFailure Str, ParsingIncomplete input]
parse = \parser, input, isParsingCompleted ->
when parsePartial parser input is
Ok { val: val, input: leftover } ->
if isParsingCompleted leftover then
Ok val
else
Err (ParsingIncomplete leftover)
Err (ParsingFailure msg) ->
Err (ParsingFailure msg)
## Parser that can never succeed, regardless of the given input.
## It will always fail with the given error message.
##
## This is mostly useful as 'base case' if all other parsers
## in a `oneOf` or `alt` have failed, to provide some more descriptive error message.
fail : Str -> Parser * *
fail = \msg ->
@Parser \_input -> Err (ParsingFailure msg)
## Parser that will always produce the given `val`, without looking at the actual input.
## This is useful as basic building block, especially in combination with
## `map` and `keep`.
const : a -> Parser * a
const = \val ->
@Parser \input ->
Ok { val: val, input: input }
## Try the `first` parser and (only) if it fails, try the `second` parser as fallback.
alt : Parser input a, Parser input a -> Parser input a
alt = \first, second ->
buildPrimitiveParser \input ->
when parsePartial first input is
Ok { val: val, input: rest } -> Ok { val: val, input: rest }
Err (ParsingFailure firstErr) ->
when parsePartial second input is
Ok { val: val, input: rest } -> Ok { val: val, input: rest }
Err (ParsingFailure secondErr) ->
Err (ParsingFailure ("\(firstErr) or \(secondErr)"))
## Runs a parser building a function, then a parser building a value,
## and finally returns the result of calling the function with the value.
##
## This is useful if you are building up a structure that requires more parameters
## than there are variants of `map`, `map2`, `map3` etc. for.
##
## For instance, the following two are the same:
##
## >>> const (\x, y, z -> Triple x y z)
## >>> |> map3 Parser.Str.nat Parser.Str.nat Parser.Str.nat
##
## >>> const (\x -> \y -> \z -> Triple x y z)
## >>> |> keep Parser.Str.nat
## >>> |> keep Parser.Str.nat
## >>> |> keep Parser.Str.nat
##
## (And indeed, this is how `map`, `map2`, `map3` etc. are implemented under the hood.)
##
## # Currying
## Be aware that when using `keep`, you need to explicitly 'curry' the parameters to the construction function.
## This means that instead of writing `\x, y, z -> ...`
## you'll need to write `\x -> \y -> \z -> ...`.
## This is because the parameters to the function will be applied one-by-one as parsing continues.
keep : Parser input (a -> b), Parser input a -> Parser input b
keep = \funParser, valParser ->
@Parser \input ->
when parsePartial funParser input is
Ok { val: funVal, input: rest } ->
when parsePartial valParser rest is
Ok { val: val, input: rest2 } ->
Ok { val: funVal val, input: rest2 }
Err e ->
Err e
Err e ->
Err e
## Skip over a parsed item as part of a pipeline
##
## This is useful if you are using a pipeline of parsers with `keep` but
## some parsed items are not part of the final result
##
## >>> const (\x -> \y -> \z -> Triple x y z)
## >>> |> keep Parser.Str.nat
## >>> |> skip (codeunit ',')
## >>> |> keep Parser.Str.nat
## >>> |> skip (codeunit ',')
## >>> |> keep Parser.Str.nat
##
skip : Parser input kept, Parser input skipped -> Parser input kept
skip = \kept, skipped ->
@Parser \input ->
when parsePartial kept input is
Ok step1 ->
when parsePartial skipped step1.input is
Ok step2 ->
Ok { val: step1.val, input: step2.input }
Err e ->
Err e
Err e ->
Err e
# Internal utility function. Not exposed to users, since usage is discouraged!
#
# Runs `firstParser` and (only) if it succeeds,
# runs the function `buildNextParser` on its result value.
# This function returns a new parser, which is finally run.
#
# `andThen` is usually more flexible than necessary, and less efficient
# than using `const` with `map` and/or `keep`.
# Consider using those functions first.
andThen : Parser input a, (a -> Parser input b) -> Parser input b
andThen = \@Parser firstParser, buildNextParser ->
@Parser \input ->
when firstParser input is
Ok step ->
(@Parser nextParser) = buildNextParser step.val
nextParser step.input
Err e ->
Err e
## Try a list of parsers in turn, until one of them succeeds
oneOf : List (Parser input a) -> Parser input a
oneOf = \parsers ->
List.walkBackwards parsers (fail "oneOf: The list of parsers was empty") (\laterParser, earlierParser -> alt earlierParser laterParser)
## Transforms the result of parsing into something else,
## using the given transformation function.
map : Parser input a, (a -> b) -> Parser input b
map = \@Parser simpleParser, transform ->
@Parser \input ->
when simpleParser input is
Ok step ->
Ok { val: transform step.val, input: step.input }
Err e ->
Err e
## Transforms the result of parsing into something else,
## using the given two-parameter transformation function.
map2 : Parser input a, Parser input b, (a, b -> c) -> Parser input c
map2 = \@Parser parserA, @Parser parserB, transform ->
@Parser \input ->
when parserA input is
Ok step1 ->
when parserB step1.input is
Ok step2 ->
Ok { val: transform step1.val step2.val, input: step2.input }
Err e ->
Err e
Err e ->
Err e
## Transforms the result of parsing into something else,
## using the given three-parameter transformation function.
##
## If you need transformations with more inputs,
## take a look at `keep`.
map3 : Parser input a, Parser input b, Parser input c, (a, b, c -> d) -> Parser input d
map3 = \@Parser parserA, @Parser parserB, @Parser parserC, transform ->
@Parser \input ->
when parserA input is
Ok step1 ->
when parserB step1.input is
Ok step2 ->
when parserC step2.input is
Ok step3 ->
Ok { val: transform step1.val step2.val step3.val, input: step3.input }
Err e ->
Err e
Err e ->
Err e
Err e ->
Err e
# ^ And this could be repeated for as high as we want, of course.
# Removes a layer of 'result' from running the parser.
#
# This allows for instance to map functions that return a result over the parser,
# where errors are turned into `ParsingFailure` s.
flatten : Parser input (Result a Str) -> Parser input a
flatten = \parser ->
buildPrimitiveParser \input ->
result = parsePartial parser input
when result is
Err problem ->
Err problem
Ok { val: Ok val, input: inputRest } ->
Ok { val: val, input: inputRest }
Ok { val: Err problem, input: _inputRest } ->
Err (ParsingFailure problem)
## Runs a parser lazily
##
## This is (only) useful when dealing with a recursive structure.
## For instance, consider a type `Comment : { message: String, responses: List Comment }`.
## Without `lazy`, you would ask the compiler to build an infinitely deep parser.
## (Resulting in a compiler error.)
##
lazy : ({} -> Parser input a) -> Parser input a
lazy = \thunk ->
const {}
|> andThen thunk
maybe : Parser input a -> Parser input (Result a [Nothing])
maybe = \parser ->
alt (parser |> map (\val -> Ok val)) (const (Err Nothing))
manyImpl : Parser input a, List a, input -> ParseResult input (List a)
manyImpl = \@Parser parser, vals, input ->
result = parser input
when result is
Err _ ->
Ok { val: vals, input: input }
Ok { val: val, input: inputRest } ->
manyImpl (@Parser parser) (List.append vals val) inputRest
## A parser which runs the element parser *zero* or more times on the input,
## returning a list containing all the parsed elements.
##
## Also see `oneOrMore`.
many : Parser input a -> Parser input (List a)
many = \parser ->
@Parser \input ->
manyImpl parser [] input
## A parser which runs the element parser *one* or more times on the input,
## returning a list containing all the parsed elements.
##
## Also see `many`.
oneOrMore : Parser input a -> Parser input (List a)
oneOrMore = \@Parser parser ->
@Parser \input ->
when parser input is
Ok step ->
manyImpl (@Parser parser) [step.val] step.input
Err e ->
Err e
## Runs a parser for an 'opening' delimiter, then your main parser, then the 'closing' delimiter,
## and only returns the result of your main parser.
##
## Useful to recognize structures surrounded by delimiters (like braces, parentheses, quotes, etc.)
##
## >>> betweenBraces = \parser -> parser |> between (scalar '[') (scalar ']')
between : Parser input a, Parser input open, Parser input close -> Parser input a
between = \parser, open, close ->
map3 open parser close (\_, val, _ -> val)
sepBy1 : Parser input a, Parser input sep -> Parser input (List a)
sepBy1 = \parser, separator ->
parserFollowedBySep =
const (\_ -> \val -> val)
|> keep separator
|> keep parser
const (\val -> \vals -> List.prepend vals val)
|> keep parser
|> keep (many parserFollowedBySep)
sepBy : Parser input a, Parser input sep -> Parser input (List a)
sepBy = \parser, separator ->
alt (sepBy1 parser separator) (const [])
ignore : Parser input a -> Parser input {}
ignore = \parser ->
map parser (\_ -> {})

View File

@ -1,250 +0,0 @@
interface ParserHttp
exposes [
Request,
Response,
request,
response,
]
imports [
ParserCore.{ Parser, ParseResult, map, keep, skip, const, oneOrMore, many },
ParserStr.{ RawStr, oneOf, string, codeunit, parseStr, codeunitSatisfies, strFromRaw, anyRawString },
]
# https://www.ietf.org/rfc/rfc2616.txt
Method : [Options, Get, Post, Put, Delete, Head, Trace, Connect, Patch]
HttpVersion : Str
Request : {
method : Method,
uri : Str,
httpVersion : HttpVersion,
headers : List [Header Str Str],
body : List U8,
}
Response : {
httpVersion : HttpVersion,
statusCode : Str,
status : Str,
headers : List [Header Str Str],
body : List U8,
}
method : Parser RawStr Method
method =
oneOf [
string "OPTIONS" |> map \_ -> Options,
string "GET" |> map \_ -> Get,
string "POST" |> map \_ -> Post,
string "PUT" |> map \_ -> Put,
string "DELETE" |> map \_ -> Delete,
string "HEAD" |> map \_ -> Head,
string "TRACE" |> map \_ -> Trace,
string "CONNECT" |> map \_ -> Connect,
string "PATCH" |> map \_ -> Patch,
]
expect parseStr method "GET" == Ok Get
expect parseStr method "DELETE" == Ok Delete
# TODO: do we want more structure in the URI, or is Str actually what programs want anyway?
# This is not a full URL!
# Request-URI = "*" | absoluteURI | abs_path | authority
RequestUri : Str
requestUri : Parser RawStr RequestUri
requestUri =
codeunitSatisfies \c -> c != ' '
|> oneOrMore
|> map strFromRaw
sp = codeunit ' '
crlf = string "\r\n"
# TODO: The 'digit' and 'digits' from ParserStr are causing repl.expect to blow up
digit = codeunitSatisfies \c -> c >= '0' && c <= '9'
digits = digit |> oneOrMore |> map strFromRaw
httpVersion : Parser RawStr HttpVersion
httpVersion =
const (\major -> \minor -> "\(major).\(minor)")
|> skip (string "HTTP/")
|> keep digits
|> skip (codeunit '.')
|> keep digits
Header : [Header Str Str]
stringWithoutColon : Parser RawStr Str
stringWithoutColon =
codeunitSatisfies \c -> c != ':'
|> oneOrMore
|> map strFromRaw
stringWithoutCr : Parser RawStr Str
stringWithoutCr =
codeunitSatisfies \c -> c != '\r'
|> oneOrMore
|> map strFromRaw
header : Parser RawStr Header
header =
const (\k -> \v -> Header k v)
|> keep stringWithoutColon
|> skip (string ": ")
|> keep stringWithoutCr
|> skip crlf
expect
actual = parseStr header "Accept-Encoding: gzip, deflate\r\n"
expected = Ok (Header "Accept-Encoding" "gzip, deflate")
actual == expected
request : Parser RawStr Request
request =
const (\m -> \u -> \hv -> \hs -> \b -> { method: m, uri: u, httpVersion: hv, headers: hs, body: b })
|> keep method
|> skip sp
|> keep requestUri
|> skip sp
|> keep httpVersion
|> skip crlf
|> keep (many header)
|> skip crlf
|> keep anyRawString
expect
requestText =
"""
GET /things?id=1 HTTP/1.1\r
Host: bar.example\r
Accept-Encoding: gzip, deflate\r
\r
Hello, world!
"""
actual =
parseStr request requestText
expected : Result Request [ParsingFailure Str, ParsingIncomplete Str]
expected = Ok {
method: Get,
uri: "/things?id=1",
httpVersion: "1.1",
headers: [
Header "Host" "bar.example",
Header "Accept-Encoding" "gzip, deflate",
],
body: "Hello, world!" |> Str.toUtf8,
}
actual == expected
expect
requestText =
"""
OPTIONS /resources/post-here/ HTTP/1.1\r
Host: bar.example\r
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r
Accept-Language: en-us,en;q=0.5\r
Accept-Encoding: gzip,deflate\r
Connection: keep-alive\r
Origin: https://foo.example\r
Access-Control-Request-Method: POST\r
Access-Control-Request-Headers: X-PINGOTHER, Content-Type\r
\r\n
"""
actual =
parseStr request requestText
expected = Ok {
method: Options,
uri: "/resources/post-here/",
httpVersion: "1.1",
headers: [
Header "Host" "bar.example",
Header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
Header "Accept-Language" "en-us,en;q=0.5",
Header "Accept-Encoding" "gzip,deflate",
Header "Connection" "keep-alive",
Header "Origin" "https://foo.example",
Header "Access-Control-Request-Method" "POST",
Header "Access-Control-Request-Headers" "X-PINGOTHER, Content-Type",
],
body: [],
}
actual == expected
response : Parser RawStr Response
response =
const (\hv -> \sc -> \s -> \hs -> \b -> { httpVersion: hv, statusCode: sc, status: s, headers: hs, body: b })
|> keep httpVersion
|> skip sp
|> keep digits
|> skip sp
|> keep stringWithoutCr
|> skip crlf
|> keep (many header)
|> skip crlf
|> keep anyRawString
expect
body =
"""
<!DOCTYPE html>\r
<html lang="en">\r
<head>\r
<meta charset="utf-8">\r
<title>A simple webpage</title>\r
</head>\r
<body>\r
<h1>Simple HTML webpage</h1>\r
<p>Hello, world!</p>\r
</body>\r
</html>\r\n
"""
responseText =
"""
HTTP/1.1 200 OK\r
Content-Type: text/html; charset=utf-8\r
Content-Length: 55743\r
Connection: keep-alive\r
Cache-Control: s-maxage=300, public, max-age=0\r
Content-Language: en-US\r
Date: Thu, 06 Dec 2018 17:37:18 GMT\r
ETag: "2e77ad1dc6ab0b53a2996dfd4653c1c3"\r
Server: meinheld/0.6.1\r
Strict-Transport-Security: max-age=63072000\r
X-Content-Type-Options: nosniff\r
X-Frame-Options: DENY\r
X-XSS-Protection: 1; mode=block\r
Vary: Accept-Encoding,Cookie\r
Age: 7\r
\r
\(body)
"""
actual =
parseStr response responseText
expected =
Ok {
httpVersion: "1.1",
statusCode: "200",
status: "OK",
headers: [
Header "Content-Type" "text/html; charset=utf-8",
Header "Content-Length" "55743",
Header "Connection" "keep-alive",
Header "Cache-Control" "s-maxage=300, public, max-age=0",
Header "Content-Language" "en-US",
Header "Date" "Thu, 06 Dec 2018 17:37:18 GMT",
Header "ETag" "\"2e77ad1dc6ab0b53a2996dfd4653c1c3\"",
Header "Server" "meinheld/0.6.1",
Header "Strict-Transport-Security" "max-age=63072000",
Header "X-Content-Type-Options" "nosniff",
Header "X-Frame-Options" "DENY",
Header "X-XSS-Protection" "1; mode=block",
Header "Vary" "Accept-Encoding,Cookie",
Header "Age" "7",
],
body: Str.toUtf8 body,
}
actual == expected

View File

@ -1,226 +0,0 @@
interface ParserStr
exposes [
RawStr,
parseStr,
parseStrPartial,
parseRawStr,
parseRawStrPartial,
string,
stringRaw,
codeunit,
codeunitSatisfies,
anyString,
anyRawString,
anyCodeunit,
ascii,
oneOf,
digit,
positiveInt,
strFromRaw,
]
imports [
ParserCore.{
Parser,
ParseResult,
map,
oneOrMore,
parse,
parsePartial,
buildPrimitiveParser,
},
]
# Specific string-based parsers:
RawStr : List U8
strFromRaw : RawStr -> Str
strFromRaw = \rawStr ->
rawStr
|> Str.fromUtf8
|> Result.withDefault "Unexpected problem while turning a List U8 (that was originally a Str) back into a Str. This should never happen!"
strToRaw : Str -> RawStr
strToRaw = \str ->
str |> Str.toUtf8
strFromAscii : U8 -> Str
strFromAscii = \asciiNum ->
when Str.fromUtf8 [asciiNum] is
Ok answer -> answer
Err _ -> crash "The number $(Num.toStr asciiNum) is not a valid ASCII constant!"
strFromCodeunit : U8 -> Str
strFromCodeunit = \cu ->
strFromRaw [cu]
## Runs a parser against the start of a list of bytes, allowing the parser to consume it only partially.
parseRawStrPartial : Parser RawStr a, RawStr -> ParseResult RawStr a
parseRawStrPartial = \parser, input ->
parsePartial parser input
## Runs a parser against the start of a string, allowing the parser to consume it only partially.
##
## - If the parser succeeds, returns the resulting value as well as the leftover input.
## - If the parser fails, returns `Err (ParsingFailure msg)`
parseStrPartial : Parser RawStr a, Str -> ParseResult Str a
parseStrPartial = \parser, input ->
parser
|> parseRawStrPartial (strToRaw input)
|> Result.map \{ val: val, input: restRaw } ->
{ val: val, input: strFromRaw restRaw }
## Runs a parser against a string, requiring the parser to consume it fully.
##
## - If the parser succeeds, returns `Ok val`
## - If the parser fails, returns `Err (ParsingFailure msg)`
## - If the parser succeeds but does not consume the full string, returns `Err (ParsingIncomplete leftover)`
parseRawStr : Parser RawStr a, RawStr -> Result a [ParsingFailure Str, ParsingIncomplete RawStr]
parseRawStr = \parser, input ->
parse parser input (\leftover -> List.len leftover == 0)
parseStr : Parser RawStr a, Str -> Result a [ParsingFailure Str, ParsingIncomplete Str]
parseStr = \parser, input ->
parser
|> parseRawStr (strToRaw input)
|> Result.mapErr \problem ->
when problem is
ParsingFailure msg ->
ParsingFailure msg
ParsingIncomplete leftoverRaw ->
ParsingIncomplete (strFromRaw leftoverRaw)
codeunitSatisfies : (U8 -> Bool) -> Parser RawStr U8
codeunitSatisfies = \check ->
buildPrimitiveParser \input ->
{ before: start, others: inputRest } = List.split input 1
when List.get start 0 is
Err OutOfBounds ->
Err (ParsingFailure "expected a codeunit satisfying a condition, but input was empty.")
Ok startCodeunit ->
if check startCodeunit then
Ok { val: startCodeunit, input: inputRest }
else
otherChar = strFromCodeunit startCodeunit
inputStr = strFromRaw input
Err (ParsingFailure "expected a codeunit satisfying a condition but found `\(otherChar)`.\n While reading: `\(inputStr)`")
# Implemented manually instead of on top of codeunitSatisfies
# because of better error messages
codeunit : U8 -> Parser RawStr U8
codeunit = \expectedCodeUnit ->
buildPrimitiveParser \input ->
{ before: start, others: inputRest } = List.split input 1
when List.get start 0 is
Err OutOfBounds ->
errorChar = strFromCodeunit expectedCodeUnit
Err (ParsingFailure "expected char `\(errorChar)` but input was empty.")
Ok startCodeunit ->
if startCodeunit == expectedCodeUnit then
Ok { val: expectedCodeUnit, input: inputRest }
else
errorChar = strFromCodeunit expectedCodeUnit
otherChar = strFromRaw start
inputStr = strFromRaw input
Err (ParsingFailure "expected char `\(errorChar)` but found `\(otherChar)`.\n While reading: `\(inputStr)`")
# Implemented manually instead of a sequence of codeunits
# because of efficiency and better error messages
stringRaw : List U8 -> Parser RawStr (List U8)
stringRaw = \expectedString ->
buildPrimitiveParser \input ->
{ before: start, others: inputRest } = List.split input (List.len expectedString)
if start == expectedString then
Ok { val: expectedString, input: inputRest }
else
errorString = strFromRaw expectedString
otherString = strFromRaw start
inputString = strFromRaw input
Err (ParsingFailure "expected string `\(errorString)` but found `\(otherString)`.\nWhile reading: \(inputString)")
string : Str -> Parser RawStr Str
string = \expectedString ->
strToRaw expectedString
|> stringRaw
|> map \_val -> expectedString
ascii : U8 -> Parser RawStr U8
ascii = \expectedAscii ->
expectedAscii
|> strFromAscii
|> string
|> map \_ -> expectedAscii
# Matches any codeunit
anyCodeunit : Parser RawStr U8
anyCodeunit = codeunitSatisfies (\_ -> Bool.true)
# Matches any bytestring
# and consumes all of it.
# Does not fail.
anyRawString : Parser RawStr RawStr
anyRawString = buildPrimitiveParser \rawStringValue ->
Ok { val: rawStringValue, input: [] }
# Matches any string
# as long as it is valid UTF8.
anyString : Parser RawStr Str
anyString = buildPrimitiveParser \fieldRawString ->
when Str.fromUtf8 fieldRawString is
Ok stringVal ->
Ok { val: stringVal, input: [] }
Err (BadUtf8 _ _) ->
Err (ParsingFailure "Expected a string field, but its contents cannot be parsed as UTF8.")
# betweenBraces : Parser RawStr a -> Parser RawStr a
# betweenBraces = \parser ->
# between parser (scalar '[') (scalar ']')
digit : Parser RawStr U8
digit =
digitParsers =
List.range { start: At '0', end: At '9' }
|> List.map \digitNum ->
digitNum
|> codeunit
|> map \_ -> digitNum
oneOf digitParsers
# NOTE: Currently happily accepts leading zeroes
positiveInt : Parser RawStr (Int *)
positiveInt =
oneOrMore digit
|> map \digitsList ->
digitsList
|> List.map \char -> Num.intCast char - '0'
|> List.walk 0 \sum, digitVal -> 10 * sum + digitVal
## Try a bunch of different parsers.
##
## The first parser which is tried is the one at the front of the list,
## and the next one is tried until one succeeds or the end of the list was reached.
##
## >>> boolParser : Parser RawStr Bool
## >>> boolParser = oneOf [string "true", string "false"] |> map (\x -> if x == "true" then True else False)
# NOTE: This implementation works, but is limited to parsing strings.
# Blocked until issue #3444 is fixed.
oneOf : List (Parser RawStr a) -> Parser RawStr a
oneOf = \parsers ->
buildPrimitiveParser \input ->
List.walkUntil parsers (Err (ParsingFailure "(no possibilities)")) \_, parser ->
when parseRawStrPartial parser input is
Ok val ->
Break (Ok val)
Err problem ->
Continue (Err problem)

View File

@ -1,8 +0,0 @@
package "parser"
exposes [
ParserCore,
ParserCSV,
ParserStr,
ParserHttp,
]
packages {}

View File

@ -0,0 +1,68 @@
app "example"
packages {
pf: "https://github.com/roc-lang/basic-cli/releases/download/0.7.1/Icc3xJoIixF3hCcfXrDwLCu4wQHtNdPyoJkEbkgIElA.tar.br",
parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/0.5/KB-TITJ4DfunB88sFBWjCtCGV7LRRDdTH5JUXp4gIb8.tar.br",
}
imports [
pf.Stdout,
pf.Stderr,
pf.Task.{ Task },
parser.Core.{ Parser, map, keep },
parser.String.{ strFromUtf8 },
parser.CSV.{ CSV },
]
provides [main] to pf
input : Str
input = "Airplane!,1980,\"Robert Hays,Julie Hagerty\"\r\nCaddyshack,1980,\"Chevy Chase,Rodney Dangerfield,Ted Knight,Michael O'Keefe,Bill Murray\""
main : Task {} *
main =
when CSV.parseStr movieInfoParser input is
Ok movies ->
moviesString =
movies
|> List.map movieInfoExplanation
|> Str.joinWith ("\n")
nMovies = List.len movies |> Num.toStr
Stdout.line "\(nMovies) movies were found:\n\n\(moviesString)\n\nParse success!\n"
Err problem ->
when problem is
ParsingFailure failure ->
Stderr.line "Parsing failure: \(failure)\n"
ParsingIncomplete leftover ->
leftoverStr = leftover |> List.map strFromUtf8 |> List.map (\val -> "\"\(val)\"") |> Str.joinWith ", "
Stderr.line "Parsing incomplete. Following leftover fields while parsing a record: \(leftoverStr)\n"
SyntaxError error ->
Stderr.line "Parsing failure. Syntax error in the CSV: \(error)"
MovieInfo := { title : Str, releaseYear : U64, actors : List Str }
movieInfoParser =
CSV.record (\title -> \releaseYear -> \actors -> @MovieInfo { title, releaseYear, actors })
|> keep (CSV.field CSV.string)
|> keep (CSV.field CSV.u64)
|> keep (CSV.field actorsParser)
actorsParser =
CSV.string
|> map \val -> Str.split val ","
movieInfoExplanation = \@MovieInfo { title, releaseYear, actors } ->
enumeratedActors = enumerate actors
releaseYearStr = Num.toStr releaseYear
"The movie '\(title)' was released in \(releaseYearStr) and stars \(enumeratedActors)"
enumerate : List Str -> Str
enumerate = \elements ->
{ before: inits, others: last } = List.split elements (List.len elements - 1)
last
|> List.prepend (inits |> Str.joinWith ", ")
|> Str.joinWith " and "