Patchouli Core 是一个通用的 Swift 补丁引擎和 DSL,它基于 JSON Patch 的操作(Add
、Remove
、Replace
、Copy
、Move
和 Test
)。
它被 Patchouli JSON 使用。
它主要包含两个部分:一个类似于 SwiftUI 的 DSL,用于构建补丁;以及一个树归约器,用于使用适当的函数来执行补丁。
可修补数据的表示和 DSL 都是通用的,这意味着您可以为任何您喜欢的东西编写补丁程序。
Patchouli Core 包含一个玩具字符串补丁程序,用于演示目的
// Input: "Hello World"
// Patched result: "Goodbye my friend"
let stringPatchContent: StringPatchContent = Content("Hello World") {
Replace(address: "Hello", with: "Goodbye")
Replace(address: "World", with: "my friend")
}
let result: String = try stringPatchContent.reduced()
Patchouli Core 包含一个玩具补丁程序示例:一个字符串补丁程序(参见 StringPatchType.swift
)。我们将在这里使用它来演示如何为任何您喜欢的数据类型编写补丁程序。
首先,我们必须定义我们要修补的内容类型是什么,以及地址类型是什么。地址是一些可以定位内容类型中的一个或多个部分的数据。
public struct StringPatchType: PatchType {
// ContentType: A string patcher works on strings
public typealias ContentType = String
// AddressType: we identify one or more parts of a string (for patching) with a (sub)string.
public typealias AddressType = String
}
我们向该结构体添加一个 empty
的定义;这只是一个 ContentType
的实例,被认为是“空内容”
public static var emptyContent: ContentType = ""
最后,我们的结构体需要知道如何执行各种可能的修补操作。为此,我们向结构体添加一个协议见证,如下所示
/// The Protocol Witness used by the reducer
static public let patcher = Patchable<StringPatchType>(
added: { (container: String, content: String, address: String) -> String in
// We interpret 'add' in string matching to mean "place a copy of content
// before every occurence of the address".
// if the address isn't found in the string, we don't care.
container.prefixing(address, with: content)
},
removed: { (container: String, address: String) in
container.replacingOccurrences(of: address, with: "")
},
replaced: { (container: String, replacement: String, address: String) -> String in
// NB this replaces all occurrences!
// But that’s expected for a content-based Address
container.replacingOccurrences(of: address, with: replacement)
},
// a 'copy' operation doesn't really make sense for a string pather, so we don't provide one
// copied: {
moved: { (container: String, fromAddress: String, toAddress: String) -> String in
container
.replacingOccurrences(of: fromAddress, with: "")
.replacingOccurrences(of: toAddress, with: fromAddress)
},
// we don't care about the expectedContent (2nd param) for our 'test' operation,
// because in this string patcher, the address *is* the content
test: { (container: String, _: String, address: String) in
if !container.contains(address) {
// your implementation must throw this error when the test operation has failed
throw PatchouliError<StringPatchType>.testFailed(container, address, address)
}
return container
}
)
请注意,我们没有为字符串补丁程序提供 copy
的实现。当您编写补丁程序时,每种操作都是可选的,但建议至少提供一个 :) (如果 DSL 的用户尝试使用此字符串补丁程序执行 copy
操作,则对 reduced()
的调用将抛出一个描述性错误。)
这就是您获得一个可工作的自定义补丁程序所需做的全部工作。
为了将其整合在一起,整个 StringPatchType
的定义如下
public struct StringPatchType: PatchType {
// ContentType: A string patcher works on strings
public typealias ContentType = String
// AddressType: we identify one or more parts of a string (for patching) with a (sub)string.
public typealias AddressType = String
public static var emptyContent: ContentType = ""
/// The Protocol Witness used by the reducer
static public let patcher = Patchable<StringPatchType>(
added: { (container: String, content: String, address: String) -> String in
// We interpret 'add' in string matching to mean "place a copy of content
// before every occurence of the address".
// if the address isn't found in the string, we don't care.
container.prefixing(address, with: content)
},
removed: { (container: String, address: String) in
container.replacingOccurrences(of: address, with: "")
},
replaced: { (container: String, replacement: String, address: String) -> String in
// NB this replaces all occurrences!
// But that’s expected for a content-based Address
container.replacingOccurrences(of: address, with: replacement)
},
// a 'copy' operation doesn't really make sense for a string pather, so we don't provide one
// copied: {
moved: { (container: String, fromAddress: String, toAddress: String) -> String in
container
.replacingOccurrences(of: fromAddress, with: "")
.replacingOccurrences(of: toAddress, with: fromAddress)
},
// we don't care about the expectedContent (2nd param) for our 'test' operation,
// because in this string patcher, the address *is* the content
test: { (container: String, _: String, address: String) in
if !container.contains(address) {
// your implementation must throw this error when the test operation has failed
throw PatchouliError<StringPatchType>.testFailed(container, address, address)
}
return container
}
)
}
以上是自定义补丁程序的最低要求。您可以通过为 DSL 添加便利性来进一步改进它。
例如,玩具字符串补丁程序包含以下便利性
/// Convenience for string patcher's test method that doesn't require an address param
/// (the expected content is all we need, we're checking to see if it's in the string)
public func Test(expectedContent: String) -> AddressedPatch<StringPatchType> {
// Note we give expectedContent for the address as well as the expectedContent,
// as it's required for this patcher func (but not used in this string patcher)
return AddressedPatch(patchSpec: .test(expectedContent, expectedContent),
contentPatch: PatchedContent<StringPatchType>(content: expectedContent))
}
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
https://apache.ac.cn/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.