From 8a5880fc5cc50cef9985dc5a1218d7b53a055b04 Mon Sep 17 00:00:00 2001 From: Jen Basch Date: Tue, 18 Nov 2025 15:20:07 -0800 Subject: [PATCH] Add build-time eval example --- Package.swift | 39 +++++++++++++++---- README.adoc | 23 +++++++++-- Sources/BuildTimeEval/Controllers/.gitkeep | 0 Sources/BuildTimeEval/config.msgpack | 1 + Sources/BuildTimeEval/configure.swift | 33 ++++++++++++++++ Sources/BuildTimeEval/entrypoint.swift | 37 ++++++++++++++++++ Sources/BuildTimeEval/routes.swift | 27 +++++++++++++ Tests/AppTests/AppTests.swift | 13 ++++--- .../BuildTimeEvalTests.swift | 34 ++++++++++++++++ pkl/dev/config.pkl | 2 + 10 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 Sources/BuildTimeEval/Controllers/.gitkeep create mode 100644 Sources/BuildTimeEval/config.msgpack create mode 100644 Sources/BuildTimeEval/configure.swift create mode 100644 Sources/BuildTimeEval/entrypoint.swift create mode 100644 Sources/BuildTimeEval/routes.swift create mode 100644 Tests/BuildTimeEvalTests/BuildTimeEvalTests.swift diff --git a/Package.swift b/Package.swift index f1f49a2..d5bee56 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,7 @@ import PackageDescription let package = Package( name: "pkl-swift-examples", platforms: [ - .macOS(.v13), + .macOS(.v13) ], dependencies: [ .package(url: "https://github.com/vapor/vapor.git", from: "4.89.0"), @@ -38,13 +38,36 @@ let package = Package( .product(name: "Vapor", package: "vapor"), ] ), - .testTarget(name: "AppTests", dependencies: [ - "Gen", - .target(name: "App"), - .product(name: "XCTVapor", package: "vapor"), + .executableTarget( + name: "BuildTimeEval", + dependencies: [ + "Gen", + .product(name: "Vapor", package: "vapor"), + .product(name: "PklSwift", package: "pkl-swift"), + ], + resources: [ + .embedInCode("config.msgpack") + ] + ), + .testTarget( + name: "AppTests", + dependencies: [ + "Gen", + .target(name: "App"), + .product(name: "XCTVapor", package: "vapor"), + + // Workaround for https://github.com/apple/swift-package-manager/issues/6940 + .product(name: "Vapor", package: "vapor"), + ]), + .testTarget( + name: "BuildTimeEvalTests", + dependencies: [ + "Gen", + .target(name: "BuildTimeEval"), + .product(name: "XCTVapor", package: "vapor"), - // Workaround for https://github.com/apple/swift-package-manager/issues/6940 - .product(name: "Vapor", package: "vapor"), - ]), + // Workaround for https://github.com/apple/swift-package-manager/issues/6940 + .product(name: "Vapor", package: "vapor"), + ]), ] ) diff --git a/README.adoc b/README.adoc index 60856b8..1c63784 100644 --- a/README.adoc +++ b/README.adoc @@ -6,8 +6,8 @@ This is a basic HTTP server that is configured via Pkl in the `pkl/` directory. == Requirements -* Swift +5.9 -* Pkl +0.25.0 +* Swift +6.1 +* Pkl +0.30.0;s == Project structure @@ -23,7 +23,10 @@ This is a basic HTTP server that is configured via Pkl in the `pkl/` directory. | Generated Swift sources from Pkl | `Sources/App/` -| Swift files +| Swift files for runtime evaluation example + +| `Sources/BuildTimeEval/` +| Swift files for build-time evaluation example |=== == Codegen @@ -45,6 +48,18 @@ With that installed, generate Swift sources via: pkl-gen-swift pkl/**.pkl -o Sources/Gen ---- +== Build-time evaluation + +The example in `Sources/BuildTimeEval/` shows how the https://pkl-lang.org/main/current/bindings-specification/binary-encoding.html[`pkl-binary` encoding] can be used to configure applications. +This mechanism separates evaluation of Pkl code from application runtime by encoding the configuration data as `pkl-binary`, storing it in a file, and loading it later during application startup. +This example works identically to the runtime evaluation example, but does not require the application to spawn the Pkl CLI as a subprocess. + +The configuration module is rendered as `pkl-binary` by running: +[source,bash] +---- +pkl eval pkl/dev/config.pkl -f pkl-binary -o pkl/dev/config.msgpack +---- + == Running the server -To run the project call `swift run`. \ No newline at end of file +To run the project call `swift run`. diff --git a/Sources/BuildTimeEval/Controllers/.gitkeep b/Sources/BuildTimeEval/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Sources/BuildTimeEval/config.msgpack b/Sources/BuildTimeEval/config.msgpack new file mode 100644 index 0000000..410df8e --- /dev/null +++ b/Sources/BuildTimeEval/config.msgpack @@ -0,0 +1 @@ +Config:file:///Users/jbasch/src/pkl-swift-examples/pkl/Config.pklhostnamelocalhostporttcpNoDelayÓbacklog̀serverNamePkl Swift Example Server \ No newline at end of file diff --git a/Sources/BuildTimeEval/configure.swift b/Sources/BuildTimeEval/configure.swift new file mode 100644 index 0000000..e3e8726 --- /dev/null +++ b/Sources/BuildTimeEval/configure.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// 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 Gen +import PklSwift +import Vapor + +// configures your application +public func configure(_ app: Application) async throws { + let conf = try! PklDecoder.decode(Config.Module.self, from: PackageResources.config_msgpack) + // configure with Pkl + app.http.server.configuration.hostname = conf.hostname + app.http.server.configuration.port = conf.port + app.http.server.configuration.tcpNoDelay = conf.tcpNoDelay + app.http.server.configuration.backlog = conf.backlog + app.http.server.configuration.serverName = conf.serverName + + // register routes + try routes(app) +} diff --git a/Sources/BuildTimeEval/entrypoint.swift b/Sources/BuildTimeEval/entrypoint.swift new file mode 100644 index 0000000..38fc4bf --- /dev/null +++ b/Sources/BuildTimeEval/entrypoint.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// 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 Logging +import Vapor + +@main +enum Entrypoint { + static func main() async throws { + var env = try Environment.detect() + try LoggingSystem.bootstrap(from: &env) + + let app = Application(env) + defer { app.shutdown() } + + do { + try await configure(app) + } catch { + app.logger.report(error: error) + throw error + } + try await app.execute() + } +} diff --git a/Sources/BuildTimeEval/routes.swift b/Sources/BuildTimeEval/routes.swift new file mode 100644 index 0000000..d10cf20 --- /dev/null +++ b/Sources/BuildTimeEval/routes.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// 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 Vapor + +func routes(_ app: Application) throws { + app.get { _ async in + "It works!" + } + + app.get("hello") { _ async -> String in + "Hello, world!" + } +} diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift index 16f22f0..f5bdba6 100644 --- a/Tests/AppTests/AppTests.swift +++ b/Tests/AppTests/AppTests.swift @@ -14,18 +14,21 @@ // limitations under the License. //===----------------------------------------------------------------------===// -@testable import App import XCTVapor +@testable import App + final class AppTests: XCTestCase { func testHelloWorld() async throws { let app = Application(.testing) defer { app.shutdown() } try await configure(app) - try app.test(.GET, "hello", afterResponse: { res in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.body.string, "Hello, world!") - }) + try app.test( + .GET, "hello", + afterResponse: { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "Hello, world!") + }) } } diff --git a/Tests/BuildTimeEvalTests/BuildTimeEvalTests.swift b/Tests/BuildTimeEvalTests/BuildTimeEvalTests.swift new file mode 100644 index 0000000..8d0fe78 --- /dev/null +++ b/Tests/BuildTimeEvalTests/BuildTimeEvalTests.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// 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 XCTVapor + +@testable import BuildTimeEval + +final class BuildTimeEvalTests: XCTestCase { + func testHelloWorld() async throws { + let app = Application(.testing) + defer { app.shutdown() } + try await configure(app) + + try app.test( + .GET, "hello", + afterResponse: { res in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "Hello, world!") + }) + } +} diff --git a/pkl/dev/config.pkl b/pkl/dev/config.pkl index 03bfd83..6c52550 100644 --- a/pkl/dev/config.pkl +++ b/pkl/dev/config.pkl @@ -14,6 +14,8 @@ // limitations under the License. //===----------------------------------------------------------------------===// +// generate config.msgpack: pkl eval pkl/dev/config.pkl -f pkl-binary -o Sources/BuildTimeEval/config.msgpack + amends "../Config.pkl" hostname = "localhost"