circuits-parser
Safe HaskellNone
LanguageGHC2024

Circuit.Parser.Json

Description

JSON for the circuits ecosystem: the aeson contact, rebuilt on Circuit.Parser combinators — decodeJson parses, encodeJson renders, and the two are exact tree inverses.

Recursive descent over the Uncons ByteString Char stream; string and number payloads are captured as zero-copy slices (bs) and converted after recognition, so UTF-8 stays bytes until unescape decodes it once.

Grammar notes: object and array tails commit on , — a trailing comma is a parse failure, not a short list. Numbers follow the JSON grammar exactly (-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?), leading zeros rejected.

Two paths, one tree: this module is the combinator path (decodeJson is the boundary); Circuit.Parser.Json.Lexer is the fast path, a flat zero-copy token stream for when composition is not the point.

Synopsis

The tree

data Json Source #

A JSON value.

JNumber is exact (Scientific: coefficient times ten to an exponent), so parsing loses nothing. JObject is an association list in source order.

Instances

Instances details
Eq Json Source # 
Instance details

Defined in Circuit.Parser.Json.Value

Methods

(==) :: Json -> Json -> Bool #

(/=) :: Json -> Json -> Bool #

Show Json Source # 
Instance details

Defined in Circuit.Parser.Json.Value

Methods

showsPrec :: Int -> Json -> ShowS #

show :: Json -> String #

showList :: [Json] -> ShowS #

Parsing

value :: Parser Identity ByteString Char Json Source #

Parse any JSON value, leading whitespace included.

jstring :: Parser Identity ByteString Char Text Source #

Parse a JSON string literal (quotes included) to Text.

Recognition is byte-level — the raw slice between the quotes is captured with escapes intact and decoded once by unescape.

ws :: Parser Identity ByteString Char () Source #

JSON whitespace: space, newline, carriage return, tab. Nothing else.

Conversion

bsToScientific :: ByteString -> Scientific Source #

Convert a recognised JSON number slice to Scientific.

The slice is trusted to match the JSON number grammar (this is what jnumber recognises); anything else is garbage in, garbage out.

>>> bsToScientific (C.pack "123")
123.0
>>> bsToScientific (C.pack "-0.5")
-0.5
>>> bsToScientific (C.pack "1e-3")
1.0e-3

Boundary

decodeJson :: ByteString -> Either String Json Source #

Parse a JSON document: a value, optional trailing whitespace, and nothing else.

Failure is a flat Left — the combinator runner backtracks to the original stream, so there is no honest offset to report. For offsets, tokenize first with Circuit.Parser.Json.Lexer.

>>> decodeJson (C.pack "{\"a\": [1, true, null]}")
Right (JObject [("a",JArray [JNumber 1.0,JBool True,JNull])])
>>> decodeJson (C.pack "  \"a\\nb\"  ")
Right (JString "a\nb")
>>> decodeJson (C.pack "[1,]")
Left "invalid JSON"
>>> decodeJson (C.pack "01")
Left "invalid JSON"

encodeJson :: Json -> ByteString Source #

Render a JSON document, compact (no insignificant whitespace).

The other half of the boundary: decodeJson and encodeJson are exact tree inverses —

decodeJson (encodeJson j) == Right j
>>> decodeJson (encodeJson (JObject [("a", JArray (V.fromList [JNumber 1, JBool True, JNull]))]))
Right (JObject [("a",JArray [JNumber 1.0,JBool True,JNull])])
>>> decodeJson (encodeJson (JString "a\nb"))
Right (JString "a\nb")
>>> decodeJson (encodeJson (JObject []))
Right (JObject [])

Numbers render in a form the parser reads back to the same Scientific (its equality is structural): zero exponent renders the bare coefficient (10), a positive exponent renders coefficient and exponent (1e1, not 10), and a negative exponent renders the decimal point positionally, keeping every fractional digit (1.50, 0.0015). Object pairs render in tree order; duplicate keys are preserved, as they are in the tree.

>>> encodeJson (JObject [("a", JArray (V.fromList [JNumber 1, JBool True, JNull]))])
"{\"a\":[1,true,null]}"
>>> encodeJson (JString "a\nb")
"\"a\\nb\""

Fast path

data JsonToken Source #

A flat JSON structural token. TString and TNumber payloads are zero-copy slices of the input; a TString keeps its escapes intact.

Instances

Instances details
Eq JsonToken Source # 
Instance details

Defined in Circuit.Parser.Json.Lexer

Show JsonToken Source # 
Instance details

Defined in Circuit.Parser.Json.Lexer

runJsonLexerBS :: ByteString -> Either (Int, String) [JsonToken] Source #

Tokenize a strict ByteString.

On failure, returns the byte offset and a message.

>>> runJsonLexerBS (C.pack "{\"a\": [1, true]}")
Right [TBraceOpen,TString "a",TColon,TBrackOpen,TNumber "1",TComma,TTrue,TBrackClose,TBraceClose]
>>> runJsonLexerBS (C.pack "[1")
Right [TBrackOpen,TNumber "1"]
>>> runJsonLexerBS (C.pack "\"ab")
Left (3,"unterminated string")
>>> runJsonLexerBS (C.pack "nul")
Left (0,"bad literal")
>>> runJsonLexerBS (C.pack "@")
Left (0,"unexpected byte 64")

Pieces

unescape :: ByteString -> Either String Text Source #

Unescape a raw JSON string slice (the bytes between the quotes) to Text.

>>> unescape (C.pack "hello")
Right "hello"
>>> unescape (C.pack "a\\nb")
Right "a\nb"
>>> unescape (C.pack "\\u0041\\u00e9")
Right "A\233"
>>> unescape (C.pack "\\uD834\\uDD1E")
Right "\119070"
>>> unescape (C.pack "bad\\x")
Left "invalid escape: \\x"