///|
/// A JSON Patch-like operation over a parsed value. Paths use RFC 6901 JSON
/// Pointer syntax, and operations are applied in array order.
pub enum PatchOperation {
Add(String, JsonValue)
Remove(String)
Replace(String, JsonValue)
}
///|
pub fn patch_add(path : String, value : JsonValue) -> PatchOperation {
PatchOperation::Add(path, value)
}
///|
pub fn patch_remove(path : String) -> PatchOperation {
PatchOperation::Remove(path)
}
///|
pub fn patch_replace(path : String, value : JsonValue) -> PatchOperation {
PatchOperation::Replace(path, value)
}
///|
pub fn PatchOperation::to_text(self : PatchOperation) -> String {
match self {
Add(path, value) => "add " + path + " " + value_preview(value)
Remove(path) => "remove " + path
Replace(path, value) => "replace " + path + " " + value_preview(value)
}
}
///|
pub fn PatchOperation::to_json(self : PatchOperation) -> String {
match self {
Add(path, value) =>
"{\"op\":\"add\",\"path\":" +
canonical_string(path) +
",\"value\":" +
value_preview(value) +
"}"
Remove(path) =>
"{\"op\":\"remove\",\"path\":" + canonical_string(path) + "}"
Replace(path, value) =>
"{\"op\":\"replace\",\"path\":" +
canonical_string(path) +
",\"value\":" +
value_preview(value) +
"}"
}
}
///|
pub fn apply_patch(
value : JsonValue,
operations : Array[PatchOperation],
) -> Result[JsonValue, String] {
let mut current = value
for operation in operations {
current = match apply_operation(current, operation) {
Ok(value) => value
Err(error) => return Err(error)
}
}
Ok(current)
}
///|
pub fn apply_patch_documents(
input : String,
operations : Array[PatchOperation],
) -> Result[String, String] {
let value = match parse(input) {
Ok(value) => value
Err(error) => return Err(error.message())
}
match apply_patch(value, operations) {
Ok(value) =>
match canonicalize(value) {
Ok(output) => Ok(output)
Err(error) => Err(error.message())
}
Err(error) => Err(error)
}
}
///|
fn apply_operation(
value : JsonValue,
operation : PatchOperation,
) -> Result[JsonValue, String] {
let (path, action) = match operation {
Add(path, value) => (path, PatchAction::Add(value))
Remove(path) => (path, PatchAction::Remove)
Replace(path, value) => (path, PatchAction::Replace(value))
}
let segments = match parse_pointer(path) {
Ok(segments) => segments
Err(error) => return Err(error)
}
patch_at(value, segments, 0, action)
}
///|
priv enum PatchAction {
Add(JsonValue)
Remove
Replace(JsonValue)
}
///|
fn patch_at(
value : JsonValue,
segments : Array[String],
depth : Int,
action : PatchAction,
) -> Result[JsonValue, String] {
if depth == segments.length() {
match action {
Add(value) | Replace(value) => Ok(value)
Remove => Err("cannot remove the document root")
}
} else {
let segment = segments[depth]
match value {
Object(entries) => patch_object(entries, segments, depth, segment, action)
Array(values) => patch_array(values, segments, depth, segment, action)
_ =>
Err("cannot apply a patch below scalar path segment '" + segment + "'")
}
}
}
///|
fn patch_object(
entries : Array[(String, JsonValue)],
segments : Array[String],
depth : Int,
segment : String,
action : PatchAction,
) -> Result[JsonValue, String] {
let final_segment = depth + 1 == segments.length()
let updated : Array[(String, JsonValue)] = []
let mut found = false
for entry in entries {
if entry.0 != segment {
updated.push(entry)
} else {
found = true
if final_segment {
match action {
Add(value) | Replace(value) => updated.push((segment, value))
Remove => ()
}
} else {
let child = match patch_at(entry.1, segments, depth + 1, action) {
Ok(value) => value
Err(error) => return Err(error)
}
updated.push((segment, child))
}
}
}
if !found {
if final_segment {
match action {
Add(value) => updated.push((segment, value))
Replace(_) =>
return Err("cannot replace missing object key: " + segment)
Remove => return Err("cannot remove missing object key: " + segment)
}
} else {
return Err("object key not found: " + segment)
}
}
Ok(Object(updated))
}
///|
fn patch_array(
values : Array[JsonValue],
segments : Array[String],
depth : Int,
segment : String,
action : PatchAction,
) -> Result[JsonValue, String] {
let index = if segment == "-" && depth + 1 == segments.length() {
values.length()
} else {
match parse_array_index(segment) {
Some(index) => index
None => return Err("array patch segment is not a valid index: " + segment)
}
}
let final_segment = depth + 1 == segments.length()
if final_segment {
match action {
Add(value) => {
if index > values.length() {
return Err("array add index out of range: " + segment)
}
Ok(Array(array_insert(values, index, value)))
}
Replace(value) => {
if index >= values.length() {
return Err("array replace index out of range: " + segment)
}
Ok(Array(array_replace(values, index, value)))
}
Remove => {
if index >= values.length() {
return Err("array remove index out of range: " + segment)
}
Ok(Array(array_remove(values, index)))
}
}
} else {
if index >= values.length() {
return Err("array index out of range: " + segment)
}
let child = match patch_at(values[index], segments, depth + 1, action) {
Ok(value) => value
Err(error) => return Err(error)
}
Ok(Array(array_replace(values, index, child)))
}
}
///|
fn array_insert(
values : Array[JsonValue],
index : Int,
value : JsonValue,
) -> Array[JsonValue] {
let output : Array[JsonValue] = []
let mut cursor = 0
while cursor < values.length() {
if cursor == index {
output.push(value)
}
output.push(values[cursor])
cursor = cursor + 1
}
if index == values.length() {
output.push(value)
}
output
}
///|
fn array_replace(
values : Array[JsonValue],
index : Int,
value : JsonValue,
) -> Array[JsonValue] {
let output : Array[JsonValue] = []
let mut cursor = 0
for item in values {
output.push(if cursor == index { value } else { item })
cursor = cursor + 1
}
output
}
///|
fn array_remove(values : Array[JsonValue], index : Int) -> Array[JsonValue] {
let output : Array[JsonValue] = []
let mut cursor = 0
for item in values {
if cursor != index {
output.push(item)
}
cursor = cursor + 1
}
output
}