Skip to content

Commit

Permalink
Implement SPICE-0009 External Readers (#26)
Browse files Browse the repository at this point in the history
* Add `EvaluatorOptions.externalModuleReaders` and `EvaluatorOptions.externalResourceReaders`.
* Add `ExternalReaderClient` to host the child process side of the external reader workflow.
  • Loading branch information
HT154 authored Jan 17, 2025
1 parent 7d451c5 commit 64c66ae
Show file tree
Hide file tree
Showing 15 changed files with 627 additions and 78 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
.build
.index-build
/Packages
/*.xcodeproj
xcuserdata/
Expand Down
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ let package = Package(
],
resources: [.embedInCode("Resources/VERSION.txt")]
),
.executableTarget(
name: "test-external-reader",
dependencies: ["PklSwift"]
),
.testTarget(
name: "PklSwiftTests",
dependencies: [
Expand Down
9 changes: 6 additions & 3 deletions Sources/MessagePack/Encoder/MessagePackEncoder.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -44,10 +44,13 @@ public final class MessagePackEncoder {
try Box<Data>(data).encode(to: encoder)
case let date as Date:
try Box<Date>(date).encode(to: encoder)
case let bytes as [UInt8]:
try Box<[UInt8]>(bytes).encode(to: encoder)
case let url as URL:
try Box<URL>(url).encode(to: encoder)
case let bytes as [UInt8]:
guard type(of: value) == [UInt8].self else {
fallthrough
}
try Box<[UInt8]>(bytes).encode(to: encoder)
default:
try value.encode(to: encoder)
}
Expand Down
11 changes: 8 additions & 3 deletions Sources/MessagePack/Encoder/SingleValueEncodingContainer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -304,8 +304,6 @@ extension _MessagePackEncoder.SingleValueContainer: SingleValueEncodingContainer
try self.encode(data)
case let date as Date:
try self.encode(date)
case let bytes as [UInt8]:
try self.encode(bytes)
case let url as URL:
try self.encode(url)
case let bool as Bool:
Expand Down Expand Up @@ -334,9 +332,16 @@ extension _MessagePackEncoder.SingleValueContainer: SingleValueEncodingContainer
try self.encode(uint64)
case let num as any BinaryInteger & Encodable:
try self.encode(num)
case let bytes as [UInt8]:
guard type(of: value) == [UInt8].self else {
fallthrough
}
try self.encode(bytes)
default:
let writer: Writer = BufferWriter()
let encoder = _MessagePackEncoder()
encoder.userInfo = userInfo
encoder.codingPath = codingPath
try value.encode(to: encoder)
try encoder.write(into: writer)
let data = (writer as! BufferWriter).bytes
Expand Down
9 changes: 7 additions & 2 deletions Sources/PklSwift/EvaluatorManager.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -108,7 +108,7 @@ public actor EvaluatorManager {

// note; when our C bindings are released, change `init()` based on compiler flags.
public init() {
self.init(transport: ChildProcessMessageTransport())
self.init(transport: ServerMessageTransport())
}

// Used for testing only.
Expand Down Expand Up @@ -264,6 +264,9 @@ public actor EvaluatorManager {
guard options.http == nil || version >= pklVersion0_26 else {
throw PklError("http options are not supported on Pkl versions lower than 0.26")
}
guard (options.externalModuleReaders == nil && options.externalResourceReaders == nil) || version >= pklVersion0_27 else {
throw PklError("external reader options are not supported on Pkl versions lower than 0.27")
}
let req = options.toMessage()
guard let response = try await ask(req) as? CreateEvaluatorResponse else {
throw PklBugError.invalidMessageCode(
Expand Down Expand Up @@ -371,8 +374,10 @@ enum PklBugError: Error {

let pklVersion0_25 = SemanticVersion("0.25.0")!
let pklVersion0_26 = SemanticVersion("0.26.0")!
let pklVersion0_27 = SemanticVersion("0.27.0")!

let supportedPklVersions = [
pklVersion0_25,
pklVersion0_26,
pklVersion0_27,
]
26 changes: 23 additions & 3 deletions Sources/PklSwift/EvaluatorOptions.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
// Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -32,7 +32,9 @@ public struct EvaluatorOptions {
logger: Logger = Loggers.noop,
projectBaseURI: URL? = nil,
http: Http? = nil,
declaredProjectDependencies: [String: ProjectDependency]? = nil
declaredProjectDependencies: [String: ProjectDependency]? = nil,
externalModuleReaders: [String: ExternalReader]? = nil,
externalResourceReaders: [String: ExternalReader]? = nil
) {
self.allowedModules = allowedModules
self.allowedResources = allowedResources
Expand All @@ -49,6 +51,8 @@ public struct EvaluatorOptions {
self.projectBaseURI = projectBaseURI
self.http = http
self.declaredProjectDependencies = declaredProjectDependencies
self.externalModuleReaders = externalModuleReaders
self.externalResourceReaders = externalResourceReaders
}

/// Regular expression patterns that control what modules are allowed to be imported in a Pkl program.
Expand Down Expand Up @@ -123,6 +127,18 @@ public struct EvaluatorOptions {
/// When importing dependencies, a `PklProject.deps.json` file must exist within ``projectBaseURI``
/// that contains the project's resolved dependencies.
public var declaredProjectDependencies: [String: ProjectDependency]?

/// Registered external commands that implement module reader schemes.
///
/// Added in Pkl 0.27.
/// If the underlying Pkl does not support external readers, evaluation will fail when a registered scheme is used.
public var externalModuleReaders: [String: ExternalReader]?

/// Registered external commands that implement resource reader schemes.
///
/// Added in Pkl 0.27.
/// If the underlying Pkl does not support external readers, evaluation will fail when a registered scheme is used.
public var externalResourceReaders: [String: ExternalReader]?
}

extension EvaluatorOptions {
Expand Down Expand Up @@ -162,7 +178,9 @@ extension EvaluatorOptions {
cacheDir: self.cacheDir,
outputFormat: self.outputFormat,
project: self.project(),
http: self.http
http: self.http,
externalModuleReaders: self.externalModuleReaders,
externalResourceReaders: self.externalResourceReaders
)
}

Expand Down Expand Up @@ -245,6 +263,8 @@ extension EvaluatorOptions {
options.cacheDir = evaluatorSettings.noCache != nil ? nil : (evaluatorSettings.moduleCacheDir ?? self.cacheDir)
options.rootDir = evaluatorSettings.rootDir ?? self.rootDir
options.http = evaluatorSettings.http ?? self.http
options.externalModuleReaders = evaluatorSettings.externalModuleReaders ?? options.externalModuleReaders
options.externalResourceReaders = evaluatorSettings.externalResourceReaders ?? options.externalResourceReaders
return options
}

Expand Down
208 changes: 208 additions & 0 deletions Sources/PklSwift/ExternalReaderClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// ===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Pkl project authors. All rights reserved.
//
// 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://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.
// ===----------------------------------------------------------------------===//

import Foundation
import MessagePack

// Example usage:
// import PklSwift
// @main
// struct Main {
// static func main() async throws {
// let client = ExternalReaderClient(
// options: ExternalReaderClientOptions(
// resourceReaders: [MyResourceReader()]
// ))
// try await client.run()
// }
// }

public struct ExternalReaderClientOptions {
/// Reader to receive requests.
public var requestReader: Reader = FileHandle.standardInput

/// Writer to publish responses.
public var responseWriter: Writer = FileHandle.standardOutput

/// Readers that allow reading custom resources in Pkl.
public var moduleReaders: [ModuleReader] = []

/// Readers that allow importing custom modules in Pkl.
public var resourceReaders: [ResourceReader] = []

public init(
requestReader: Reader = FileHandle.standardInput,
responseWriter: Writer = FileHandle.standardOutput,
moduleReaders: [ModuleReader] = [],
resourceReaders: [ResourceReader] = []
) {
self.requestReader = requestReader
self.responseWriter = responseWriter
self.moduleReaders = moduleReaders
self.resourceReaders = resourceReaders
}
}

public class ExternalReaderClient {
private let moduleReaders: [ModuleReader]
private let resourceReaders: [ResourceReader]
private let transport: MessageTransport

public init(options: ExternalReaderClientOptions) {
self.moduleReaders = options.moduleReaders
self.resourceReaders = options.resourceReaders
self.transport = ExternalReaderMessageTransport(
reader: options.requestReader, writer: options.responseWriter
)
}

public func run() async throws {
for try await message in try self.transport.getMessages() {
switch message {
case let message as InitializeModuleReaderRequest:
try self.handleInitializeModuleReaderRequest(request: message)
case let message as InitializeResourceReaderRequest:
try self.handleInitializeResourceReaderRequest(request: message)
case let message as ReadModuleRequest:
try await self.handleReadModuleRequest(request: message)
case let message as ReadResourceRequest:
try await self.handleReadResourceRequest(request: message)
case let message as ListModulesRequest:
try await self.handleListModulesRequest(request: message)
case let message as ListResourcesRequest:
try await self.handleListResourcesRequest(request: message)
case _ as CloseExternalProcess:
self.close()
default:
throw PklBugError.unknownMessage("Got request for unknown message: \(message)")
}
}
}

public func close() {
self.transport.close()
}

func handleInitializeModuleReaderRequest(request: InitializeModuleReaderRequest) throws {
var response = InitializeModuleReaderResponse(requestId: request.requestId, spec: nil)
guard let reader = moduleReaders.first(where: { $0.scheme == request.scheme }) else {
try self.transport.send(response)
return
}
response.spec = reader.toMessage()
try self.transport.send(response)
}

func handleInitializeResourceReaderRequest(request: InitializeResourceReaderRequest) throws {
var response = InitializeResourceReaderResponse(requestId: request.requestId, spec: nil)
guard let reader = resourceReaders.first(where: { $0.scheme == request.scheme }) else {
try self.transport.send(response)
return
}
response.spec = reader.toMessage()
try self.transport.send(response)
}

func handleReadModuleRequest(request: ReadModuleRequest) async throws {
var response = ReadModuleResponse(
requestId: request.requestId,
evaluatorId: request.evaluatorId,
contents: nil,
error: nil
)
guard let reader = moduleReaders.first(where: { $0.scheme == request.uri.scheme }) else {
response.error = "No module reader found for scheme \(request.uri.scheme!)"
try self.transport.send(response)
return
}
do {
let result = try await reader.read(url: request.uri)
response.contents = result
try self.transport.send(response)
} catch {
response.error = "\(error)"
try self.transport.send(response)
}
}

func handleReadResourceRequest(request: ReadResourceRequest) async throws {
var response = ReadResourceResponse(
requestId: request.requestId,
evaluatorId: request.evaluatorId,
contents: nil,
error: nil
)
guard
let reader = resourceReaders.first(where: { $0.scheme == request.uri.scheme }) else {
response.error = "No resource reader found for scheme \(request.uri.scheme!)"
try self.transport.send(response)
return
}
do {
let result = try await reader.read(url: request.uri)
response.contents = result
try self.transport.send(response)
} catch {
response.error = "\(error)"
try self.transport.send(response)
}
}

func handleListModulesRequest(request: ListModulesRequest) async throws {
var response = ListModulesResponse(
requestId: request.requestId,
evaluatorId: request.evaluatorId,
pathElements: nil,
error: nil
)
guard let reader = moduleReaders.first(where: { $0.scheme == request.uri.scheme }) else {
response.error = "No module reader found for scheme \(request.uri.scheme!)"
try self.transport.send(response)
return
}
do {
let elems = try await reader.listElements(uri: request.uri)
response.pathElements = elems.map { $0.toMessage() }
try self.transport.send(response)
} catch {
response.error = "\(error)"
try self.transport.send(response)
}
}

func handleListResourcesRequest(request: ListResourcesRequest) async throws {
var response = ListResourcesResponse(
requestId: request.requestId,
evaluatorId: request.evaluatorId,
pathElements: nil,
error: nil
)
guard
let reader = resourceReaders.first(where: { $0.scheme == request.uri.scheme }) else {
response.error = "No resource reader found for scheme \(request.uri.scheme!)"
try self.transport.send(response)
return
}
do {
let elems = try await reader.listElements(uri: request.uri)
response.pathElements = elems.map { $0.toMessage() }
try self.transport.send(response)
} catch {
response.error = "\(error)"
try self.transport.send(response)
}
}
}
Loading

0 comments on commit 64c66ae

Please sign in to comment.