Skip to content

Commit 51d2c6f

Browse files
committed
[feat]: introduce runtime configuration SPI
1 parent 53fc18d commit 51d2c6f

File tree

5 files changed

+218
-7
lines changed

5 files changed

+218
-7
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/// System for managing configuration options for Workflow runtime behaviors.
18+
/// - important: These interfaces are subject to breaking changes without corresponding semantic
19+
/// versioning changes.
20+
@_spi(RuntimeConfig)
21+
public enum Runtime {
22+
@TaskLocal
23+
static var _currentConfiguration: Configuration?
24+
25+
static var _bootstrapConfiguration = BootstrappableConfiguration()
26+
27+
/// Bootstrap the workflow runtime with the given configuration.
28+
/// This can only be called once per process and must be called from the main thread.
29+
///
30+
/// - Parameter configuration: The runtime configuration to use.
31+
@MainActor
32+
public static func bootstrap(
33+
_ configureBlock: (inout Configuration) -> Void
34+
) {
35+
MainActor.preconditionIsolated(
36+
"The Workflow runtime must be bootstrapped from the main actor."
37+
)
38+
guard !_isBootstrapped else {
39+
fatalError("The Workflow runtime can only be bootstrapped once.")
40+
}
41+
42+
var config = _bootstrapConfiguration.currentConfiguration
43+
defer { _bootstrapConfiguration._bootstrapConfig = config }
44+
configureBlock(&config)
45+
}
46+
47+
static var configuration: Configuration {
48+
_currentConfiguration ?? _bootstrapConfiguration.currentConfiguration
49+
}
50+
51+
/// Executes the given closure with the current runtime configuration applied as a task-local.
52+
///
53+
/// - Parameters:
54+
/// - operation: The closure to execute with the current runtime configuration
55+
/// - Returns: The result of executing the closure
56+
static func withCurrentConfiguration<T>(
57+
operation: (Configuration) -> T
58+
) -> T {
59+
Runtime
60+
.$_currentConfiguration
61+
.withValue(configuration) {
62+
operation(Runtime._currentConfiguration!)
63+
}
64+
}
65+
66+
// MARK: -
67+
68+
private static var _isBootstrapped: Bool {
69+
_bootstrapConfiguration._bootstrapConfig != nil
70+
}
71+
72+
/// The current runtime configuration that may have been set via `bootstrap()`.
73+
private static var _currentBootstrapConfiguration: Configuration {
74+
_bootstrapConfiguration.currentConfiguration
75+
}
76+
}
77+
78+
extension Runtime {
79+
/// Configuration options for the Workflow runtime.
80+
public struct Configuration: Equatable {
81+
/// The default runtime configuration.
82+
static let `default` = Configuration()
83+
84+
/// Note: this doesn't control anything yet, but is here as a placeholder
85+
public var renderOnlyIfStateChanged: Bool = false
86+
}
87+
88+
struct BootstrappableConfiguration {
89+
var _bootstrapConfig: Configuration?
90+
let _defaultConfig: Configuration = .default
91+
92+
/// The current runtime configuration that may have been set via `Runtime.bootstrap()`.
93+
var currentConfiguration: Configuration {
94+
_bootstrapConfig ?? _defaultConfig
95+
}
96+
}
97+
}

Workflow/Sources/WorkflowHost.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ public final class WorkflowHost<WorkflowType: Workflow> {
6868

6969
self.context = HostContext(
7070
observer: observer,
71-
debugger: debugger
71+
debugger: debugger,
72+
runtimeConfig: Runtime.configuration
7273
)
7374

7475
self.rootNode = WorkflowNode(
@@ -130,16 +131,19 @@ public final class WorkflowHost<WorkflowType: Workflow> {
130131

131132
/// A context object to expose certain root-level information to each node
132133
/// in the Workflow tree.
133-
final class HostContext {
134+
struct HostContext {
134135
let observer: WorkflowObserver?
135136
let debugger: WorkflowDebugger?
137+
let runtimeConfig: Runtime.Configuration
136138

137139
init(
138140
observer: WorkflowObserver?,
139-
debugger: WorkflowDebugger?
141+
debugger: WorkflowDebugger?,
142+
runtimeConfig: Runtime.Configuration
140143
) {
141144
self.observer = observer
142145
self.debugger = debugger
146+
self.runtimeConfig = runtimeConfig
143147
}
144148
}
145149

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Testing
18+
19+
@_spi(RuntimeConfig) @testable import Workflow
20+
21+
@MainActor
22+
struct RuntimeConfigTests {
23+
@Test
24+
private func runtime_config_inits_to_default() {
25+
let cfg = Runtime.Configuration()
26+
#expect(cfg == Runtime.Configuration.default)
27+
}
28+
29+
@Test
30+
private func test_global_config_defaults_to_default() {
31+
#expect(Runtime.configuration == .default)
32+
}
33+
34+
@Test
35+
private func runtime_config_prefers_bootstrap_value() {
36+
#expect(Runtime.configuration.renderOnlyIfStateChanged == false)
37+
38+
defer {
39+
// reset global state...
40+
Runtime.resetConfig()
41+
}
42+
Runtime.bootstrap { cfg in
43+
cfg.renderOnlyIfStateChanged = true
44+
}
45+
46+
#expect(Runtime.configuration.renderOnlyIfStateChanged == true)
47+
}
48+
49+
@Test
50+
private func test_config_respects_task_local_overrides() {
51+
var customConfig = Runtime.configuration
52+
customConfig.renderOnlyIfStateChanged = true
53+
54+
Runtime.$_currentConfiguration.withValue(customConfig) {
55+
#expect(Runtime.configuration.renderOnlyIfStateChanged == true)
56+
}
57+
}
58+
59+
@Test
60+
private func test_withCurrentConfiguration() {
61+
Runtime.withCurrentConfiguration { cfg in
62+
#expect(cfg.renderOnlyIfStateChanged == false)
63+
64+
var override = cfg
65+
override.renderOnlyIfStateChanged = true
66+
67+
Runtime.$_currentConfiguration.withValue(override) {
68+
Runtime.withCurrentConfiguration { overrideCfg in
69+
#expect(overrideCfg.renderOnlyIfStateChanged == true)
70+
}
71+
}
72+
}
73+
}
74+
}

Workflow/Tests/TestUtilities.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import Foundation
1818

19-
@testable import Workflow
19+
@_spi(RuntimeConfig) @testable import Workflow
2020

2121
/// Renders to a model that contains a callback, which in turn sends an output event.
2222
struct StateTransitioningWorkflow: Workflow {
@@ -62,11 +62,13 @@ struct StateTransitioningWorkflow: Workflow {
6262
extension HostContext {
6363
static func testing(
6464
observer: WorkflowObserver? = nil,
65-
debugger: WorkflowDebugger? = nil
65+
debugger: WorkflowDebugger? = nil,
66+
runtimeConfig: Runtime.Configuration = Runtime.configuration
6667
) -> HostContext {
6768
HostContext(
6869
observer: observer,
69-
debugger: debugger
70+
debugger: debugger,
71+
runtimeConfig: runtimeConfig
7072
)
7173
}
7274
}
@@ -95,3 +97,11 @@ extension ApplyContext {
9597
wrappedConcreteContext?.storage
9698
}
9799
}
100+
101+
// MARK: - Runtime.Config
102+
103+
extension Runtime {
104+
static func resetConfig() {
105+
Runtime._bootstrapConfiguration = .init()
106+
}
107+
}

Workflow/Tests/WorkflowHostTests.swift

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import Workflow
1817
import XCTest
18+
@_spi(RuntimeConfig) @testable import Workflow
1919

2020
final class WorkflowHostTests: XCTestCase {
2121
func test_updatedInputCausesRenderPass() {
@@ -87,6 +87,32 @@ final class WorkflowHost_EventEmissionTests: XCTestCase {
8787
}
8888
}
8989

90+
// MARK: Runtime Configuration
91+
92+
extension WorkflowHostTests {
93+
func test_inherits_default_runtime_config() {
94+
let host = WorkflowHost(
95+
workflow: TestWorkflow(step: .first)
96+
)
97+
98+
XCTAssertEqual(host.context.runtimeConfig, .default)
99+
}
100+
101+
func test_inherits_custom_runtime_config() {
102+
var customConfig = Runtime.configuration
103+
XCTAssertFalse(customConfig.renderOnlyIfStateChanged)
104+
105+
customConfig.renderOnlyIfStateChanged = true
106+
let host = Runtime.$_currentConfiguration.withValue(customConfig) {
107+
WorkflowHost(
108+
workflow: TestWorkflow(step: .first)
109+
)
110+
}
111+
112+
XCTAssertEqual(host.context.runtimeConfig.renderOnlyIfStateChanged, true)
113+
}
114+
}
115+
90116
// MARK: Utility Types
91117

92118
extension WorkflowHost_EventEmissionTests {

0 commit comments

Comments
 (0)