///|
/// Errors at the JSON boundary. Syntax offsets count UTF-16 code units from
/// the start of the document. Unsupported values are never replaced by null.
pub(all) suberror JsonError {
Utf8
Syntax(offset~ : Int32, message~ : String)
Unsupported(String)
Bindings(String)
} derive(Debug)
///|
pub fn JsonError::message(self : JsonError) -> String {
match self {
Utf8 => "JSON text is not valid UTF-8"
Syntax(offset~, message~) => "JSON at offset \{offset}: \{message}"
Unsupported(message) => "cannot represent as JSON: " + message
Bindings(message) => message
}
}
///|
priv struct Reader {
source : StringView
mut pos : Int32
}
///|
fn Reader::error(self : Reader, message : String) -> JsonError {
Syntax(offset=self.pos, message~)
}
///|
fn Reader::skip_space(self : Reader) -> Unit {
while self.pos < self.source.length() {
match self.source[self.pos] {
' ' | '\t' | '\r' | '\n' => self.pos += 1
_ => break
}
}
}
///|
fn Reader::take(self : Reader, char : UInt16) -> Bool {
self.skip_space()
if self.pos < self.source.length() && self.source[self.pos] == char {
self.pos += 1
true
} else {
false
}
}
///|
// Core parser positions are relative to the token, not the whole document.
fn Reader::token_error(
self : Reader,
start : Int32,
error : @core_json.ParseError,
) -> JsonError {
let (position, message) = match error {
InvalidChar(position, char) =>
(position, "invalid character " + char.escape())
InvalidNumber(position, number) => (position, "invalid number " + number)
InvalidIdentEscape(position) => (position, "invalid escape sequence")
InvalidEof =>
return Syntax(offset=self.pos, message="unexpected end of JSON token")
DepthLimitExceeded =>
return Syntax(offset=start, message="JSON token nesting limit exceeded")
}
let mut offset = start
let mut line = 1
while offset < self.pos && line < position.line {
if self.source[offset] == '\n' {
line += 1
}
offset += 1
}
Syntax(offset=offset + position.column, message~)
}
///|
fn Reader::token(self : Reader, start : Int32) -> Json raise JsonError {
@core_json.parse(self.source[start:self.pos]) catch {
e => raise self.token_error(start, e)
}
}
///|
fn Reader::string(self : Reader) -> String raise JsonError {
self.skip_space()
let start = self.pos
if !self.take('"') {
raise self.error("expected a quoted object key")
}
while self.pos < self.source.length() {
let c = self.source[self.pos]
self.pos += 1
if c == '"' {
let json = self.token(start)
match json {
String(s) => return s
_ => raise self.error("expected a string")
}
}
if c == '\\' && self.pos < self.source.length() {
self.pos += 1
}
}
raise self.error("unterminated string")
}
///|
fn Reader::value(self : Reader, depth : Int32) -> @value.Value raise JsonError {
self.skip_space()
guard self.pos < self.source.length() else {
raise self.error("expected a JSON value")
}
match self.source[self.pos] {
'"' => @value.Value::from_string(self.string())
'[' => {
if depth >= 256 {
raise self.error("nesting exceeds 256 containers")
}
self.pos += 1
let items : Array[@value.Value] = []
if self.take(']') {
return @value.Value::from_list(items)
}
while true {
items.push(self.value(depth + 1))
if self.take(']') {
break
}
if !self.take(',') {
raise self.error("expected ',' or ']'")
}
}
@value.Value::from_list(items)
}
'{' => {
if depth >= 256 {
raise self.error("nesting exceeds 256 containers")
}
self.pos += 1
let fields : Array[(String, @value.Value)] = []
if self.take('}') {
return @value.Value::from_tuple(@value.Tuple(fields))
}
while true {
let name = self.string()
if !self.take(':') {
raise self.error("expected ':'")
}
fields.push((name, self.value(depth + 1)))
if self.take('}') {
break
}
if !self.take(',') {
raise self.error("expected ',' or '}'")
}
}
@value.Value::from_tuple(@value.Tuple(fields))
}
_ => self.scalar()
}
}
///|
// Validate scalar syntax with the base library, but decode number spelling
// ourselves: Json's Double payload cannot retain decimal precision or scale.
fn Reader::scalar(self : Reader) -> @value.Value raise JsonError {
let start = self.pos
while self.pos < self.source.length() {
match self.source[self.pos] {
' ' | '\t' | '\r' | '\n' | ',' | ']' | '}' => break
_ => self.pos += 1
}
}
let token = self.source[start:self.pos]
let json = self.token(start)
match json {
Null => Null
True => @value.Value::from_bool(true)
False => @value.Value::from_bool(false)
Number(_) => {
let spelling = token.to_owned()
if spelling.contains(".") ||
spelling.contains("e") ||
spelling.contains("E") {
let decimal = @ion_core.Decimal::parse(
spelling.replace(old="e", new="d").replace(old="E", new="d"),
) catch {
e => raise Syntax(offset=start, message=e.message())
}
@value.Value::from_decimal(decimal)
} else {
@value.Value::from_bigint(@bigint.BigInt::from_string(spelling))
}
}
_ => raise Syntax(offset=start, message="expected a JSON scalar")
}
}
///|
/// Read exactly one strict JSON document. Integers normalize to Int/BigInt;
/// fractions and exponents become exact Decimal values. Duplicate keys survive.
/// Strings must contain Unicode scalar values; unpaired surrogates are errors.
/// At most 256 nested containers and Int32 decimal exponents are supported.
pub fn read_text(source : StringView) -> @value.Value raise JsonError {
let reader = Reader::{ source, pos: 0, }
let value = reader.value(0)
reader.skip_space()
if reader.pos != source.length() {
raise reader.error("unexpected trailing input; expected one JSON document")
}
value
}
///|
/// Read one strict UTF-8 JSON document. A BOM is not accepted.
pub fn read_bytes(source : BytesView) -> @value.Value raise JsonError {
let text = @utf8.decode(source) catch { _ => raise Utf8 }
read_text(text)
}