///|
test "construct Ion values from MoonBit values" {
let counts : @ion.IonValue = @ion.Ion(Map([("counts", [1, 2, 3])]))
inspect(@text.write_ion(counts), content="{counts: [1, 2, 3]}")
let json = @ion.Ion(Json({ "ready": true }))
inspect(@text.write_ion(json), content="{ready: true}")
let decimal = @ion.decimal(123N, -2)
inspect(@text.write_ion(@ion.Ion(decimal)), content="1.23")
let timestamp = @ion.timestamp(year=2024)
inspect(@text.write_ion(@ion.Ion(timestamp)), content="2024T")
let field = @ion.field("status", @ion.Ion(@ion.symbol("ready")))
inspect(
@text.write_ion(@ion.Ion::from_fields([field])),
content="{status: ready}",
)
}///|
struct ExampleCount {
count : Int32
}
///|
impl @ion.ToIon for ExampleCount with fn to_ion(self) -> @ion.Ion {
@ion.Ion(Map([("count", self.count)]))
}
///|
test "construct Ion from an application type" {
inspect(
@text.write_ion(@ion.Ion(ExampleCount::{ count: 3, })),
content="{count: 3}",
)
}///|
test "build a value and compare it" {
let book = @ion.IonValue::from_fields([
@ion.field("title", @ion.IonValue::string("Ion")),
@ion.field(
"tags",
@ion.IonValue::list([
@ion.IonValue::symbol("data"),
@ion.IonValue::symbol("format"),
]),
),
]).with_annotation(@ion.symbol("book"))
inspect(book.ion_type().to_text(), content="struct")
debug_inspect(
book.annotations().map(token => token.text()),
content=(
#|[Some("book")]
),
)
// Field order does not matter to equivalence.
let reordered = @ion.IonValue::from_fields([
@ion.field(
"tags",
@ion.IonValue::list([
@ion.IonValue::symbol("data"),
@ion.IonValue::symbol("format"),
]),
),
@ion.field("title", @ion.IonValue::string("Ion")),
]).with_annotation(@ion.symbol("book"))
assert_true(book.equals(reordered))
// Annotations do.
assert_false(book.equals(reordered.with_annotations([])))
}///|
test "read a value by its kind" {
let value = @text.read_ion("{ name: \"Ada\", born: 1815 }")
match value.kind() {
Struct(fields) =>
debug_inspect(
fields.map(field => field.name().text().unwrap_or("?")),
content=(
#|["name", "born"]
),
)
_ => fail("expected a struct")
}
}///|
test "decimals keep their precision" {
let price = @ion.Decimal::parse("1.50")
inspect(price.coefficient(), content="150")
inspect(price.exponent(), content="-2")
assert_false(
@ion.IonValue::decimal(price).equals(
@ion.IonValue::decimal(@ion.Decimal::parse("1.5")),
),
)
inspect(@ion.Decimal::parse("-0.0").is_negative_zero(), content="true")
debug_inspect(
price.add(@ion.Decimal::parse("0.25")).map(sum => sum.to_ion_string()),
content=(
#|Some("1.75")
),
)
}///|
test "timestamps" {
let launch = @ion.Timestamp::parse("1969-07-20T20:17:40Z")
debug_inspect(launch.precision(), content="Second")
inspect(launch.to_ion_string(), content="1969-07-20T20:17:40Z")
// The offset is kept, in minutes; `-00:00` is an unknown offset.
let eastern = @ion.Timestamp::parse("2024-01-01T01:30+02:00")
debug_inspect(eastern.offset(), content="Some(Known(120))")
debug_inspect(
@ion.Timestamp::parse("2024-01-01T01:30-00:00").offset(),
content="Some(Unknown)",
)
// 2001 was not a leap year.
let invalid = @ion.timestamp(year=2001, month=Some(2), day=Some(29))
let message = try invalid.validate() catch {
error => error.message()
} noraise {
_ => "valid"
}
inspect(message, content="timestamp day is out of range for its month")
}///|
test "offsets and instants" {
let eastern = @ion.Timestamp::parse("2024-01-01T01:30+02:00")
inspect(eastern.as_utc().to_ion_string(), content="2023-12-31T23:30Z")
inspect(
eastern.with_offset(@ion.TimestampOffset::Known(-300)).to_ion_string(),
content="2023-12-31T18:30-05:00",
)
let midnight = @ion.Timestamp::parse("2024-01-01")
let also_midnight = @ion.Timestamp::parse("2024-01-01T01:00:00.000+01:00")
inspect(midnight.compare_instant(also_midnight), content="0")
assert_false(
@ion.IonValue::timestamp(midnight).equals(
@ion.IonValue::timestamp(also_midnight),
),
)
}///|
test "a symbol table resolves symbol IDs" {
let table = @ion.SymbolTable::system()
debug_inspect(table.text(4), content="Some(\"name\")")
let declaration = @text.read_ion("{ symbols: [\"red\", \"green\"] }").with_annotation(
@ion.symbol("$ion_symbol_table"),
)
assert_true(table.apply_local_symbol_table(declaration))
inspect(table.max_id(), content="11")
debug_inspect(
table.lookup(11).text(),
content=(
#|Some("green")
),
)
// An ID beyond the table is an error.
let beyond = try table.lookup(12) catch {
error => error.message()
} noraise {
token => token.text().unwrap_or("?")
}
inspect(
beyond,
content="symbol ID $12 is beyond the symbol table, whose largest ID is 11",
)
}///|
test "imports resolve against a catalog" {
let catalog = @ion.Catalog::load(
@text.read_ion_datagram(
"$ion_shared_symbol_table::{ name: \"colors\", version: 1, symbols: [\"red\", \"green\"] }",
),
)
let values = @text.read_ion_datagram(
"$ion_symbol_table::{ imports: [{ name: \"colors\", version: 1 }], symbols: [\"local\"] } $11 $12",
catalog~,
)
inspect(
@text.write_all(values),
content=(
#|green
#|local
),
)
}///|
test "unknown imports keep their identity through text" {
let symbol = @ion.SymbolToken::from_import(20, "example", 1, 2)
let shifted = @ion.SymbolToken::from_import(30, "example", 2, 2)
assert_true(symbol.equivalent(shifted))
assert_false(symbol.equivalent(@ion.SymbolToken::from_sid(0)))
let value = @ion.IonValue::symbol_token(symbol)
assert_true(@text.read_ion(@text.write_ion(value)).equals(value))
}///|
struct FieldNames {
names : Array[String]
}
///|
impl @ion.IonVisitor for FieldNames with fn visit_field_name(self, token) {
self.names.push(token.text().unwrap_or("?"))
}
///|
test "visit every field name" {
let value = @text.read_ion("{ a: 1, b: { c: 2 }, d: [ { e: 3 } ] }")
let visitor : FieldNames = { names: [], }
value.accept(visitor)
debug_inspect(
visitor.names,
content=(
#|["a", "b", "c", "d", "e"]
),
)
}
///|
test "sum the integers outside annotated values" {
let value = @text.read_ion("[1, 2, skip::[100, 200], (3 4)]")
let sum = value.fold(0, {
..@ion.IonFold::default(),
visit_value: (total, value) => {
if value.has_annotations() {
return SkipChildren(total)
}
match value.kind() {
Int(n) => Continue(total + n.to_int())
_ => Continue(total)
}
},
})
inspect(sum, content="10")
}///|
test "walk a value as events" {
let value = @text.read_ion("{ a: [1, 2], b: null.list }")
let events = value
.walk()
.map(event => {
match event {
Enter(v, ..) => "enter " + v.ion_type().to_text()
Exit(v, ..) => "exit " + v.ion_type().to_text()
Scalar(v, ..) => "scalar " + v.ion_type().to_text()
}
})
.collect()
debug_inspect(
events,
content=(
#|[
#| "enter struct",
#| "enter list",
#| "scalar int",
#| "scalar int",
#| "exit list",
#| "scalar list",
#| "exit struct",
#|]
),
)
}
///|
test "skip the children of annotated containers" {
let walker = @text.read_ion("[1, skip::[2, 3], 4]").walker()
let ints : Array[String] = []
while walker.next() is Some(event) {
match event {
Enter(value, ..) => if value.has_annotations() { walker.skip_children() }
Scalar(value, ..) =>
if value.kind() is Int(n) {
ints.push(n.to_string())
}
Exit(_, ..) => ()
}
}
debug_inspect(
ints,
content=(
#|["1", "4"]
),
)
}
///|
test "measure the depth with fold_up" {
let value = @text.read_ion("[1, [2, [3]], {a: (4)}]")
let depth = value.fold_up(scalar=_ => 0, container=(_, children) => {
let mut deepest = 0
for child in children {
if child.1 > deepest {
deepest = child.1
}
}
deepest + 1
})
inspect(depth, content="3")
}pub(open) trait IonVisitor {
fn visit_value(Self, Ion) -> Unit = _
fn visit_annotation(Self, SymbolToken) -> Unit = _
fn visit_field_name(Self, SymbolToken) -> Unit = _
fn visit_list(Self, Array[Ion]) -> Unit = _
fn visit_sexp(Self, Array[Ion]) -> Unit = _
fn visit_struct(Self, Array[Field]) -> Unit = _
}pub(all) suberror IonError {
Syntax(String)
DataModel(String)
Unsupported(String)
FuelExhausted(budget~ : Budget, operation~ : FuelOperation, location~ : FuelLocation)
} derive(Debug)type Catalog#alias(IonValue)
pub struct Ion {
annotations : Array[SymbolToken]
kind : IonValueKind
} derive(Debug)pub(all) struct IonFold[A] {
visit_value : (A, Ion) -> FoldAction[A]
visit_annotation : (A, SymbolToken) -> FoldAction[A]
visit_field_name : (A, SymbolToken) -> FoldAction[A]
}type SharedSymbolTablefn SharedSymbolTable::new(name : String, version : Int, symbols : Array[String?]) -> SharedSymbolTabletype SymbolTablepub struct SymbolToken {
text : String?
sid : Int?
import_location : ImportLocation?
import_version : Int?
} derive(Eq, Hash, Debug)impl ToIon for SymbolToken#alias(new, deprecated="Use `@ion.symbol(...)` instead")
#as_free_fn(symbol)
fn SymbolToken::SymbolToken(text : String) -> SymbolTokenfn SymbolToken::from_import(sid : Int, name : String, version : Int, import_sid : Int) -> SymbolTokenpub struct Timestamp {
year : Int
month : Int?
day : Int?
hour : Int?
minute : Int?
second : Int?
fraction : Decimal?
offset : TimestampOffset?
} derive(Debug)#alias(new, deprecated="Use `@ion.timestamp(...)` instead")
#as_free_fn(timestamp)
fn Timestamp::Timestamp(year~ : Int, month? : Int?, day? : Int?, hour? : Int?, minute? : Int?, second? : Int?, fraction? : Decimal?, offset? : TimestampOffset?) -> Timestamppub struct ValueWalker {
// private fields
}pub(all) enum WalkEvent {
Scalar(Ion, field~ : SymbolToken?)
Enter(Ion, field~ : SymbolToken?)
Exit(Ion, field~ : SymbolToken?)
} derive(Debug)pub struct WritePosition {
depth : Int
parent : IonType?
index : Int
field : SymbolToken?
annotations : Array[SymbolToken]
}type WriterStatefn WriterState::set_annotations(self : WriterState, annotations : Array[SymbolToken]) -> Unit raise IonErrorfn days_in_month(year : Int, month : Int) -> Intfn is_leap_year(year : Int) -> Boolfn shift_date(year : Int, month : Int, day : Int, days : Int) -> (Int, Int, Int)fn system_symbol_text(sid : Int) -> String?Install
Download zipA MoonBit implementation of the Amazon Ion data format and Ion Schema.
Dependencies