///|
priv struct SchemaContext {
types : Array[String?]
children : Array[Array[Int32]]
declarations : Map[String, String]
metadata : Array[Map[String, String]]
}
///|
fn schema_context(doc : Document) -> SchemaContext {
let declarations : Map[String, String] = Map([])
for node in doc.nodes {
if node.tag == "TAG" &&
node.parent is Some(p) &&
doc.nodes[p].tag == "SCHMA" &&
doc.nodes[p].parent == Some(0) {
if node.payload is Text(s) &&
s.split_once(" ") is Some((tag, uri)) &&
valid_tag(tag.to_owned()) &&
tag.has_prefix("_") &&
valid_uri_reference(uri.to_owned()) {
declarations.set(tag.to_owned(), uri.to_owned())
}
}
}
let types : Array[String?] = []
let metadata : Array[Map[String, String]] = Array::makei(
doc.nodes.length() + 1,
fn(_) { Map([]) },
)
let children : Array[Array[Int32]] = Array::makei(doc.nodes.length() + 1, fn(
_,
) {
[]
})
for i, node in doc.nodes {
let p = node.parent.unwrap_or(doc.nodes.length())
children[p].push(i)
if node.payload is Text(s) && !metadata[p].contains(node.tag) {
metadata[p].set(node.tag, s)
}
let parent = match node.parent {
None => Some("")
Some(p) => types[p]
}
let edges = standard_children.get_or_default(parent.unwrap_or("?"), [])
let mut resolved : String? = None
if node.tag.has_prefix("_") {
if declarations.get(node.tag) is Some(uri) &&
uri.has_prefix("https://gedcom.io/terms/v7/") {
let t = uri[27:].to_owned()
if standard_payloads.contains(t) {
resolved = Some(t)
}
}
} else if parent != None {
for (tag, t, _, _) in edges {
if tag == node.tag {
resolved = Some(t)
break
}
}
}
types.push(resolved)
}
{ types, children, declarations, metadata, }
}
///|
fn enum_is_valid(
s : String,
allowed : Array[String],
ctx : SchemaContext,
structure : String,
) -> Bool {
if allowed.contains(s) {
return true
}
if s.has_prefix("_") && valid_tag(s) {
if ctx.declarations.get(s) is Some(uri) {
// A relocated standard enumeration cannot reuse a standard value in its
// original set. Unknown extension enumerations are accepted, unassessed.
if standard_enum_uris.get_or_default(structure, []).contains(uri) {
return false
}
}
return true
}
false
}
///|
fn check_payload(
kind : String,
values : Array[String],
payload : Payload,
ctx : SchemaContext,
structure~ : String = "",
) -> Bool {
if kind == "" {
return payload == Empty
}
if kind.has_prefix("pointer:") {
return payload is Pointer(_) || payload == Void
}
if payload is Pointer(_) || payload == Void {
return false
}
let s = payload.text().unwrap_or("")
match kind {
"xsd:string" => true
"xsd:nonNegativeInteger" => ascii_digits(s)
"Y|" => s == "" || s == "Y"
"type-Enum" => enum_is_valid(s, values, ctx, structure)
"type-List#Enum" =>
!s.has_prefix(" ") &&
!s.has_suffix(" ") &&
s
.split(",")
.all(fn(x) {
enum_is_valid(trim_spaces(x.to_owned()), values, ctx, structure)
})
"type-List#Text" => {
let parts = s.split(",").to_array()
// Separator spaces may surround empty endpoint items as well.
(parts.length() > 1 || s == trim_spaces(s)) &&
(!s.has_prefix(" ") || trim_spaces(parts[0].to_owned()) == "") &&
(
!s.has_suffix(" ") ||
trim_spaces(parts[parts.length() - 1].to_owned()) == ""
)
}
"type-Age" => valid_age(s)
"type-Time" => valid_time(s)
"type-Name" => {
let slashes = s.code_units().filter(fn(c) { c == 47 }).length()
(slashes == 0 || slashes == 2) &&
s.code_units().all(fn(c) { c >= 32 }) &&
!s.is_empty()
}
"xsd:Language" => valid_language(s)
"dcat:mediaType" => valid_media_type(s)
"xsd:anyURI" => valid_uri_reference(s)
"type-FilePath" => valid_file_path(s)
"type-Latitude" => valid_coordinate(s, true)
"type-Longitude" => valid_coordinate(s, false)
"type-TagDef" =>
match s.split_once(" ") {
Some((tag, uri)) =>
tag.has_prefix("_") &&
valid_tag(tag.to_owned()) &&
valid_uri_reference(uri.to_owned())
None => false
}
"type-Date" | "type-Date#period" | "type-Date#exact" =>
valid_date_payload(s, kind, ctx)
_ => false
}
}
///|
fn audit_schema(doc : Document, ctx : SchemaContext) -> Array[Diagnostic] {
let out = []
for i, node in doc.nodes {
let parent = match node.parent {
Some(p) => ctx.types[p]
None => Some("")
}
let edges = standard_children.get_or_default(parent.unwrap_or("?"), [])
let t = ctx.types[i]
if t is None {
if !node.tag.has_prefix("_") && parent != None {
out.push(
diagnostic(
"structure-context",
node.line,
"standard tag " +
node.tag +
" is not permitted in " +
parent.unwrap_or("dataset"),
),
)
}
continue
}
let t = t.unwrap()
if node.tag.has_prefix("_") {
if node.parent == None || t.has_prefix("record-") {
out.push(
diagnostic(
"relocated-record",
node.line,
"standard records cannot be relocated; standard substructures cannot become records",
),
)
} else if edges.any(fn(e) { e.1 == t }) {
out.push(
diagnostic(
"relocated-context",
node.line,
"relocated standard structure must use its standard tag in an already permitted context",
),
)
}
}
let (kind, values) = standard_payloads.get_or_default(t, ("?", []))
if !check_payload(kind, values, node.payload, ctx, structure=t) {
out.push(diagnostic("payload-type", node.line, t + " requires " + kind))
}
if kind.has_prefix("pointer:") &&
node.payload is Pointer(id) &&
doc.lookup(id) is Some(target) {
if ctx.types[target] != Some(kind[8:].to_owned()) {
out.push(
diagnostic(
"schema-pointer-type",
node.line,
"pointer target requires " + kind[8:].to_owned(),
related_line=doc.nodes[target].line,
),
)
}
}
}
for p = 0; p < ctx.children.length(); p = p + 1 {
let parent = if p == doc.nodes.length() { Some("") } else { ctx.types[p] }
if parent is None {
continue
}
let edges = standard_children.get_or_default(parent.unwrap(), [])
let counts : Map[String, Int32] = Map([])
for index in ctx.children[p] {
if ctx.types[index] is Some(t) && !doc.nodes[index].tag.has_prefix("_") {
counts.set(t, counts.get_or_default(t, 0) + 1)
}
}
for (tag, t, required, singular) in edges {
let count = counts.get_or_default(t, 0)
let line = if p < doc.nodes.length() { doc.nodes[p].line } else { 1 }
if required && count == 0 {
out.push(
diagnostic(
"required-structure",
line,
parent.unwrap() + " requires " + tag,
),
)
}
if singular && count > 1 {
out.push(
diagnostic(
"structure-cardinality",
line,
parent.unwrap() + " permits at most one " + tag,
),
)
}
}
}
out
}