///|
pub fn parse_json(text : String) -> JsonParseResult {
parse_json_with_limit(text, 64)
}
///|
pub fn parse_json_with_limit(text : String, max_depth : Int) -> JsonParseResult {
let state = ParserState::new(text, if max_depth < 1 { 1 } else { max_depth })
state.skip_space()
let value = parse_json_value(state, 0)
state.skip_space()
if value is Some(_) && state.cursor[0] != state.text.length() {
state.error("$", "trailing content after JSON value")
}
let final_value = if state.diagnostics.length() == 0 { value } else { None }
{ value: final_value, diagnostics: { items: state.diagnostics } }
}
///|
fn parse_json_value(state : ParserState, depth : Int) -> JsonValue? {
if depth > state.max_depth {
state.error("$", "maximum JSON nesting depth exceeded")
return None
}
match state.peek() {
None => {
state.error("$", "expected a JSON value")
None
}
Some(ch) if ch == '"' => parse_json_string(state)
Some(ch) if ch == '{' => parse_json_object(state, depth)
Some(ch) if ch == '[' => parse_json_array(state, depth)
Some(ch) if ch == 't' => parse_literal(state, "true", JBool(true))
Some(ch) if ch == 'f' => parse_literal(state, "false", JBool(false))
Some(ch) if ch == 'n' => parse_literal(state, "null", JNull)
Some(ch) if ch == '-' || (ch >= '0' && ch <= '9') =>
parse_json_number(state)
Some(_) => {
state.error("$", "unexpected character in JSON value")
None
}
}
}
///|
fn parse_json_string(state : ParserState) -> JsonValue? {
ignore(state.advance())
let builder = StringBuilder::new()
while state.cursor[0] < state.text.length() {
let ch = state.advance().unwrap()
if ch == '"' {
return Some(JString(builder.to_string()))
}
if ch == '\\' {
if !parse_json_escape(state, builder) {
return None
}
} else if ch < 32 {
state.error("$", "control character is not allowed in a JSON string")
return None
} else if !append_code_unit(builder, ch) {
state.error("$", "invalid character in JSON string")
return None
}
}
state.error("$", "unterminated JSON string")
None
}
///|
fn parse_json_escape(state : ParserState, builder : StringBuilder) -> Bool {
match state.advance() {
None => {
state.error("$", "unterminated JSON escape")
false
}
Some(ch) => {
match ch {
'"' => builder.write_char('"')
'\\' => builder.write_char('\\')
'/' => builder.write_char('/')
'b' => builder.write_char('\u{0008}')
'f' => builder.write_char('\u{000C}')
'n' => builder.write_char('\n')
'r' => builder.write_char('\r')
't' => builder.write_char('\t')
_ => {
state.error("$", "unsupported or invalid JSON escape")
return false
}
}
true
}
}
}
///|
fn parse_json_number(state : ParserState) -> JsonValue? {
let builder = StringBuilder::new()
if state.peek() is Some('-') {
ignore(state.advance())
builder.write_char('-')
}
let mut digits = 0
while state.cursor[0] < state.text.length() {
let ch = state.text[state.cursor[0]]
if ch >= '0' && ch <= '9' {
digits += 1
ignore(state.advance())
match ch.to_char() {
Some(c) => builder.write_char(c)
None => ()
}
} else {
break
}
}
if digits == 0 {
state.error("$", "JSON number requires digits")
return None
}
if state.peek() is Some('.') {
ignore(state.advance())
builder.write_char('.')
let mut fraction_digits = 0
while state.cursor[0] < state.text.length() {
let ch = state.text[state.cursor[0]]
if ch >= '0' && ch <= '9' {
fraction_digits += 1
ignore(state.advance())
match ch.to_char() {
Some(c) => builder.write_char(c)
None => ()
}
} else {
break
}
}
if fraction_digits == 0 {
state.error("$", "JSON fraction requires digits")
return None
}
}
match state.peek() {
Some('e') | Some('E') => {
ignore(state.advance())
builder.write_char('e')
match state.peek() {
Some('+') | Some('-') => {
let sign = state.advance().unwrap()
match sign.to_char() {
Some(c) => builder.write_char(c)
None => ()
}
}
_ => ()
}
let mut exponent_digits = 0
while state.cursor[0] < state.text.length() {
let ch = state.text[state.cursor[0]]
if ch >= '0' && ch <= '9' {
exponent_digits += 1
ignore(state.advance())
match ch.to_char() {
Some(c) => builder.write_char(c)
None => ()
}
} else {
break
}
}
if exponent_digits == 0 {
state.error("$", "JSON exponent requires digits")
return None
}
}
_ => ()
}
Some(JNumber(builder.to_string()))
}
///|
fn parse_literal(
state : ParserState,
expected : String,
value : JsonValue,
) -> JsonValue? {
for i in 0.. ignore(state.advance())
_ => {
state.error("$", "invalid JSON literal")
return None
}
}
}
Some(value)
}
///|
fn parse_json_array(state : ParserState, depth : Int) -> JsonValue? {
ignore(state.advance())
state.skip_space()
let values : Array[JsonValue] = []
if state.peek() is Some(']') {
ignore(state.advance())
return Some(JArray(values))
}
while true {
state.skip_space()
match parse_json_value(state, depth + 1) {
Some(value) => values.push(value)
None => return None
}
state.skip_space()
match state.peek() {
Some(',') => {
ignore(state.advance())
state.skip_space()
}
Some(']') => {
ignore(state.advance())
return Some(JArray(values))
}
_ => {
state.error("$", "expected comma or closing array bracket")
return None
}
}
}
None
}
///|
fn parse_json_object(state : ParserState, depth : Int) -> JsonValue? {
ignore(state.advance())
state.skip_space()
let fields : Array[JsonPair] = []
if state.peek() is Some('}') {
ignore(state.advance())
return Some(JObject(fields))
}
while true {
state.skip_space()
let key = match parse_json_string(state) {
Some(JString(value)) => value
_ => {
state.error("$", "object key must be a JSON string")
return None
}
}
if fields.any(fn(item) { item.key == key }) {
state.error(key, "duplicate object key")
return None
}
state.skip_space()
if state.peek() is Some(':') {
ignore(state.advance())
} else {
state.error(key, "expected colon after object key")
return None
}
state.skip_space()
let value = match parse_json_value(state, depth + 1) {
Some(value) => value
None => return None
}
fields.push(JsonPair::new(key, value))
state.skip_space()
match state.peek() {
Some(',') => {
ignore(state.advance())
state.skip_space()
}
Some('}') => {
ignore(state.advance())
return Some(JObject(fields))
}
_ => {
state.error("$", "expected comma or closing object brace")
return None
}
}
}
None
}