├── 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 | ![Step 1](images/xcode11-step1.jpg) 7 | 8 | Search for `swift-jsonpatch`, select the repository and click the `Next` button. 9 | ![Step 2](images/xcode11-step2.jpg) 10 | 11 | Enter the version number 1 and click the `Next` button. 12 | ![Step 3](images/xcode11-step3.jpg) 13 | 14 | Click the `Finish` button. 15 | ![Step 4](images/xcode11-step4.jpg) 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 | [![Apache 2 License](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://opensource.org/licenses/Apache-2.0) 3 | [![Supported Platforms](https://img.shields.io/badge/platform-ios%20%7C%20macos%20%7C%20tvos-lightgrey.svg)](http://developer.apple.com) 4 | [![Build System](https://img.shields.io/badge/dependency%20management-spm%20%7C%20cocoapods-yellow.svg)](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)..