// Converting JSON to Ion.
//
// JSON's data model is a strict subset of Ion's, so this direction is
// faithful: every `Json` value maps to an Ion value that converts back to the
// same JSON. The mapping follows the Ion cookbook's "Migrating JSON data to
// Ion" rules:
//
// * `null`, booleans, and strings map to the matching Ion type;
// * an array maps to an Ion `list`;
// * an object maps to an Ion `struct`, with each key a symbol field name;
// * a number maps to an Ion `int`, `decimal`, or `float`.
//
// Numbers are the only interesting case, and they follow the cookbook's three
// rules: an integer spelling is an Ion `int`, a spelling with a decimal point
// but no exponent is an Ion `decimal`, and a spelling with an exponent is an
// Ion `float`.
//
// The spelling that decides is the one core kept in `repr`, or, when core
// dropped it, the number's shortest round-trip spelling. Core keeps `repr`
// only for an integer-spelled literal above 2^53 - 1, or for a literal that
// overflows a `Double` to infinity; a fractional or exponent-spelled literal
// that fits a `Double` is rounded to the nearest `Double` and its spelling is
// lost. So `1.50` and `0.1` arrive as bare doubles and become the Ion decimals
// `1.5` and `0.1`, while `1e2` becomes the Ion int `100` and `1e-7`, whose
// shortest spelling really does carry an exponent, becomes an Ion float.
///|
/// Convert a core `Json` value to an Ion value. Object fields keep their
/// order, and any nesting depth works.
///
/// See the module comment for the mapping, especially for numbers.
pub impl ToIon for Json with fn to_ion(self : Json) -> IonValue {
ion_shallow(self, @work.SHALLOW_DEPTH)
}
///|
/// Convert `json` with recursion while `levels` is above zero, then with
/// `ion_deep`. Recursion is the fastest way to convert a small value; the
/// limit keeps the call stack small at any depth.
fn ion_shallow(json : Json, levels : Int32) -> IonValue {
match json {
Json::Array(elements) if levels > 0 => {
let items : Array[IonValue] = Array(capacity=elements.length())
for element in elements {
items.push(ion_shallow(element, levels - 1))
}
IonValue::list(items)
}
Json::Object(entries) if levels > 0 => {
let fields : Array[Field] = Array(capacity=entries.length())
for name, item in entries {
fields.push(Field::Field(name, ion_shallow(item, levels - 1)))
}
IonValue::from_fields(fields)
}
Json::Array(_) | Json::Object(_) => ion_deep(json)
scalar => scalar_to_ion(scalar)
}
}
///|
/// A container that `ion_deep` is filling: its JSON children, and the Ion
/// elements or fields that receive their conversions. An object's members are
/// copied into an array so that they can be taken by index, in order.
priv enum IonContainer {
Items(Array[Json], Array[IonValue])
Fields(Array[(String, Json)], Array[Field])
}
///|
/// Convert `json` top-down with a stack of open containers, not recursion.
/// Each container's Ion value is added to its parent when the walk enters it,
/// and filled while the walk is inside it.
fn ion_deep(json : Json) -> IonValue {
let open : Array[IonContainer] = []
// The index of the next child of each open container.
let next : Array[Int32] = []
let root = ion_start(json, open, next)
while open.last() is Some(container) {
let top = next.length() - 1
let index = next[top]
match container {
Items(elements, items) if index < elements.length() => {
next[top] = index + 1
items.push(ion_start(elements[index], open, next))
}
Fields(members, fields) if index < members.length() => {
next[top] = index + 1
let (name, item) = members[index]
fields.push(Field::Field(name, ion_start(item, open, next)))
}
_ => {
ignore(open.pop())
ignore(next.pop())
}
}
}
root
}
///|
/// The Ion value of `json`. A container's value is empty here: it is pushed
/// onto `open`, and `ion_deep` fills it in place.
fn ion_start(
json : Json,
open : Array[IonContainer],
next : Array[Int32],
) -> IonValue {
match json {
Json::Array(elements) => {
let items : Array[IonValue] = Array(capacity=elements.length())
open.push(Items(elements, items))
next.push(0)
IonValue::list(items)
}
Json::Object(entries) => {
let members : Array[(String, Json)] = Array(capacity=entries.length())
for name, item in entries {
members.push((name, item))
}
let fields : Array[Field] = Array(capacity=entries.length())
open.push(Fields(members, fields))
next.push(0)
IonValue::from_fields(fields)
}
scalar => scalar_to_ion(scalar)
}
}
///|
/// The Ion value of a JSON value with no children.
fn scalar_to_ion(json : Json) -> IonValue {
match json {
Json::Null => IonValue::untyped_null()
Json::True => IonValue::bool(true)
Json::False => IonValue::bool(false)
Json::String(text) => IonValue::string(text)
Json::Number(value, repr~) =>
number_text_to_ion(
match repr {
Some(text) => text
None => shortest_spelling(value)
},
value,
)
Json::Array(_) | Json::Object(_) =>
abort("ion_shallow and ion_start convert containers")
}
}
///|
/// Convert a JSON number from a decimal spelling.
fn number_text_to_ion(text : String, fallback : Double) -> IonValue {
if has_exponent(text) {
let value = @string.parse_double(text) catch { _ => fallback }
return IonValue::float(value)
}
if text.contains(".") {
let parsed = Decimal::parse(text) catch {
_ => return double_to_ion(fallback)
}
return IonValue::decimal(parsed)
}
let parsed = @string.parse_bigint(text) catch {
_ => return double_to_ion(fallback)
}
IonValue::big_int(parsed)
}
///|
/// Whether a JSON number spelling carries a base-10 exponent.
fn has_exponent(text : String) -> Bool {
text.contains("e") || text.contains("E")
}
///|
///|
/// The shortest decimal spelling that round-trips to `value`, used when core
/// did not keep the source text in `repr`.
fn shortest_spelling(value : Double) -> String {
Json::number(value).stringify()
}
///|
/// Convert a JSON number to Ion from the `Double` alone, the fallback for a
/// spelling that does not parse.
fn double_to_ion(value : Double) -> IonValue {
if is_integral_64(value) {
IonValue::big_int(@bigint.BigInt::from_int64(value.to_int64()))
} else {
IonValue::float(value)
}
}
///|
/// Whether a double is a finite integer that fits in an `Int64`.
fn is_integral_64(value : Double) -> Bool {
!value.is_nan() &&
!value.is_inf() &&
value == value.floor() &&
value >= -9.223372036854776e18 &&
value < 9.223372036854776e18
}