Packages

A fast, spec compliant, generic JSON parser and encoder in Gleam

Current section

Files

Jump to
gj src gj_decode.gleam
Raw

src/gj_decode.gleam

//// Decoder module for GJ
////
//// See module gj for the public interface
import gleam/bit_builder.{BitBuilder}
import gleam/bit_string
import gleam/float
import gleam/int
import gleam/io
import gleam/list
import gleam/result
import gleam/string
import generic_json.{Array, Boolean, JSON, Null, Number, Object, String}
/// Types of errors that can occur during parsing
pub type ParseError {
UnexpectedToken(String)
UnexpectedEndOfInput
InvalidString
InvalidUnicodeSequence
InvalidNumber
}
/// Location of a parser error
pub type ParseErrorLocation {
ParseErrorLocation(row: Int, column: Int)
}
type State {
State(column: Int, row: Int, rest: String)
}
pub fn decode(
str: String,
) -> Result(JSON, tuple(ParseError, ParseErrorLocation)) {
let state0 = State(column: 0, row: 0, rest: str)
let parse_result = parse_value(state0)
let final = case parse_result {
tuple(state1, Ok(val)) -> {
let state2 = chomp_whitespace(state1)
case next_grapheme(state2.rest) {
Error(Nil) -> tuple(state2, Ok(val))
Ok(tuple(c, _)) -> tuple(state2, Error(UnexpectedToken(c)))
}
}
_ -> parse_result
}
case final {
tuple(_, Ok(v)) -> Ok(v)
tuple(s, Error(pe)) ->
Error(tuple(pe, ParseErrorLocation(row: s.row, column: s.column)))
}
}
fn parse_value(state0: State) -> tuple(State, Result(JSON, ParseError)) {
let state = chomp_whitespace(state0)
let parse_result = case next_grapheme(state.rest) {
Error(Nil) -> tuple(state, Error(UnexpectedEndOfInput))
Ok(tuple(c, rest)) ->
case c {
// null
"n" ->
case pop_str(rest, 3) {
Ok(tuple(["u", "l", "l"], rest1)) -> tuple(
State(..state, column: state.column + 4, rest: rest1),
Ok(Null),
)
_ -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
// true
"t" ->
case pop_str(rest, 3) {
Ok(tuple(["r", "u", "e"], rest1)) -> tuple(
State(..state, column: state.column + 4, rest: rest1),
Ok(Boolean(True)),
)
_ -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
// false
"f" ->
case pop_str(rest, 4) {
Ok(tuple(["a", "l", "s", "e"], rest1)) -> tuple(
State(..state, column: state.column + 5, rest: rest1),
Ok(Boolean(False)),
)
_ -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
// array start
"[" ->
parse_array(
State(..state, column: state.column + 1, rest: rest),
AValueOrEnd,
[],
)
// object start
"{" ->
parse_object(
State(..state, column: state.column + 1, rest: rest),
OKeyOrEnd,
[],
)
// string start
"\"" ->
State(..state, column: state.column + 1, rest: rest)
|> parse_string()
// number start
"-" ->
State(..state, column: state.column + 1, rest: rest)
|> parse_number(LeadingMinus, bit_builder.from_string(c))
"0" ->
State(..state, column: state.column + 1, rest: rest)
|> parse_number(LeadingZero, bit_builder.from_string(c))
"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ->
State(..state, column: state.column + 1, rest: rest)
|> parse_number(Whole, bit_builder.from_string(c))
// unexpected char
_ -> tuple(state, Error(UnexpectedToken(c)))
}
}
parse_result
}
type NumberPhase {
LeadingMinus
LeadingZero
Whole
DecimalInit
Decimal
ExponentInit
ExponentLeadingSign
DigitOrEnd
NumberError
}
fn parse_number(
state0: State,
phase: NumberPhase,
acc: BitBuilder,
) -> tuple(State, Result(JSON, ParseError)) {
let can_end = case phase {
LeadingZero | Whole | Decimal | DigitOrEnd -> True
LeadingMinus | DecimalInit | ExponentInit | NumberError | ExponentLeadingSign ->
False
}
case next_grapheme(state0.rest) {
Error(Nil) ->
case can_end {
True ->
case resolve_number(acc) {
Ok(json) -> tuple(state0, Ok(json))
Error(_) -> tuple(state0, Error(InvalidNumber))
}
False -> tuple(state0, Error(UnexpectedEndOfInput))
}
Ok(tuple(c, rest)) ->
case c {
" " | "\n" | "]" | "}" | "," | "\t" ->
case can_end {
True ->
case resolve_number(acc) {
Ok(json) -> tuple(state0, Ok(json))
Error(_) -> tuple(state0, Error(InvalidNumber))
}
False -> tuple(state0, Error(UnexpectedEndOfInput))
}
_ -> {
let new_phase = case phase {
LeadingMinus ->
case c {
"0" -> LeadingZero
"1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" -> Whole
_ -> NumberError
}
LeadingZero ->
case c {
"." -> DecimalInit
"e" | "E" -> ExponentInit
_ -> NumberError
}
Whole ->
case c {
"." -> DecimalInit
"e" | "E" -> ExponentInit
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ->
Whole
_ -> NumberError
}
DecimalInit ->
case c {
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ->
Decimal
_ -> NumberError
}
Decimal ->
case c {
"e" | "E" -> ExponentInit
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ->
Decimal
_ -> NumberError
}
ExponentInit ->
case c {
"+" | "-" -> ExponentLeadingSign
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ->
DigitOrEnd
_ -> NumberError
}
ExponentLeadingSign | DigitOrEnd ->
case c {
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ->
DigitOrEnd
_ -> NumberError
}
NumberError -> NumberError
}
case new_phase {
NumberError -> tuple(state0, Error(UnexpectedToken(c)))
_ -> {
// These coersions are to play nice with Gleam's int.parse and float.parse
let new_acc = case phase, new_phase, c {
LeadingZero, ExponentInit, _ ->
acc
|> bit_builder.append_string(".0")
|> bit_builder.append_string(c)
Whole, ExponentInit, _ ->
acc
|> bit_builder.append_string(".0")
|> bit_builder.append_string(c)
ExponentInit, ExponentLeadingSign, "+" -> acc
_, _, _ ->
acc
|> bit_builder.append_string(c)
}
parse_number(
State(..state0, column: state0.column + 1, rest: rest),
new_phase,
new_acc,
)
}
}
}
}
}
}
type ObjectSearch {
OKeyOrEnd
OKey
OColon(String)
OValue(String)
OCommaOrEnd
}
fn parse_object(
state0: State,
search: ObjectSearch,
stack: List(tuple(String, JSON)),
) -> tuple(State, Result(JSON, ParseError)) {
let state = chomp_whitespace(state0)
// TODO: can you have multiple guards?
let can_parse_string = case search {
OKey | OKeyOrEnd -> True
OColon(_) | OValue(_) | OCommaOrEnd -> False
}
case next_grapheme(state.rest) {
Error(Nil) -> tuple(state, Error(UnexpectedEndOfInput))
Ok(tuple(c, rest)) ->
case c {
"," ->
case search {
OCommaOrEnd ->
State(..state, column: state.column + 1, rest: rest)
|> parse_object(OKey, stack)
OColon(_) | OValue(_) | OKeyOrEnd | OKey -> tuple(
state,
Error(UnexpectedToken(c)),
)
}
"}" ->
case search {
OCommaOrEnd | OKeyOrEnd -> tuple(
State(..state, column: state.column + 1, rest: rest),
Ok(Object(list.reverse(stack))),
)
OValue(_) | OColon(_) | OKey -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
":" ->
case search {
OColon(str) ->
State(..state, column: state.column + 1, rest: rest)
|> parse_object(OValue(str), stack)
OValue(_) | OCommaOrEnd | OKeyOrEnd | OKey -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
"\"" if can_parse_string -> {
let state1 = State(..state, column: state.column + 1, rest: rest)
case parse_string(state1) {
tuple(state2, Ok(String(str))) ->
parse_object(state2, OColon(str), stack)
err -> err
}
}
_ ->
case search {
OValue(key) ->
case parse_value(state) {
tuple(state2, Ok(json)) ->
parse_object(state2, OCommaOrEnd, [tuple(key, json), ..stack])
err -> err
}
OColon(_) | OKey | OCommaOrEnd | OKeyOrEnd -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
}
}
}
type CharStatus {
Allowed(String)
NotAllowed
}
fn char_status(c: String) -> CharStatus {
case valid_in_string(c) {
False -> NotAllowed
True -> Allowed(c)
}
}
fn parse_string(state0: State) -> tuple(State, Result(JSON, ParseError)) {
do_parse_string(state0, bit_builder.from_string(""))
}
type EscapeResult {
Add(String)
InvalidEscape
Unicode(tuple(State, Result(String, ParseError)))
}
fn do_parse_string(
state0: State,
acc: BitBuilder,
) -> tuple(State, Result(JSON, ParseError)) {
case next_grapheme(state0.rest) {
Error(Nil) -> tuple(state0, Error(UnexpectedEndOfInput))
Ok(tuple(c, rest)) ->
case c {
"\\" ->
case next_grapheme(rest) {
Error(Nil) -> tuple(state0, Error(UnexpectedEndOfInput))
Ok(tuple(c2, rest2)) -> {
let next = case c2 {
"\"" -> Add("\"")
"/" -> Add("/")
"\\" -> Add("\\")
"b" -> Add("\b")
"f" -> Add("\f")
"n" -> Add("\n")
"r" -> Add("\r")
"t" -> Add("\t")
"u" ->
State(..state0, column: state0.column + 2, rest: rest2)
|> parse_unicode()
|> Unicode()
_ -> InvalidEscape
}
case next {
Add(n) ->
State(..state0, column: state0.column + 2, rest: rest2)
|> do_parse_string(bit_builder.append_string(acc, n))
InvalidEscape -> tuple(state0, Error(UnexpectedToken(c2)))
Unicode(tuple(state2, uni)) ->
case uni {
Ok(str) ->
state2
|> do_parse_string(bit_builder.append_string(acc, str))
Error(e) -> tuple(state2, Error(e))
}
}
}
}
_ ->
case char_status(c) {
Allowed("\"") ->
case bit_string.to_string(bit_builder.to_bit_string(acc)) {
Ok(str) -> tuple(
State(..state0, column: state0.column + 1, rest: rest),
Ok(String(str)),
)
_ -> tuple(state0, Error(InvalidString))
}
Allowed(_) ->
State(..state0, column: state0.column + 1, rest: rest)
|> do_parse_string(bit_builder.append_string(acc, c))
NotAllowed -> tuple(state0, Error(UnexpectedToken(c)))
}
}
}
}
type UnicodeToBinResult {
Incomplete
Nogood
Complete(String)
}
fn parse_unicode(state0: State) -> tuple(State, Result(String, ParseError)) {
case pop_str(state0.rest, 4) {
Ok(tuple([a, b, c, d], rest)) ->
case hex_to_unicode(a, b, c, d) {
Ok(uni) ->
case uni_to_bin([uni]) {
Incomplete -> {
let state = State(..state0, column: state0.column + 4, rest: rest)
case pop_str(state.rest, 6) {
Ok(tuple(["\\", "u", e, f, g, h], rest2)) ->
case hex_to_unicode(e, f, g, h) {
Ok(uni2) ->
case uni_to_bin([uni, uni2]) {
Complete(str) -> tuple(
State(..state, column: state.column + 4, rest: rest2),
Ok(str),
)
_ -> tuple(state, Error(InvalidUnicodeSequence))
}
Error(int) -> tuple(
State(..state, column: state.column + int, rest: rest),
Error(UnexpectedToken(
list.at([e, f, g, h], int - 1)
|> result.unwrap(""),
)),
)
}
_ -> tuple(state, Error(UnexpectedEndOfInput))
}
}
Nogood -> tuple(state0, Error(InvalidUnicodeSequence))
Complete(str) -> tuple(
State(..state0, column: state0.column + 4, rest: rest),
Ok(str),
)
}
Error(int) -> tuple(
State(..state0, column: state0.column + int, rest: rest),
Error(UnexpectedToken(
list.at([a, b, c, d], int - 1)
|> result.unwrap(""),
)),
)
}
_ -> tuple(state0, Error(UnexpectedEndOfInput))
}
}
type ArraySearch {
AValueOrEnd
AValue
ACommaOrEnd
}
fn parse_array(
state0: State,
search: ArraySearch,
stack: List(JSON),
) -> tuple(State, Result(JSON, ParseError)) {
let state = chomp_whitespace(state0)
case next_grapheme(state.rest) {
Error(Nil) -> tuple(state, Error(UnexpectedEndOfInput))
Ok(tuple(c, rest)) ->
case c {
"," ->
case search {
ACommaOrEnd ->
parse_array(
State(..state, column: state.column + 1, rest: rest),
AValue,
stack,
)
AValueOrEnd | AValue -> tuple(state, Error(UnexpectedToken(c)))
}
"]" ->
case search {
ACommaOrEnd | AValueOrEnd -> tuple(
State(..state, column: state.column + 1, rest: rest),
Ok(Array(list.reverse(stack))),
)
AValue -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
_ ->
case search {
AValue | AValueOrEnd ->
case parse_value(state) {
tuple(state1, Ok(val)) ->
parse_array(state1, ACommaOrEnd, [val, ..stack])
e -> e
}
ACommaOrEnd -> tuple(
State(..state, column: state.column + 1, rest: rest),
Error(UnexpectedToken(c)),
)
}
}
}
}
fn chomp_whitespace(state0: State) -> State {
case next_grapheme(state0.rest) {
Ok(tuple(c, rest)) ->
case c {
" " | "\t" ->
State(..state0, column: state0.column + 1, rest: rest)
|> chomp_whitespace()
"\n" ->
State(column: 0, row: state0.row + 1, rest: rest)
|> chomp_whitespace()
_ -> state0
}
Error(Nil) -> state0
}
}
// Get more than 1 grapheme of the head of a string
fn pop_str(rest: String, n: Int) -> Result(tuple(List(String), String), Nil) {
pop_str_help(n, [], rest)
}
fn pop_str_help(
n: Int,
acc: List(String),
rest: String,
) -> Result(tuple(List(String), String), Nil) {
case next_grapheme(rest) {
Ok(tuple(c, rest)) ->
case n {
1 -> Ok(tuple(list.reverse([c, ..acc]), rest))
_ -> pop_str_help(n - 1, [c, ..acc], rest)
}
_ -> Error(Nil)
}
}
fn hex_to_int(c: String) -> Result(Int, Nil) {
let i = case c {
"0" -> 0
"1" -> 1
"2" -> 2
"3" -> 3
"4" -> 4
"5" -> 5
"6" -> 6
"7" -> 7
"8" -> 8
"9" -> 9
"a" | "A" -> 10
"b" | "B" -> 11
"c" | "C" -> 12
"d" | "D" -> 13
"e" | "E" -> 14
"f" | "F" -> 15
_ -> 16
}
case i {
16 -> Error(Nil)
x -> Ok(x)
}
}
fn hex_to_unicode(
a: String,
b: String,
c: String,
d: String,
) -> Result(BitString, Int) {
case hex_to_int(a) {
Error(_) -> Error(1)
Ok(aa) ->
case hex_to_int(b) {
Error(_) -> Error(2)
Ok(bb) ->
case hex_to_int(c) {
Error(_) -> Error(3)
Ok(cc) ->
case hex_to_int(d) {
Error(_) -> Error(4)
Ok(dd) -> {
assert <<out:16>> = <<aa:4, bb:4, cc:4, dd:4>>
Ok(<<out:16>>)
}
}
}
}
}
}
fn resolve_number(bb: BitBuilder) -> Result(JSON, Nil) {
let str =
bb
|> bit_builder.to_bit_string()
|> bit_string.to_string()
|> result.unwrap("")
case float.parse(str) {
Ok(f) -> Ok(Number(f))
Error(_) ->
case int.parse(str) {
Ok(i) -> Ok(Number(int.to_float(i)))
Error(_) -> Error(Nil)
}
}
}
// TODO: Something is broken with string.pop_grapheme
// figure it out and provide a patch?
fn next_grapheme(str: String) -> Result(tuple(String, String), Nil) {
// string.pop_grapheme(str)
erl_next_grapheme(str)
}
external fn erl_next_grapheme(str: String) -> Result(tuple(String, String), Nil) =
"gj_bridge" "next_grapheme"
external fn uni_to_bin(uni: List(BitString)) -> UnicodeToBinResult =
"gj_bridge" "uni_to_bin"
external fn valid_in_string(in: String) -> Bool =
"gj_bridge" "valid_in_string"