This is a JSON Patch library for Swift featuring an ergonomic DSL. It is built with the Patchouli Core engine and uses JSONPatch lib for the patch execution.
To install via SPM, add a dependency for https://github.com/alexhunsley/patchouli-jsonpatch
.
Or to take PatchouliJSON for a spin in a playground, visit https://swiftpackageindex.com/alexhunsley/patchouli-jsonpatch and select "Try in a playground".
import PatchouliJSON
// Use the DSL to construct a patch on a file's JSON content.
// This step doesn't do the actual patching!
let patchedJSONContent: PatchedJSON = Content(fileURL: someJSONFileURL) {
Add(address: "/users/-", jsonContent: "alex")
Add(address: "/users/-", jsonContent: [1, "hi", 5.0])
// note: please use `null` (and never `nil`) to represent JSON's `null`
Add(address: "/users/-", jsonContent: null)
Remove(address: "/temp/log")
Replace(address: "/user_type", jsonContent: "admin")
Move(fromAddress: "/v1/last_login", toAddress: "/v2/last_login")
Copy(fromAddress: "/v1/login_count", toAddress: "/v2/login_count")
Test(address: "/magic_string", jsonContent: "0xdeadbeef")
}
// execute the patch
let resultJSONContent: JSONContent = try content.reduced()
// Get the result in various forms
let jsonData = try dataResult.data()
let jsonString = try dataResult.string() // defaults to UTF8
let jsonStringUTF16 = try dataResult.string(encoding: utf16)
You can also fetch JSON content from bundles:
// loads User.json from the main bundle
let patchedJSONContent: PatchedJSON = Content(resource: "User", bundle: Bundle.main) {
Add(address: "/users/-", jsonContent: "alex")
...
And you can use string literals for your source json (utf8 is assumed):
let patchedJSONContent: PatchedJSON = Content(string:
"""
{ "greet": "Hello", "bye": "auf wiedersehen", "users": [] }
"""
) {
Add(address: "/users/-", jsonContent: "alex")
...
You can also use an empty JSON object or array as your starting point:
let patchedEmptyObject = EmptyJSONObject {
Add(address: "/asdf", jsonContent: "Hello")
}
let patchedEmptyArray1 = EmptyJSONArray {
Add(address: "/-", jsonContent: "xyz")
Add(address: "/-", jsonContent: "abc")
}
You can use @JSONPatchListBuilder
in the same way as SwiftUI's @ViewBuilder
to break up your patch declarations:
@JSONPatchListBuilder
func builderFunc(name: String) -> JSONPatchList {
Add(address: "/asdf", jsonContent: "Hello \(name)")
Add(address: "/qwer", jsonContent: "Bye \(name)")
}
@JSONPatchListBuilder
var builderVar: JSONPatchList {
Add(address: "/addr1", jsonContent: "Hello")
Add(address: "/addr2", jsonContent: "Bye")
builderFunc(name: "boo")
}
func useBuilderHelpers() throws {
let patchedJSONContent = Content(JSONPatchType.emptyObjectContent) {
builderFunc(name: "fred")
builderVar
}
This is particularly useful if you want to declare a patch list just once for use on multiple different bits of JSON.
The DSL can handle if
, if-else
, for
, for-in
, and optionals. For example:
let patchedJSONContent: PatchedJSON = Content(fileURL: someJSONFileURL) {
for index in 0...5 {
if someCheck(index) {
Add(address: "/abc", jsonContent: "\(index)")
}
if someCondition {
Add(address: "/cde", jsonContent: "\(index)")
} else {
Remove(address: "/cde")
}
}
for user in users {
Add(address: "/usernames/-", jsonContent: "\(user.username)")
}
}
The DSL can handle nesting, which means you can have patches-within-patches:
let patchedJSONContent: PatchedJSON = Content(fileURL: jsonFile1URL) {
Add(address: "/some_key", content: Content(fileURL: jsonFile2URL) {
Replace(address: "hello", jsonContent: "friend")
})
Remove(address: "/remove_me")
}
Note that with nested patching, the deepest operations are resolved first: in the above, the Replace
patch is applied to the contents of JSON file 2. The result of that is then added at /some_key
in the content from JSON file 1. And finally the Remove
is peformed on what we have.
Patchouli JSON is built on top of Patchouli Core, a generic patching engine. You can use Pathcouli Core to make patchers for other kinds of data.
resultbuilder, protocol witness, generics, json, jsonpatch
Copyright 2024 Alex Hunsley
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.