///|
/// An immutable configuration value over an Ion value. Every format and source
/// gives values of this one type before the merge.
pub struct ConfigValue {
priv ion : @ion.Ion
} derive(@debug.Debug)
///|
/// A source tree from an Ion value. The root must be a struct, else this
/// raises `InvalidRoot` with `origin`. A struct field name with no text, such
/// as an unresolved symbol ID, raises `Parse` for the Ion format. Two fields
/// with the same name in one struct raise `DuplicateKey`. The key path of a
/// field in a list item holds the struct field names only, because a key path
/// does not select list items.
pub fn ConfigValue::from_ion(
ion : @ion.Ion,
origin? : String = "value",
) -> ConfigValue raise ConfigError {
let value = { ion, }
value.check_root(origin)
check_fields(ion, origin, [])
value
}
///|
/// A source tree from a JSON value, through its Ion form. The rules of
/// `ConfigValue::from_ion` apply.
pub fn ConfigValue::from_json(
json : Json,
origin? : String = "value",
) -> ConfigValue raise ConfigError {
ConfigValue::from_ion(@ion.Ion::Ion(json), origin~)
}
///|
/// The Ion value.
pub fn ConfigValue::to_ion(self : ConfigValue) -> @ion.Ion {
self.ion
}
///|
/// Equality of the Ion data model. Struct fields compare as an unordered set.
pub impl Eq for ConfigValue with equal(self, other) {
self.ion.equals(other.ion)
}
///|
pub extend ConfigValue with Eq::{equal, not_equal}
///|
pub extend ConfigValue with @debug.Debug::{to_repr}
///|
/// A string value.
pub fn string(text : String) -> ConfigValue {
{ ion: @ion.Ion::string(text) }
}
///|
/// An integer value.
pub fn int(value : Int64) -> ConfigValue {
{ ion: @ion.Ion::Ion(value) }
}
///|
/// A floating-point value.
pub fn float(value : Double) -> ConfigValue {
{ ion: @ion.Ion::float(value) }
}
///|
/// A Boolean value.
pub fn bool(value : Bool) -> ConfigValue {
{ ion: @ion.Ion::bool(value) }
}
///|
/// A list value.
pub fn list(items : Array[ConfigValue]) -> ConfigValue {
{ ion: @ion.Ion::list(items.map(item => item.ion)) }
}
///|
/// A struct value with the fields in the given order. When a name occurs more
/// than once, the last value wins at the position of the first.
pub fn fields(fields : Array[(String, ConfigValue)]) -> ConfigValue {
let unique : Array[(String, ConfigValue)] = []
for field in fields {
match unique.search_by(existing => existing.0 == field.0) {
Some(index) => unique[index] = field
None => unique.push(field)
}
}
{
ion: @ion.Ion::from_fields(
unique.map(field => @ion.field(field.0, field.1.ion)),
),
}
}
///|
/// The Ion type name in lowercase, for example `struct` or `list`.
pub fn ConfigValue::type_name(self : ConfigValue) -> String {
match self.ion.ion_type() {
Null => "null"
Bool => "bool"
Int => "int"
Float => "float"
Decimal => "decimal"
Timestamp => "timestamp"
Symbol => "symbol"
String => "string"
Clob => "clob"
Blob => "blob"
List => "list"
Sexp => "sexp"
Struct => "struct"
}
}
///|
/// The field at `name` when the value is a struct.
fn ConfigValue::field(self : ConfigValue, name : String) -> ConfigValue? {
guard self.ion.kind() is Struct(fields) else { None }
for field in fields {
if field.name().text() == Some(name) {
return Some({ ion: field.value() })
}
}
None
}
///|
/// The value at the struct field names `names`, from this value down.
fn ConfigValue::at(self : ConfigValue, names : Array[String]) -> ConfigValue? {
let mut value = self
for name in names {
guard value.field(name) is Some(next) else { return None }
value = next
}
Some(value)
}
///|
/// Raise `InvalidRoot` when the value is not a struct.
fn ConfigValue::check_root(
self : ConfigValue,
origin : String,
) -> Unit raise ConfigError {
guard self.ion.kind() is Struct(_) else {
raise InvalidRoot(origin~, found=self.type_name())
}
}
///|
/// Raise when a struct field name in `ion` has no text or occurs twice in one
/// struct. `path` holds the struct field names from the root to `ion`.
fn check_fields(
ion : @ion.Ion,
origin : String,
path : Array[String],
) -> Unit raise ConfigError {
match ion.kind() {
Struct(fields) => {
let seen : Set[String] = Set([])
for field in fields {
let token = field.name()
guard token.text() is Some(name) else {
let symbol = match token.sid() {
Some(sid) => "$\{sid}"
None => "symbol"
}
let place = if path.is_empty() {
"the root"
} else {
KeyPath::{ names: path }.to_string()
}
raise Parse(
format="Ion",
origin~,
message="the field name \{symbol} in \{place} has no text",
line=None,
column=None,
)
}
let names = [..path, name]
if seen.contains(name) {
raise DuplicateKey(origin~, key={ names, })
}
seen.add(name)
check_fields(field.value(), origin, names)
}
}
List(items) | Sexp(items) =>
for item in items {
check_fields(item, origin, path)
}
_ => ()
}
}