///|
/// A small immutable patch language for configuration and event documents.
pub(all) enum CborPatch {
Set(Array[CborPathSegment], CborValue)
Remove(Array[CborPathSegment])
Merge(Array[CborPathSegment], CborValue)
} derive(Eq, Debug)
///|
/// Create a replacement patch.
pub fn cbor_patch_set(
path : Array[CborPathSegment],
value : CborValue,
) -> CborPatch {
Set(path, value)
}
///|
/// Create a removal patch.
pub fn cbor_patch_remove(path : Array[CborPathSegment]) -> CborPatch {
Remove(path)
}
///|
/// Create an object merge patch.
pub fn cbor_patch_merge(
path : Array[CborPathSegment],
value : CborValue,
) -> CborPatch {
Merge(path, value)
}
///|
/// Return the path targeted by a patch.
pub fn cbor_patch_path(patch : CborPatch) -> Array[CborPathSegment] {
match patch {
Set(path, _) | Remove(path) | Merge(path, _) => path
}
}
///|
/// Return a short operation name for metrics and audit logs.
pub fn cbor_patch_operation(patch : CborPatch) -> String {
match patch {
Set(_, _) => "set"
Remove(_) => "remove"
Merge(_, _) => "merge"
}
}
///|
fn remove_path(
value : CborValue,
path : Array[CborPathSegment],
) -> CborValue raise CborError {
if path.length() == 0 {
raise SemanticError("patch cannot remove the document root")
}
let head = path[0]
let tail = path[1:].to_owned()
if tail.length() == 0 {
match head {
Key(name) => cbor_object_remove(value, name)
Index(index) =>
match value {
Array(items) => {
if index < 0 || index >= items.length() {
raise SemanticError("patch array index is out of range")
}
let result = []
for i = 0; i < items.length(); i = i + 1 {
if i != index {
result.push(items[i])
}
}
Array(result)
}
_ => raise SemanticError("patch remove requires an array")
}
}
} else {
match head {
Key(name) => {
let child = cbor_query_at(value, [Key(name)])
let updated = remove_path(child, tail)
cbor_object_set(value, name, updated)
}
Index(index) =>
match value {
Array(items) => {
if index < 0 || index >= items.length() {
raise SemanticError("patch array index is out of range")
}
let updated = remove_path(items[index], tail)
let result = []
for item in items {
result.push(item)
}
result[index] = updated
Array(result)
}
_ => raise SemanticError("patch remove requires an array")
}
}
}
}
///|
fn apply_one_patch(
value : CborValue,
patch : CborPatch,
) -> CborValue raise CborError {
let path = cbor_patch_path(patch)
if path.length() == 0 {
match patch {
Set(_, replacement) => replacement
Remove(_) => raise SemanticError("patch cannot remove the document root")
Merge(_, overlay) => cbor_object_merge(value, overlay)
}
} else {
match patch {
Set(_, replacement) =>
if path.length() == 1 {
match path[0] {
Key(name) => cbor_object_set(value, name, replacement)
Index(_) => cbor_set_path(value, path, replacement)
}
} else {
cbor_set_path(value, path, replacement)
}
Remove(_) => remove_path(value, path)
Merge(_, overlay) => {
let selected = cbor_query_at(value, path)
let merged = cbor_object_merge(selected, overlay)
cbor_set_path(value, path, merged)
}
}
}
}
///|
/// Apply patches in order, returning a new document.
pub fn cbor_apply_patches(
value : CborValue,
patches : Array[CborPatch],
) -> CborValue raise CborError {
let mut result = value
for patch in patches {
result = apply_one_patch(result, patch)
}
result
}
///|
/// Apply exactly one patch and return its operation label with the result.
pub fn cbor_apply_patch_with_label(
value : CborValue,
patch : CborPatch,
) -> (String, CborValue) raise CborError {
let operation = cbor_patch_operation(patch)
(operation, apply_one_patch(value, patch))
}
///|
/// Construct patches for changed or newly added top-level object fields.
pub fn cbor_diff_objects(
before : CborValue,
after : CborValue,
) -> Array[CborPatch] raise CborError {
match (before, after) {
(Map(before_entries), Map(after_entries)) => {
let patches = []
for entry in after_entries {
match entry.0 {
Text(name) =>
match map_value(before, name) {
Some(old) =>
if old != entry.1 {
patches.push(Set([Key(name)], entry.1))
}
None => patches.push(Set([Key(name)], entry.1))
}
_ => raise SemanticError("patch diff requires text object keys")
}
}
for entry in before_entries {
match entry.0 {
Text(name) =>
if !cbor_object_has(after, name) {
patches.push(Remove([Key(name)]))
}
_ => raise SemanticError("patch diff requires text object keys")
}
}
patches
}
_ => raise SemanticError("patch diff requires two objects")
}
}
///|
/// Return a deterministic audit line for a patch batch.
pub fn cbor_patch_audit_line(patches : Array[CborPatch]) -> String {
let builder = StringBuilder::new()
builder.write_string("patches=")
builder.write_string(patches.length().to_string())
builder.write_string(" operations=")
for i = 0; i < patches.length(); i = i + 1 {
if i > 0 {
builder.write_string(",")
}
builder.write_string(cbor_patch_operation(patches[i]))
}
builder.to_string()
}
///|
/// Return a patch list that removes every top-level key in an object.
pub fn cbor_clear_object(value : CborValue) -> Array[CborPatch] raise CborError {
match value {
Map(entries) => {
let patches = []
for entry in entries {
match entry.0 {
Text(name) => patches.push(Remove([Key(name)]))
_ => raise SemanticError("patch clear requires text object keys")
}
}
patches
}
_ => raise SemanticError("patch clear requires an object")
}
}
///|
/// Return true when applying a patch does not change the encoded document.
pub fn cbor_patch_is_noop(
value : CborValue,
patch : CborPatch,
) -> Bool raise CborError {
encode(value) == encode(apply_one_patch(value, patch))
}
///|
/// Apply a batch and reject documents that exceed a node budget.
pub fn cbor_apply_patches_bounded(
value : CborValue,
patches : Array[CborPatch],
max_nodes : Int,
) -> CborValue raise CborError {
let result = cbor_apply_patches(value, patches)
if cbor_value_node_count(result) > max_nodes {
raise SemanticError("patch result exceeds node budget")
}
result
}