Skip to content

alexhunsley/patchouli-jsonpatch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

75 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Patchouli JSON Patcher

Apache 2 License Build System

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.

Installation

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".

Usage

    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.

DSL features

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)")
        }
    }

Nested patches

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.

Built on top of Patchouli, a generic patching engine

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.

Project Keywords

resultbuilder, protocol witness, generics, json, jsonpatch

License

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.