File: JSONPatchApplier.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (273 lines) | stat: -rw-r--r-- 11,457 bytes parent folder | download
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"
            }
        }
    }
}