init
This commit is contained in:
commit
a0f14d0c04
|
@ -0,0 +1,9 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -0,0 +1,27 @@
|
|||
// swift-tools-version: 5.6
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Plugin",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
.iOS(.v14),
|
||||
.watchOS(.v7),
|
||||
.tvOS(.v14)
|
||||
],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "Plugin",
|
||||
targets: ["Plugin"]
|
||||
)
|
||||
],
|
||||
targets: [
|
||||
.target(name: "Plugin"),
|
||||
.testTarget(
|
||||
name: "PluginTests",
|
||||
dependencies: ["Plugin"]
|
||||
)
|
||||
]
|
||||
)
|
|
@ -0,0 +1,65 @@
|
|||
# Plugin
|
||||
|
||||
*Plug and Play*
|
||||
|
||||
This package provides a way to extend the functionality of objects through the use of plugins.
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a plugin
|
||||
|
||||
To create a plugin, create a struct or class that conforms to the Plugin protocol and implement its requirements.
|
||||
|
||||
Here's an example of a simple plugin that adds an `auth` header to a `URLRequest` object:
|
||||
|
||||
```swift
|
||||
struct AuthPlugin: Plugin {
|
||||
var keyPath: WritableKeyPath<PluginURLRequest, URLRequest>
|
||||
|
||||
func handle(value: Void, output: inout URLRequest) async throws {
|
||||
output.allHTTPHeaderFields = ["auth": "token"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Making an object pluginable
|
||||
|
||||
To use plugins with an object, make it conform to the `Pluginable` protocol. Here's an example of a `PluginURLRequest` class that can have plugins added to it:
|
||||
|
||||
```swift
|
||||
class PluginURLRequest: Pluginable {
|
||||
var plugins: [any Plugin] = []
|
||||
|
||||
var request: URLRequest
|
||||
|
||||
init(url: URL) {
|
||||
self.request = URLRequest(url: url)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering and handling plugins
|
||||
|
||||
Plugins can be registered to a `Pluginable` object using the `register(plugin: any Plugin)` method. Once registered, the `handle(value: Any)` method can be called on the object to apply the registered plugins to it.
|
||||
|
||||
```swift
|
||||
let urlRequest = PluginURLRequest(url: URL(string: "https://example.com")!)
|
||||
|
||||
urlRequest.register(plugin: AuthPlugin())
|
||||
|
||||
try await urlRequest.handle()
|
||||
print(urlRequest.request.allHTTPHeaderFields) // ["auth": "token"]
|
||||
```
|
||||
|
||||
### Additional Features
|
||||
|
||||
You can also access the `inputType` and `outputType` properties of any plugin and get the list of plugins with their `inputType` and `outputType` properties.
|
||||
|
||||
```swift
|
||||
let authPlugin = AuthPlugin()
|
||||
print(authPlugin.inputType) // Void
|
||||
print(authPlugin.outputType) // URLRequest
|
||||
|
||||
let pluginTypes = urlRequest.pluginTypes
|
||||
print(pluginTypes) // [(input: Void, output: URLRequest)]
|
||||
```
|
|
@ -0,0 +1,34 @@
|
|||
public protocol Plugin {
|
||||
associatedtype Source
|
||||
associatedtype Input
|
||||
associatedtype Output
|
||||
|
||||
var keyPath: WritableKeyPath<Source, Output> { get set }
|
||||
|
||||
func handle(value: Input, output: inout Output) async throws
|
||||
}
|
||||
|
||||
// MARK: Internal Implementation
|
||||
|
||||
internal extension Plugin {
|
||||
var inputType: String { "\(Input.self)" }
|
||||
var outputType: String { "\(Output.self)" }
|
||||
|
||||
func isValid(value: Any) -> Bool {
|
||||
guard let _ = value as? Input else { return false }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func handle(_ value: Any, source: inout Any) async throws {
|
||||
guard
|
||||
let value = value as? Input,
|
||||
var source = source as? Source
|
||||
else { return }
|
||||
|
||||
try await handle(
|
||||
value: value,
|
||||
output: &source[keyPath: keyPath]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
public protocol Pluginable: AnyObject {
|
||||
var plugins: [any Plugin] { get set }
|
||||
|
||||
func register(plugin: any Plugin)
|
||||
func handle(value: Any) async throws
|
||||
}
|
||||
|
||||
// MARK: Public Implementation
|
||||
|
||||
public extension Pluginable {
|
||||
var pluginInputTypes: [String] { plugins.map(\.inputType) }
|
||||
var pluginOutputTypes: [String] { plugins.map(\.outputType) }
|
||||
var pluginTypes: [String] {
|
||||
zip(pluginInputTypes, pluginOutputTypes)
|
||||
.map { inputType, outputType in
|
||||
"(input: \(inputType), output: \(outputType))"
|
||||
}
|
||||
}
|
||||
|
||||
func register(plugin: any Plugin) {
|
||||
plugins.append(plugin)
|
||||
}
|
||||
|
||||
func handle(value: Any) async throws {
|
||||
let validPlugins: [any Plugin] = plugins
|
||||
.filter { plugin in
|
||||
plugin.isValid(value: value)
|
||||
}
|
||||
|
||||
for plugin in validPlugins {
|
||||
var source = self as Any
|
||||
try await plugin.handle(value, source: &source)
|
||||
}
|
||||
}
|
||||
|
||||
func handle() async throws {
|
||||
try await handle(value: ())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import XCTest
|
||||
@testable import Plugin
|
||||
|
||||
final class PluginTests: XCTestCase {
|
||||
func testExample() async throws {
|
||||
class MockService: Pluginable {
|
||||
var plugins: [any Plugin] = []
|
||||
|
||||
var count: Int = 0
|
||||
}
|
||||
|
||||
struct ExamplePlugin: Plugin {
|
||||
var keyPath: WritableKeyPath<MockService, Int>
|
||||
|
||||
func handle(value: Void, output: inout Int) async throws {
|
||||
output += 1
|
||||
}
|
||||
}
|
||||
|
||||
let service = MockService()
|
||||
|
||||
XCTAssertEqual(service.pluginTypes, [])
|
||||
|
||||
service.register(
|
||||
plugin: ExamplePlugin(
|
||||
keyPath: \.count
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(service.pluginTypes, ["(input: (), output: Int)"])
|
||||
|
||||
try await service.handle(value: "Woot")
|
||||
|
||||
XCTAssertEqual(service.count, 0)
|
||||
|
||||
try await service.handle()
|
||||
|
||||
XCTAssertEqual(service.count, 1)
|
||||
|
||||
service.register(
|
||||
plugin: ExamplePlugin(
|
||||
keyPath: \.count
|
||||
)
|
||||
)
|
||||
|
||||
XCTAssertEqual(service.pluginTypes.count, 2)
|
||||
|
||||
try await service.handle()
|
||||
|
||||
XCTAssertEqual(service.count, 3)
|
||||
}
|
||||
|
||||
func testURLRequestExample() async throws {
|
||||
class PluginURLRequest: Pluginable {
|
||||
var plugins: [any Plugin] = []
|
||||
|
||||
var request: URLRequest
|
||||
|
||||
init(url: URL) {
|
||||
self.request = URLRequest(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
struct AuthPlugin: Plugin {
|
||||
var keyPath: WritableKeyPath<PluginURLRequest, URLRequest>
|
||||
|
||||
func handle(value: Void, output: inout URLRequest) async throws {
|
||||
output.allHTTPHeaderFields = ["auth": "token"]
|
||||
}
|
||||
}
|
||||
|
||||
let urlRequest = PluginURLRequest(
|
||||
url: try XCTUnwrap(URL(string: "localhost"))
|
||||
)
|
||||
|
||||
XCTAssertEqual(urlRequest.pluginTypes, [])
|
||||
|
||||
XCTAssertNil(urlRequest.request.allHTTPHeaderFields)
|
||||
|
||||
urlRequest.register(plugin: AuthPlugin(keyPath: \.request))
|
||||
|
||||
XCTAssertEqual(urlRequest.pluginTypes, ["(input: (), output: URLRequest)"])
|
||||
|
||||
try await urlRequest.handle()
|
||||
|
||||
let token = try XCTUnwrap(urlRequest.request.allHTTPHeaderFields)["auth"]
|
||||
XCTAssertEqual(token, "token")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue