///|
priv struct GraphFact {
assertion : Assertion
object : @ion.IonValue
data : @ion.IonValue?
is_json : Bool
}
///|
fn record(entries : Array[(String, @ion.IonValue)]) -> @ion.IonValue {
@ion.Ion(@ion.IonValueKind::Struct(entries.map(e => @ion.field(e.0, e.1))))
}
///|
fn object(
value : @ion.IonValue,
coercion : String?,
) -> (@ion.IonValue, @ion.IonValue?, Bool) raise {
if coercion == Some("@json") {
return (
record([("@value", value), ("@type", @ion.IonValue::string("@json"))]),
Some(value),
true,
)
}
if value.kind() is Struct(_) {
let parts = fields(value, "fact object")
if parts.contains("@json") {
if parts.length() != 1 {
raise Invalid(
"metadata.invalid_object", "@json", "A typed data wrapper has exactly one @json member",
)
}
let data = parts["@json"]
return (
record([("@value", data), ("@type", @ion.IonValue::string("@json"))]),
Some(data),
true,
)
}
if parts.contains("@id") {
if parts.length() != 1 || coercion == Some("@json") {
raise Invalid(
"metadata.invalid_object", "@id", "Node links cannot replace declared typed data",
)
}
ignore(address(text(parts["@id"], "@id")))
return (value, None, false)
}
raise Invalid(
"metadata.invalid_object", "fact object", "Use @json coercion or the @json or @id object form",
)
}
if coercion == Some("@id") {
let uri = text(value, "node link")
ignore(address(uri))
return (record([("@id", @ion.IonValue::string(uri))]), None, false)
}
(record([("@value", value)]), Some(value), false)
}
///|
fn graph(
metadata : @ion.IonValue,
context : Context,
holder : String,
document : String,
) -> Array[GraphFact] raise {
let meta = fields(metadata, "$meta")
let result = []
if meta.get("@graph") is Some(value) {
guard value.kind() is List(rows) else {
raise Invalid(
"metadata.invalid_graph", document, "@graph must be an array",
)
}
if rows.length() > 100000 {
raise Invalid(
"metadata.graph_limit", document, "Graph exceeds 100000 rows",
)
}
for ordinal, row in rows {
let row = fields(row, document)
let subject = text(required(row, "@id", document), "graph subject")
ignore(address(subject))
let keys = row.keys().collect()
keys.sort()
for property in keys {
if property == "@id" {
continue
}
if property.has_prefix("@") {
raise Invalid(
"metadata.unsupported_graph_keyword", property, "Only the default graph is supported",
)
}
let (predicate, coercion) = context.expand(property)
let objects = if coercion == Some("@json") {
[row[property]]
} else {
match row[property].kind() {
List(items) => items
_ => [row[property]]
}
}
for object_ordinal, value in objects {
if coercion != Some("@json") && value.kind() is Null(_) {
continue
}
let (expanded, data, is_json) = object(value, coercion)
if result.length() >= 100000 {
raise Invalid(
"metadata.graph_limit", document, "Graph exceeds 100000 assertions",
)
}
result.push({
assertion: {
holder,
document,
carrier: "documentGraph",
subject,
predicate,
property,
ordinal,
object_ordinal,
},
object: expanded,
data,
is_json,
})
}
}
}
}
if meta.get("assertionSources") is Some(value) {
guard value.kind() is List(rows) else {
raise Invalid(
"metadata.invalid_assertion_sources", document, "assertionSources must be an array",
)
}
let selectors : Array[@ion.IonValue] = []
for row in rows {
let row = fields(row, document)
let selector = required(row, "selector", document)
let parsed = fields(selector, document)
if parsed.length() != 4 {
raise Invalid(
"metadata.invalid_assertion_selector", document, "Supply carrier, subject, predicate and expanded object",
)
}
let carrier = text(required(parsed, "carrier", document), document)
let subject = text(required(parsed, "subject", document), document)
let predicate = text(required(parsed, "predicate", document), document)
let expanded = required(parsed, "object", document)
if !result.any(f => {
f.assertion.carrier == carrier &&
f.assertion.subject == subject &&
f.assertion.predicate == predicate &&
f.object.equals(expanded)
}) {
raise Invalid(
"metadata.unmatched_assertion_selector", document, "Match an authored assertion in this document; update selectors when editing facts",
)
}
if selectors.any(s => s.equals(selector)) {
raise Invalid(
"metadata.duplicate_assertion_selector", document, "Supply one complete source set per selector",
)
}
selectors.push(selector)
guard required(row, "sources", document).kind() is List(sources) &&
!sources.is_empty() else {
raise Invalid(
"metadata.invalid_assertion_sources", document, "Supply a nonempty complete source set",
)
}
for source in sources {
let source = fields(source, document)
let kind = text(required(source, "kind", document), document)
if !["document", "compiler", "author"].contains(kind) {
raise Invalid(
"metadata.invalid_assertion_sources", document, "Unknown source kind",
)
}
ignore(text(required(source, "ref", document), document))
if kind == "compiler" {
ignore(text(required(source, "producer", document), document))
}
}
}
}
result
}
///|
fn scoped_context(
metadata : @ion.IonValue,
parent : Context,
capabilities : Capabilities,
base : String,
) -> Context raise {
let meta = fields(metadata, "$meta")
match meta.get("@context") {
Some(value) =>
resolve_context(value, parent, capabilities, base, Map([]), Map([]))
None => parent.copy()
}
}