This commit is contained in:
Leif 2023-01-28 16:47:11 -07:00
commit a0f14d0c04
6 changed files with 264 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

27
Package.swift Normal file
View File

@ -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"]
)
]
)

65
README.md Normal file
View File

@ -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)]
```

View File

@ -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]
)
}
}

View File

@ -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: ())
}
}

View File

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