├── Docs
├── images
│ ├── xcode11-step1.jpg
│ ├── xcode11-step2.jpg
│ ├── xcode11-step3.jpg
│ └── xcode11-step4.jpg
├── Carthage.md
├── CocoaPods.md
├── SPM.md
└── Usage.md
├── JSONPatch.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ ├── JSONPatchFramework.xcscheme
│ │ └── JSONPatch.xcscheme
└── project.pbxproj
├── Tests
└── JSONPatchTests
│ ├── Info.plist
│ ├── Bundle.swift
│ ├── TestFileTestCase.swift
│ ├── JSONPointerTests.swift
│ ├── extra.json
│ ├── JSONCodableTests.swift
│ ├── JSONPatchGeneratorTests.swift
│ ├── JSONElementTests.swift
│ ├── ArrayTests.swift
│ ├── JSONFileTestCase.swift
│ ├── spec_tests.json
│ ├── JSONPatchTests.swift
│ └── tests.json
├── Sources
└── JSONPatch
│ ├── Info.plist
│ ├── NSArray+DeepCopy.swift
│ ├── NSDictionary+DeepCopy.swift
│ ├── JSONError.swift
│ ├── JSONEquality.swift
│ ├── JSONPointer.swift
│ ├── JSONPatchCodable.swift
│ ├── JSONPatchGenerator.swift
│ ├── JSONPatch.swift
│ └── JSONElement.swift
├── JSONPatchFramework
├── Info.plist
└── JSONPatch.h
├── Package.swift
├── .gitignore
├── README.md
├── RMJSONPatch.podspec
└── LICENSE
/Docs/images/xcode11-step1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raymccrae/swift-jsonpatch/HEAD/Docs/images/xcode11-step1.jpg
--------------------------------------------------------------------------------
/Docs/images/xcode11-step2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raymccrae/swift-jsonpatch/HEAD/Docs/images/xcode11-step2.jpg
--------------------------------------------------------------------------------
/Docs/images/xcode11-step3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raymccrae/swift-jsonpatch/HEAD/Docs/images/xcode11-step3.jpg
--------------------------------------------------------------------------------
/Docs/images/xcode11-step4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raymccrae/swift-jsonpatch/HEAD/Docs/images/xcode11-step4.jpg
--------------------------------------------------------------------------------
/Docs/Carthage.md:
--------------------------------------------------------------------------------
1 | # Carthage
2 | To use JSONPatch within your project, specify it in your `Cartfile`:
3 | ```
4 | github "raymccrae/swift-jsonpatch" "v1.0.2"
5 | ```
--------------------------------------------------------------------------------
/JSONPatch.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Docs/CocoaPods.md:
--------------------------------------------------------------------------------
1 | # CocoaPods
2 | To use JSONPatch within your project. Add the "RMJSONPatch" into your `Podfile`:
3 | ```ruby
4 | platform :ios, '8.0'
5 | use_frameworks!
6 |
7 | target '' do
8 | pod 'RMJSONPatch', :git => 'https://github.com/raymccrae/swift-jsonpatch.git'
9 | end
10 | ```
11 |
--------------------------------------------------------------------------------
/JSONPatch.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0.3
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
--------------------------------------------------------------------------------
/JSONPatchFramework/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/Bundle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bundle.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 19/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | extension Bundle {
24 | static let test = Bundle(identifier: "scot.raymccrae.JSONPatchTests")!
25 | }
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:4.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "JSONPatch",
8 | products: [
9 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
10 | .library(
11 | name: "JSONPatch",
12 | targets: ["JSONPatch"]),
13 | ],
14 | dependencies: [
15 | // Dependencies declare other packages that this package depends on.
16 | // .package(url: /* package url */, from: "1.0.0"),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
21 | .target(
22 | name: "JSONPatch",
23 | dependencies: []),
24 | .testTarget(
25 | name: "JSONPatchTests",
26 | dependencies: ["JSONPatch"],
27 | path: "Tests"
28 | )
29 | ]
30 | )
31 |
--------------------------------------------------------------------------------
/JSONPatchFramework/JSONPatch.h:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatch.h
3 | // JSONPatch
4 | //
5 | // Created by Cassiano Monteiro on 2019-08-22.
6 | // Copyright © 2019 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | #import
22 |
23 | //! Project version number for JSONPatchFramework.
24 | FOUNDATION_EXPORT double JSONPatchVersionNumber;
25 |
26 | //! Project version string for JSONPatchFramework.
27 | FOUNDATION_EXPORT const unsigned char JSONPatchVersionString[];
28 |
29 | // In this header, you should import all the public headers of your framework using statements like #import
30 |
31 | #import
32 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/TestFileTestCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestFileTestCase.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 21/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | class TestFileTestCase: JSONFileTestCase {
22 | override class var filename: String? {
23 | return "tests"
24 | }
25 | }
26 |
27 | class SpecTestFileTestCase: JSONFileTestCase {
28 | override class var filename: String? {
29 | return "spec_tests"
30 | }
31 | }
32 |
33 | class ExtraFileTestCase: JSONFileTestCase {
34 | override class var filename: String? {
35 | return "extra"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Docs/SPM.md:
--------------------------------------------------------------------------------
1 | # Swift Package Manager
2 |
3 | ## Xcode 11 SPM Integration
4 |
5 | Within the `File` menu go to `Swift Packages` -> `Add Package Dependency...`.
6 | 
7 |
8 | Search for `swift-jsonpatch`, select the repository and click the `Next` button.
9 | 
10 |
11 | Enter the version number 1 and click the `Next` button.
12 | 
13 |
14 | Click the `Finish` button.
15 | 
16 |
17 | ## Manual Swift Package Manager
18 | Add JSONPatch as a dependency to your projects Package.swift. For example: -
19 |
20 | ```swift
21 | // swift-tools-version:4.0
22 | import PackageDescription
23 |
24 | let package = Package(
25 | name: "YourProject",
26 | dependencies: [
27 | // Dependencies declare other packages that this package depends on.
28 | .package(url: "https://github.com/raymccrae/swift-jsonpatch.git", .branch("master"))
29 | ],
30 | targets: [
31 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
32 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
33 | .target(
34 | name: "YourProject",
35 | dependencies: ["JSONPatch"]),
36 | ]
37 | )
38 | ```
--------------------------------------------------------------------------------
/Sources/JSONPatch/NSArray+DeepCopy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSArray+DeepCopy.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 13/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | extension NSArray {
24 |
25 | func deepMutableCopy() -> NSMutableArray {
26 | let result = NSMutableArray(capacity: count)
27 |
28 | for element in self {
29 | switch element {
30 | case let array as NSArray:
31 | result.add(array.deepMutableCopy())
32 | case let dict as NSDictionary:
33 | result.add(dict.deepMutableCopy())
34 | case let str as NSMutableString:
35 | result.add(str.mutableCopy())
36 | case let obj as NSObject:
37 | result.add(obj.copy())
38 | default:
39 | result.add(element)
40 | }
41 | }
42 |
43 | return result
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | .DS_Store
6 |
7 | ## Build generated
8 | build/
9 | DerivedData/
10 |
11 | ## Various settings
12 | *.pbxuser
13 | !default.pbxuser
14 | *.mode1v3
15 | !default.mode1v3
16 | *.mode2v3
17 | !default.mode2v3
18 | *.perspectivev3
19 | !default.perspectivev3
20 | xcuserdata/
21 |
22 | ## Other
23 | *.moved-aside
24 | *.xccheckout
25 | *.xcscmblueprint
26 |
27 | ## Obj-C/Swift specific
28 | *.hmap
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | .build/
44 |
45 | # CocoaPods
46 | #
47 | # We recommend against adding the Pods directory to your .gitignore. However
48 | # you should judge for yourself, the pros and cons are mentioned at:
49 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
50 | #
51 | # Pods/
52 |
53 | # Carthage
54 | #
55 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
56 | # Carthage/Checkouts
57 |
58 | Carthage/Build
59 |
60 | # fastlane
61 | #
62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
63 | # screenshots whenever they are needed.
64 | # For more information about the recommended setup visit:
65 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
66 |
67 | fastlane/report.xml
68 | fastlane/Preview.html
69 | fastlane/screenshots/**/*.png
70 | fastlane/test_output
71 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/JSONPointerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPointerTests.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 11/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import JSONPatch
23 |
24 | class JSONPointerTests: XCTestCase {
25 |
26 | func parent(_ string: String) -> String? {
27 | let pointer = try? JSONPointer(string: string)
28 | let parent = pointer?.parent
29 | return parent?.string
30 | }
31 |
32 | func testParent() {
33 | XCTAssertNil(parent(""))
34 | XCTAssertEqual(parent("/a"), "")
35 | XCTAssertEqual(parent("/a/b"), "/a")
36 | XCTAssertEqual(parent("/a/b/c"), "/a/b")
37 | XCTAssertEqual(parent("/"), "")
38 | XCTAssertEqual(parent("//"), "/")
39 | XCTAssertEqual(parent("///"), "//")
40 | }
41 |
42 | func testArrayIndexFormat() {
43 | XCTAssertTrue(JSONPointer.isValidArrayIndex("-"))
44 | XCTAssertFalse(JSONPointer.isValidArrayIndex("--"))
45 | XCTAssertTrue(JSONPointer.isValidArrayIndex("0"))
46 | XCTAssertTrue(JSONPointer.isValidArrayIndex("1"))
47 | XCTAssertTrue(JSONPointer.isValidArrayIndex("10"))
48 | XCTAssertFalse(JSONPointer.isValidArrayIndex("00"))
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/extra.json:
--------------------------------------------------------------------------------
1 | [
2 | { "comment": "Deep copy applied - Level 1",
3 | "doc": [{"foo": "bar"}],
4 | "patch": [{"op": "copy", "from": "/0", "path": "/1"}, {"op": "replace", "path": "/1/foo", "value": "baz"}],
5 | "expected": [{"foo": "bar"}, {"foo": "baz"}]
6 | },
7 |
8 | { "comment": "Deep copy applied - Level 2",
9 | "doc": [{"a": {"b": {"c": "d"}}}],
10 | "patch": [{"op": "copy", "from": "/0", "path": "/1"},
11 | {"op": "replace", "path": "/1/a/b/c", "value": "e"},
12 | {"op": "copy", "from": "/1", "path": "/2"},
13 | {"op": "replace", "path": "/2/a/b/c", "value": "f"},
14 | ],
15 | "expected": [{"a": {"b": {"c": "d"}}}, {"a": {"b": {"c": "e"}}}, {"a": {"b": {"c": "f"}}}]
16 | },
17 |
18 | { "comment": "Deep copy applied - Level 2",
19 | "doc": [{"a": {"b": {"c": null}}}],
20 | "patch": [{"op": "copy", "from": "/0", "path": "/1"},
21 | {"op": "replace", "path": "/1/a/b/c", "value": "e"},
22 | {"op": "copy", "from": "/1", "path": "/2"},
23 | {"op": "replace", "path": "/2/a/b/c", "value": "f"},
24 | ],
25 | "expected": [{"a": {"b": {"c": null}}}, {"a": {"b": {"c": "e"}}}, {"a": {"b": {"c": "f"}}}]
26 | },
27 |
28 | { "comment": "Avoid recursive copy",
29 | "doc": {"a": {"b": []}},
30 | "patch": [{"op": "copy", "from": "/a", "path": "/a/b/-"}],
31 | "expected": {"a": {"b": [{"b": []}]}}
32 | },
33 |
34 | { "comment": "Prevent recursive move",
35 | "doc": {"a": {"b": []}},
36 | "patch": [{"op": "move", "from": "/a", "path": "/a/b/-"}],
37 | "error": "Unable to move element inside itself"
38 | },
39 |
40 | { "comment": "Boolean not eqivalent to number",
41 | "doc": false,
42 | "patch": [{"op": "test", "path": "", "value": 0}],
43 | "error": "Test op failed"
44 | }
45 |
46 | ]
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JSONPatch - Swift 4/5 json-patch implementation
2 | [](https://opensource.org/licenses/Apache-2.0)
3 | [](http://developer.apple.com)
4 | [](https://swift.org/package-manager/)
5 |
6 | JSONPatch is a a swift module implements json-patch [RFC6902](https://tools.ietf.org/html/rfc6902). JSONPatch uses [JSONSerialization](https://developer.apple.com/documentation/foundation/jsonserialization) from Foundation, and has no dependencies on third-party libraries.
7 |
8 | The implementation uses the [JSON Patch Tests](https://github.com/json-patch/json-patch-tests) project for unit tests to validate its correctness.
9 |
10 | # Release
11 | 1.0.6 - Added support for Linux.
12 |
13 | # Installation
14 |
15 | ## CocoaPods
16 | See [CocoaPods.md](Docs/CocoaPods.md)
17 |
18 | ## Swift Package Manager
19 | See [SPM.md](Docs/SPM.md)
20 |
21 | ## Carthage
22 | See [Carthage.md](Docs/Carthage.md)
23 |
24 | # Usage
25 |
26 | A more detailed explanation of JSONPatch is given in [Usage.md](Docs/Usage.md).
27 |
28 | ## Applying Patches
29 | ```swift
30 | import JSONPatch
31 |
32 | let sourceData = Data("""
33 | {"foo": "bar"}
34 | """.utf8)
35 | let patchData = Data("""
36 | [{"op": "add", "path": "/baz", "value": "qux"}]
37 | """.utf8)
38 |
39 | let patch = try! JSONPatch(data: patchData)
40 | let patched = try! patch.apply(to: sourceData)
41 | ```
42 |
43 | ## Generating Patches
44 | ```swift
45 | import JSONPatch
46 |
47 | let sourceData = Data("""
48 | {"foo": "bar"}
49 | """.utf8)
50 | let targetData = Data("""
51 | {"foo": "bar", "baz": "qux"}
52 | """.utf8)
53 | let patch = try! JSONPatch(source: sourceData, target: targetData)
54 | let patchData = try! patch.data()
55 | ```
56 |
57 | # License
58 |
59 | Apache License v2.0
60 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/NSDictionary+DeepCopy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSDictionary+DeepCopy.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 13/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | extension NSDictionary {
24 |
25 | func deepMutableCopy() -> NSMutableDictionary {
26 | let result = NSMutableDictionary()
27 |
28 | self.enumerateKeysAndObjects { (key, value, stop) in
29 | #if os(Linux)
30 | switch value {
31 | case let array as NSArray:
32 | result.setObject(array.deepMutableCopy(), forKey: key as! NSString)
33 | case let dict as NSDictionary:
34 | result.setObject(dict.deepMutableCopy(), forKey: key as! NSString)
35 | case let str as NSMutableString:
36 | result.setObject(str, forKey: key as! NSString)
37 | case let obj as NSObject:
38 | result.setObject(obj.copy(), forKey: key as! NSString)
39 | default:
40 | result.setObject(value, forKey: key as! NSString)
41 | }
42 | #else
43 | switch value {
44 | case let array as NSArray:
45 | result[key] = array.deepMutableCopy()
46 | case let dict as NSDictionary:
47 | result[key] = dict.deepMutableCopy()
48 | case let str as NSMutableString:
49 | result[key] = str
50 | case let obj as NSObject:
51 | result[key] = obj.copy()
52 | default:
53 | result[key] = value
54 | }
55 | #endif
56 | }
57 |
58 | return result
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/JSONCodableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchTests.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Michiel Horvers on 01/24/2020.
6 | // Copyright © 2020 Michiel Horvers.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import JSONPatch
23 |
24 | fileprivate struct Person: Codable {
25 | var firstName: String
26 | var lastName: String
27 | var age: Int
28 | }
29 |
30 | class JSONCodableTests: XCTestCase {
31 |
32 | func testCreatePatch() throws {
33 | let source = Person(firstName: "Michiel", lastName: "Horvers", age: 99)
34 | let target = Person(firstName: "Michiel", lastName: "Horvers", age: 100)
35 |
36 | let patch = try JSONPatch.createPatch(from: source, to: target)
37 | XCTAssert(patch.operations.count == 1, "Patch should have only 1 operation, but has \(patch.operations.count)")
38 |
39 | guard patch.operations.count == 1 else { return }
40 | let dict = patch.operations[0].jsonObject
41 |
42 | XCTAssert((dict["op"] as? String) == "replace", "Operation should be 'replace', but is: \(String(describing: dict["op"]))")
43 | XCTAssert((dict["path"] as? String) == "/age", "Path should be 'age', but is: \(String(describing: dict["path"]))")
44 | XCTAssert((dict["value"] as? Int) == 100, "Value should be 100, but is: \(String(describing: dict["value"]))")
45 | }
46 |
47 | func testApplyPatch() throws {
48 | let person = Person(firstName: "Michiel", lastName: "Horvers", age: 99)
49 | let patchData = Data("""
50 | [
51 | { "op": "replace", "path": "/age", "value": 100 }
52 | ]
53 | """.utf8)
54 | let patch = try JSONDecoder().decode(JSONPatch.self, from: patchData)
55 |
56 | let patchedPerson = try patch.applied(to: person)
57 | XCTAssert(patchedPerson.age == 100, "Age should be patchd to 100, but is: \(patchedPerson.age)")
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/JSONPatchGeneratorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchGeneratorTests.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 02/12/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import JSONPatch
23 |
24 | class JSONPatchGeneratorTests: XCTestCase {
25 |
26 | static let sourceURL = Bundle.test.url(forResource: "bigexample1", withExtension: "json")!
27 | static let targetURL = Bundle.test.url(forResource: "bigexample2", withExtension: "json")!
28 |
29 | var sourceData: Data { return try! Data(contentsOf: JSONPatchGeneratorTests.sourceURL) }
30 | var targetData: Data { return try! Data(contentsOf: JSONPatchGeneratorTests.targetURL) }
31 |
32 | func testBigPatch() throws {
33 | var source = try JSONSerialization.jsonElement(with: sourceData, options: [.mutableContainers])
34 | let target = try JSONSerialization.jsonElement(with: targetData, options: [])
35 | let patch = try JSONPatch(source: source, target: target)
36 |
37 | try source.apply(patch: patch)
38 |
39 | XCTAssertFalse(patch.operations.isEmpty)
40 | XCTAssertEqual(source, target)
41 | }
42 |
43 | func testPerformanceGenerate() throws {
44 | let source = try JSONSerialization.jsonElement(with: sourceData, options: [.mutableContainers])
45 | let target = try JSONSerialization.jsonElement(with: targetData, options: [])
46 |
47 | self.measure {
48 | do {
49 | _ = try JSONPatch(source: source, target: target)
50 | } catch {
51 | XCTFail("Error: \(error)")
52 | }
53 | }
54 | }
55 |
56 | func testNoDifferences() throws {
57 | let source = try JSONSerialization.jsonElement(with: sourceData, options: [.mutableContainers])
58 | let patch = try JSONPatch(source: source, target: source)
59 | XCTAssertEqual(patch.operations.count, 0)
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/JSONError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONError.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 11/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | public enum JSONError: Error {
24 | case invalidObjectType
25 | case invalidPointerSyntax
26 | case invalidPatchFormat
27 | case referencesNonexistentValue
28 | case unknownPatchOperation
29 | case missingRequiredPatchField(op: String, index: Int, field: String)
30 | case patchTestFailed(path: String, expected: Any, found: Any?)
31 | }
32 |
33 | extension JSONError: Equatable {
34 | public static func ==(lhs: JSONError, rhs: JSONError) -> Bool {
35 | switch lhs {
36 | case .invalidObjectType:
37 | if case .invalidObjectType = rhs {
38 | return true
39 | }
40 |
41 | case .invalidPointerSyntax:
42 | if case .invalidPointerSyntax = rhs {
43 | return true
44 | }
45 |
46 | case .invalidPatchFormat:
47 | if case .invalidPatchFormat = rhs {
48 | return true
49 | }
50 |
51 | case .referencesNonexistentValue:
52 | if case .referencesNonexistentValue = rhs {
53 | return true
54 | }
55 |
56 | case .unknownPatchOperation:
57 | if case .unknownPatchOperation = rhs {
58 | return true
59 | }
60 |
61 | case .missingRequiredPatchField(let op1, let index1, let field1):
62 | if case .missingRequiredPatchField(let op2, let index2, let field2) = rhs,
63 | op1 == op2 && index1 == index2 && field1 == field2 {
64 | return true
65 | }
66 |
67 | case .patchTestFailed(let path1, _, _):
68 | if case .patchTestFailed(let path2, _, _) = rhs,
69 | path1 == path2 {
70 | return true
71 | }
72 | }
73 |
74 | return false
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/JSONElementTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONElementTests.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 17/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import JSONPatch
23 |
24 | class JSONElementTests: XCTestCase {
25 |
26 | func testNumericEquality() throws {
27 | let boolFalse = try JSONElement(any: NSNumber(value: false))
28 | let int0 = try JSONElement(any: NSNumber(value: 0))
29 | let double0 = try JSONElement(any: NSNumber(value: 0.0))
30 | let int42 = try JSONElement(any: NSNumber(value: 42))
31 | let double42 = try JSONElement(any: NSNumber(value: 42.0))
32 | let double42_5 = try JSONElement(any: NSNumber(value: 42.5))
33 |
34 | XCTAssertNotEqual(boolFalse, int0)
35 | XCTAssertNotEqual(boolFalse, double0)
36 |
37 | XCTAssertEqual(int0, double0)
38 | XCTAssertEqual(int42, double42)
39 |
40 | XCTAssertNotEqual(int42, double42_5)
41 | }
42 |
43 | func testDecode() throws {
44 | let json = Data("""
45 | {
46 | "string": "hello",
47 | "int": 42,
48 | "double": 4.2,
49 | "boolean": true,
50 | "array": [1, 2, 3],
51 | "object": {"a": "b"}
52 | }
53 | """.utf8)
54 |
55 | let decoder = JSONDecoder()
56 | let jsonDecoded = try decoder.decode(JSONElement.self, from: json)
57 | let jsonSerialization = try JSONSerialization.jsonElement(with: json, options: [])
58 |
59 | XCTAssertEqual(jsonDecoded, jsonSerialization)
60 | }
61 |
62 | func testCopy() throws {
63 | do {
64 | let int = try JSONElement(any: NSNumber(value: 0))
65 | let string = try JSONElement(any: "Test")
66 | let array = try JSONElement(any: [1, 2, 3])
67 | let dict = try JSONElement(any: ["Test": 1])
68 |
69 | let _ = try int.copy()
70 | let _ = try string.copy()
71 | let _ = try array.copy()
72 | let _ = try dict.copy()
73 | } catch {
74 | XCTFail("Should not throw an exception, but caught: \(error)")
75 | }
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/ArrayTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayTests.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 21/12/2018.
6 | // Copyright © 2018 Raymond McCrae. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import JSONPatch
11 |
12 | class ArrayTests: XCTestCase {
13 |
14 | func testLevel1DeepCopy() {
15 | let a = NSArray(array: ["a", "b", "c"])
16 | let b = a.deepMutableCopy()
17 | XCTAssertEqual(b.count, 3)
18 | XCTAssertEqual(b[0] as? String, "a")
19 | XCTAssertEqual(b[1] as? String, "b")
20 | XCTAssertEqual(b[2] as? String, "c")
21 | }
22 |
23 | func testLevel2DeepCopy() {
24 | let a = NSMutableArray(array: ["a", "b", "c"])
25 | let b = NSMutableArray(array: [a])
26 | let c = b.deepMutableCopy()
27 | b.add("d")
28 | a.add("e")
29 |
30 | XCTAssertEqual(c.count, 1)
31 | guard let d = c[0] as? NSMutableArray else {
32 | XCTFail()
33 | return
34 | }
35 | XCTAssertEqual(d.count, 3)
36 | XCTAssertEqual(d[0] as? String, "a")
37 | XCTAssertEqual(d[1] as? String, "b")
38 | XCTAssertEqual(d[2] as? String, "c")
39 | }
40 |
41 | func testLevel3DeepCopy() {
42 | let a = NSMutableArray(array: ["a", "b", "c"])
43 | let b = NSMutableArray(array: [a])
44 | let c = NSMutableArray(array: [b])
45 | let d = c.deepMutableCopy()
46 | b.add("d")
47 | a.add("e")
48 | c.add("f")
49 |
50 | XCTAssertEqual(d.count, 1)
51 | guard let e = d[0] as? NSMutableArray else {
52 | XCTFail()
53 | return
54 | }
55 | XCTAssertEqual(e.count, 1)
56 | guard let f = e[0] as? NSMutableArray else {
57 | XCTFail()
58 | return
59 | }
60 |
61 | XCTAssertEqual(f.count, 3)
62 | XCTAssertEqual(f[0] as? String, "a")
63 | XCTAssertEqual(f[1] as? String, "b")
64 | XCTAssertEqual(f[2] as? String, "c")
65 | }
66 |
67 | func testDictDeepCopy() {
68 | let dict = NSMutableDictionary(dictionary: ["a": "1"])
69 | let array = NSMutableArray(array: [dict])
70 | let copy = array.deepMutableCopy()
71 | array.add("b")
72 | dict["b"] = "2"
73 |
74 | XCTAssertEqual(copy.count, 1)
75 | guard let copyDict = copy[0] as? NSMutableDictionary else {
76 | XCTFail()
77 | return
78 | }
79 | XCTAssertEqual(copyDict.count, 1)
80 | XCTAssertEqual(copyDict["a"] as? String, "1")
81 | }
82 |
83 | func testStringDeepCopy() {
84 | let array = NSMutableArray(array: [NSMutableString(string: "1")])
85 | let copy = array.deepMutableCopy()
86 | (array[0] as! NSMutableString).setString("2")
87 |
88 | XCTAssertEqual(copy.count, 1)
89 | XCTAssertEqual(copy[0] as? String, "1")
90 | }
91 |
92 | }
93 |
--------------------------------------------------------------------------------
/JSONPatch.xcodeproj/xcshareddata/xcschemes/JSONPatchFramework.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/JSONFileTestCase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONFileTestCase.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 21/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import JSONPatch
23 |
24 | class JSONFileTestCase: XCTestCase {
25 |
26 | let index: Int
27 | let testJson: [String: Any]
28 |
29 | class var filename: String? {
30 | return nil
31 | }
32 |
33 | override class var defaultTestSuite: XCTestSuite {
34 | let suite = XCTestSuite(forTestCaseClass: self)
35 | guard let filename = self.filename else {
36 | return suite
37 | }
38 |
39 | do {
40 | let tests = try loadJSONTestFile(filename)
41 | for (index, test) in tests.enumerated() {
42 | if let disabled = test["disabled"] as? NSNumber, disabled.boolValue {
43 | continue
44 | }
45 | suite.addTest(JSONFileTestCase(test, index: index))
46 | }
47 | } catch {
48 | XCTFail()
49 | }
50 |
51 | return suite
52 | }
53 |
54 | override var testRunClass: AnyClass? {
55 | return XCTestCaseRun.self
56 | }
57 |
58 | override var name: String {
59 | return "test_\(index)"
60 | }
61 |
62 | init(_ testJson: [String: Any], index: Int) {
63 | self.testJson = testJson
64 | self.index = index
65 | super.init(invocation: nil)
66 | }
67 |
68 | override func perform(_ run: XCTestRun) {
69 | run.start()
70 | let comment = testJson["comment"] ?? name
71 | print("Started: \(comment)")
72 | performJsonTest()
73 | print("Finished: \(comment)")
74 | run.stop()
75 | }
76 |
77 | func performJsonTest() {
78 | guard let doc = testJson["doc"] else {
79 | XCTFail("doc not found")
80 | return
81 | }
82 |
83 | guard let patch = testJson["patch"] as? NSArray else {
84 | XCTFail("patch not found")
85 | return
86 | }
87 |
88 | let comment = testJson["comment"] ?? name
89 |
90 | do {
91 | let jsonPatch = try JSONPatch(jsonArray: patch)
92 | let result = try jsonPatch.apply(to: doc)
93 |
94 | if let expected = testJson["expected"] {
95 | guard (result as? NSObject)?.isEqual(expected) ?? false else {
96 | XCTFail("result does not match expected: \(comment)")
97 | return
98 | }
99 | } else {
100 | XCTFail("Error should occur: \(comment)")
101 | }
102 | } catch {
103 | guard let _ = testJson["error"] as? String else {
104 | XCTFail("Unexpected error: \(comment)")
105 | return
106 | }
107 | }
108 | }
109 |
110 | }
111 |
112 | extension JSONFileTestCase {
113 |
114 | private class func loadJSONTestFile(_ filename: String) throws -> [[String: Any]] {
115 | guard let url = Bundle.test.url(forResource: filename,
116 | withExtension: "json") else {
117 | return []
118 | }
119 | let data = try Data(contentsOf: url)
120 | let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
121 | guard let jsonArray = jsonObject as? [[String: Any]] else {
122 | return []
123 | }
124 | return jsonArray
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/JSONEquality.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONEquality.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 14/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | protocol JSONEquatable {
24 | func isJSONEquals(to element: JSONElement) -> Bool
25 | }
26 |
27 | extension NSNumber: JSONEquatable {
28 | var isBoolean: Bool {
29 | #if os(Linux)
30 | return objCType.pointee == 0x63 // character code for 'c'
31 | #else
32 | return CFNumberGetType(self) == .charType
33 | #endif
34 | }
35 |
36 | func isJSONEquals(to element: JSONElement) -> Bool {
37 | guard case let .number(num) = element else {
38 | return false
39 | }
40 |
41 | let selfBool = isBoolean
42 | let otherBool = num.isBoolean
43 | switch (selfBool, otherBool) {
44 | case (false, false), (true, true):
45 | return isEqual(to: num)
46 | default:
47 | return false
48 | }
49 | }
50 | }
51 |
52 | extension NSString: JSONEquatable {
53 | func isJSONEquals(to element: JSONElement) -> Bool {
54 | guard case let .string(str) = element else {
55 | return false
56 | }
57 |
58 | return isEqual(str)
59 | }
60 | }
61 |
62 | extension String: JSONEquatable {
63 | func isJSONEquals(to element: JSONElement) -> Bool {
64 | return (self as NSString).isJSONEquals(to: element)
65 | }
66 | }
67 |
68 | extension NSNull: JSONEquatable {
69 | func isJSONEquals(to element: JSONElement) -> Bool {
70 | guard case .null = element else {
71 | return false
72 | }
73 |
74 | return true
75 | }
76 | }
77 |
78 | extension NSArray: JSONEquatable {
79 | func isJSONEquals(to element: JSONElement) -> Bool {
80 | switch element {
81 | case let .array(arr) where arr.count == count,
82 | let .mutableArray(arr as NSArray) where arr.count == count:
83 | for index in 0.. Bool {
100 | return (self as NSArray).isJSONEquals(to: element)
101 | }
102 | }
103 |
104 | extension NSDictionary: JSONEquatable {
105 | func isJSONEquals(to element: JSONElement) -> Bool {
106 | switch element {
107 | case let .object(dict) where dict.count == count,
108 | let .mutableObject(dict as NSDictionary) where dict.count == count:
109 | let keys = self.allKeys
110 | for key in keys {
111 | guard
112 | let dictValue = dict[key],
113 | let dictElement = try? JSONElement(any: dictValue) else {
114 | return false
115 | }
116 | let selfValue = self[key] as! JSONEquatable
117 | if !selfValue.isJSONEquals(to: dictElement) {
118 | return false
119 | }
120 | }
121 | return true
122 | default:
123 | return false
124 | }
125 | }
126 | }
127 |
128 | extension Dictionary: JSONEquatable {
129 | func isJSONEquals(to element: JSONElement) -> Bool {
130 | return (self as NSDictionary).isJSONEquals(to: element)
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/JSONPatch.xcodeproj/xcshareddata/xcschemes/JSONPatch.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
38 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
86 |
92 |
93 |
99 |
100 |
101 |
102 |
104 |
105 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/spec_tests.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "comment": "4.1. add with missing object",
4 | "doc": { "q": { "bar": 2 } },
5 | "patch": [ {"op": "add", "path": "/a/b", "value": 1} ],
6 | "error":
7 | "path /a does not exist -- missing objects are not created recursively"
8 | },
9 |
10 | {
11 | "comment": "A.1. Adding an Object Member",
12 | "doc": {
13 | "foo": "bar"
14 | },
15 | "patch": [
16 | { "op": "add", "path": "/baz", "value": "qux" }
17 | ],
18 | "expected": {
19 | "baz": "qux",
20 | "foo": "bar"
21 | }
22 | },
23 |
24 | {
25 | "comment": "A.2. Adding an Array Element",
26 | "doc": {
27 | "foo": [ "bar", "baz" ]
28 | },
29 | "patch": [
30 | { "op": "add", "path": "/foo/1", "value": "qux" }
31 | ],
32 | "expected": {
33 | "foo": [ "bar", "qux", "baz" ]
34 | }
35 | },
36 |
37 | {
38 | "comment": "A.3. Removing an Object Member",
39 | "doc": {
40 | "baz": "qux",
41 | "foo": "bar"
42 | },
43 | "patch": [
44 | { "op": "remove", "path": "/baz" }
45 | ],
46 | "expected": {
47 | "foo": "bar"
48 | }
49 | },
50 |
51 | {
52 | "comment": "A.4. Removing an Array Element",
53 | "doc": {
54 | "foo": [ "bar", "qux", "baz" ]
55 | },
56 | "patch": [
57 | { "op": "remove", "path": "/foo/1" }
58 | ],
59 | "expected": {
60 | "foo": [ "bar", "baz" ]
61 | }
62 | },
63 |
64 | {
65 | "comment": "A.5. Replacing a Value",
66 | "doc": {
67 | "baz": "qux",
68 | "foo": "bar"
69 | },
70 | "patch": [
71 | { "op": "replace", "path": "/baz", "value": "boo" }
72 | ],
73 | "expected": {
74 | "baz": "boo",
75 | "foo": "bar"
76 | }
77 | },
78 |
79 | {
80 | "comment": "A.6. Moving a Value",
81 | "doc": {
82 | "foo": {
83 | "bar": "baz",
84 | "waldo": "fred"
85 | },
86 | "qux": {
87 | "corge": "grault"
88 | }
89 | },
90 | "patch": [
91 | { "op": "move", "from": "/foo/waldo", "path": "/qux/thud" }
92 | ],
93 | "expected": {
94 | "foo": {
95 | "bar": "baz"
96 | },
97 | "qux": {
98 | "corge": "grault",
99 | "thud": "fred"
100 | }
101 | }
102 | },
103 |
104 | {
105 | "comment": "A.7. Moving an Array Element",
106 | "doc": {
107 | "foo": [ "all", "grass", "cows", "eat" ]
108 | },
109 | "patch": [
110 | { "op": "move", "from": "/foo/1", "path": "/foo/3" }
111 | ],
112 | "expected": {
113 | "foo": [ "all", "cows", "eat", "grass" ]
114 | }
115 |
116 | },
117 |
118 | {
119 | "comment": "A.8. Testing a Value: Success",
120 | "doc": {
121 | "baz": "qux",
122 | "foo": [ "a", 2, "c" ]
123 | },
124 | "patch": [
125 | { "op": "test", "path": "/baz", "value": "qux" },
126 | { "op": "test", "path": "/foo/1", "value": 2 }
127 | ],
128 | "expected": {
129 | "baz": "qux",
130 | "foo": [ "a", 2, "c" ]
131 | }
132 | },
133 |
134 | {
135 | "comment": "A.9. Testing a Value: Error",
136 | "doc": {
137 | "baz": "qux"
138 | },
139 | "patch": [
140 | { "op": "test", "path": "/baz", "value": "bar" }
141 | ],
142 | "error": "string not equivalent"
143 | },
144 |
145 | {
146 | "comment": "A.10. Adding a nested Member Object",
147 | "doc": {
148 | "foo": "bar"
149 | },
150 | "patch": [
151 | { "op": "add", "path": "/child", "value": { "grandchild": { } } }
152 | ],
153 | "expected": {
154 | "foo": "bar",
155 | "child": {
156 | "grandchild": {
157 | }
158 | }
159 | }
160 | },
161 |
162 | {
163 | "comment": "A.11. Ignoring Unrecognized Elements",
164 | "doc": {
165 | "foo":"bar"
166 | },
167 | "patch": [
168 | { "op": "add", "path": "/baz", "value": "qux", "xyz": 123 }
169 | ],
170 | "expected": {
171 | "foo":"bar",
172 | "baz":"qux"
173 | }
174 | },
175 |
176 | {
177 | "comment": "A.12. Adding to a Non-existent Target",
178 | "doc": {
179 | "foo": "bar"
180 | },
181 | "patch": [
182 | { "op": "add", "path": "/baz/bat", "value": "qux" }
183 | ],
184 | "error": "add to a non-existent target"
185 | },
186 |
187 | {
188 | "comment": "A.13 Invalid JSON Patch Document",
189 | "doc": {
190 | "foo": "bar"
191 | },
192 | "patch": [
193 | { "op": "add", "path": "/baz", "value": "qux", "op": "remove" }
194 | ],
195 | "error": "operation has two 'op' members",
196 | "disabled": true
197 | },
198 |
199 | {
200 | "comment": "A.14. ~ Escape Ordering",
201 | "doc": {
202 | "/": 9,
203 | "~1": 10
204 | },
205 | "patch": [{"op": "test", "path": "/~01", "value": 10}],
206 | "expected": {
207 | "/": 9,
208 | "~1": 10
209 | }
210 | },
211 |
212 | {
213 | "comment": "A.15. Comparing Strings and Numbers",
214 | "doc": {
215 | "/": 9,
216 | "~1": 10
217 | },
218 | "patch": [{"op": "test", "path": "/~01", "value": "10"}],
219 | "error": "number is not equal to string"
220 | },
221 |
222 | {
223 | "comment": "A.16. Adding an Array Value",
224 | "doc": {
225 | "foo": ["bar"]
226 | },
227 | "patch": [{ "op": "add", "path": "/foo/-", "value": ["abc", "def"] }],
228 | "expected": {
229 | "foo": ["bar", ["abc", "def"]]
230 | }
231 | }
232 |
233 | ]
234 |
--------------------------------------------------------------------------------
/RMJSONPatch.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod spec lint RMJSONPatch.podspec' to ensure this is a
3 | # valid spec and to remove all comments including this before submitting the spec.
4 | #
5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html
6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
7 | #
8 |
9 | Pod::Spec.new do |s|
10 |
11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
12 | #
13 | # These will help people to find your library, and whilst it
14 | # can feel like a chore to fill in it's definitely to your advantage. The
15 | # summary should be tweet-length, and the description more in depth.
16 | #
17 |
18 | s.name = "RMJSONPatch"
19 | s.version = "1.0.6"
20 | s.summary = "JSONPatch is a swift library for applying and generating RFC-6902 compliant JSON patches."
21 | s.module_name = "JSONPatch"
22 |
23 | # This description is used to generate tags and improve search results.
24 | # * Think: What does it do? Why did you write it? What is the focus?
25 | # * Try to keep it short, snappy and to the point.
26 | # * Write the description between the DESC delimiters below.
27 | # * Finally, don't worry about the indent, CocoaPods strips it!
28 | s.description = <<-DESC
29 | JSONPatch is a swift library for applying and generating RFC-6902 compliant JSON patches.
30 | The library is built on top of JSONSerialization from Foundation and offers many convenience
31 | methods to make it easy to work with json-patches.
32 | DESC
33 |
34 | s.homepage = "https://github.com/raymccrae/swift-jsonpatch"
35 | # s.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"
36 |
37 |
38 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
39 | #
40 | # Licensing your code is important. See http://choosealicense.com for more info.
41 | # CocoaPods will detect a license file if there is a named LICENSE*
42 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
43 | #
44 |
45 | s.license = { :type => "Apache License, Version 2.0", :file => "LICENSE" }
46 |
47 |
48 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
49 | #
50 | # Specify the authors of the library, with email addresses. Email addresses
51 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
52 | # accepts just a name if you'd rather not provide an email address.
53 | #
54 | # Specify a social_media_url where others can refer to, for example a twitter
55 | # profile URL.
56 | #
57 |
58 | s.author = { "Raymond McCrae" => "raymccrae@yahoo.com" }
59 | # Or just: s.author = "Raymond McCrae"
60 | # s.authors = { "Raymond McCrae" => "raymccrae@yahoo.com" }
61 | s.social_media_url = "http://twitter.com/raymccrae"
62 |
63 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
64 | #
65 | # If this Pod runs only on iOS or OS X, then specify the platform and
66 | # the deployment target. You can optionally include the target after the platform.
67 | #
68 |
69 | # s.platform = :ios
70 | # s.platform = :ios, "5.0"
71 |
72 | # When using multiple platforms
73 | s.ios.deployment_target = "11.0"
74 | s.osx.deployment_target = "10.12"
75 | s.watchos.deployment_target = "3.0"
76 | s.tvos.deployment_target = "10.0"
77 |
78 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
79 | #
80 | # Specify the location from where the source should be retrieved.
81 | # Supports git, hg, bzr, svn and HTTP.
82 | #
83 |
84 | s.source = { :git => "https://github.com/raymccrae/swift-jsonpatch.git", :tag => "v#{s.version}" }
85 |
86 |
87 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
88 | #
89 | # CocoaPods is smart about how it includes source code. For source files
90 | # giving a folder will include any swift, h, m, mm, c & cpp files.
91 | # For header files it will include any header in the folder.
92 | # Not including the public_header_files will make all headers public.
93 | #
94 |
95 | s.source_files = "JSONPatch", "Sources/**/*.{h,swift}"
96 | # s.exclude_files = "Classes/Exclude"
97 |
98 | # s.public_header_files = "Classes/**/*.h"
99 |
100 |
101 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
102 | #
103 | # A list of resources included with the Pod. These are copied into the
104 | # target bundle with a build phase script. Anything else will be cleaned.
105 | # You can preserve files from being cleaned, please don't preserve
106 | # non-essential files like tests, examples and documentation.
107 | #
108 |
109 | # s.resource = "icon.png"
110 | # s.resources = "Resources/*.png"
111 |
112 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave"
113 |
114 |
115 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
116 | #
117 | # Link your library with frameworks, or libraries. Libraries do not include
118 | # the lib prefix of their name.
119 | #
120 |
121 | # s.framework = "SomeFramework"
122 | # s.frameworks = "SomeFramework", "AnotherFramework"
123 |
124 | # s.library = "iconv"
125 | # s.libraries = "iconv", "xml2"
126 |
127 |
128 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
129 | #
130 | # If your library depends on compiler flags you can set them in the xcconfig hash
131 | # where they will only apply to your library. If you depend on other Podspecs
132 | # you can include multiple dependencies to ensure it works.
133 |
134 | # s.requires_arc = true
135 | s.swift_version = ['5']
136 |
137 | # s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
138 | # s.dependency "JSONKit", "~> 1.4"
139 |
140 | end
141 |
--------------------------------------------------------------------------------
/Docs/Usage.md:
--------------------------------------------------------------------------------
1 | # JSONPatch Usage
2 |
3 | ## Key Type
4 |
5 | | Type | Description |
6 | | ----------- | ------------------------------------------------------------ |
7 | | JSONPatch | A class representing a RFC6902 json-patch. |
8 | | JSONPointer | A struct representing a RFC6901 json-pointer. |
9 | | JSONElement | A struct wrapper that holds a reference to an element of a json document compatible with JSONSerialization. |
10 | | JSONError | A enum representing all the errors that may be thrown by the methods within the JSONPatch library. |
11 |
12 | ## Creating JSONPatch Instance
13 |
14 | JSONPatch library is designed to work flexibly to work with a number of scenarios. The JSONPatch class represents a [RFC6902](https://tools.ietf.org/html/rfc6902) json-patch instance. This section demonstrates a number of ways a JSONPatch instance can be instantiated. Note that JSONPatch instances are immutable and can not be modified after creation.
15 |
16 | ### Decoding a json-patch from Data
17 |
18 | If you have a raw Data representation of the json-patch, then the below example shows the initialization. The data must represent a json document with a top-level json array as defined within the RFC6902 specification.
19 |
20 | ```swift
21 | let data: Data = ... // wherever your app gets data from.
22 | let patch = JSONPatch(data: data)
23 | ```
24 |
25 | The data must be one of the supported encoding of [JSONSerialization](https://developer.apple.com/documentation/foundation/jsonserialization):-
26 |
27 | > The data must be in one of the 5 supported encodings listed in the JSON specification: UTF-8, UTF-16LE, UTF-16BE, UTF-32LE, UTF-32BE. The data may or may not have a BOM. The most efficient encoding to use for parsing is UTF-8, so if you have a choice in encoding the data passed to this method, use UTF-8.
28 |
29 | ### Decoding a json-patch from a sub-element of a json document (JSONSerialization)
30 |
31 | The previous scenario works if your json-patch is availble in isolation, if the data represents only the json-patch. However, if the json-patch is a sub-element of a larger json document and your app is using JSONSerialization to parse that json document; then JSONPatch can be initialized from a NSArray. You will need to extract the subelement of the parsed json object to get the array representing the json-patch.
32 |
33 | ```swift
34 | do {
35 | let jsonobj = try JSONSerialization.jsonObject(with: data)
36 | guard let jsondoc = jsonobj as? NSDictionary else { throw ParseError }
37 | guard let subelement = jsondoc["patch"] as? NSArray { throw ParseError }
38 |
39 | let patch = JSONPatch(jsonArray: subelement)
40 | } catch {
41 | // handle error
42 | }
43 | ```
44 |
45 | ### Decoding a json-patch from a sub-element of a json document (JSONDecoder)
46 |
47 | Alternatively if your app is using [Codable](https://developer.apple.com/documentation/swift/codable) then you can include the JSONPatch class in your type. JSONPatch is fully Codable.
48 |
49 | ```swift
50 | struct Document: Codable {
51 | let patch: JSONPatch
52 | }
53 |
54 | do {
55 | let decoder = JSONDecoder()
56 | let doc = try decoder.decode(Document.self, from: data)
57 |
58 | let patch = doc.patch
59 | } catch {
60 | // handle error
61 | }
62 | ```
63 |
64 | ### Generate a json-patch from the differences between two json documents
65 |
66 | A json-patch can be computed from the differences between two json documents. The created json-patch will consist of all the operations required to transform the source json document into the target json document.
67 |
68 | ```swift
69 | let sourceData = ... // a data representation of the before json document
70 | let targetData = ... // a data representation of the after json document
71 |
72 | let patch = try! JSONPatch(source: sourceData, target: targetData)
73 | ```
74 |
75 | Alternatively if you would rather work with parsed json elements from JSONSerialization. Then wrap these elements in a JSONElement struct and initialise the JSONPatch with them. This approach can also be used when computing the patch based on sub-elements of the json document.
76 |
77 | ```swift
78 | let source = ... // a JSONSerialization compatable json object - Before
79 | let target = ... // a JSONSerialization compatable json object - After
80 |
81 | let sourceElement = try! JSONElement(any: source)
82 | let targetElement = try! JSONElement(any: target)
83 |
84 | let patch = try! JSONPatch(source: sourceElement, target: targetElement)
85 | ```
86 |
87 | ## Applying a JSONPatch
88 |
89 | A JSONPatch instance can be applied to a json document to result in a new transformed json document.
90 |
91 | ### Apply patch to a json document
92 |
93 | JSONPatch can be applied to Data representations of a json document.
94 |
95 | ```swift
96 | let sourceData = ... // a data representation of the before json document
97 | let patch = ... // a json patch
98 |
99 | let patchedData = try! patch.apply(to: sourceData)
100 | ```
101 |
102 | Alternatively if you would rather work with parsed json elements from JSONSerialization. This approach has options to apply the patch inplace, which results in the apply process modifying (where possible) the original json document with the updates in the patch, avoiding making a copy of the original document.
103 |
104 | ```swift
105 | var jsonObject = try! JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
106 | let patch = ... // a json patch
107 |
108 | jsonObject = try! patch.apply(to: jsonObject, inplace: true)
109 | ```
110 |
111 | ### Apply patch to a sub-element of a json document
112 |
113 | A JSONPatch can be applied relative to a sub-element of a json document. This can be achieved by specifying a json-pointer to the sub-element to apply the patch. When specified the sub-element will be treated as the root element for the purposes of applying the patch.
114 |
115 | ```swift
116 | let sourceData = ... // a data representation of the before json document
117 | let patch = ... // a json patch
118 | let pointer = try! JSONPointer(string: "/subelement")
119 |
120 | let patchedData = try! patch.apply(to: sourceData, relativeTo: pointer)
121 | ```
122 |
123 | ## Serializing a JSONPatch
124 |
125 | This section demonstrates a number of ways a JSONPatch instance can be serialized to Data.
126 |
127 | ### Convert JSONPatch to Data
128 |
129 | A JSONPatch instance can supply a serialized Data representation by calling the data method. Resulting in a UTF-8 data repesentation of the json-patch.
130 |
131 | ```swift
132 | let data = try! patch.data()
133 | ```
134 |
135 | ### Inserting a JSONPatch as a sub-element of a json document (JSONSerialization)
136 |
137 | If the json-patch is a sub-element of a larger json document, then a JSONSerialization complient representation can be computed via the jsonArray property. This will create a json array compatible for JSONSerialization.
138 |
139 | ```swift
140 | var dict: [String: Any] = [:]
141 | dict["patch"] = patch.jsonArray
142 |
143 | let data = try! JSONSerialization.data(withJSONObject: dict, options: [])
144 | ```
145 |
146 | ### Inserting a JSONPatch as a sub-element of a json document (JSONEncoder)
147 |
148 | Alternatively if your app is using [Codable](https://developer.apple.com/documentation/swift/codable) then you can include the JSONPatch class in your type. JSONPatch is fully Codable.
149 |
150 | ```swift
151 | struct Document: Codable {
152 | let patch: JSONPatch
153 | }
154 |
155 | do {
156 | var encoder = JSONEncoder()
157 | let data = try encoder.encode(doc)
158 | } catch {
159 | // handle error
160 | }
161 | ```
162 |
163 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/JSONPointer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPointer.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 11/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | /// A JSON Pointer implementation, based on RFC 6901.
24 | /// https://tools.ietf.org/html/rfc6901
25 | public struct JSONPointer {
26 |
27 | /// An array of the unescaped components of the json-pointer.
28 | private let components: ArraySlice
29 |
30 | /// An internal initalizer for the JSONPointer to force public access to use init(string:).
31 | private init(components: ArraySlice) {
32 | self.components = components
33 | }
34 |
35 | }
36 |
37 | extension JSONPointer {
38 |
39 | /// A json-pointer that represents the whole json document.
40 | static let wholeDocument: JSONPointer = JSONPointer(components: [])
41 |
42 | /// A string representation of the json-pointer.
43 | public var string: String {
44 | guard !components.isEmpty else {
45 | return ""
46 | }
47 | return "/" + components.map(JSONPointer.escape).joined(separator: "/")
48 | }
49 |
50 | /// A JSON Pointer to the container of the element of the reciever, or nil if the reciever
51 | /// references the root element of the whole JSON document.
52 | public var parent: JSONPointer? {
53 | guard !components.isEmpty else {
54 | return nil
55 | }
56 | return JSONPointer(components: components.dropLast())
57 | }
58 |
59 | /// The path component of the receiver.
60 | public var lastComponent: String? {
61 | return components.last
62 | }
63 |
64 | /// Determines if receiver represents the whole document.
65 | public var isWholeDocument: Bool {
66 | return components.isEmpty
67 | }
68 |
69 | /// Returns a URI Fragment Identifier representation. See Section 6 of RFC 6901.
70 | public var fragment: String? {
71 | guard let s = string.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed) else {
72 | return nil
73 | }
74 | return "#\(s)"
75 | }
76 |
77 | /// Initializer for JSONPointer
78 | public init(string: String) throws {
79 | guard !string.isEmpty else {
80 | self.init(components: [])
81 | return
82 | }
83 | guard !string.hasPrefix("#") else {
84 | let index = string.index(after: string.startIndex)
85 | guard let unescaped = string[index...].removingPercentEncoding else {
86 | throw JSONError.invalidPointerSyntax
87 | }
88 | try self.init(string: unescaped)
89 | return
90 | }
91 | guard string.hasPrefix("/") else {
92 | throw JSONError.invalidPointerSyntax
93 | }
94 |
95 | let escapedComponents = string.components(separatedBy: "/").dropFirst()
96 | let unescapedComponents = escapedComponents.map(JSONPointer.unescape)
97 | self.init(components: ArraySlice(unescapedComponents))
98 | }
99 |
100 | /// Unescapes the escape sequence within the string.
101 | ///
102 | /// - Parameters:
103 | /// - escaped: The escaped string.
104 | /// - Returns: The unescaped string.
105 | public static func unescape(_ escaped: String) -> String {
106 | var value = escaped
107 | value = value.replacingOccurrences(of: "~1", with: "/")
108 | value = value.replacingOccurrences(of: "~0", with: "~")
109 | return value
110 | }
111 |
112 | /// Escapes the characters required for the RFC 6901 standard.
113 | ///
114 | /// - Parameters:
115 | /// - unescaped: The unescaped string.
116 | /// - Returns: The escaped string.
117 | public static func escape(_ unescaped: String) -> String {
118 | var value = unescaped
119 | value = value.replacingOccurrences(of: "~", with: "~0")
120 | value = value.replacingOccurrences(of: "/", with: "~1")
121 | return value
122 | }
123 |
124 | /// Creates a new json-pointer based on the reciever with the given component appended.
125 | ///
126 | /// - Parameters:
127 | /// - component: A non-escaped path component.
128 | /// - Returns: A json-pointer with the given component appended.
129 | func appended(withComponent component: String) -> JSONPointer {
130 | return JSONPointer(components: ArraySlice(components + [component]))
131 | }
132 |
133 | /// Creates a new json-pointer based on the reciever with the given index appended.
134 | ///
135 | /// - Parameters:
136 | /// - index: A path index.
137 | /// - Returns: A json-pointer with the given component appended.
138 | func appended(withIndex index: Int) -> JSONPointer {
139 | return JSONPointer(components: ArraySlice(components + [String(index)]))
140 | }
141 |
142 | }
143 |
144 | extension JSONPointer {
145 | private static let arrayIndexPattern: NSRegularExpression = {
146 | return try! NSRegularExpression(pattern: "^(?:-|0|(?:[1-9][0-9]*))$", options: [])
147 | }()
148 |
149 | /// Determines if the given path component represents a valid array index.
150 | ///
151 | /// - Parameters:
152 | /// - component: A path component.
153 | /// - Returns: true if the given path component is a valid array index, otherwise false.
154 | static func isValidArrayIndex(_ component: String) -> Bool {
155 | let match = arrayIndexPattern.firstMatch(in: component,
156 | options: [.anchored],
157 | range: NSRange(location: 0, length: component.utf16.count))
158 | return match != nil
159 | }
160 | }
161 |
162 | extension JSONPointer: CustomDebugStringConvertible {
163 | public var debugDescription: String {
164 | return "JSONPointer(string: \"\(string)\")"
165 | }
166 | }
167 |
168 | extension JSONPointer: Equatable {
169 | /// Returns a Boolean value indicating whether two json-pointer are equal.
170 | ///
171 | /// - Parameters:
172 | /// - lhs: Left-hand side of the equality test.
173 | /// - rhs: Right-hand side of the equality test.
174 | /// - Returns: true is the lhs is equal to the rhs.
175 | public static func == (lhs: JSONPointer, rhs: JSONPointer) -> Bool {
176 | return lhs.components == rhs.components
177 | }
178 | }
179 |
180 | extension JSONPointer: Hashable {
181 | #if swift(>=5.0)
182 | public func hash(into hasher: inout Hasher) {
183 | hasher.combine(components)
184 | }
185 | #else
186 | public var hashValue: Int {
187 | return components.hashValue
188 | }
189 | #endif
190 | }
191 |
192 | extension JSONPointer: Collection {
193 | public var startIndex: Int {
194 | return components.startIndex
195 | }
196 |
197 | public var endIndex: Int {
198 | return components.endIndex
199 | }
200 |
201 | public func index(after i: Int) -> Int {
202 | return i + 1
203 | }
204 |
205 | public subscript(index: Int) -> String {
206 | return components[index]
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/JSONPatchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchTests.swift
3 | // JSONPatchTests
4 | //
5 | // Created by Raymond Mccrae on 11/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import XCTest
22 | @testable import JSONPatch
23 |
24 | class JSONPatchTests: XCTestCase {
25 |
26 | func evaluate(path: String, on json: JSONElement) -> JSONElement? {
27 | guard let ptr = try? JSONPointer(string: path) else {
28 | return nil
29 | }
30 | return try? json.evaluate(pointer: ptr)
31 | }
32 |
33 | // This test is based on the sample given in section 5 of RFC 6901
34 | // https://tools.ietf.org/html/rfc6901
35 | func testExample() throws {
36 | let sample = """
37 | {
38 | "foo": ["bar", "baz"],
39 | "": 0,
40 | "a/b": 1,
41 | "c%d": 2,
42 | "e^f": 3,
43 | "g|h": 4,
44 | "i\\\\j": 5,
45 | "k\\"l": 6,
46 | " ": 7,
47 | "m~n": 8
48 | }
49 | """
50 |
51 | let jsonObject = try JSONSerialization.jsonObject(with: Data(sample.utf8), options: [])
52 | let json = try JSONElement(any: jsonObject)
53 |
54 | XCTAssertEqual(evaluate(path: "", on: json), json)
55 | XCTAssertEqual(evaluate(path: "/foo", on: json), .array(value: ["bar", "baz"]))
56 | XCTAssertEqual(evaluate(path: "/foo/0", on: json), .string(value: "bar"))
57 | XCTAssertEqual(evaluate(path: "/", on: json), .number(value: NSNumber(value: 0)))
58 | XCTAssertEqual(evaluate(path: "/a~1b", on: json), .number(value: NSNumber(value: 1)))
59 | XCTAssertEqual(evaluate(path: "/c%d", on: json), .number(value: NSNumber(value: 2)))
60 | XCTAssertEqual(evaluate(path: "/e^f", on: json), .number(value: NSNumber(value: 3)))
61 | XCTAssertEqual(evaluate(path: "/g|h", on: json), .number(value: NSNumber(value: 4)))
62 | XCTAssertEqual(evaluate(path: "/i\\j", on: json), .number(value: NSNumber(value: 5)))
63 | XCTAssertEqual(evaluate(path: "/k\"l", on: json), .number(value: NSNumber(value: 6)))
64 | XCTAssertEqual(evaluate(path: "/ ", on: json), .number(value: NSNumber(value: 7)))
65 | XCTAssertEqual(evaluate(path: "/m~0n", on: json), .number(value: NSNumber(value: 8)))
66 |
67 | XCTAssertEqual(evaluate(path: "#", on: json), json)
68 | XCTAssertEqual(evaluate(path: "#/foo", on: json), .array(value: ["bar", "baz"]))
69 | XCTAssertEqual(evaluate(path: "#/foo/0", on: json), .string(value: "bar"))
70 | XCTAssertEqual(evaluate(path: "#/", on: json), .number(value: NSNumber(value: 0)))
71 | XCTAssertEqual(evaluate(path: "#/a~1b", on: json), .number(value: NSNumber(value: 1)))
72 | XCTAssertEqual(evaluate(path: "#/c%25d", on: json), .number(value: NSNumber(value: 2)))
73 | XCTAssertEqual(evaluate(path: "#/e%5Ef", on: json), .number(value: NSNumber(value: 3)))
74 | XCTAssertEqual(evaluate(path: "#/g%7Ch", on: json), .number(value: NSNumber(value: 4)))
75 | XCTAssertEqual(evaluate(path: "#/i%5Cj", on: json), .number(value: NSNumber(value: 5)))
76 | XCTAssertEqual(evaluate(path: "#/k%22l", on: json), .number(value: NSNumber(value: 6)))
77 | XCTAssertEqual(evaluate(path: "#/%20", on: json), .number(value: NSNumber(value: 7)))
78 | XCTAssertEqual(evaluate(path: "#/m~0n", on: json), .number(value: NSNumber(value: 8)))
79 | }
80 |
81 | func testOperationEquality() throws {
82 | let ptr = try JSONPointer(string: "")
83 | let oppa = JSONPatch.Operation.add(path: ptr, value: JSONElement(false))
84 | let oppb = JSONPatch.Operation.add(path: ptr, value: JSONElement(0))
85 | XCTAssertNotEqual(oppa, oppb)
86 | }
87 |
88 | func testTopLevelFragments() throws {
89 | let ptr = try JSONPointer(string: "")
90 | let doc = Data("3".utf8)
91 | let op = JSONPatch.Operation.replace(path: ptr, value: JSONElement(false))
92 | let patch = JSONPatch(operations: [op])
93 | let result = try patch.apply(to: doc,
94 | readingOptions: [.allowFragments],
95 | writingOptions: [])
96 | XCTAssertEqual(String(data: result, encoding: .utf8), "false")
97 | }
98 |
99 | func testLargeJson() throws {
100 | let sourceURL = Bundle.test.url(forResource: "bigexample1", withExtension: "json")!
101 | let targetURL = Bundle.test.url(forResource: "bigexample2", withExtension: "json")!
102 | let patchURL = Bundle.test.url(forResource: "bigpatch", withExtension: "json")!
103 |
104 | let sourceData = try Data(contentsOf: sourceURL)
105 | let targetData = try Data(contentsOf: targetURL)
106 | let patchData = try Data(contentsOf: patchURL)
107 |
108 | var sourceElem = try JSONSerialization.jsonElement(with: sourceData, options: [.mutableContainers])
109 | let targetElem = try JSONSerialization.jsonElement(with: targetData, options: [.mutableContainers])
110 |
111 | let patch = try JSONPatch(data: patchData)
112 | try sourceElem.apply(patch: patch)
113 | XCTAssertEqual(sourceElem, targetElem)
114 | }
115 |
116 | func testLargeJSONPerformance() throws {
117 | let sourceURL = Bundle.test.url(forResource: "bigexample1", withExtension: "json")!
118 | let patchURL = Bundle.test.url(forResource: "bigpatch", withExtension: "json")!
119 |
120 | let sourceData = try Data(contentsOf: sourceURL)
121 | let patchData = try Data(contentsOf: patchURL)
122 |
123 | measure {
124 | let patch = try? JSONPatch(data: patchData)
125 | _ = try? patch?.apply(to: sourceData)
126 | }
127 | }
128 |
129 | func testPatchRelative() throws {
130 | let source = """
131 | {"a": {}}
132 | """
133 | let patch = Data("""
134 | [{ "op": "add", "path": "/b", "value": "qux" }]
135 | """.utf8)
136 |
137 | let p = try JSONPatch(data: patch)
138 | let s = try JSONSerialization.jsonObject(with: Data(source.utf8), options: [])
139 | let applied = try p.apply(to: s, options: [.relative(to: try JSONPointer(string: "/a"))])
140 | XCTAssertEqual(applied as? NSDictionary, ["a":["b":"qux"]] as NSDictionary)
141 | }
142 |
143 | func testNonexistentValue() throws {
144 | let objectData = Data("""
145 | {
146 | "prop1": "Value1",
147 | "prop2": "Value2"
148 | }
149 | """.utf8)
150 |
151 | let patchData = Data("""
152 | [
153 | { "op": "replace", "path": "/prop3", "value": "Value3" }
154 | ]
155 | """.utf8)
156 |
157 | let patch = try JSONDecoder().decode(JSONPatch.self, from: patchData)
158 |
159 | do {
160 | let _ = try patch.apply(to: objectData)
161 | XCTFail("Should have thrown a nonExistentValue error")
162 | } catch {
163 | if let error = error as? JSONError, error == .referencesNonexistentValue {
164 | // Succeeded
165 | } else {
166 | XCTFail("Should have thrown JSONError.referencesNonexistentValue, but throwed: \(error)")
167 | }
168 | }
169 | }
170 |
171 | func testIgnoreNonexistentValue() throws {
172 | let objectData = Data("""
173 | {
174 | "prop1": "Value1",
175 | "prop2": "Value2"
176 | }
177 | """.utf8)
178 |
179 | let patchData = Data("""
180 | [
181 | { "op": "replace", "path": "/prop3", "value": "Value3" }
182 | ]
183 | """.utf8)
184 |
185 | let patch = try JSONDecoder().decode(JSONPatch.self, from: patchData)
186 |
187 | do {
188 | let _ = try patch.apply(to: objectData, applyingOptions: [.ignoreNonexistentValues])
189 | // Succeeded
190 | } catch {
191 | XCTFail("Should not have thrown JSONError.referencesNonexistentValue, throwed: \(error)")
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/JSONPatchCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchCodable.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 01/12/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | extension JSONPointer: Codable {
24 | public init(from decoder: Decoder) throws {
25 | let container = try decoder.singleValueContainer()
26 | try self.init(string: try container.decode(String.self))
27 | }
28 |
29 | public func encode(to encoder: Encoder) throws {
30 | var container = encoder.singleValueContainer()
31 | try container.encode(string)
32 | }
33 | }
34 |
35 | extension JSONElement: Codable {
36 | public init(from decoder: Decoder) throws {
37 | let container = try decoder.singleValueContainer()
38 |
39 | if let value = try? container.decode(String.self) {
40 | self = .string(value: value as NSString)
41 | } else if let value = try? container.decode(Bool.self) {
42 | self = .number(value: value as NSNumber)
43 | } else if let value = try? container.decode(Int.self) {
44 | self = .number(value: value as NSNumber)
45 | } else if let value = try? container.decode(Double.self) {
46 | self = .number(value: value as NSNumber)
47 | } else if container.decodeNil() {
48 | self = .null
49 | } else if let value = try? container.decode([JSONElement].self) {
50 | let array = value.map { $0.rawValue }
51 | self = .array(value: array as NSArray)
52 | } else if let keyedContainer = try? decoder.container(keyedBy: NSDictionaryCodingKey.self) {
53 | let keys = keyedContainer.allKeys
54 | let dict = NSMutableDictionary()
55 | for key in keys {
56 | let value = try keyedContainer.decode(JSONElement.self, forKey: key)
57 | dict[key.stringValue] = value.rawValue
58 | }
59 | self = .mutableObject(value: dict)
60 | } else {
61 | self = .null
62 | }
63 | }
64 |
65 | public func encode(to encoder: Encoder) throws {
66 | var container = encoder.singleValueContainer()
67 | switch self {
68 | case .string(let value):
69 | try container.encode(value as String)
70 | case .number(let value):
71 | try container.encodeNSNumber(value)
72 | case .null:
73 | try container.encodeNil()
74 | case .array(let array), .mutableArray(let array as NSArray):
75 | let elements = try array.map { try JSONElement(any: $0) }
76 | try container.encode(elements)
77 | case .object(let dict), .mutableObject(let dict as NSDictionary):
78 | var keyContainer = encoder.container(keyedBy: NSDictionaryCodingKey.self)
79 | try keyContainer.encodeNSDictionary(dict)
80 | }
81 | }
82 | }
83 |
84 | extension JSONPatch.Operation: Codable {
85 | enum CodingKeys: String, CodingKey {
86 | case op
87 | case path
88 | case value
89 | case from
90 | }
91 |
92 | public init(from decoder: Decoder) throws {
93 | let container = try decoder.container(keyedBy: CodingKeys.self)
94 | let op = try container.decode(String.self, forKey: .op)
95 | switch op {
96 | case "add":
97 | let path = try container.decode(JSONPointer.self, forKey: .path)
98 | let value = try container.decode(JSONElement.self, forKey: .value)
99 | self = .add(path: path, value: value)
100 | case "remove":
101 | let path = try container.decode(JSONPointer.self, forKey: .path)
102 | self = .remove(path: path)
103 | case "replace":
104 | let path = try container.decode(JSONPointer.self, forKey: .path)
105 | let value = try container.decode(JSONElement.self, forKey: .value)
106 | self = .replace(path: path, value: value)
107 | case "move":
108 | let from = try container.decode(JSONPointer.self, forKey: .from)
109 | let path = try container.decode(JSONPointer.self, forKey: .path)
110 | self = .move(from: from, path: path)
111 | case "copy":
112 | let from = try container.decode(JSONPointer.self, forKey: .from)
113 | let path = try container.decode(JSONPointer.self, forKey: .path)
114 | self = .copy(from: from, path: path)
115 | case "test":
116 | let path = try container.decode(JSONPointer.self, forKey: .path)
117 | let value = try container.decode(JSONElement.self, forKey: .value)
118 | self = .test(path: path, value: value)
119 | default:
120 | throw JSONError.unknownPatchOperation
121 | }
122 | }
123 |
124 | public func encode(to encoder: Encoder) throws {
125 | var container = encoder.container(keyedBy: CodingKeys.self)
126 | switch self {
127 | case let .add(path, value):
128 | try container.encode("add", forKey: .op)
129 | try container.encode(path, forKey: .path)
130 | try container.encode(value, forKey: .value)
131 | case let .remove(path):
132 | try container.encode("remove", forKey: .op)
133 | try container.encode(path, forKey: .path)
134 | case let .replace(path, value):
135 | try container.encode("replace", forKey: .op)
136 | try container.encode(path, forKey: .path)
137 | try container.encode(value, forKey: .value)
138 | case let .move(from, path):
139 | try container.encode("move", forKey: .op)
140 | try container.encode(path, forKey: .path)
141 | try container.encode(from, forKey: .from)
142 | case let .copy(from, path):
143 | try container.encode("copy", forKey: .op)
144 | try container.encode(path, forKey: .path)
145 | try container.encode(from, forKey: .from)
146 | case let .test(path, value):
147 | try container.encode("test", forKey: .op)
148 | try container.encode(path, forKey: .path)
149 | try container.encode(value, forKey: .value)
150 | }
151 | }
152 | }
153 |
154 | extension SingleValueEncodingContainer {
155 |
156 | fileprivate mutating func encodeNSNumber(_ value: NSNumber) throws {
157 | #if os(Linux)
158 | switch value.objCType.pointee {
159 | case 0x63:
160 | try encode(value.boolValue)
161 | case 0x64, 0x71, 0x51, 0x4C:
162 | try encode(value.doubleValue)
163 | case 0x66, 0x49:
164 | try encode(value.floatValue)
165 | default:
166 | try encode(value.int64Value)
167 | }
168 | #else
169 | switch CFNumberGetType(value) {
170 | case .charType:
171 | try encode(value.boolValue)
172 | case .cgFloatType, .doubleType, .float64Type:
173 | try encode(value.doubleValue)
174 | case .floatType, .float32Type:
175 | try encode(value.floatValue)
176 | default:
177 | try encode(value.int64Value)
178 | }
179 | #endif
180 | }
181 | }
182 |
183 | extension KeyedEncodingContainer where Key == NSDictionaryCodingKey {
184 |
185 | fileprivate mutating func encodeNSDictionary(_ value: NSDictionary) throws {
186 | var encodingError: Error? = nil
187 | value.enumerateKeysAndObjects { (key, value, stop) in
188 | do {
189 | guard
190 | let keyString = key as? String,
191 | let codingKey = NSDictionaryCodingKey(stringValue: keyString)
192 | else {
193 | assertionFailure()
194 | return
195 | }
196 | let element = try JSONElement(any: value)
197 | try encode(element, forKey: codingKey)
198 | } catch {
199 | encodingError = error
200 | stop.pointee = true
201 | }
202 | }
203 | if let error = encodingError {
204 | throw error
205 | }
206 | }
207 | }
208 |
209 | fileprivate struct NSDictionaryCodingKey: CodingKey {
210 | var stringValue: String
211 | var intValue: Int?
212 |
213 | init?(stringValue: String) {
214 | self.stringValue = stringValue
215 | }
216 |
217 | init?(intValue: Int) {
218 | self.stringValue = ""
219 | self.intValue = intValue
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/JSONPatchGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatchGenerator.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 30/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | struct JSONPatchGenerator {
24 |
25 | fileprivate enum Operation {
26 | case add(path: JSONPointer, value: JSONElement)
27 | case remove(path: JSONPointer, value: JSONElement)
28 | case replace(path: JSONPointer, old: JSONElement, value: JSONElement)
29 | case copy(from: JSONPointer, path: JSONPointer, value: JSONElement)
30 | case move(from: JSONPointer, old: JSONElement, path: JSONPointer, value: JSONElement)
31 | }
32 |
33 | private var unchanged: [JSONPointer: JSONElement] = [:]
34 | private var operations: [Operation] = []
35 | private var patchOperations: [JSONPatch.Operation] {
36 | return operations.map(JSONPatch.Operation.init)
37 | }
38 |
39 | static func generatePatch(source: JSONElement, target: JSONElement) throws -> [JSONPatch.Operation] {
40 | var generator = JSONPatchGenerator()
41 | try generator.computeUnchanged(pointer: JSONPointer.wholeDocument, a: source, b: target)
42 | try generator.generateDiffs(pointer: JSONPointer.wholeDocument, source: source, target: target)
43 | return generator.patchOperations
44 | }
45 |
46 | private mutating func computeUnchanged(pointer: JSONPointer, a: JSONElement, b: JSONElement) throws {
47 | guard a != b else {
48 | unchanged[pointer] = a
49 | return
50 | }
51 |
52 | switch (a, b) {
53 | case (.object(let dictA), .object(let dictB)),
54 | (.object(let dictA), .mutableObject(let dictB as NSDictionary)),
55 | (.mutableObject(let dictA as NSDictionary), .object(let dictB)),
56 | (.mutableObject(let dictA as NSDictionary), .mutableObject(let dictB as NSDictionary)):
57 | try computeObjectUnchanged(pointer: pointer, a: dictA, b: dictB)
58 |
59 | case (.array(let arrayA), .array(let arrayB)),
60 | (.array(let arrayA), .mutableArray(let arrayB as NSArray)),
61 | (.mutableArray(let arrayA as NSArray), .array(let arrayB)),
62 | (.mutableArray(let arrayA as NSArray), .mutableArray(let arrayB as NSArray)):
63 | try computeArrayUnchanged(pointer: pointer, a: arrayA, b: arrayB)
64 |
65 | default:
66 | break
67 | }
68 | }
69 |
70 | private mutating func computeObjectUnchanged(pointer: JSONPointer,
71 | a: NSDictionary,
72 | b: NSDictionary) throws {
73 | guard let keys = a.allKeys as? [String] else {
74 | return
75 | }
76 |
77 | for key in keys {
78 | guard let valueA = a[key], let valueB = b[key] else {
79 | continue
80 | }
81 | try computeUnchanged(pointer: pointer.appended(withComponent: key),
82 | a: try JSONElement(any: valueA),
83 | b: try JSONElement(any: valueB))
84 | }
85 | }
86 |
87 | private mutating func computeArrayUnchanged(pointer: JSONPointer,
88 | a: NSArray,
89 | b: NSArray) throws {
90 | let count = min(a.count, b.count)
91 | for index in 0.. target.count {
170 | // target is smaller than source, remove end elements.
171 | for index in (target.count.. JSONPointer? {
218 | for (pointer, old) in unchanged where value == old {
219 | return pointer
220 | }
221 | return nil
222 | }
223 |
224 | private func findPreviouslyRemoved(value: JSONElement) -> Int? {
225 | return operations.firstIndex { (op) -> Bool in
226 | guard case let .remove(_, old) = op else {
227 | return false
228 | }
229 | return value == old
230 | }
231 | }
232 | }
233 |
234 | extension JSONPatch.Operation {
235 | fileprivate init(_ value: JSONPatchGenerator.Operation) {
236 | switch value {
237 | case let .add(path, value):
238 | self = .add(path: path, value: value)
239 | case let .remove(path, _):
240 | self = .remove(path: path)
241 | case let .copy(from, path, _):
242 | self = .copy(from: from, path: path)
243 | case let .replace(path, _, value):
244 | self = .replace(path: path, value: value)
245 | case let .move(from, _, path, _):
246 | self = .move(from: from, path: path)
247 | }
248 | }
249 | }
250 |
251 | extension JSONPatch {
252 |
253 | /// Initializes a JSONPatch instance with all json-patch operations required to transform the source
254 | /// json document into the target json document.
255 | ///
256 | /// - Parameters:
257 | /// - source: The source json document.
258 | /// - target: The target json document.
259 | public convenience init(source: JSONElement, target: JSONElement) throws {
260 | self.init(operations: try JSONPatchGenerator.generatePatch(source: source, target: target))
261 | }
262 |
263 | /// Initialize a JSONPatch instance with all json-patch operations required to transform the source
264 | /// json document into the target json document.
265 | ///
266 | /// - Parameters:
267 | /// - source: The source json document as data.
268 | /// - target: The target json document as data.
269 | /// - options: The JSONSerialization options for reading the source and target data.
270 | public convenience init(source: Data, target: Data, options: JSONSerialization.ReadingOptions = []) throws {
271 | let sourceJson = try JSONSerialization.jsonElement(with: source, options: options)
272 | let targetJson = try JSONSerialization.jsonElement(with: target, options: options)
273 | try self.init(source: sourceJson, target: targetJson)
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/Tests/JSONPatchTests/tests.json:
--------------------------------------------------------------------------------
1 | [
2 | { "comment": "empty list, empty docs",
3 | "doc": {},
4 | "patch": [],
5 | "expected": {} },
6 |
7 | { "comment": "empty patch list",
8 | "doc": {"foo": 1},
9 | "patch": [],
10 | "expected": {"foo": 1} },
11 |
12 | { "comment": "rearrangements OK?",
13 | "doc": {"foo": 1, "bar": 2},
14 | "patch": [],
15 | "expected": {"bar":2, "foo": 1} },
16 |
17 | { "comment": "rearrangements OK? How about one level down ... array",
18 | "doc": [{"foo": 1, "bar": 2}],
19 | "patch": [],
20 | "expected": [{"bar":2, "foo": 1}] },
21 |
22 | { "comment": "rearrangements OK? How about one level down...",
23 | "doc": {"foo":{"foo": 1, "bar": 2}},
24 | "patch": [],
25 | "expected": {"foo":{"bar":2, "foo": 1}} },
26 |
27 | { "comment": "add replaces any existing field",
28 | "doc": {"foo": null},
29 | "patch": [{"op": "add", "path": "/foo", "value":1}],
30 | "expected": {"foo": 1} },
31 |
32 | { "comment": "toplevel array",
33 | "doc": [],
34 | "patch": [{"op": "add", "path": "/0", "value": "foo"}],
35 | "expected": ["foo"] },
36 |
37 | { "comment": "toplevel array, no change",
38 | "doc": ["foo"],
39 | "patch": [],
40 | "expected": ["foo"] },
41 |
42 | { "comment": "toplevel object, numeric string",
43 | "doc": {},
44 | "patch": [{"op": "add", "path": "/foo", "value": "1"}],
45 | "expected": {"foo":"1"} },
46 |
47 | { "comment": "toplevel object, integer",
48 | "doc": {},
49 | "patch": [{"op": "add", "path": "/foo", "value": 1}],
50 | "expected": {"foo":1} },
51 |
52 | { "comment": "Toplevel scalar values OK?",
53 | "doc": "foo",
54 | "patch": [{"op": "replace", "path": "", "value": "bar"}],
55 | "expected": "bar",
56 | "disabled": true },
57 |
58 | { "comment": "replace object document with array document?",
59 | "doc": {},
60 | "patch": [{"op": "add", "path": "", "value": []}],
61 | "expected": [] },
62 |
63 | { "comment": "replace array document with object document?",
64 | "doc": [],
65 | "patch": [{"op": "add", "path": "", "value": {}}],
66 | "expected": {} },
67 |
68 | { "comment": "append to root array document?",
69 | "doc": [],
70 | "patch": [{"op": "add", "path": "/-", "value": "hi"}],
71 | "expected": ["hi"] },
72 |
73 | { "comment": "Add, / target",
74 | "doc": {},
75 | "patch": [ {"op": "add", "path": "/", "value":1 } ],
76 | "expected": {"":1} },
77 |
78 | { "comment": "Add, /foo/ deep target (trailing slash)",
79 | "doc": {"foo": {}},
80 | "patch": [ {"op": "add", "path": "/foo/", "value":1 } ],
81 | "expected": {"foo":{"": 1}} },
82 |
83 | { "comment": "Add composite value at top level",
84 | "doc": {"foo": 1},
85 | "patch": [{"op": "add", "path": "/bar", "value": [1, 2]}],
86 | "expected": {"foo": 1, "bar": [1, 2]} },
87 |
88 | { "comment": "Add into composite value",
89 | "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
90 | "patch": [{"op": "add", "path": "/baz/0/foo", "value": "world"}],
91 | "expected": {"foo": 1, "baz": [{"qux": "hello", "foo": "world"}]} },
92 |
93 | { "doc": {"bar": [1, 2]},
94 | "patch": [{"op": "add", "path": "/bar/8", "value": "5"}],
95 | "error": "Out of bounds (upper)" },
96 |
97 | { "doc": {"bar": [1, 2]},
98 | "patch": [{"op": "add", "path": "/bar/-1", "value": "5"}],
99 | "error": "Out of bounds (lower)" },
100 |
101 | { "doc": {"foo": 1},
102 | "patch": [{"op": "add", "path": "/bar", "value": true}],
103 | "expected": {"foo": 1, "bar": true} },
104 |
105 | { "doc": {"foo": 1},
106 | "patch": [{"op": "add", "path": "/bar", "value": false}],
107 | "expected": {"foo": 1, "bar": false} },
108 |
109 | { "doc": {"foo": 1},
110 | "patch": [{"op": "add", "path": "/bar", "value": null}],
111 | "expected": {"foo": 1, "bar": null} },
112 |
113 | { "comment": "0 can be an array index or object element name",
114 | "doc": {"foo": 1},
115 | "patch": [{"op": "add", "path": "/0", "value": "bar"}],
116 | "expected": {"foo": 1, "0": "bar" } },
117 |
118 | { "doc": ["foo"],
119 | "patch": [{"op": "add", "path": "/1", "value": "bar"}],
120 | "expected": ["foo", "bar"] },
121 |
122 | { "doc": ["foo", "sil"],
123 | "patch": [{"op": "add", "path": "/1", "value": "bar"}],
124 | "expected": ["foo", "bar", "sil"] },
125 |
126 | { "doc": ["foo", "sil"],
127 | "patch": [{"op": "add", "path": "/0", "value": "bar"}],
128 | "expected": ["bar", "foo", "sil"] },
129 |
130 | { "comment": "push item to array via last index + 1",
131 | "doc": ["foo", "sil"],
132 | "patch": [{"op":"add", "path": "/2", "value": "bar"}],
133 | "expected": ["foo", "sil", "bar"] },
134 |
135 | { "comment": "add item to array at index > length should fail",
136 | "doc": ["foo", "sil"],
137 | "patch": [{"op":"add", "path": "/3", "value": "bar"}],
138 | "error": "index is greater than number of items in array" },
139 |
140 | { "comment": "test against implementation-specific numeric parsing",
141 | "doc": {"1e0": "foo"},
142 | "patch": [{"op": "test", "path": "/1e0", "value": "foo"}],
143 | "expected": {"1e0": "foo"} },
144 |
145 | { "comment": "test with bad number should fail",
146 | "doc": ["foo", "bar"],
147 | "patch": [{"op": "test", "path": "/1e0", "value": "bar"}],
148 | "error": "test op shouldn't get array element 1" },
149 |
150 | { "doc": ["foo", "sil"],
151 | "patch": [{"op": "add", "path": "/bar", "value": 42}],
152 | "error": "Object operation on array target" },
153 |
154 | { "doc": ["foo", "sil"],
155 | "patch": [{"op": "add", "path": "/1", "value": ["bar", "baz"]}],
156 | "expected": ["foo", ["bar", "baz"], "sil"],
157 | "comment": "value in array add not flattened" },
158 |
159 | { "doc": {"foo": 1, "bar": [1, 2, 3, 4]},
160 | "patch": [{"op": "remove", "path": "/bar"}],
161 | "expected": {"foo": 1} },
162 |
163 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
164 | "patch": [{"op": "remove", "path": "/baz/0/qux"}],
165 | "expected": {"foo": 1, "baz": [{}]} },
166 |
167 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
168 | "patch": [{"op": "replace", "path": "/foo", "value": [1, 2, 3, 4]}],
169 | "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]} },
170 |
171 | { "doc": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]},
172 | "patch": [{"op": "replace", "path": "/baz/0/qux", "value": "world"}],
173 | "expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "world"}]} },
174 |
175 | { "doc": ["foo"],
176 | "patch": [{"op": "replace", "path": "/0", "value": "bar"}],
177 | "expected": ["bar"] },
178 |
179 | { "doc": [""],
180 | "patch": [{"op": "replace", "path": "/0", "value": 0}],
181 | "expected": [0] },
182 |
183 | { "doc": [""],
184 | "patch": [{"op": "replace", "path": "/0", "value": true}],
185 | "expected": [true] },
186 |
187 | { "doc": [""],
188 | "patch": [{"op": "replace", "path": "/0", "value": false}],
189 | "expected": [false] },
190 |
191 | { "doc": [""],
192 | "patch": [{"op": "replace", "path": "/0", "value": null}],
193 | "expected": [null] },
194 |
195 | { "doc": ["foo", "sil"],
196 | "patch": [{"op": "replace", "path": "/1", "value": ["bar", "baz"]}],
197 | "expected": ["foo", ["bar", "baz"]],
198 | "comment": "value in array replace not flattened" },
199 |
200 | { "comment": "replace whole document",
201 | "doc": {"foo": "bar"},
202 | "patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}],
203 | "expected": {"baz": "qux"} },
204 |
205 | { "comment": "test replace with missing parent key should fail",
206 | "doc": {"bar": "baz"},
207 | "patch": [{"op": "replace", "path": "/foo/bar", "value": false}],
208 | "error": "replace op should fail with missing parent key" },
209 |
210 | { "comment": "spurious patch properties",
211 | "doc": {"foo": 1},
212 | "patch": [{"op": "test", "path": "/foo", "value": 1, "spurious": 1}],
213 | "expected": {"foo": 1} },
214 |
215 | { "doc": {"foo": null},
216 | "patch": [{"op": "test", "path": "/foo", "value": null}],
217 | "expected": {"foo": null},
218 | "comment": "null value should be valid obj property" },
219 |
220 | { "doc": {"foo": null},
221 | "patch": [{"op": "replace", "path": "/foo", "value": "truthy"}],
222 | "expected": {"foo": "truthy"},
223 | "comment": "null value should be valid obj property to be replaced with something truthy" },
224 |
225 | { "doc": {"foo": null},
226 | "patch": [{"op": "move", "from": "/foo", "path": "/bar"}],
227 | "expected": {"bar": null},
228 | "comment": "null value should be valid obj property to be moved" },
229 |
230 | { "doc": {"foo": null},
231 | "patch": [{"op": "copy", "from": "/foo", "path": "/bar"}],
232 | "expected": {"foo": null, "bar": null},
233 | "comment": "null value should be valid obj property to be copied" },
234 |
235 | { "doc": {"foo": null},
236 | "patch": [{"op": "remove", "path": "/foo"}],
237 | "expected": {},
238 | "comment": "null value should be valid obj property to be removed" },
239 |
240 | { "doc": {"foo": "bar"},
241 | "patch": [{"op": "replace", "path": "/foo", "value": null}],
242 | "expected": {"foo": null},
243 | "comment": "null value should still be valid obj property replace other value" },
244 |
245 | { "doc": {"foo": {"foo": 1, "bar": 2}},
246 | "patch": [{"op": "test", "path": "/foo", "value": {"bar": 2, "foo": 1}}],
247 | "expected": {"foo": {"foo": 1, "bar": 2}},
248 | "comment": "test should pass despite rearrangement" },
249 |
250 | { "doc": {"foo": [{"foo": 1, "bar": 2}]},
251 | "patch": [{"op": "test", "path": "/foo", "value": [{"bar": 2, "foo": 1}]}],
252 | "expected": {"foo": [{"foo": 1, "bar": 2}]},
253 | "comment": "test should pass despite (nested) rearrangement" },
254 |
255 | { "doc": {"foo": {"bar": [1, 2, 5, 4]}},
256 | "patch": [{"op": "test", "path": "/foo", "value": {"bar": [1, 2, 5, 4]}}],
257 | "expected": {"foo": {"bar": [1, 2, 5, 4]}},
258 | "comment": "test should pass - no error" },
259 |
260 | { "doc": {"foo": {"bar": [1, 2, 5, 4]}},
261 | "patch": [{"op": "test", "path": "/foo", "value": [1, 2]}],
262 | "error": "test op should fail" },
263 |
264 | { "comment": "Whole document",
265 | "doc": { "foo": 1 },
266 | "patch": [{"op": "test", "path": "", "value": {"foo": 1}}],
267 | "disabled": true },
268 |
269 | { "comment": "Empty-string element",
270 | "doc": { "": 1 },
271 | "patch": [{"op": "test", "path": "/", "value": 1}],
272 | "expected": { "": 1 } },
273 |
274 | { "doc": {
275 | "foo": ["bar", "baz"],
276 | "": 0,
277 | "a/b": 1,
278 | "c%d": 2,
279 | "e^f": 3,
280 | "g|h": 4,
281 | "i\\j": 5,
282 | "k\"l": 6,
283 | " ": 7,
284 | "m~n": 8
285 | },
286 | "patch": [{"op": "test", "path": "/foo", "value": ["bar", "baz"]},
287 | {"op": "test", "path": "/foo/0", "value": "bar"},
288 | {"op": "test", "path": "/", "value": 0},
289 | {"op": "test", "path": "/a~1b", "value": 1},
290 | {"op": "test", "path": "/c%d", "value": 2},
291 | {"op": "test", "path": "/e^f", "value": 3},
292 | {"op": "test", "path": "/g|h", "value": 4},
293 | {"op": "test", "path": "/i\\j", "value": 5},
294 | {"op": "test", "path": "/k\"l", "value": 6},
295 | {"op": "test", "path": "/ ", "value": 7},
296 | {"op": "test", "path": "/m~0n", "value": 8}],
297 | "expected": {
298 | "": 0,
299 | " ": 7,
300 | "a/b": 1,
301 | "c%d": 2,
302 | "e^f": 3,
303 | "foo": [
304 | "bar",
305 | "baz"
306 | ],
307 | "g|h": 4,
308 | "i\\j": 5,
309 | "k\"l": 6,
310 | "m~n": 8
311 | }
312 | },
313 | { "comment": "Move to same location has no effect",
314 | "doc": {"foo": 1},
315 | "patch": [{"op": "move", "from": "/foo", "path": "/foo"}],
316 | "expected": {"foo": 1} },
317 |
318 | { "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
319 | "patch": [{"op": "move", "from": "/foo", "path": "/bar"}],
320 | "expected": {"baz": [{"qux": "hello"}], "bar": 1} },
321 |
322 | { "doc": {"baz": [{"qux": "hello"}], "bar": 1},
323 | "patch": [{"op": "move", "from": "/baz/0/qux", "path": "/baz/1"}],
324 | "expected": {"baz": [{}, "hello"], "bar": 1} },
325 |
326 | { "doc": {"baz": [{"qux": "hello"}], "bar": 1},
327 | "patch": [{"op": "copy", "from": "/baz/0", "path": "/boo"}],
328 | "expected": {"baz":[{"qux":"hello"}],"bar":1,"boo":{"qux":"hello"}} },
329 |
330 | { "comment": "replacing the root of the document is possible with add",
331 | "doc": {"foo": "bar"},
332 | "patch": [{"op": "add", "path": "", "value": {"baz": "qux"}}],
333 | "expected": {"baz":"qux"}},
334 |
335 | { "comment": "Adding to \"/-\" adds to the end of the array",
336 | "doc": [ 1, 2 ],
337 | "patch": [ { "op": "add", "path": "/-", "value": { "foo": [ "bar", "baz" ] } } ],
338 | "expected": [ 1, 2, { "foo": [ "bar", "baz" ] } ]},
339 |
340 | { "comment": "Adding to \"/-\" adds to the end of the array, even n levels down",
341 | "doc": [ 1, 2, [ 3, [ 4, 5 ] ] ],
342 | "patch": [ { "op": "add", "path": "/2/1/-", "value": { "foo": [ "bar", "baz" ] } } ],
343 | "expected": [ 1, 2, [ 3, [ 4, 5, { "foo": [ "bar", "baz" ] } ] ] ]},
344 |
345 | { "comment": "test remove with bad number should fail",
346 | "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
347 | "patch": [{"op": "remove", "path": "/baz/1e0/qux"}],
348 | "error": "remove op shouldn't remove from array with bad number" },
349 |
350 | { "comment": "test remove on array",
351 | "doc": [1, 2, 3, 4],
352 | "patch": [{"op": "remove", "path": "/0"}],
353 | "expected": [2, 3, 4] },
354 |
355 | { "comment": "test repeated removes",
356 | "doc": [1, 2, 3, 4],
357 | "patch": [{ "op": "remove", "path": "/1" },
358 | { "op": "remove", "path": "/2" }],
359 | "expected": [1, 3] },
360 |
361 | { "comment": "test remove with bad index should fail",
362 | "doc": [1, 2, 3, 4],
363 | "patch": [{"op": "remove", "path": "/1e0"}],
364 | "error": "remove op shouldn't remove from array with bad number" },
365 |
366 | { "comment": "test replace with bad number should fail",
367 | "doc": [""],
368 | "patch": [{"op": "replace", "path": "/1e0", "value": false}],
369 | "error": "replace op shouldn't replace in array with bad number" },
370 |
371 | { "comment": "test copy with bad number should fail",
372 | "doc": {"baz": [1,2,3], "bar": 1},
373 | "patch": [{"op": "copy", "from": "/baz/1e0", "path": "/boo"}],
374 | "error": "copy op shouldn't work with bad number" },
375 |
376 | { "comment": "test move with bad number should fail",
377 | "doc": {"foo": 1, "baz": [1,2,3,4]},
378 | "patch": [{"op": "move", "from": "/baz/1e0", "path": "/foo"}],
379 | "error": "move op shouldn't work with bad number" },
380 |
381 | { "comment": "test add with bad number should fail",
382 | "doc": ["foo", "sil"],
383 | "patch": [{"op": "add", "path": "/1e0", "value": "bar"}],
384 | "error": "add op shouldn't add to array with bad number" },
385 |
386 | { "comment": "missing 'value' parameter to add",
387 | "doc": [ 1 ],
388 | "patch": [ { "op": "add", "path": "/-" } ],
389 | "error": "missing 'value' parameter" },
390 |
391 | { "comment": "missing 'value' parameter to replace",
392 | "doc": [ 1 ],
393 | "patch": [ { "op": "replace", "path": "/0" } ],
394 | "error": "missing 'value' parameter" },
395 |
396 | { "comment": "missing 'value' parameter to test",
397 | "doc": [ null ],
398 | "patch": [ { "op": "test", "path": "/0" } ],
399 | "error": "missing 'value' parameter" },
400 |
401 | { "comment": "missing value parameter to test - where undef is falsy",
402 | "doc": [ false ],
403 | "patch": [ { "op": "test", "path": "/0" } ],
404 | "error": "missing 'value' parameter" },
405 |
406 | { "comment": "missing from parameter to copy",
407 | "doc": [ 1 ],
408 | "patch": [ { "op": "copy", "path": "/-" } ],
409 | "error": "missing 'from' parameter" },
410 |
411 | { "comment": "missing from location to copy",
412 | "doc": { "foo": 1 },
413 | "patch": [ { "op": "copy", "from": "/bar", "path": "/foo" } ],
414 | "error": "missing 'from' location" },
415 |
416 | { "comment": "missing from parameter to move",
417 | "doc": { "foo": 1 },
418 | "patch": [ { "op": "move", "path": "" } ],
419 | "error": "missing 'from' parameter" },
420 |
421 | { "comment": "missing from location to move",
422 | "doc": { "foo": 1 },
423 | "patch": [ { "op": "move", "from": "/bar", "path": "/foo" } ],
424 | "error": "missing 'from' location" },
425 |
426 | { "comment": "duplicate ops",
427 | "doc": { "foo": "bar" },
428 | "patch": [ { "op": "add", "path": "/baz", "value": "qux",
429 | "op": "move", "from":"/foo" } ],
430 | "error": "patch has two 'op' members",
431 | "disabled": true },
432 |
433 | { "comment": "unrecognized op should fail",
434 | "doc": {"foo": 1},
435 | "patch": [{"op": "spam", "path": "/foo", "value": 1}],
436 | "error": "Unrecognized op 'spam'" },
437 |
438 | { "comment": "test with bad array number that has leading zeros",
439 | "doc": ["foo", "bar"],
440 | "patch": [{"op": "test", "path": "/00", "value": "foo"}],
441 | "error": "test op should reject the array value, it has leading zeros" },
442 |
443 | { "comment": "test with bad array number that has leading zeros",
444 | "doc": ["foo", "bar"],
445 | "patch": [{"op": "test", "path": "/01", "value": "bar"}],
446 | "error": "test op should reject the array value, it has leading zeros" },
447 |
448 | { "comment": "Removing nonexistent field",
449 | "doc": {"foo" : "bar"},
450 | "patch": [{"op": "remove", "path": "/baz"}],
451 | "error": "removing a nonexistent field should fail" },
452 |
453 | { "comment": "Removing nonexistent index",
454 | "doc": ["foo", "bar"],
455 | "patch": [{"op": "remove", "path": "/2"}],
456 | "error": "removing a nonexistent index should fail" },
457 |
458 | { "comment": "Patch with different capitalisation than doc",
459 | "doc": {"foo":"bar"},
460 | "patch": [{"op": "add", "path": "/FOO", "value": "BAR"}],
461 | "expected": {"foo": "bar", "FOO": "BAR"}
462 | }
463 |
464 | ]
465 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/JSONPatch.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONPatch.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 11/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | /// Implementation of IETF JSON Patch (RFC6902). JSON Patch is a format
24 | /// for expressing a sequence of operations to apply to a target JSON document.
25 | /// This implementation works with the representions of JSON produced with
26 | /// JSONSerialization.
27 | ///
28 | /// https://tools.ietf.org/html/rfc6902
29 | public class JSONPatch: Codable {
30 |
31 | /// The mimetype for json-patch
32 | public static let mimetype = "application/json-patch+json"
33 |
34 | /// Options given to the patch process.
35 | public enum ApplyOption: Equatable {
36 | /// By default the patch will be applied directly on to the json object
37 | /// given, which is the most memory efficient option. However when applying
38 | /// a patch directly the result is not atomic, if an error occurs then the
39 | /// json object may be left in a partial state. If applyOnCopy is given then a copy of
40 | /// the json document is created and the patch applied to the copy.
41 | case applyOnCopy
42 | /// All references to non existent values will be ignored and the patch will continue applying all remaining operations
43 | case ignoreNonexistentValues
44 | /// Can be used to apply the patch to sub-element within the json document. Using this option will cause the patch process to
45 | /// treat the specified json element as the root element while applying the patch.
46 | case relative(to: JSONPointer)
47 | }
48 |
49 | /// A representation of the supported operations json-patch.
50 | /// (see [RFC6902], Section 4)
51 | public enum Operation {
52 | case add(path: JSONPointer, value: JSONElement)
53 | case remove(path: JSONPointer)
54 | case replace(path: JSONPointer, value: JSONElement)
55 | case move(from: JSONPointer, path: JSONPointer)
56 | case copy(from: JSONPointer, path: JSONPointer)
57 | case test(path: JSONPointer, value: JSONElement)
58 | }
59 |
60 | /// An array of json-patch operations that will be applied in sequence.
61 | public let operations: [JSONPatch.Operation]
62 |
63 | /// A JSON Array represent of the receiver compatible with JSONSerialization.
64 | public var jsonArray: NSArray {
65 | return operations.map { $0.jsonObject } as NSArray
66 | }
67 |
68 | /// Initializes a JSONPatch instance with an array of operations.
69 | ///
70 | /// - Parameters:
71 | /// - operations: An array of operations.
72 | public init(operations: [JSONPatch.Operation]) {
73 | self.operations = operations
74 | }
75 |
76 | /// Initializes a JSONPatch instance from a JSON array (the result of using
77 | /// JSONSerialization). The array should directly contain a list of json-patch
78 | /// operations as NSDictionary representations.
79 | ///
80 | /// - Parameters:
81 | /// - jsonArray: An array obtained from JSONSerialization containing json-patch operations.
82 | public convenience init(jsonArray: NSArray) throws {
83 | var operations: [JSONPatch.Operation] = []
84 | for (index, element) in (jsonArray as Array).enumerated() {
85 | guard let obj = element as? NSDictionary else {
86 | throw JSONError.invalidPatchFormat
87 | }
88 | let operation = try JSONPatch.Operation(jsonObject: obj, index: index)
89 | operations.append(operation)
90 | }
91 | self.init(operations: operations)
92 | }
93 |
94 | /// Initializes a JSONPatch instance from JSON represention. This should be a
95 | /// top-level array with the json-patch operations.
96 | ///
97 | /// - Parameters:
98 | /// - data: The json-patch document as data.
99 | public convenience init(data: Data) throws {
100 | let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
101 | guard let jsonArray = jsonObject as? NSArray else {
102 | throw JSONError.invalidPatchFormat
103 | }
104 | try self.init(jsonArray: jsonArray)
105 | }
106 |
107 | public required init(from decoder: Decoder) throws {
108 | let container = try decoder.singleValueContainer()
109 | operations = try container.decode([JSONPatch.Operation].self)
110 | }
111 |
112 | public func encode(to encoder: Encoder) throws {
113 | var container = encoder.singleValueContainer()
114 | try container.encode(operations)
115 | }
116 |
117 | /// Returns a representation of the patch as UTF-8 encoded json.
118 | ///
119 | /// - Parameters:
120 | /// - option: The writing options.
121 | /// - Returns: UTF-8 encoded json.
122 | public func data(options: JSONSerialization.WritingOptions = []) throws -> Data {
123 | return try JSONSerialization.data(withJSONObject: jsonArray, options: options)
124 | }
125 |
126 | /// Applies a json-patch to a target json document. Operations are applied
127 | /// sequentially in the order they appear in the operations array.
128 | /// Each operation in the sequence is applied to the target document;
129 | /// the resulting document becomes the target of the next operation.
130 | /// Evaluation continues until all operations are successfully applied
131 | /// or until an error condition is encountered. If you are going to apply
132 | /// the patch inplace then it can be more performant if the jsonObject
133 | /// has been parsing using the .mutableContainers reading option on
134 | /// JSONSerialization, as this will eliminate the need to make copies of sections
135 | /// of the json document while applying the patch.
136 | ///
137 | /// - Parameters:
138 | /// - jsonObject: The target json document to patch the patch to.
139 | /// - path: Can be used to apply the patch to sub-element within the json document.
140 | /// If nil then the patch is applied directly to the jsonObject given.
141 | /// - options: The options to be used when applying the patch.
142 | /// - Returns: A transformed json document with the patch applied.
143 | public func apply(to jsonObject: Any,
144 | options: [ApplyOption] = []) throws -> Any {
145 | var jsonDocument = try JSONElement(any: jsonObject)
146 | if options.contains(.applyOnCopy) {
147 | jsonDocument = try jsonDocument.copy()
148 | }
149 | try jsonDocument.apply(patch: self, options: options)
150 | return jsonDocument.rawValue
151 | }
152 |
153 | /// Applies a json-patch to a target json document. Operations are applied
154 | /// sequentially in the order they appear in the operations array.
155 | /// Each operation in the sequence is applied to the target document;
156 | /// the resulting document becomes the target of the next operation.
157 | /// Evaluation continues until all operations are successfully applied
158 | /// or until an error condition is encountered. If you are going to apply
159 | /// the patch inplace then it can be more performant if the jsonObject
160 | /// has been parsing using the .mutableContainers reading option on
161 | /// JSONSerialization, as this will eliminate the need to make copies of sections
162 | /// of the json document while applying the patch.
163 | ///
164 | /// - Parameters:
165 | /// - jsonObject: The target json document to patch the patch to.
166 | /// - path: Can be used to apply the patch to sub-element within the json document.
167 | /// If nil then the patch is applied directly to the jsonObject given.
168 | /// - inplace: If true the patch will be applied directly on to the json object
169 | /// given, which is the most memory efficient option. However when applying
170 | /// a patch inplace the result is not atomic, if an error occurs then the
171 | /// json object may be left in a partial state. If false then a copy of
172 | /// the json document is created and the patch applied to the copy.
173 | /// - Returns: A transformed json document with the patch applied.
174 | @available(*, deprecated, message: "Use apply(to: options:) instead")
175 | public func apply(to jsonObject: Any,
176 | relativeTo path: JSONPointer? = nil,
177 | inplace: Bool) throws -> Any {
178 | var options: [ApplyOption] = []
179 | if let path = path {
180 | options.append(.relative(to: path))
181 | }
182 | if !inplace {
183 | options.append(.applyOnCopy)
184 | }
185 | return try apply(to: jsonObject, options: options)
186 | }
187 |
188 | /// Applies a json-patch to a target json document represented as data (see apply(to jsonObject:)
189 | /// for more details. The given data will be parsed using JSONSerialization using the
190 | /// reading options. If the patch was successfully applied with no errors, the result will be
191 | /// serialized back to data with the writing options.
192 | ///
193 | /// - Parameters:
194 | /// - data: A data representation of the json document to apply the patch to.
195 | /// - readingOptions: The options given to JSONSerialization to parse the json data.
196 | /// - writingOptions: The options given to JSONSerialization to write the result to data.
197 | /// - applyingOptions: The options to be used when applying the patch.
198 | /// - Returns: The transformed json document as data.
199 | public func apply(to data: Data,
200 | readingOptions: JSONSerialization.ReadingOptions = [.mutableContainers],
201 | writingOptions: JSONSerialization.WritingOptions = [],
202 | applyingOptions: [JSONPatch.ApplyOption] = []) throws -> Data {
203 | let jsonObject = try JSONSerialization.jsonObject(with: data,
204 | options: readingOptions)
205 | var jsonElement = try JSONElement(any: jsonObject)
206 | try jsonElement.apply(patch: self, options: applyingOptions)
207 | let transformedData = try JSONSerialization.data(with: jsonElement,
208 | options: writingOptions)
209 | return transformedData
210 | }
211 |
212 | /// Applies a json-patch to a target json document represented as data (see apply(to jsonObject:)
213 | /// for more details. The given data will be parsed using JSONSerialization using the
214 | /// reading options. If the patch was successfully applied with no errors, the result will be
215 | /// serialized back to data with the writing options.
216 | ///
217 | /// - Parameters:
218 | /// - data: A data representation of the json document to apply the patch to.
219 | /// - path: Can be used to apply the patch to sub-element within the json document.
220 | /// If nil then the patch is applied directly to the whole json document.
221 | /// - readingOptions: The options given to JSONSerialization to parse the json data.
222 | /// - writingOptions: The options given to JSONSerialization to write the result to data.
223 | /// - Returns: The transformed json document as data.
224 | @available(*, deprecated, message: "Use apply(to: readingOptions, writingOptions, applyingOptions:)")
225 | public func apply(to data: Data,
226 | relativeTo path: JSONPointer?,
227 | readingOptions: JSONSerialization.ReadingOptions = [.mutableContainers],
228 | writingOptions: JSONSerialization.WritingOptions = []) throws -> Data {
229 | var applyingOptions: [ApplyOption] = []
230 | if let path = path {
231 | applyingOptions.append(.relative(to: path))
232 | }
233 | return try apply(to: data,
234 | readingOptions: readingOptions,
235 | writingOptions: writingOptions,
236 | applyingOptions: applyingOptions)
237 | }
238 | }
239 |
240 | extension JSONPatch.Operation {
241 |
242 | /// Initialize a json-operation from a JSON Object representation.
243 | /// If the operation is not recogized or is missing a required field
244 | /// then an error is thrown. Unrecogized extra fields are ignored.
245 | ///
246 | /// - Parameters:
247 | /// - jsonObject: The json object representing the operation.
248 | /// - index: The index this operation occurs at within the json-patch document.
249 | public init(jsonObject: NSDictionary, index: Int = 0) throws {
250 | guard let op = jsonObject["op"] as? String else {
251 | throw JSONError.missingRequiredPatchField(op: "", index: index, field: "op")
252 | }
253 |
254 | switch op {
255 | case "add":
256 | let path: String = try JSONPatch.Operation.val(jsonObject, "add", "path", index)
257 | let value: Any = try JSONPatch.Operation.val(jsonObject, "add", "value", index)
258 | let pointer = try JSONPointer(string: path)
259 | let element = try JSONElement(any: value)
260 | self = .add(path: pointer, value: element)
261 | case "remove":
262 | let path: String = try JSONPatch.Operation.val(jsonObject, "remove", "path", index)
263 | let pointer = try JSONPointer(string: path)
264 | self = .remove(path: pointer)
265 | case "replace":
266 | let path: String = try JSONPatch.Operation.val(jsonObject, "replace", "path", index)
267 | let value: Any = try JSONPatch.Operation.val(jsonObject, "replace", "value", index)
268 | let pointer = try JSONPointer(string: path)
269 | let element = try JSONElement(any: value)
270 | self = .replace(path: pointer, value: element)
271 | case "move":
272 | let from: String = try JSONPatch.Operation.val(jsonObject, "move", "from", index)
273 | let path: String = try JSONPatch.Operation.val(jsonObject, "move", "path", index)
274 | let fpointer = try JSONPointer(string: from)
275 | let ppointer = try JSONPointer(string: path)
276 | self = .move(from: fpointer, path: ppointer)
277 | case "copy":
278 | let from: String = try JSONPatch.Operation.val(jsonObject, "copy", "from", index)
279 | let path: String = try JSONPatch.Operation.val(jsonObject, "copy", "path", index)
280 | let fpointer = try JSONPointer(string: from)
281 | let ppointer = try JSONPointer(string: path)
282 | self = .copy(from: fpointer, path: ppointer)
283 | case "test":
284 | let path: String = try JSONPatch.Operation.val(jsonObject, "test", "path", index)
285 | let value: Any = try JSONPatch.Operation.val(jsonObject, "test", "value", index)
286 | let pointer = try JSONPointer(string: path)
287 | let element = try JSONElement(any: value)
288 | self = .test(path: pointer, value: element)
289 | default:
290 | throw JSONError.unknownPatchOperation
291 | }
292 | }
293 |
294 | private static func val(_ jsonObject: NSDictionary,
295 | _ op: String,
296 | _ field: String,
297 | _ index: Int) throws -> T {
298 | guard let value = jsonObject[field] as? T else {
299 | throw JSONError.missingRequiredPatchField(op: op, index: index, field: field)
300 | }
301 | return value
302 | }
303 |
304 | var jsonObject: NSDictionary {
305 | let dict = NSMutableDictionary()
306 | switch self {
307 | case let .add(path, value):
308 | dict["op"] = "add"
309 | dict["path"] = path.string
310 | dict["value"] = value.rawValue
311 | case let .remove(path):
312 | dict["op"] = "remove"
313 | dict["path"] = path.string
314 | case let .replace(path, value):
315 | dict["op"] = "replace"
316 | dict["path"] = path.string
317 | dict["value"] = value.rawValue
318 | case let .move(from, path):
319 | dict["op"] = "move"
320 | dict["from"] = from.string
321 | dict["path"] = path.string
322 | case let .copy(from, path):
323 | dict["op"] = "copy"
324 | dict["from"] = from.string
325 | dict["path"] = path.string
326 | case let .test(path, value):
327 | dict["op"] = "test"
328 | dict["path"] = path.string
329 | dict["value"] = value.rawValue
330 | }
331 | return dict
332 | }
333 | }
334 |
335 | extension JSONPatch.Operation: Equatable {
336 |
337 | /// Tests the equality of two json-patch operations.
338 | ///
339 | /// - Parameters:
340 | /// - lhs: Left-hand side of the equality test.
341 | /// - rhs: Right-hand side of the equality test.
342 | /// - Returns: true is the lhs is equal to the rhs.
343 | public static func == (lhs: JSONPatch.Operation, rhs: JSONPatch.Operation) -> Bool {
344 | switch (lhs, rhs) {
345 | case let (.add(lpath, lvalue), .add(rpath, rvalue)),
346 | let (.replace(lpath, lvalue), .replace(rpath, rvalue)),
347 | let (.test(lpath, lvalue), .test(rpath, rvalue)):
348 | return lpath == rpath && lvalue == rvalue
349 | case let (.remove(lpath), .remove(rpath)):
350 | return lpath == rpath
351 | case let (.move(lfrom, lpath), .move(rfrom, rpath)),
352 | let (.copy(lfrom, lpath), .copy(rfrom, rpath)):
353 | return lfrom == rfrom && lpath == rpath
354 | default:
355 | return false
356 | }
357 | }
358 |
359 | }
360 |
361 | extension JSONPatch: Equatable {
362 |
363 | /// Tests the equality of two json-patchs.
364 | ///
365 | /// - Parameters:
366 | /// - lhs: Left-hand side of the equality test.
367 | /// - rhs: Right-hand side of the equality test.
368 | /// - Returns: true is the lhs is equal to the rhs.
369 | public static func == (lhs: JSONPatch, rhs: JSONPatch) -> Bool {
370 | return lhs.operations == rhs.operations
371 | }
372 |
373 | }
374 |
375 | // MARK: - Patch Codable
376 |
377 | public extension JSONPatch {
378 | /**
379 | Applies this patch on the specified Codable object
380 |
381 | The originated object won't be changed, a new object will be returned with this patch applied on it
382 |
383 | e.g.
384 |
385 | ```swift
386 | let patch = ... // get your JSONPatch from somewhere
387 |
388 | let patchedDevice = try! patch.applied(to: self.device)
389 | ```
390 |
391 | - parameter object: The object to apply the patch on
392 |
393 | - returns: A new object with the applied patch
394 | */
395 | func applied(to object: T) throws -> T {
396 | let data = try JSONEncoder().encode(object)
397 | let patchedData = try self.apply(to: data)
398 |
399 | return try JSONDecoder().decode(T.self, from: patchedData)
400 | }
401 |
402 | /**
403 | Creates a patch from the source object to the target object
404 |
405 | - note:
406 | Generic use case would be that the `from` object is **an older** version of the `to` object.
407 |
408 | e.g.:
409 |
410 | ```swift
411 | self.device.state.isPowered = false
412 | var lastSent: Device = IOManager.send(self.device)
413 |
414 | self.device.state.isPowered = true
415 |
416 | let patch = try JSONPatch.createPatch(from: lastSent, to: self.device)
417 |
418 | // Patch will now be a patch that changes
419 | // the state's `isPowered` from false to true
420 | ```
421 |
422 | - parameter source: The source object
423 | - parameter target: The target object
424 |
425 | - returns: The JSONPatch to get from the source to the target object
426 | */
427 | static func createPatch(from source: T, to target: T) throws -> JSONPatch {
428 | let sourceData = try JSONEncoder().encode(source)
429 | let targetData = try JSONEncoder().encode(target)
430 |
431 | return try JSONPatch(source: sourceData, target: targetData)
432 | }
433 | }
434 |
--------------------------------------------------------------------------------
/Sources/JSONPatch/JSONElement.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONElement.swift
3 | // JSONPatch
4 | //
5 | // Created by Raymond Mccrae on 11/11/2018.
6 | // Copyright © 2018 Raymond McCrae.
7 | //
8 | // Licensed under the Apache License, Version 2.0 (the "License");
9 | // you may not use this file except in compliance with the License.
10 | // You may obtain a copy of the License at
11 | //
12 | // http://www.apache.org/licenses/LICENSE-2.0
13 | //
14 | // Unless required by applicable law or agreed to in writing, software
15 | // distributed under the License is distributed on an "AS IS" BASIS,
16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | // See the License for the specific language governing permissions and
18 | // limitations under the License.
19 | //
20 |
21 | import Foundation
22 |
23 | /// JSON Element holds a reference an element of the parsed JSON structure
24 | /// produced by JSONSerialization.
25 | public enum JSONElement {
26 | case object(value: NSDictionary)
27 | case mutableObject(value: NSMutableDictionary)
28 | case array(value: NSArray)
29 | case mutableArray(value: NSMutableArray)
30 | case string(value: NSString)
31 | case number(value: NSNumber)
32 | case null
33 | }
34 |
35 | extension JSONElement {
36 |
37 | public init(_ value: String) {
38 | self = .string(value: value as NSString)
39 | }
40 |
41 | public init(_ value: Int) {
42 | self = .number(value: value as NSNumber)
43 | }
44 |
45 | public init(_ value: Double) {
46 | self = .number(value: value as NSNumber)
47 | }
48 |
49 | public init(_ value: Bool) {
50 | self = .number(value: value as NSNumber)
51 | }
52 | }
53 |
54 | extension JSONElement {
55 |
56 | /// The raw value of the underlying JSON representation.
57 | public var rawValue: Any {
58 | switch self {
59 | case .object(let value):
60 | return value
61 | case .mutableObject(let value):
62 | return value
63 | case .array(let value):
64 | return value
65 | case .mutableArray(let value):
66 | return value
67 | case .string(let value):
68 | return value
69 | case .number(let value):
70 | return value
71 | case .null:
72 | return NSNull()
73 | }
74 | }
75 |
76 | /// Indicates if the receiver is a container element (dictionary or array).
77 | public var isContainer: Bool {
78 | switch self {
79 | case .object, .mutableObject, .array, .mutableArray:
80 | return true
81 | default:
82 | return false
83 | }
84 | }
85 |
86 | /// Indicates if the receiver is a mutable container (dictionary or array).
87 | public var isMutable: Bool {
88 | switch self {
89 | case .mutableObject, .mutableArray:
90 | return true
91 | case .object, .array, .string, .number, .null:
92 | return false
93 | }
94 | }
95 |
96 | /// Initialize a JSONElement with that is compatable with JSONSerialization.
97 | /// See JSONSerialization.isValidJSONObject for more details.
98 | ///
99 | /// - Parameters:
100 | /// - any: The raw value.
101 | public init(any: Any) throws {
102 | switch any {
103 | case let dict as NSMutableDictionary:
104 | self = .mutableObject(value: dict)
105 | case let dict as NSDictionary:
106 | self = .object(value: dict)
107 | case let array as NSMutableArray:
108 | self = .mutableArray(value: array)
109 | case let array as NSArray:
110 | self = .array(value: array)
111 | case let str as NSString:
112 | self = .string(value: str)
113 | case let num as NSNumber:
114 | self = .number(value: num)
115 | case is NSNull:
116 | self = .null
117 | default:
118 | throw JSONError.invalidObjectType
119 | }
120 | }
121 |
122 | /// Creates a new JSONElement with a copied raw value of the reciever.
123 | ///
124 | /// - Returns: A JSONElement representing a copy of the reciever.
125 | public func copy() throws -> JSONElement {
126 | switch rawValue {
127 | case let dict as NSDictionary:
128 | return try! JSONElement(any: dict.deepMutableCopy())
129 | case let arr as NSArray:
130 | return try! JSONElement(any: arr.deepMutableCopy())
131 | case let null as NSNull:
132 | return try! JSONElement(any: null)
133 | case let obj as NSObject:
134 | // Not all NSObject subclasses (e.g. NSNumber) supports mutableCopy
135 | // and will crash the app (NSInvalidArgumentException)
136 | // We could make a separate case above for NSNumber, but it's unknown
137 | // whether there are more of such cases.
138 | //
139 | // Since JSONElement's `isMutable` will return `false` anyway for these
140 | // types, we might just (normal-)copy the object instead
141 | //
142 | // See also: https://stackoverflow.com/questions/42074197/nsnumber-responds-positively-to-mutablecopy
143 | return try! JSONElement(any: obj.copy())
144 | default:
145 | throw JSONError.invalidObjectType
146 | }
147 | }
148 |
149 | // MARK: - Container Helper Methods
150 |
151 | /// Converts the receiver json element from a nonmutable container to a
152 | /// mutable container. .object will become .mutableObject and .array becomes
153 | /// .mutableArray. The raw value container will be copied to its mutable
154 | /// equivalent (e.g. NSMutableArray & NSMutableDictionary). This does NOT cause
155 | /// a deep copy. Only the reciever's raw value is copied to a new mutable container.
156 | /// If a deep copy is required then see the copy method.
157 | /// If the receiver is already mutable then this method has no effect.
158 | private mutating func makeMutable() {
159 | switch self {
160 | case .object(let dictionary):
161 | #if os(Linux)
162 | let mutable = dictionary.mutableCopy() as! NSMutableDictionary
163 | #else
164 | let mutable = NSMutableDictionary(dictionary: dictionary)
165 | #endif
166 | self = .mutableObject(value: mutable)
167 | case .array(let array):
168 | let mutable = NSMutableArray(array: array)
169 | self = .mutableArray(value: mutable)
170 | case .mutableObject, .mutableArray:
171 | break
172 | case .string, .number, .null:
173 | assertionFailure("Unsupported type to make mutable")
174 | break
175 | }
176 | }
177 |
178 | /// Converts the json elements along the path of evaluation for a json pointer into
179 | /// mutable equivalents.
180 | ///
181 | /// - Parameters:
182 | /// - pointer: The json pointer to identify the path of json element containers.
183 | /// - Returns: The last json element in the path.
184 | private mutating func makePathMutable(_ pointer: JSONPointer) throws -> JSONElement {
185 | if !self.isMutable {
186 | self.makeMutable()
187 | }
188 |
189 | guard pointer.string != "/" else {
190 | return self
191 | }
192 |
193 | var element = self
194 | for component in pointer {
195 | var child = try element.value(for: component)
196 | if !child.isMutable {
197 | child.makeMutable()
198 | try element.setValue(child, component: component, replace: true)
199 | }
200 | element = child
201 | }
202 |
203 | return element
204 | }
205 |
206 | /// This method is used to evalute a single component of a JSON Pointer.
207 | /// If the receiver represents a container (dictionary or array) with the JSON
208 | /// structure, then this method will get the value within the container referenced
209 | /// by the component of the JSON Pointer. If the receiver does not represent a
210 | /// container then an error is thrown.
211 | ///
212 | /// - Parameters:
213 | /// - component: A single component of a JSON Pointer to evaluate.
214 | /// - Returns: The referenced value.
215 | private func value(for component: String) throws -> JSONElement {
216 | switch self {
217 | case .object(let dictionary), .mutableObject(let dictionary as NSDictionary):
218 | guard let property = dictionary[component] else {
219 | throw JSONError.referencesNonexistentValue
220 | }
221 | let child = try JSONElement(any: property)
222 | return child
223 |
224 | case .array(let array) where component == "-",
225 | .mutableArray(let array as NSArray) where component == "-":
226 | guard let lastElement = array.lastObject else {
227 | throw JSONError.referencesNonexistentValue
228 | }
229 | let child = try JSONElement(any: lastElement)
230 | return child
231 |
232 | case .array(let array), .mutableArray(let array as NSArray):
233 | guard
234 | JSONPointer.isValidArrayIndex(component),
235 | let index = Int(component),
236 | 0.. 0 {
264 | array.replaceObject(at: array.count - 1, with: value.rawValue)
265 | } else {
266 | array.add(value.rawValue)
267 | }
268 | } else {
269 | guard
270 | JSONPointer.isValidArrayIndex(component),
271 | let index = Int(component),
272 | 0...array.count ~= index else {
273 | throw JSONError.referencesNonexistentValue
274 | }
275 | if replace {
276 | array.replaceObject(at: index, with: value.rawValue)
277 | } else {
278 | array.insert(value.rawValue, at: index)
279 | }
280 | }
281 | default:
282 | assertionFailure("Receiver is not a mutable container")
283 | break
284 | }
285 | }
286 |
287 | /// Remove a value from the receiver json element. This method is only valid for
288 | /// mutable container json elements. The given component of a json pointer identifies
289 | /// the value to remove. If the value referenced by the component does not exist
290 | /// then an error is thrown.
291 | ///
292 | /// - Parameters:
293 | /// - component: The component of a json pointer the identifies the value to remove.
294 | /// - Throws: JSONError.referencesNonexistentValue if the value is not found.
295 | private mutating func removeValue(component: String) throws {
296 | switch self {
297 | case .mutableObject(let dictionary):
298 | guard dictionary[component] != nil else {
299 | throw JSONError.referencesNonexistentValue
300 | }
301 | dictionary.removeObject(forKey: component)
302 | case .mutableArray(let array):
303 | if component == "-" {
304 | guard array.count > 0 else {
305 | throw JSONError.referencesNonexistentValue
306 | }
307 | array.removeLastObject()
308 | } else {
309 | guard
310 | JSONPointer.isValidArrayIndex(component),
311 | let index = Int(component),
312 | 0.. JSONElement {
330 | return try pointer.reduce(self, { return try $0.value(for: $1) })
331 | }
332 |
333 | // MARK:- Apply JSON Patch Operation Methods
334 |
335 | /// Adds the value to the JSON structure pointed to by the JSON Pointer.
336 | ///
337 | /// - Parameters:
338 | /// - value: A JSON Element holding a reference to the value to add.
339 | /// - pointer: A JSON Pointer of the location to insert the value.
340 | public mutating func add(value: JSONElement, to pointer: JSONPointer) throws {
341 | guard let parent = pointer.parent else {
342 | self = value
343 | return
344 | }
345 |
346 | var parentElement = try makePathMutable(parent)
347 | try parentElement.setValue(value, component: pointer.lastComponent!, replace: false)
348 | }
349 |
350 | /// Removes a value from a JSON structure pointed to by the JSON Pointer.
351 | ///
352 | /// - Parameters:
353 | /// - pointer: A JSON Pointer of the location of the value to remove.
354 | public mutating func remove(at pointer: JSONPointer) throws {
355 | guard let parent = pointer.parent else {
356 | self = .null
357 | return
358 | }
359 |
360 | var parentElement = try makePathMutable(parent)
361 | try parentElement.removeValue(component: pointer.lastComponent!)
362 | }
363 |
364 | /// Replaces a value at the location pointed to by the JSON Pointer with
365 | /// the given value. There must be an existing value to replace for this
366 | /// operation to be successful.
367 | ///
368 | /// - Parameters:
369 | /// - value: A JSON Element holding a reference to the value to add.
370 | /// - pointer: A JSON Pointer of the location of the value to replace.
371 | public mutating func replace(value: JSONElement, to pointer: JSONPointer) throws {
372 | guard let parent = pointer.parent else {
373 | self = value
374 | return
375 | }
376 |
377 | var parentElement = try makePathMutable(parent)
378 | _ = try parentElement.value(for: pointer.lastComponent!)
379 | try parentElement.setValue(value, component: pointer.lastComponent!, replace: true)
380 | }
381 |
382 | /// Moves a value at the from location to a new location within the JSON Structure.
383 | ///
384 | /// - Parameters:
385 | /// - from: The location of the JSON element to move.
386 | /// - to: The location to move the value to.
387 | public mutating func move(from: JSONPointer, to: JSONPointer) throws {
388 | guard let toParent = to.parent else {
389 | self = try evaluate(pointer: from)
390 | return
391 | }
392 |
393 | guard let fromParent = from.parent else {
394 | throw JSONError.referencesNonexistentValue
395 | }
396 |
397 | var fromParentElement = try makePathMutable(fromParent)
398 | let value = try fromParentElement.value(for: from.lastComponent!)
399 | try fromParentElement.removeValue(component: from.lastComponent!)
400 |
401 | var toParentElement = try makePathMutable(toParent)
402 | try toParentElement.setValue(value, component: to.lastComponent!, replace: false)
403 | }
404 |
405 | /// Copies a JSON element within the JSON structure to a new location.
406 | ///
407 | /// - Parameters:
408 | /// - from: The location of the value to copy.
409 | /// - to: The location to insert the new value.
410 | public mutating func copy(from: JSONPointer, to: JSONPointer) throws {
411 | guard let toParent = to.parent else {
412 | self = try evaluate(pointer: from)
413 | return
414 | }
415 |
416 | guard let fromParent = from.parent else {
417 | throw JSONError.referencesNonexistentValue
418 | }
419 |
420 | let fromParentElement = try makePathMutable(fromParent)
421 | var toParentElement = try makePathMutable(toParent)
422 | let value = try fromParentElement.value(for: from.lastComponent!)
423 | let valueCopy = try value.copy()
424 | try toParentElement.setValue(valueCopy, component: to.lastComponent!, replace: false)
425 | }
426 |
427 | /// Tests a value within the JSON structure against the given value.
428 | ///
429 | /// - Parameters:
430 | /// - value: The expected value.
431 | /// - pointer: The location of the value to test.
432 | public func test(value: JSONElement, at pointer: JSONPointer) throws {
433 | do {
434 | let found = try evaluate(pointer: pointer)
435 | if found != value {
436 | throw JSONError.patchTestFailed(path: pointer.string,
437 | expected: value.rawValue,
438 | found: found.rawValue)
439 | }
440 | } catch {
441 | throw JSONError.patchTestFailed(path: pointer.string,
442 | expected: value.rawValue,
443 | found: nil)
444 | }
445 | }
446 |
447 | /// Applys a json-patch operation to the reciever.
448 | ///
449 | /// - Parameters:
450 | /// - operation: The operation to apply.
451 | public mutating func apply(_ operation: JSONPatch.Operation, options: [JSONPatch.ApplyOption] = []) throws {
452 | do {
453 | switch operation {
454 | case let .add(path, value):
455 | try add(value: value, to: path)
456 | case let .remove(path):
457 | try remove(at: path)
458 | case let .replace(path, value):
459 | try replace(value: value, to: path)
460 | case let .move(from, path):
461 | try move(from: from, to: path)
462 | case let .copy(from, path):
463 | try copy(from: from, to: path)
464 | case let .test(path, value):
465 | try test(value: value, at: path)
466 | }
467 | } catch JSONError.referencesNonexistentValue where options.contains(.ignoreNonexistentValues) {
468 | // Don't throw, just continue
469 | }
470 | }
471 |
472 | /// Applys a json-patch to the the reciever. If a relative path is given then
473 | /// the json pointer is evaluated on the reciever, and then the patch is applied
474 | /// on the evaluted element.
475 | ///
476 | /// - Parameters:
477 | /// - patch: The json-patch to be applied.
478 | /// - options: The options for applying the patch.
479 | public mutating func apply(patch: JSONPatch,
480 | options: [JSONPatch.ApplyOption] = []) throws {
481 | var path: JSONPointer? = nil
482 | for case let .relative(pointer) in options {
483 | path = pointer
484 | break
485 | }
486 |
487 | if let path = path, let parent = path.parent {
488 | var parentElement = try makePathMutable(parent)
489 | var relativeRoot = try parentElement.value(for: path.lastComponent!)
490 | let relativeOptions = options.filter { if case .relative = $0 { return false } else { return true } }
491 | try relativeRoot.apply(patch: patch, options: relativeOptions)
492 | try parentElement.setValue(relativeRoot, component: path.lastComponent!, replace: true)
493 | } else {
494 | for operation in patch.operations {
495 | try apply(operation, options: options)
496 | }
497 | }
498 | }
499 |
500 | /// Applys a json-patch to the the reciever. If a relative path is given then
501 | /// the json pointer is evaluated on the reciever, and then the patch is applied
502 | /// on the evaluted element.
503 | ///
504 | /// - Parameters:
505 | /// - patch: The json-patch to be applied.
506 | /// - path: If present then the patch is applied to the child element at the path.
507 | @available(*, deprecated, message: "Use apply(patch: options:) instead")
508 | public mutating func apply(patch: JSONPatch, relativeTo path: JSONPointer) throws {
509 | return try apply(patch: patch, options: [.relative(to: path)])
510 | }
511 |
512 | }
513 |
514 | extension JSONElement: Equatable {
515 |
516 | /// Tests if two JSON Elements are structurally.
517 | ///
518 | /// - Parameters:
519 | /// - lhs: Left-hand side of the equality test.
520 | /// - rhs: Right-hand side of the equality test.
521 | /// - Returns: true if lhs and rhs are structurally, otherwise false.
522 | public static func == (lhs: JSONElement, rhs: JSONElement) -> Bool {
523 | guard let lobj = lhs.rawValue as? JSONEquatable else {
524 | return false
525 | }
526 | return lobj.isJSONEquals(to: rhs)
527 | }
528 |
529 | /// Determines if two json elements have equivalent types.
530 | ///
531 | /// - Parameters:
532 | /// - lhs: Left-hand side of the equivalent type test.
533 | /// - rhs: Right-hand side of the equivalent type test.
534 | /// - Returns: true if lhs and rhs are of equivalent types, otherwise false.
535 | public static func equivalentTypes(lhs: JSONElement, rhs: JSONElement) -> Bool {
536 | switch (lhs, rhs) {
537 | case (.object, .object),
538 | (.object, .mutableObject),
539 | (.mutableObject, .object),
540 | (.mutableObject, .mutableObject),
541 | (.array, .array),
542 | (.array, .mutableArray),
543 | (.mutableArray, .array),
544 | (.mutableArray, .mutableArray),
545 | (.string, .string),
546 | (.null, .null):
547 | return true
548 | case (.number(let numl), .number(let numr)):
549 | return numl.isBoolean == numr.isBoolean
550 | default:
551 | return false
552 | }
553 | }
554 |
555 | }
556 |
557 | extension JSONSerialization {
558 |
559 | /// Parse JSON data to a JSOE Element. This wraps the jsonObject(with:options:) method
560 | /// however the resuting Any type is wrapped in a JSONElement type.
561 | ///
562 | /// - Parameters:
563 | /// - data: The JSON data to parse. See jsonObject(with:options:) for supported encodings.
564 | /// - options: The reading options. See jsonObject(with:options:) for supported options.
565 | /// - Returns: The JSON Element representing the top-level element of the json document.
566 | public static func jsonElement(with data: Data, options: ReadingOptions) throws -> JSONElement {
567 | let jsonObject = try JSONSerialization.jsonObject(with: data, options: options)
568 | return try JSONElement(any: jsonObject)
569 | }
570 |
571 | /// Generate JSON data from a JSONElement using JSONSerialization. This method supports
572 | /// top-level fragments (root elements that are not containers).
573 | ///
574 | /// - Parameters:
575 | /// - jsonElement: The top-level json element to generate data for.
576 | /// - options: The wripting options for generating the json data.
577 | /// - Returns: A UTF-8 represention of the json document with the jsonElement as the root.
578 | public static func data(with jsonElement: JSONElement, options: WritingOptions = []) throws -> Data {
579 | // JSONSerialization only supports writing top-level containers.
580 | switch jsonElement {
581 | case let .object(obj as NSObject),
582 | let .mutableObject(obj as NSObject),
583 | let .array(obj as NSObject),
584 | let .mutableArray(obj as NSObject):
585 | return try JSONSerialization.data(withJSONObject: obj, options: options)
586 | default:
587 | // If the element is not a container then wrap the element in an array and the
588 | // return the sub-sequence of the result that represents the original element.
589 | let array = [jsonElement.rawValue]
590 | // We ignore the passed in writing options for this case, as it only effects
591 | // containers and could cause indexes to shift.
592 | let data = try JSONSerialization.data(withJSONObject: array, options: [])
593 | guard let arrayEndRange = data.range(of: Data("]".utf8),
594 | options: [.backwards],
595 | in: nil) else {
596 | throw JSONError.invalidObjectType
597 | }
598 | let subdata = data.subdata(in: data.index(after: data.startIndex)..