///|
/// A metadata shape or format-conversion failure.
pub(all) suberror MetadataError {
ExpectedObject
InvalidKey(String)
NotJson(path~ : String, reason~ : String)
} derive(Eq, Debug)
///|
pub extend MetadataError with Eq::{equal, not_equal}
///|
pub extend MetadataError with @debug.Debug::{to_repr}
///|
pub fn MetadataError::message(self : MetadataError) -> String {
match self {
ExpectedObject => "metadata must be a non-null Ion struct"
InvalidKey(message) => message
NotJson(path~, reason~) => "\{path}: \{reason}"
}
}
///|
/// Immutable Ion metadata with unique, textual top-level field names.
/// Import and export copy nested containers and annotations defensively.
struct Metadata {
value : @ion.Ion
} derive(Debug)
///|
pub impl Eq for Metadata with fn equal(self, other) {
self.value.equals(other.value)
}
///|
pub extend Metadata with Eq::{equal, not_equal}
///|
pub extend Metadata with @debug.Debug::{to_repr}
///|
fn copy_metadata_ion(value : @ion.Ion) -> @ion.Ion {
let kind = match value.kind() {
List(items) => @ion.IonValueKind::List(items.map(copy_metadata_ion))
Sexp(items) => @ion.IonValueKind::Sexp(items.map(copy_metadata_ion))
Struct(fields) => @ion.IonValueKind::Struct(fields.map(field => {
@ion.Field::from_token(field.name(), copy_metadata_ion(field.value()))
}))
scalar => scalar
}
@ion.Ion(kind).with_annotations(value.annotations().copy())
}
///|
/// Construct from a JSON object. Use from_ion for the richer Ion data model.
/// This JSON input adapter retains the existing constructor's behavior.
pub fn Metadata::Metadata(value : Json) -> Metadata raise MetadataError {
Metadata::from_ion(@ion.Ion(value))
}
///|
/// Require a non-null Ion struct with unique, textual top-level field names.
/// Nested Ion values retain their types, annotations and repeated fields.
pub fn Metadata::from_ion(value : @ion.Ion) -> Metadata raise MetadataError {
guard value.kind() is Struct(fields) else { raise ExpectedObject }
let seen = Map([])
for field in fields {
guard field.name().text() is Some(name) else {
raise InvalidKey("metadata field names must have known text")
}
if seen.contains(name) {
raise InvalidKey("duplicate metadata field: \{name}")
}
seen[name] = true
}
{ value: copy_metadata_ion(value), }
}
///|
pub fn Metadata::empty() -> Metadata {
{ value: @ion.Ion::from_fields([]), }
}
///|
/// Look up a field as an independent Ion value. Typed nulls are present;
/// an absent field is None. No format conversion is performed.
pub fn Metadata::get(self : Metadata, key : String) -> @ion.Ion? {
if self.value.kind() is Struct(fields) {
for field in fields {
if field.name().text() == Some(key) {
return Some(copy_metadata_ion(field.value()))
}
}
}
None
}
///|
/// Explicit JSON lookup. Raise NotJson if the selected value needs Ion's
/// richer types; None still means the key is absent.
pub fn Metadata::get_json(self : Metadata, key : String) -> Json? raise MetadataError {
self.get(key).map(value => metadata_json(value, "$." + key))
}
///|
pub fn Metadata::keys(self : Metadata) -> ReadOnlyArray[String] {
match self.value.kind() {
Struct(fields) => ReadOnlyArray::from_iter(fields.iter().map(field => {
field.name().text().unwrap()
}))
_ => []
}
}
///|
pub fn Metadata::is_empty(self : Metadata) -> Bool {
match self.value.kind() {
Struct(fields) => fields.is_empty() && !self.value.has_annotations()
_ => false
}
}
///|
/// Export a defensive copy of the native Ion value, retaining every type.
pub fn Metadata::to_ion(self : Metadata) -> @ion.Ion {
copy_metadata_ion(self.value)
}
///|
/// Export independent JSON. Reject annotations, typed nulls, non-finite
/// floats, symbols, timestamps, lob values, sexps and repeated struct fields.
/// Integers and decimals retain their exact numeric spelling.
pub fn Metadata::to_json(self : Metadata) -> Json raise MetadataError {
metadata_json(self.value, "$")
}
///|
fn metadata_json(value : @ion.Ion, path : String) -> Json raise MetadataError {
if value.has_annotations() {
raise NotJson(path~, reason="annotations require Ion")
}
match value.kind() {
Struct(fields) => {
let result = Map([])
for field in fields {
guard field.name().text() is Some(name) else {
raise NotJson(path~, reason="field name has no known text")
}
if result.contains(name) {
raise NotJson(path~, reason="repeated field: \{name}")
}
result[name] = metadata_json(field.value(), "\{path}.\{name}")
}
Json::object(result)
}
List(items) => Json::array(items.mapi((i, item) => {
metadata_json(item, "\{path}[\{i}]")
}))
Decimal(decimal) => {
// Keep exponent notation compact rather than expanding large powers
// into millions of zeroes during automatic DIB format selection.
let text = decimal.to_ion_string().replace_all(old="d", new="e")
let text = if text.has_suffix(".") { text + "0" } else { text }
let number = @json.parse(text) catch {
_ => raise NotJson(path~, reason="invalid decimal representation")
}
guard number is Number(value, ..) else {
raise NotJson(path~, reason="expected a decimal number")
}
Json::number(value, repr=text)
}
Null(@ion.IonType::Null) | Bool(_) | String(_) | Int(_) =>
@ion_json.to_json(value) catch {
error => raise NotJson(path~, reason=error.message())
}
Float(number) if !number.is_inf() && !number.is_nan() =>
Json::number(number)
_ => raise NotJson(path~, reason="value requires Ion")
}
}