///|
fn delivery_schema(message : String) -> InputError {
{ code: "INPUT_SCHEMA", message, }
}
///|
fn delivery_document(
text : String,
format : String,
allowed : Array[String],
) -> Result[Map[String, Json], InputError] {
let value = @json.parse(text) catch {
_ => return Err({ code: "INPUT_JSON", message: "Invalid delivery JSON.", })
}
guard value is Object(fields) else {
return Err(delivery_schema("Delivery document must be an object."))
}
match reject_unknown_fields(fields, allowed) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard fields.get("format") is Some(String(found)) && found == format else {
return Err(delivery_schema("Unsupported delivery format."))
}
guard fields.get("version") is Some(Number(version, ..)) && version == 1.0 else {
return Err(delivery_schema("Unsupported delivery version."))
}
Ok(fields)
}
///|
fn delivery_tree(value : Json) -> Result[DeliveryTree, InputError] {
guard value is Object(fields) else {
return Err(delivery_schema("Delivery root must be an object."))
}
match reject_unknown_fields(fields, ["id", "snapshot"]) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard fields.get("id") is Some(String(id)) else {
return Err(delivery_schema("Delivery root requires a string id."))
}
guard fields.get("snapshot") is Some(snapshot) && snapshot is Object(_) else {
return Err(delivery_schema("Delivery root requires a snapshot object."))
}
let snapshot = match parse_snapshot(snapshot.stringify()) {
Err(error) => return Err(error)
Ok(value) => value
}
Ok({ id, snapshot, })
}
///|
/// Parse structure only; path, namespace and mapping feasibility belong to planning.
pub fn parse_delivery_spec(text : String) -> Result[DeliverySpec, InputError] {
let fields = match
delivery_document(text, "moonportcheck-delivery-spec", [
"format", "version", "sources", "target", "mappings",
]) {
Err(error) => return Err(error)
Ok(value) => value
}
guard fields.get("sources") is Some(Array(items)) else {
return Err(delivery_schema("Delivery spec requires sources array."))
}
let sources : Array[DeliveryTree] = []
for item in items {
match delivery_tree(item) {
Err(error) => return Err(error)
Ok(tree) => sources.push(tree)
}
}
guard fields.get("target") is Some(target) else {
return Err(delivery_schema("Delivery spec requires target."))
}
let target = match delivery_tree(target) {
Err(error) => return Err(error)
Ok(tree) => tree
}
guard fields.get("mappings") is Some(Array(items)) else {
return Err(delivery_schema("Delivery spec requires mappings array."))
}
let mappings : Array[DeliveryMapping] = []
for item in items {
guard item is Object(fields) else {
return Err(delivery_schema("Mapping must be an object."))
}
match reject_unknown_fields(fields, ["mode", "source", "from", "to"]) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard fields.get("mode") is Some(String(mode)) else {
return Err(delivery_schema("Mapping requires string mode."))
}
guard fields.get("source") is Some(String(source)) else {
return Err(delivery_schema("Mapping requires string source."))
}
guard fields.get("from") is Some(String(from)) else {
return Err(delivery_schema("Mapping requires string from."))
}
guard fields.get("to") is Some(String(to)) else {
return Err(delivery_schema("Mapping requires string to."))
}
mappings.push({ mode, source, from, to, })
}
Ok({ sources, target, mappings, })
}
///|
fn delivery_origin(value : Json) -> Result[DeliveryOrigin, InputError] {
guard value is Object(fields) else {
return Err(delivery_schema("Origin must be an object."))
}
match reject_unknown_fields(fields, ["root", "path"]) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard fields.get("root") is Some(String(root)) else {
return Err(delivery_schema("Origin requires string root."))
}
guard fields.get("path") is Some(String(path)) else {
return Err(delivery_schema("Origin requires string path."))
}
Ok({ root, path, })
}
///|
fn delivery_diagnostics(
items : Array[Json],
) -> Result[Array[DeliveryDiagnostic], InputError] {
let diagnostics : Array[DeliveryDiagnostic] = []
for item in items {
guard item is Object(fields) else {
return Err(delivery_schema("Diagnostic must be an object."))
}
match reject_unknown_fields(fields, ["code", "path", "message", "step"]) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard fields.get("code") is Some(String(code)) else {
return Err(delivery_schema("Diagnostic requires string code."))
}
guard fields.get("path") is Some(String(path)) else {
return Err(delivery_schema("Diagnostic requires string path."))
}
guard fields.get("message") is Some(String(message)) else {
return Err(delivery_schema("Diagnostic requires string message."))
}
let step = match parse_int_field(fields, "step") {
Err(error) => return Err(error)
Ok(value) => value
}
diagnostics.push({ code, path, message, step, })
}
Ok(diagnostics)
}
///|
/// Strict external plan parser. Operation sequencing and identity are audited separately.
pub fn parse_delivery_plan(text : String) -> Result[DeliveryPlan, InputError] {
let fields = match
delivery_document(text, "moonportcheck-delivery-plan", [
"format", "version", "status", "basis", "operations", "temporary_paths", "diagnostics",
]) {
Err(error) => return Err(error)
Ok(value) => value
}
guard fields.get("status") is Some(String(status)) else {
return Err(delivery_schema("Plan requires string status."))
}
if !["ready", "blocked", "incomplete"].contains(status) {
return Err(delivery_schema("Unknown plan status."))
}
guard fields.get("basis") is Some(String(basis)) else {
return Err(delivery_schema("Plan requires string basis."))
}
guard fields.get("operations") is Some(Array(items)) else {
return Err(delivery_schema("Plan requires operations array."))
}
let operations : Array[DeliveryOperation] = []
for item in items {
guard item is Object(fields) else {
return Err(delivery_schema("Operation must be an object."))
}
match
reject_unknown_fields(fields, ["kind", "source", "from", "to", "origin"]) {
Err(error) => return Err(error)
Ok(_) => ()
}
guard fields.get("kind") is Some(String(kind)) else {
return Err(delivery_schema("Operation requires string kind."))
}
if !["mkdir", "copy", "move"].contains(kind) {
return Err(delivery_schema("Unknown operation kind."))
}
guard fields.get("source") is Some(String(source)) else {
return Err(delivery_schema("Operation requires string source."))
}
guard fields.get("from") is Some(String(from)) else {
return Err(delivery_schema("Operation requires string from."))
}
guard fields.get("to") is Some(String(to)) else {
return Err(delivery_schema("Operation requires string to."))
}
guard fields.get("origin") is Some(value) else {
return Err(delivery_schema("Operation requires origin."))
}
let origin = match delivery_origin(value) {
Err(error) => return Err(error)
Ok(origin) => origin
}
operations.push({ kind, source, from, to, origin, })
}
guard fields.get("temporary_paths") is Some(Array(items)) else {
return Err(delivery_schema("Plan requires temporary_paths array."))
}
let temporary_paths = match
parse_diagnostic_strings(items, "temporary_paths") {
Err(error) => return Err(error)
Ok(values) => values
}
guard fields.get("diagnostics") is Some(Array(items)) else {
return Err(delivery_schema("Plan requires diagnostics array."))
}
let diagnostics = match delivery_diagnostics(items) {
Err(error) => return Err(error)
Ok(values) => values
}
Ok({ status, basis, operations, temporary_paths, diagnostics, })
}
///|
fn delivery_tree_json(tree : DeliveryTree) -> String {
"{\"id\":" +
quote(tree.id) +
",\"snapshot\":" +
render_snapshot_json(tree.snapshot) +
"}"
}
///|
fn delivery_mapping_order(a : DeliveryMapping, b : DeliveryMapping) -> Int {
for
pair in [
(a.mode, b.mode),
(a.source, b.source),
(a.from, b.from),
(a.to, b.to),
] {
let order = ordinal_compare(pair.0, pair.1)
if order != 0 {
return order
}
}
0
}
///|
/// Canonical expanded spec content used as a plan's basis; caller order is irrelevant.
pub fn render_delivery_spec_json(spec : DeliverySpec) -> String {
let sources = spec.sources.copy()
sources.sort_by((a, b) => {
let order = ordinal_compare(a.id, b.id)
if order != 0 {
order
} else {
ordinal_compare(
render_snapshot_json(a.snapshot),
render_snapshot_json(b.snapshot),
)
}
})
let mappings = spec.mappings.copy()
mappings.sort_by(delivery_mapping_order)
"{\"format\":\"moonportcheck-delivery-spec\",\"version\":1,\"sources\":[" +
sources.map(delivery_tree_json).join(",") +
"],\"target\":" +
delivery_tree_json(spec.target) +
",\"mappings\":[" +
mappings
.map(mapping => {
"{\"mode\":" +
quote(mapping.mode) +
",\"source\":" +
quote(mapping.source) +
",\"from\":" +
quote(mapping.from) +
",\"to\":" +
quote(mapping.to) +
"}"
})
.join(",") +
"]}"
}
///|
fn delivery_origin_json(origin : DeliveryOrigin) -> String {
"{\"root\":" + quote(origin.root) + ",\"path\":" + quote(origin.path) + "}"
}
///|
fn delivery_diagnostics_json(diagnostics : Array[DeliveryDiagnostic]) -> String {
"[" +
diagnostics
.map(d => {
"{\"code\":" +
quote(d.code) +
",\"path\":" +
quote(d.path) +
",\"message\":" +
quote(d.message) +
",\"step\":" +
d.step.to_string() +
"}"
})
.join(",") +
"]"
}
///|
pub fn render_delivery_plan_json(plan : DeliveryPlan) -> String {
"{\"format\":\"moonportcheck-delivery-plan\",\"version\":1,\"status\":" +
quote(plan.status) +
",\"basis\":" +
quote(plan.basis) +
",\"operations\":[" +
plan.operations
.map(op => {
"{\"kind\":" +
quote(op.kind) +
",\"source\":" +
quote(op.source) +
",\"from\":" +
quote(op.from) +
",\"to\":" +
quote(op.to) +
",\"origin\":" +
delivery_origin_json(op.origin) +
"}"
})
.join(",") +
"],\"temporary_paths\":" +
json_strings(plan.temporary_paths) +
",\"diagnostics\":" +
delivery_diagnostics_json(plan.diagnostics) +
"}"
}
///|
pub fn render_delivery_audit_json(audit : DeliveryAudit) -> String {
let entries = audit.final_entries.copy()
entries.sort_by((a, b) => ordinal_compare(a.path, b.path))
"{\"format\":\"moonportcheck-delivery-audit\",\"version\":1,\"status\":" +
quote(audit.status) +
",\"valid\":" +
audit.valid.to_string() +
",\"first_failed_step\":" +
audit.first_failed_step.to_string() +
",\"diagnostics\":" +
delivery_diagnostics_json(audit.diagnostics) +
",\"final_entries\":[" +
entries
.map(entry => {
"{\"path\":" +
quote(entry.path) +
",\"kind\":" +
quote(source_kind_name(entry.kind)) +
",\"origin\":" +
delivery_origin_json(entry.origin) +
"}"
})
.join(",") +
"],\"trace\":[" +
audit.trace
.map(trace => {
"{\"step\":" +
trace.step.to_string() +
",\"kind\":" +
quote(trace.kind) +
",\"from\":" +
quote(trace.from) +
",\"to\":" +
quote(trace.to) +
",\"origin\":" +
delivery_origin_json(trace.origin) +
"}"
})
.join(",") +
"]}"
}
///|
pub fn render_delivery_plan_text(plan : DeliveryPlan) -> String {
let lines = [
"Delivery plan: " + plan.status,
"Operations: " + plan.operations.length().to_string(),
]
for step, op in plan.operations {
lines.push(
step.to_string() +
": " +
op.kind +
" " +
quote(op.source) +
":" +
quote(op.from) +
" -> " +
quote(op.to),
)
}
for d in plan.diagnostics {
lines.push(
d.code +
" at " +
quote(d.path) +
" (step " +
d.step.to_string() +
"): " +
d.message,
)
}
lines.join("\n") + "\n"
}
///|
pub fn render_delivery_plan_markdown(plan : DeliveryPlan) -> String {
"# Delivery plan\n\nStatus: " +
plan.status +
"\n\nOperations: " +
plan.operations.length().to_string() +
"\n\n```json\n" +
render_delivery_plan_json(plan) +
"\n```\n"
}
///|
pub fn render_delivery_audit_text(audit : DeliveryAudit) -> String {
let lines = [
"Delivery audit: " + audit.status,
"Valid: " + audit.valid.to_string(),
"First failed step: " + audit.first_failed_step.to_string(),
]
for trace in audit.trace {
lines.push(
trace.step.to_string() +
": " +
trace.kind +
" " +
quote(trace.from) +
" -> " +
quote(trace.to),
)
}
for d in audit.diagnostics {
lines.push(
d.code +
" at " +
quote(d.path) +
" (step " +
d.step.to_string() +
"): " +
d.message,
)
}
lines.join("\n") + "\n"
}
///|
pub fn render_delivery_audit_markdown(audit : DeliveryAudit) -> String {
"# Delivery audit\n\nStatus: " +
audit.status +
"\n\nValid: " +
audit.valid.to_string() +
"\n\nFirst failed step: " +
audit.first_failed_step.to_string() +
"\n\n```json\n" +
render_delivery_audit_json(audit) +
"\n```\n"
}