///|
fn required_field(
o : Map[String, Json],
field : String,
) -> Json raise ConfigError {
guard o.get(field) is Some(value) else {
raise InvalidPatch("missing " + field)
}
value
}
///|
fn required_string(
o : Map[String, Json],
field : String,
) -> String raise ConfigError {
guard required_field(o, field) is String(value) else {
raise InvalidPatch(field + " must be a string")
}
value
}
///|
/// Unknown operation members are ignored, as required by RFC 6902.
pub fn parse_patch(value : Json) -> Array[Operation] raise ConfigError {
guard value is Array(ops) else {
raise InvalidPatch("patch must be an array")
}
if ops.length() > 10000 {
raise LimitExceeded("patch exceeds 10000 operations")
}
ops.map(op => {
guard op is Object(o) else {
raise InvalidPatch("operation must be an object")
}
let path = required_string(o, "path")
let _ = pointer_tokens(path)
match required_string(o, "op") {
"add" => {
let v = required_field(o, "value")
validate_document(v)
Add(path, v)
}
"remove" => Remove(path)
"replace" => {
let v = required_field(o, "value")
validate_document(v)
Replace(path, v)
}
"test" => {
let v = required_field(o, "value")
validate_document(v)
Test(path, v)
}
"move" => {
let source = required_string(o, "from")
let _ = pointer_tokens(source)
Move(source, path)
}
"copy" => {
let source = required_string(o, "from")
let _ = pointer_tokens(source)
Copy(source, path)
}
other => raise InvalidPatch("unknown operation: " + other)
}
})
}
///|
pub fn patch_json(ops : Array[Operation]) -> Json {
Json::array(
ops.map(op => {
let o : Map[String, Json] = Map([])
match op {
Add(path, value) | Replace(path, value) | Test(path, value) => {
o["op"] = Json::string(
match op {
Add(_, _) => "add"
Replace(_, _) => "replace"
_ => "test"
},
)
o["path"] = Json::string(path)
o["value"] = clone_json(value)
}
Remove(path) => {
o["op"] = Json::string("remove")
o["path"] = Json::string(path)
}
Move(source, path) | Copy(source, path) => {
o["op"] = Json::string(if op is Move(_, _) { "move" } else { "copy" })
o["path"] = Json::string(path)
o["from"] = Json::string(source)
}
}
Json::object(o)
}),
)
}
///|
/// Apply in order. On any error, the caller's document and patch stay unchanged.
/// Root removal is rejected because this API must return a JSON document.
pub fn apply(document : Json, ops : Array[Operation]) -> Json raise ConfigError {
validate_document(document)
if ops.length() > 10000 {
raise LimitExceeded("patch exceeds 10000 operations")
}
let mut current = clone_json(document)
for op in ops {
current = match op {
Add(path, value) | Replace(path, value) => {
validate_document(value)
edit_at(
current,
pointer_tokens(path),
0,
path,
if op is Add(_, _) {
Insert
} else {
Update
},
value,
)
}
Remove(path) =>
edit_at(current, pointer_tokens(path), 0, path, Delete, Json::null())
Test(path, value) => {
validate_document(value)
if !same(get_at(current, pointer_tokens(path), 0, path), value) {
raise TestFailed(path)
}
current
}
Copy(source, path) => {
let value = get_at(current, pointer_tokens(source), 0, source)
edit_at(current, pointer_tokens(path), 0, path, Insert, value)
}
Move(source, path) => {
let from = pointer_tokens(source)
let to = pointer_tokens(path)
let value = get_at(current, from, 0, source)
if from == to {
current
} else {
if to.length() > from.length() &&
to.exact_view(end=from.length()) == from.exact_view() {
raise InvalidMove("destination is inside source")
}
let removed = edit_at(current, from, 0, source, Delete, Json::null())
edit_at(removed, to, 0, path, Insert, value)
}
}
}
validate_document(current)
}
current
}