Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ for target in package.targets {
// https://docs.swift.org/compiler/documentation/diagnostics/nonisolated-nonsending-by-default/
settings.append(.enableUpcomingFeature("NonisolatedNonsendingByDefault"))

settings.append(.enableExperimentalFeature("AvailabilityMacro=Configuration 1.0:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"))
settings.append(
.enableExperimentalFeature(
"AvailabilityMacro=Configuration 1.0:macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0"
)
)

if enableAllCIFlags {
// Ensure all public types are explicitly annotated as Sendable or not Sendable.
Expand Down
51 changes: 22 additions & 29 deletions Sources/Configuration/ConfigSnapshotReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,19 @@ import Synchronization
///
/// ## Usage
///
/// Get a ``ConfigSnapshotReader`` from a ``ConfigReader`` by using ``ConfigReader/withSnapshot(_:)``
/// to retrieve a snapshot. The values of the snapshot are guaranteed to be from the same point in time:
/// Get a ``ConfigSnapshotReader`` from a ``ConfigReader`` by using ``ConfigReader/snapshot()``
/// to retrieve a snapshot. All values in the snapshot are guaranteed to be from the same point in time:
/// ```swift
/// // Get a snapshot from a ConfigReader
/// let config = ConfigReader(provider: EnvironmentVariablesProvider())
/// let result = config.withSnapshot { snapshot in
/// // Use snapshot to read config values
/// let cert = snapshot.string(forKey: "cert")
/// let privateKey = snapshot.string(forKey: "privateKey")
/// // Ensures that both values are coming from the same
/// // underlying snapshot and that a provider didn't change
/// // its internal state between the two `string(...)` calls.
/// return MyCert(cert: cert, privateKey: privateKey)
/// }
/// let snapshot = config.snapshot()
/// // Use snapshot to read config values
/// let cert = snapshot.string(forKey: "cert")
/// let privateKey = snapshot.string(forKey: "privateKey")
/// // Ensures that both values are coming from the same
/// // underlying snapshot and that a provider didn't change
/// // its internal state between the two `string(...)` calls.
/// let identity = MyIdentity(cert: cert, privateKey: privateKey)
/// ```
///
/// Or you can watch for snapshot updates using the ``ConfigReader/watchSnapshot(fileID:line:updatesHandler:)``:
Expand Down Expand Up @@ -297,29 +296,23 @@ public struct ConfigSnapshotReader: Sendable {

@available(Configuration 1.0, *)
extension ConfigReader {
/// Provides a snapshot of the current configuration state and passes it to the provided closure.
/// Returns a snapshot of the current configuration state.
///
/// This method creates a snapshot of the current configuration state and passes it to the
/// provided closure. The snapshot reader provides read-only access to the configuration's state
/// The snapshot reader provides read-only access to the configuration's state
/// at the time the method was called.
///
/// ```swift
/// let result = config.withSnapshot { snapshot in
/// // Use snapshot to read config values
/// let cert = snapshot.string(forKey: "cert")
/// let privateKey = snapshot.string(forKey: "privateKey")
/// // Ensures that both values are coming from the same underlying snapshot and that a provider
/// // didn't change its internal state between the two `string(...)` calls.
/// return MyCert(cert: cert, privateKey: privateKey)
/// }
/// let snapshot = config.snapshot()
/// // Use snapshot to read config values
/// let cert = snapshot.string(forKey: "cert")
/// let privateKey = snapshot.string(forKey: "privateKey")
/// // Ensures that both values are coming from the same underlying snapshot and that a provider
/// // didn't change its internal state between the two `string(...)` calls.
/// let identity = MyIdentity(cert: cert, privateKey: privateKey)
/// ```
///
/// - Parameter body: A closure that takes a `ConfigSnapshotReader` and returns a value.
/// - Returns: The value returned by the closure.
/// - Throws: Rethrows any errors thrown by the provided closure.
public func withSnapshot<Failure: Error, Return>(
_ body: (ConfigSnapshotReader) throws(Failure) -> Return
) throws(Failure) -> Return {
/// - Returns: The snapshot.
public func snapshot() -> ConfigSnapshotReader {
let multiSnapshot = provider.snapshot()
let snapshotReader = ConfigSnapshotReader(
keyPrefix: keyPrefix,
Expand All @@ -329,7 +322,7 @@ extension ConfigReader {
accessReporter: accessReporter
)
)
return try body(snapshotReader)
return snapshotReader
}

/// Watches the configuration for changes.
Expand Down
39 changes: 39 additions & 0 deletions Sources/Configuration/Deprecations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,42 @@ public typealias ReloadingYAMLProvider = ReloadingFileProvider<YAMLSnapshot>
@available(Configuration 1.0, *)
@available(*, deprecated, renamed: "ConfigSnapshot")
public typealias ConfigSnapshotProtocol = ConfigSnapshot

@available(Configuration 1.0, *)
extension ConfigReader {
/// Provides a snapshot of the current configuration state and passes it to the provided closure.
///
/// This method creates a snapshot of the current configuration state and passes it to the
/// provided closure. The snapshot reader provides read-only access to the configuration's state
/// at the time the method was called.
///
/// ```swift
/// let result = config.withSnapshot { snapshot in
/// // Use snapshot to read config values
/// let cert = snapshot.string(forKey: "cert")
/// let privateKey = snapshot.string(forKey: "privateKey")
/// // Ensures that both values are coming from the same underlying snapshot and that a provider
/// // didn't change its internal state between the two `string(...)` calls.
/// return MyCert(cert: cert, privateKey: privateKey)
/// }
/// ```
///
/// - Parameter body: A closure that takes a `ConfigSnapshotReader` and returns a value.
/// - Returns: The value returned by the closure.
/// - Throws: Rethrows any errors thrown by the provided closure.
@available(*, deprecated, message: "Renamed to snapshot().")
public func withSnapshot<Failure: Error, Return>(
_ body: (ConfigSnapshotReader) throws(Failure) -> Return
) throws(Failure) -> Return {
let multiSnapshot = provider.snapshot()
let snapshotReader = ConfigSnapshotReader(
keyPrefix: keyPrefix,
storage: .init(
keyDecoder: keyDecoder,
snapshot: multiSnapshot,
accessReporter: accessReporter
)
)
return try body(snapshotReader)
}
}
11 changes: 5 additions & 6 deletions Sources/Configuration/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,19 +358,18 @@ Read <doc:Handling-secrets-correctly> for guidance on best practices for secrets
#### Consistent snapshots

Retrieve related values from a consistent snapshot using ``ConfigSnapshotReader``, which you
get from calling ``ConfigReader/withSnapshot(_:)``.
get by calling ``ConfigReader/snapshot()``.

This ensures that multiple values are read from a single snapshot inside each provider, even when using
providers that update their internal values.
For example by downloading new data periodically:

```swift
let config = /* a reader with one or more providers that change values over time */
try config.withSnapshot { snapshot in
let certificate = try snapshot.requiredString(forKey: "mtls.certificate")
let privateKey = try snapshot.requiredString(forKey: "mtls.privateKey", isSecret: true)
// `certificate` and `privateKey` are guaranteed to come from the same snapshot in the provider
}
let snapshot = config.snapshot()
let certificate = try snapshot.requiredString(forKey: "mtls.certificate")
let privateKey = try snapshot.requiredString(forKey: "mtls.privateKey", isSecret: true)
// `certificate` and `privateKey` are guaranteed to come from the same snapshot in the provider
```

#### Custom key syntax
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- ``ConfigReader/scoped(to:keyDecoderOverride:)``

### Reading from a snapshot
- ``ConfigReader/withSnapshot(_:)``
- ``ConfigReader/snapshot()``
- ``ConfigReader/watchSnapshot(fileID:line:updatesHandler:)``

- <doc:ConfigReader-Get>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Topics

### Creating a snapshot
- ``ConfigReader/withSnapshot(_:)``
- ``ConfigReader/snapshot()``
- ``ConfigReader/watchSnapshot(fileID:line:updatesHandler:)``

### Namespacing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
@Test func get() async throws {
let config = ConfigReaderTests.config

try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.string(forKey: ConfigKey(["string"])) == Defaults.string)
#expect(snapshot.string(forKey: "string") == Defaults.string)
Expand Down Expand Up @@ -78,7 +80,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: ConfigKey(["failure"])) }
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredString(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.int(forKey: ConfigKey(["int"])) == Defaults.int)
#expect(snapshot.int(forKey: "int") == Defaults.int)
Expand Down Expand Up @@ -119,7 +123,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredInt(forKey: ConfigKey(["failure"])) }
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredInt(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.double(forKey: ConfigKey(["double"])) == Defaults.double)
#expect(snapshot.double(forKey: "double") == Defaults.double)
Expand Down Expand Up @@ -165,7 +171,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredDouble(forKey: ConfigKey(["failure"])) }
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredDouble(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.bool(forKey: ConfigKey(["bool"])) == Defaults.bool)
#expect(snapshot.bool(forKey: "bool") == Defaults.bool)
Expand Down Expand Up @@ -206,7 +214,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredBool(forKey: ConfigKey(["failure"])) }
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredBool(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.bytes(forKey: ConfigKey(["bytes"])) == Defaults.bytes)
#expect(snapshot.bytes(forKey: "bytes") == Defaults.bytes)
Expand Down Expand Up @@ -249,7 +259,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredBytes(forKey: ConfigKey(["failure"])) }
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredBytes(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.stringArray(forKey: ConfigKey(["stringArray"])) == Defaults.stringArray)
#expect(snapshot.stringArray(forKey: "stringArray") == Defaults.stringArray)
Expand Down Expand Up @@ -310,7 +322,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
}
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredStringArray(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.intArray(forKey: ConfigKey(["intArray"])) == Defaults.intArray)
#expect(snapshot.intArray(forKey: "intArray") == Defaults.intArray)
Expand Down Expand Up @@ -363,7 +377,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
}
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredIntArray(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.doubleArray(forKey: ConfigKey(["doubleArray"])) == Defaults.doubleArray)
#expect(snapshot.doubleArray(forKey: "doubleArray") == Defaults.doubleArray)
Expand Down Expand Up @@ -424,7 +440,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
}
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredDoubleArray(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.boolArray(forKey: ConfigKey(["boolArray"])) == Defaults.boolArray)
#expect(snapshot.boolArray(forKey: "boolArray") == Defaults.boolArray)
Expand Down Expand Up @@ -479,7 +497,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
}
#expect(throws: TestProvider.TestError.self) { try snapshot.requiredBoolArray(forKey: "failure") }
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.byteChunkArray(forKey: ConfigKey(["byteChunkArray"])) == Defaults.byteChunkArray)
#expect(snapshot.byteChunkArray(forKey: "byteChunkArray") == Defaults.byteChunkArray)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
% for primitive_type in primitive_types:
% name = primitive_type["name"]
% lower_name = lower_first(name)
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.${lower_name}(forKey: ConfigKey(["${lower_name}"])) == Defaults.${lower_name})
#expect(snapshot.${lower_name}(forKey: "${lower_name}") == Defaults.${lower_name})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ struct ConfigSnapshotReaderMethodTestsGet2 {
@Test func get() async throws {
let config = ConfigReaderTests.config

try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(
snapshot.string(forKey: ConfigKey(["stringConvertible"]), as: TestStringConvertible.self)
Expand Down Expand Up @@ -132,7 +134,9 @@ struct ConfigSnapshotReaderMethodTestsGet2 {
try snapshot.requiredString(forKey: "failure", as: TestStringConvertible.self)
}
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.string(forKey: ConfigKey(["stringEnum"]), as: TestEnum.self) == Defaults.stringEnum)
#expect(snapshot.string(forKey: "stringEnum", as: TestEnum.self) == Defaults.stringEnum)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ struct ConfigSnapshotReaderMethodTestsGet2 {
% test_type = string_convertible_type["testType"]
% test_suffix = string_convertible_type["testSuffix"]
% lower_test_suffix = lower_first(test_suffix)
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.string(forKey: ConfigKey(["${lower_test_suffix}"]), as: ${test_type}.self) == Defaults.${lower_test_suffix})
#expect(snapshot.string(forKey: "${lower_test_suffix}", as: ${test_type}.self) == Defaults.${lower_test_suffix})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ struct ConfigSnapshotReaderMethodTestsGet3 {
@Test func get() async throws {
let config = ConfigReaderTests.config

try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(
snapshot.stringArray(forKey: ConfigKey(["stringConvertibleArray"]), as: TestStringConvertible.self)
Expand Down Expand Up @@ -137,7 +139,9 @@ struct ConfigSnapshotReaderMethodTestsGet3 {
try snapshot.requiredStringArray(forKey: "failure", as: TestStringConvertible.self)
}
}
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(
snapshot.stringArray(forKey: ConfigKey(["stringEnumArray"]), as: TestEnum.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ struct ConfigSnapshotReaderMethodTestsGet3 {
% test_type = string_convertible_type["testType"]
% test_suffix = string_convertible_type["testSuffix"] + "Array"
% lower_test_suffix = lower_first(test_suffix)
try config.withSnapshot { snapshot in
do {
let snapshot = config.snapshot()

// Optional - success
#expect(snapshot.stringArray(forKey: ConfigKey(["${lower_test_suffix}"]), as: ${test_type}.self) == Defaults.${lower_test_suffix})
#expect(snapshot.stringArray(forKey: "${lower_test_suffix}", as: ${test_type}.self) == Defaults.${lower_test_suffix})
Expand Down
Loading