{"id":32151203,"url":"https://github.com/alexhunsley/patchouli-core","last_synced_at":"2026-02-21T15:02:23.289Z","repository":{"id":240083619,"uuid":"801618097","full_name":"alexhunsley/patchouli-core","owner":"alexhunsley","description":"Generic patching engine in Swift with a handy DSL","archived":false,"fork":false,"pushed_at":"2024-07-15T09:53:19.000Z","size":127,"stargazers_count":1,"open_issues_count":4,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-12T19:56:45.679Z","etag":null,"topics":["dsl","json","jsonpatch","patcher","patching","patchouli-patcher","resultbuilder","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexhunsley.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-16T15:22:50.000Z","updated_at":"2024-07-15T09:53:23.000Z","dependencies_parsed_at":"2024-05-21T21:48:18.774Z","dependency_job_id":"55fb92ef-cb4e-4adc-9f9a-14e0012d78ac","html_url":"https://github.com/alexhunsley/patchouli-core","commit_stats":null,"previous_names":["alexhunsley/patchouli-core"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/alexhunsley/patchouli-core","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexhunsley%2Fpatchouli-core","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexhunsley%2Fpatchouli-core/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexhunsley%2Fpatchouli-core/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexhunsley%2Fpatchouli-core/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexhunsley","download_url":"https://codeload.github.com/alexhunsley/patchouli-core/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexhunsley%2Fpatchouli-core/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29659752,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-20T16:33:43.953Z","status":"ssl_error","status_checked_at":"2026-02-20T16:33:43.598Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","json","jsonpatch","patcher","patching","patchouli-patcher","resultbuilder","swift"],"created_at":"2025-10-21T10:38:17.287Z","updated_at":"2026-02-21T15:02:23.281Z","avatar_url":"https://github.com/alexhunsley.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://github.com/alexhunsley/patchouli-jsonpatch/blob/main/Tests/Resources/patchouli-core-logo-1-scaled-378x379.jpg\"\u003e\n\u003c/div\u003e\n\n[![Apache 2 License](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Falexhunsley%2Fpatchouli-core%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/alexhunsley/patchouli-core)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Falexhunsley%2Fpatchouli-core%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/alexhunsley/patchouli-core)\n[![Build System](https://img.shields.io/badge/dependency%20management-spm-yellow.svg)](https://swift.org/package-manager/)\n\nPatchouli Core is a generic patching engine and DSL for Swift, based on JSON Patch's operations (`Add`, `Remove`, `Replace`, `Copy`, `Move`, and `Test`).\n\nIt is used by [Patchouli JSON](https://github.com/alexhunsley/patchouli-jsonpatch).\n\n# How Patchouli Core works\nIt has two major parts: a DSL that feels similar to SwiftUI, for constructing the patch, and a tree reducer which then performs the patching using appropriate functions.\n\nThe representation of patchable data and the DSL are both generic, which means that you can write a patcher for anything you like.\n\nPatchouli Core contains a toy string patcher for demonstration purposes:\n\n```swift\n// Input: \"Hello World\"\n// Patched result: \"Goodbye my friend\"\n\nlet stringPatchContent: StringPatchContent = Content(\"Hello World\") {\n    Replace(address: \"Hello\", with: \"Goodbye\")\n    Replace(address: \"World\", with: \"my friend\")\n}\n\nlet result: String = try stringPatchContent.reduced()\n```\n\n# Writing a custom patcher using Patchouli Core\n\nPatchouli Core contains a toy patcher example: a string patcher (see `StringPatchType.swift`). We'll use that here to demonstrate how to write a patcher for any data type you like.\n\nFirstly, we have to define what the content type is that we're patching, and what the address type is. An address is some data that can locate one or more parts in a piece of the content type.\n\n```swift\npublic struct StringPatchType: PatchType {\n    // ContentType: A string patcher works on strings\n    public typealias ContentType = String\n\n    // AddressType: we identify one or more parts of a string (for patching) with a (sub)string.\n    public typealias AddressType = String\n}\n```\n\nTo this struct we add a definition of `empty`; this is just an instance of `ContentType` that is considered 'empty content':\n\n```swift\n   public static var emptyContent: ContentType = \"\"\n```\n\nAnd finally, our struct needs to be told how to perform the various kinds of patching operation possible. To do this, we add a **protocol witness** to the struct, which looks like this:\n\n```swift\n    /// The Protocol Witness used by the reducer\n    static public let patcher = Patchable\u003cStringPatchType\u003e(\n        added: { (container: String, content: String, address: String) -\u003e String in\n            // We interpret 'add' in string matching to mean \"place a copy of content\n            // before every occurence of the address\".\n            // if the address isn't found in the string, we don't care.\n            container.prefixing(address, with: content)\n        },\n        removed: { (container: String, address: String) in\n            container.replacingOccurrences(of: address, with: \"\")\n        },\n        replaced: { (container: String, replacement: String, address: String) -\u003e String in\n            // NB this replaces all occurrences!\n            // But that’s expected for a content-based Address\n            container.replacingOccurrences(of: address, with: replacement)\n        },\n        // a 'copy' operation doesn't really make sense for a string pather, so we don't provide one\n        //    copied: {\n        moved: { (container: String, fromAddress: String, toAddress: String) -\u003e String in\n            container\n                .replacingOccurrences(of: fromAddress, with: \"\")\n                .replacingOccurrences(of: toAddress, with: fromAddress)\n        },\n        // we don't care about the expectedContent (2nd param) for our 'test' operation,\n        // because in this string patcher, the address *is* the content\n        test: { (container: String, _: String, address: String) in\n            if !container.contains(address) {\n                // your implementation must throw this error when the test operation has failed\n                throw PatchouliError\u003cStringPatchType\u003e.testFailed(container, address, address)\n            }\n            return container\n        }\n    )\n```\n\nNote that we don't provide an implementation of `copy` for our string patcher. Every kind of operation is optional when you write a patcher, but providing at least one is recommended :)\n(If the user of the DSL tries to execute a `copy` operation with this string patcher, the call to `reduced()` will throw a descriptive error.)\n\nAnd that's all you need to do to get a working custom patcher.\n\nTo pull it all together, the entire `StringPatchType` definition is this:\n\n```swift\npublic struct StringPatchType: PatchType {\n    // ContentType: A string patcher works on strings\n    public typealias ContentType = String\n\n    // AddressType: we identify one or more parts of a string (for patching) with a (sub)string.\n    public typealias AddressType = String\n\n    public static var emptyContent: ContentType = \"\"\n\n    /// The Protocol Witness used by the reducer\n    static public let patcher = Patchable\u003cStringPatchType\u003e(\n        added: { (container: String, content: String, address: String) -\u003e String in\n            // We interpret 'add' in string matching to mean \"place a copy of content\n            // before every occurence of the address\".\n            // if the address isn't found in the string, we don't care.\n            container.prefixing(address, with: content)\n        },\n        removed: { (container: String, address: String) in\n            container.replacingOccurrences(of: address, with: \"\")\n        },\n        replaced: { (container: String, replacement: String, address: String) -\u003e String in\n            // NB this replaces all occurrences!\n            // But that’s expected for a content-based Address\n            container.replacingOccurrences(of: address, with: replacement)\n        },\n        // a 'copy' operation doesn't really make sense for a string pather, so we don't provide one\n        //    copied: {\n        moved: { (container: String, fromAddress: String, toAddress: String) -\u003e String in\n            container\n                .replacingOccurrences(of: fromAddress, with: \"\")\n                .replacingOccurrences(of: toAddress, with: fromAddress)\n        },\n        // we don't care about the expectedContent (2nd param) for our 'test' operation,\n        // because in this string patcher, the address *is* the content\n        test: { (container: String, _: String, address: String) in\n            if !container.contains(address) {\n                // your implementation must throw this error when the test operation has failed\n                throw PatchouliError\u003cStringPatchType\u003e.testFailed(container, address, address)\n            }\n            return container\n        }\n    )\n}\n```\n\n# Refining your custom patcher\n\nThe above is the bare minimum for a custom patcher. You can improve it beyond that by adding conveneniences for the DSL.\n\nFor example, the toy String patcher contains the following convenience:\n\n```swift\n/// Convenience for string patcher's test method that doesn't require an address param\n/// (the expected content is all we need, we're checking to see if it's in the string)\npublic func Test(expectedContent: String) -\u003e AddressedPatch\u003cStringPatchType\u003e {\n    // Note we give expectedContent for the address as well as the expectedContent,\n    // as it's required for this patcher func (but not used in this string patcher)\n    return AddressedPatch(patchSpec: .test(expectedContent, expectedContent),\n                          contentPatch: PatchedContent\u003cStringPatchType\u003e(content: expectedContent))\n}\n```\n\n# License\n\n```\nCopyright 2024 Alex Hunsley\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexhunsley%2Fpatchouli-core","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexhunsley%2Fpatchouli-core","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexhunsley%2Fpatchouli-core/lists"}