1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
|
/*
This source file is part of the Swift.org open source project
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception
See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/
import Foundation
/// A utility type for applying JSON patches.
///
/// Use this type to apply ``JSONPatchOperation`` values onto JSON.
public struct JSONPatchApplier {
/// Creates a new JSON patch applier.
public init() {}
/// Applies the given patch onto the given JSON data.
///
/// - Parameters:
/// - patch: The patch to apply.
/// - jsonData: The data on which to apply the patch.
/// - Returns: The JSON data with the patch applied.
/// - Throws: This function throws an ``Error`` if the application was not successful.
public func apply(_ patch: JSONPatch, to jsonData: Data) throws -> Data {
var json = try JSONDecoder().decode(JSON.self, from: jsonData)
for operation in patch {
guard let newValue = try apply(operation, to: json, originalPointer: operation.pointer) else {
// If the application of the operation onto the top-level JSON element results in a `nil` value (i.e.,
// the entire value was removed), throw an error since this is not supported.
throw Error.invalidPatch
}
json = newValue
}
return try JSONEncoder().encode(json)
}
private func apply(_ operation: JSONPatchOperation, to json: JSON, originalPointer: JSONPointer) throws -> JSON? {
// If the pointer has no path components left, this is the value we need to update.
guard let component = operation.pointer.pathComponents.first else {
switch operation {
case .replace(_, let value), .add(_, let value):
if let json = value.value as? JSON {
return json
} else {
// If the value is not encoded as a `JSON` value already, convert it.
let data = try JSONEncoder().encode(value)
return try JSONDecoder().decode(JSON.self, from: data)
}
case .remove(_):
return nil
}
}
let nextOperation = operation.removingPointerFirstPathComponent()
// Traverse the JSON element and apply the operation recursively.
switch json {
case .dictionary(let dictionary):
return try apply(
nextOperation,
toDictionary: dictionary,
component: component,
originalPointer: originalPointer
)
case .array(let array):
return try apply(
nextOperation,
toArray: array,
component: component,
originalPointer: originalPointer
)
default:
// The pointer is invalid because it has a non-empty path component, but the JSON element is not
// traversable, i.e., it's a number, string, boolean, or null value.
throw Error.invalidValuePointer(
originalPointer,
component: component,
jsonValue: String(describing: json)
)
}
}
private func apply(
_ operation: JSONPatchOperation,
toDictionary dictionary: [String: JSON],
component: String,
originalPointer: JSONPointer
) throws -> JSON? {
var dictionary = dictionary
func throwInvalidObjectPointerError() throws -> Never {
throw Error.invalidObjectPointer(
originalPointer,
component: component,
availableObjectKeys: dictionary.keys
)
}
switch operation.operation {
case .replace:
// If we're replacing, there must be an existing value for this key.
guard let value = dictionary[component] else {
try throwInvalidObjectPointerError()
}
dictionary[component] = try apply(operation, to: value, originalPointer: originalPointer)
case .add:
if let value = dictionary[component] {
// If there's already a value for this key, replace its value recursively.
dictionary[component] = try apply(operation, to: value, originalPointer: originalPointer)
} else if operation.pointer.pathComponents.isEmpty {
// Otherwise, if the pointer is empty, just write the value.
dictionary[component] = try apply(operation, to: .dictionary(dictionary), originalPointer: originalPointer)
} else {
// Otherwise, the pointer is invalid.
try throwInvalidObjectPointerError()
}
case .remove:
if let value = dictionary[component] {
// If there's a value at this key, remove its value recursively.
dictionary[component] = try apply(operation, to: value, originalPointer: originalPointer)
} else if operation.pointer.pathComponents.isEmpty {
// Otherwise, if the pointer is empty, just remove the value.
dictionary[component] = nil
} else {
// Otherwise, the pointer is invalid.
try throwInvalidObjectPointerError()
}
}
return .dictionary(dictionary)
}
private func apply(
_ operation: JSONPatchOperation,
toArray array: [JSON],
component: String,
originalPointer: JSONPointer
) throws -> JSON? {
var array = array
func throwInvalidArrayPointerError() throws -> Never {
throw Error.invalidArrayPointer(
originalPointer,
index: component,
arrayCount: array.count
)
}
guard let index = Int(component) else {
throw Error.invalidArrayPointer(
originalPointer,
index: component,
arrayCount: array.count
)
}
switch operation {
case .replace:
guard array.indices.contains(index),
let newValue = try apply(operation, to: array[index], originalPointer: originalPointer)
else {
try throwInvalidArrayPointerError()
}
array[index] = newValue
case .add:
if operation.pointer.pathComponents.isEmpty,
let newValue = try apply(operation, to: .array(array), originalPointer: originalPointer)
{
if index == array.indices.endIndex {
array.append(newValue)
} else if array.indices.contains(index) {
array.insert(newValue, at: index)
} else {
throw Error.invalidArrayPointer(
originalPointer,
index: component,
arrayCount: array.count
)
}
} else if array.indices.contains(index),
let newValue = try apply(operation, to: array[index], originalPointer: originalPointer)
{
array[index] = newValue
} else {
try throwInvalidArrayPointerError()
}
case .remove:
if array.indices.contains(index),
let newValue = try apply(operation, to: array[index], originalPointer: originalPointer)
{
array[index] = newValue
} else if array.indices.contains(index), operation.pointer.pathComponents.isEmpty {
array.remove(at: index)
} else {
try throwInvalidArrayPointerError()
}
}
return .array(array)
}
/// An error that occurred during the application of a JSON patch.
public enum Error: DescribedError {
/// An error indicating that the pointer of a patch operation is invalid for a JSON object.
///
/// - Parameters:
/// - component: The component that's causing the pointer to be invalid in the JSON object.
/// - availableKeys: The keys available in the JSON object.
case invalidObjectPointer(JSONPointer, component: String, availableKeys: [String])
/// An error indicating that the pointer of a patch operation is invalid for a JSON object.
///
/// - Parameters:
/// - pointer: A pointer to the invalid object the JSON document.
/// - component: The component that's causing the pointer to be invalid in the JSON object.
/// - availableObjectKeys: The keys available in the JSON object.
public static func invalidObjectPointer(
_ pointer: JSONPointer,
component: String,
availableObjectKeys: some Collection<String>
) -> Self {
return .invalidObjectPointer(pointer, component: component, availableKeys: Array(availableObjectKeys))
}
/// An error indicating that the pointer of a patch operation is invalid for a JSON array.
///
/// - Parameters:
/// - index: The index component that's causing the pointer to be invalid in the JSON array.
/// - arrayCount: The size of the JSON array.
case invalidArrayPointer(JSONPointer, index: String, arrayCount: Int)
/// An error indicating that the pointer of a patch operation is invalid for a JSON value.
///
/// - Parameters:
/// - component: The component that's causing the pointer to be invalid, since the JSON element is a non-traversable value.
/// - jsonValue: The string-encoded description of the JSON value.
case invalidValuePointer(JSONPointer, component: String, jsonValue: String)
/// An error indicating that a patch operation is invalid.
case invalidPatch
public var errorDescription: String {
switch self {
case .invalidObjectPointer(let pointer, let component, let availableKeys):
return """
Invalid dictionary pointer '\(pointer)'. The component '\(component)' is not valid for the object with \
keys \(availableKeys.sorted().map(\.singleQuoted).list(finalConjunction: .and)).
"""
case .invalidArrayPointer(let pointer, let index, let arrayCount):
return """
Invalid array pointer '\(pointer)'. The index '\(index)' is not valid for array of \(arrayCount) \
elements.
"""
case .invalidValuePointer(let pointer, let component, let jsonValue):
return """
Invalid value pointer '\(pointer)'. The component '\(component)' is not valid for the non-traversable \
value '\(jsonValue)'.
"""
case .invalidPatch:
return "Invalid patch"
}
}
}
}
|