///|
priv struct WireComponent {
component : Component
position : Int32
values : Array[String]?
fallback : String?
}
///|
priv struct JsonLayout {
structure : Structure
kind : ExchangeKind
reference : StructureRef
dataset_dimensions : Array[WireComponent]
series_dimensions : Array[WireComponent]
observation_dimensions : Array[WireComponent]
measures : Array[WireComponent]
dataset_attributes : Array[WireComponent]
series_attributes : Array[WireComponent]
observation_attributes : Array[WireComponent]
notes : Array[Annotation]
}
///|
fn json_level(
object : Map[String, Json],
name : String,
) -> Array[Json] raise SdmxError {
match object.get(name) {
None | Some(Null) => []
Some(value) => j_array(value, name)
}
}
///|
fn json_format(object : Map[String, Json]) -> Representation raise SdmxError {
let format = match object.get("format") {
None | Some(Null) => return Text
Some(value) => j_object(value, "format")
}
j_known(
format,
["dataType", "minOccurs", "maxOccurs", "isMultiLingual"],
"format",
)
for name in ["minOccurs", "maxOccurs"] {
if format.get(name) is Some(value) && j_int(value, name, 1) > 1 {
raise Unsupported(feature="multi-valued format")
}
}
if j_optional_bool(format, "isMultiLingual", false) {
raise Unsupported(feature="multilingual values")
}
let kind = j_optional_string(format, "dataType", "String")
match kind {
"String" => Text
"Integer" | "Long" | "Short" | "BigInteger" => Integer
"Decimal" | "Double" | "Float" => Decimal
"ObservationalTimePeriod"
| "ReportingTimePeriod"
| "StandardTimePeriod"
| "GregorianTimePeriod"
| "Date" => Period
_ => raise Unsupported(feature="format dataType: \{kind}")
}
}
///|
fn json_relationship(
value : Json,
dimensions : Array[String],
) -> Role raise SdmxError {
let object = j_object(value, "relationship")
j_known(
object,
["dataflow", "dimensions", "observation", "primaryMeasure", "measures"],
"relationship",
)
if object.get("measures") is Some(value) &&
!j_array(value, "measures").is_empty() {
raise Unsupported(feature="measure-specific attribute relationship")
}
let mut keys : Array[String]? = None
if object.contains("dataflow") {
if !j_object(object["dataflow"], "relationship.dataflow").is_empty() {
raise Invalid(
path="relationship.dataflow",
reason="expected empty object",
)
}
keys = Some([])
}
if object.get("dimensions") is Some(value) {
if keys is Some(_) {
raise Invalid(path="relationship", reason="multiple attachment kinds")
}
let names = j_strings(value, "relationship.dimensions")
if names.is_empty() {
raise Invalid(path="relationship.dimensions", reason="empty attachment")
}
keys = Some(names)
}
if object.contains("observation") || object.contains("primaryMeasure") {
if keys is Some(_) {
raise Invalid(path="relationship", reason="multiple attachment kinds")
}
if object.contains("observation") && object.contains("primaryMeasure") {
raise Invalid(
path="relationship",
reason="multiple observation attachment forms",
)
}
if object.get("observation") is Some(value) &&
!j_object(value, "relationship.observation").is_empty() {
raise Invalid(
path="relationship.observation",
reason="expected empty object",
)
}
keys = Some(dimensions.copy())
}
match keys {
Some(keys) => Attribute(keys)
None => raise Invalid(path="relationship", reason="missing attachment kind")
}
}
///|
fn json_wire_component(
value : Json,
role : Role,
limits : Limits,
) -> WireComponent raise SdmxError {
let object = j_object(value, "component")
let id = j_string(j_required(object, "id"), "component.id")
let fallback = match object.get("default") {
None | Some(Null) => None
Some(value) => {
if !(role is Attribute(_)) {
raise Invalid(path=id, reason="only attributes may have defaults")
}
j_scalar(value, "default")
}
}
if object.get("annotations") is Some(value) &&
!j_array(value, "component.annotations").is_empty() {
raise Unsupported(feature="component annotations")
}
let position = match role {
Dimension | TimeDimension =>
j_int(
j_required(object, "keyPosition"),
"keyPosition",
limits.max_columns - 1,
)
_ => {
if object.contains("keyPosition") {
raise Invalid(path=id, reason="keyPosition only belongs to dimensions")
}
-1
}
}
let mut representation = json_format(object)
let mut actual_role = role
let roles = match object.get("roles") {
None | Some(Null) => []
Some(value) => j_strings(value, "component.roles")
}
if role is Dimension &&
(
id == "TIME_PERIOD" ||
representation is Period ||
roles.contains("TIME_PERIOD")
) {
actual_role = TimeDimension
representation = Period
}
let values = match object.get("values") {
None | Some(Null) => None
Some(value) => {
let array = j_array(value, "component.values")
if array.length() > limits.max_codes {
raise Limit(resource="component values", maximum=limits.max_codes)
}
let entries : Array[(String, String)] = array.map(value => {
let entry = j_object(value, "component value")
if entry.get("annotations") is Some(value) &&
!j_array(value, "component value annotations").is_empty() {
raise Unsupported(feature="component-value annotations")
}
if entry.contains("values") {
raise Unsupported(feature="multi-valued component dictionary")
}
let text = match (entry.get("id"), entry.get("value")) {
(Some(value), None) => j_string(value, "value.id")
(None, Some(value)) => j_scalar(value, "value").unwrap_or("")
_ =>
raise Invalid(
path="component value",
reason="expected exactly one id or scalar value",
)
}
if text.is_empty() {
raise Invalid(path="component value", reason="empty dictionary value")
}
(text, j_optional_string(entry, "name", ""))
})
// This is a message vocabulary, not a claim about a registry's full DSD.
let vocabulary = CodeList::new(id, entries, limits~)
let coded = array.all(v => j_object(v, "component value").contains("id"))
if !coded && array.any(v => j_object(v, "component value").contains("id")) {
raise Unsupported(
feature="mixed coded and uncoded component dictionary",
)
}
if coded && !(actual_role is TimeDimension) && representation is Text {
representation = Coded(vocabulary)
}
Some(entries.map(pair => pair.0))
}
}
if (actual_role is Dimension || actual_role is TimeDimension) &&
values is None {
raise Invalid(path=id, reason="dimension values are mandatory")
}
let component = Component::new(
id,
actual_role,
representation,
name=j_optional_string(object, "name", ""),
required=j_optional_bool(object, "isMandatory", false),
)
if fallback is Some(text) {
validate_value(component, text, limits)
}
{ component, position, values, fallback, }
}
///|
fn json_exchange_reference(
object : Map[String, Json],
) -> (ExchangeKind, StructureRef) raise SdmxError {
let links = j_array(j_required(object, "links"), "structure.links")
if links.is_empty() {
raise Invalid(
path="structure.links",
reason="need identified source structure",
)
}
let first = j_object(links[0], "structure link")
let urn = j_string(j_required(first, "urn"), "structure link urn")
let prefixes = [
("urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure=", Datastructure),
("urn:sdmx:org.sdmx.infomodel.datastructure.Dataflow=", Dataflow),
(
"urn:sdmx:org.sdmx.infomodel.provisionagreement.ProvisionAgreement=",
Dataprovision,
),
]
for (prefix, kind) in prefixes {
if urn.has_prefix(prefix) {
return (kind, StructureRef::parse(urn[prefix.length():].to_owned()))
}
}
raise Unsupported(feature="source structure URN")
}
///|
fn json_layout(
value : Json,
expected : Structure?,
limits : Limits,
) -> JsonLayout raise SdmxError {
let object = j_object(value, "structure")
let note_values = json_level(object, "annotations")
if note_values.length() > limits.max_codes {
raise Limit(resource="annotations", maximum=limits.max_codes)
}
let notes = note_values.map(value => {
ignore(j_object(value, "annotation"))
{ content: canonical_json(value, limits), }
})
let (kind, reference) = json_exchange_reference(object)
if expected is Some(s) {
// An explicitly identified DSD must not be silently replaced by the caller.
let prefix = "urn:sdmx:org.sdmx.infomodel.datastructure.DataStructure="
for value in j_array(j_required(object, "links"), "structure.links") {
let link = j_object(value, "structure link")
if link.get("urn") is Some(String(urn)) && urn.has_prefix(prefix) {
let actual = StructureRef::parse(urn[prefix.length():].to_owned())
if actual != s.reference {
raise Conflict(
key=actual.format(),
reason="expected DSD reference differs from source link",
)
}
}
}
}
let dimension_object = j_object(
j_required(object, "dimensions"),
"dimensions",
)
j_known(dimension_object, ["dataSet", "series", "observation"], "dimensions")
let dataset_dimensions = json_level(dimension_object, "dataSet").map(v => {
json_wire_component(v, Dimension, limits)
})
let series_dimensions = json_level(dimension_object, "series").map(v => {
json_wire_component(v, Dimension, limits)
})
let observation_dimensions = json_level(dimension_object, "observation").map(v => {
json_wire_component(v, Dimension, limits)
})
let all_dimensions = dataset_dimensions.copy()
for wire in series_dimensions {
all_dimensions.push(wire)
}
for wire in observation_dimensions {
all_dimensions.push(wire)
}
if all_dimensions.length() > limits.max_columns {
raise Limit(resource="dimensions", maximum=limits.max_columns)
}
all_dimensions.sort_by((a, b) => a.position.compare(b.position))
for i, wire in all_dimensions {
if wire.position != i {
raise Invalid(
path="keyPosition",
reason="positions must be contiguous and unique from zero",
)
}
}
for wire in dataset_dimensions {
if wire.values.unwrap().length() != 1 {
raise Invalid(
path=wire.component.id,
reason="dataset dimension must have one value",
)
}
}
let measures = match object.get("measures") {
None =>
[
{
component: Component::new("OBS_VALUE", Measure, Decimal),
position: -1,
values: None,
fallback: None,
},
]
Some(value) => {
let object = j_object(value, "measures")
j_known(object, ["observation"], "measures")
json_level(object, "observation").map(v => {
json_wire_component(v, Measure, limits)
})
}
}
let attribute_object = match object.get("attributes") {
None | Some(Null) => Map([])
Some(value) => j_object(value, "attributes")
}
j_known(
attribute_object,
["dataSet", "dimensionGroup", "series", "observation"],
"attributes",
)
if !json_level(attribute_object, "dimensionGroup").is_empty() {
raise Unsupported(feature="dimensionGroup attributes")
}
let dimension_names = all_dimensions.map(w => w.component.id)
fn read_attribute(value : Json) -> WireComponent raise SdmxError {
let object = j_object(value, "attribute")
json_wire_component(
value,
json_relationship(j_required(object, "relationship"), dimension_names),
limits,
)
}
let dataset_attributes = json_level(attribute_object, "dataSet").map(
read_attribute,
)
let series_attributes = json_level(attribute_object, "series").map(
read_attribute,
)
let observation_attributes = json_level(attribute_object, "observation").map(
read_attribute,
)
let components = all_dimensions.map(w => w.component)
for w in measures {
components.push(w.component)
}
for group in [dataset_attributes, series_attributes, observation_attributes] {
for w in group {
components.push(w.component)
}
}
let structure_ref = match expected {
Some(s) => s.reference
None => reference
}
let structure = Structure::new(structure_ref, components, limits~)
{
structure,
kind,
reference,
dataset_dimensions,
series_dimensions,
observation_dimensions,
measures,
dataset_attributes,
series_attributes,
observation_attributes,
notes,
}
}