Skip to content

Commit 6d6ea48

Browse files
committed
[feat]: introduce runtime configuration SPI
1 parent ff4aaea commit 6d6ea48

File tree

5 files changed

+237
-7
lines changed

5 files changed

+237
-7
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
configureBlock(&config)
44+
_bootstrapConfiguration._bootstrapConfig = config
45+
}
46+
47+
static var configuration: Configuration {
48+
_currentConfiguration ?? _bootstrapConfiguration.currentConfiguration
49+
}
50+
51+
/// Allows temporary customization of the runtime configuration during the execution of the `operation`.
52+
///
53+
/// - Parameters:
54+
/// - override: An option block to reconfigure the current configuration value.
55+
/// - operation: The operation to perform with the customized configuration.
56+
public static func withConfiguration<T>(
57+
override: ((inout Configuration) -> Void)? = nil,
58+
operation: () -> T
59+
) -> T {
60+
var configSnapshot = configuration
61+
override?(&configSnapshot)
62+
63+
return Runtime
64+
.$_currentConfiguration
65+
.withValue(
66+
configSnapshot,
67+
operation: operation
68+
)
69+
}
70+
71+
// MARK: -
72+
73+
private static var _isBootstrapped: Bool {
74+
_bootstrapConfiguration._bootstrapConfig != nil
75+
}
76+
77+
/// The current runtime configuration that may have been set via `bootstrap()`.
78+
private static var _currentBootstrapConfiguration: Configuration {
79+
_bootstrapConfiguration.currentConfiguration
80+
}
81+
}
82+
83+
extension Runtime {
84+
/// Configuration options for the Workflow runtime.
85+
public struct Configuration: Equatable {
86+
/// The default runtime configuration.
87+
static let `default` = Configuration()
88+
89+
/// Note: this doesn't control anything yet, but is here as a placeholder
90+
public var renderOnlyIfStateChanged: Bool = false
91+
}
92+
93+
struct BootstrappableConfiguration {
94+
var _bootstrapConfig: Configuration?
95+
let _defaultConfig: Configuration = .default
96+
97+
/// The current runtime configuration that may have been set via `Runtime.bootstrap()`.
98+
var currentConfiguration: Configuration {
99+
_bootstrapConfig ?? _defaultConfig
100+
}
101+
}
102+
}

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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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_withConfiguration() {
61+
#expect(Runtime.configuration.renderOnlyIfStateChanged == false)
62+
63+
var override = Runtime.configuration
64+
override.renderOnlyIfStateChanged = true
65+
66+
Runtime.$_currentConfiguration.withValue(override) {
67+
Runtime.withConfiguration {
68+
#expect(Runtime.configuration.renderOnlyIfStateChanged == true)
69+
}
70+
}
71+
}
72+
73+
@Test
74+
private func test_withConfigurationOverride() {
75+
let newValue = Runtime.withConfiguration(
76+
override: { cfg in
77+
#expect(Runtime.configuration.renderOnlyIfStateChanged == false)
78+
#expect(cfg.renderOnlyIfStateChanged == false)
79+
cfg.renderOnlyIfStateChanged = true
80+
},
81+
operation: {
82+
Runtime.configuration.renderOnlyIfStateChanged
83+
}
84+
)
85+
86+
#expect(newValue == true)
87+
}
88+
}

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)