Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@ -226,29 +225,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 @@ -257,7 +250,7 @@ extension ConfigReader {
accessReporter: accessReporter
)
)
return try body(snapshotReader)
return snapshotReader
}

/// Watches the configuration for changes.
Expand Down
38 changes: 38 additions & 0 deletions Sources/Configuration/Deprecations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,41 @@ 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(
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
```

#### Extensible ecosystem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- ``ConfigReader/scoped(to:)``

### 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: "string") == Defaults.string)

Expand Down Expand Up @@ -61,7 +63,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "int") == Defaults.int)

Expand Down Expand Up @@ -90,7 +94,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "double") == Defaults.double)

Expand Down Expand Up @@ -119,7 +125,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "bool") == Defaults.bool)

Expand Down Expand Up @@ -148,7 +156,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "bytes") == Defaults.bytes)

Expand Down Expand Up @@ -177,7 +187,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "stringArray") == Defaults.stringArray)

Expand Down Expand Up @@ -215,7 +227,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "intArray") == Defaults.intArray)

Expand Down Expand Up @@ -246,7 +260,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "doubleArray") == Defaults.doubleArray)

Expand Down Expand Up @@ -284,7 +300,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "boolArray") == Defaults.boolArray)

Expand Down Expand Up @@ -316,7 +334,9 @@ struct ConfigSnapshotReaderMethodTestsGet1 {
// Required - failing
#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: "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: "${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: "stringConvertible", as: TestStringConvertible.self)
Expand Down Expand Up @@ -89,7 +91,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: "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: "${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: "stringConvertibleArray", as: TestStringConvertible.self)
Expand Down Expand Up @@ -89,7 +91,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: "stringEnumArray", as: TestEnum.self) == Defaults.stringEnumArray)

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: "${lower_test_suffix}", as: ${test_type}.self) == Defaults.${lower_test_suffix})

Expand Down
16 changes: 7 additions & 9 deletions Tests/ConfigurationTests/ConfigSnapshotReaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ struct ConfigSnapshotReaderTests {
]
)
let config = ConfigReader(provider: provider)
try config.withSnapshot { snapshot in
try #require(snapshot.string(forKey: "http.stuff", default: "test") == "test")
try #require(snapshot.string(forKey: "http.client.user-agent") == "Config/1.0 (Test)")
}
let snapshot = config.snapshot()
try #require(snapshot.string(forKey: "http.stuff", default: "test") == "test")
try #require(snapshot.string(forKey: "http.client.user-agent") == "Config/1.0 (Test)")
}

@available(Configuration 1.0, *)
Expand Down Expand Up @@ -60,10 +59,9 @@ struct ConfigSnapshotReaderTests {
]
)
let config = ConfigReader(provider: provider)
config.withSnapshot { snapshot in
#expect(snapshot.string(forKey: "user-agent") == nil)
let scoped = snapshot.scoped(to: "http.client")
#expect(scoped.string(forKey: "user-agent") == "Config/1.0 (Test)")
}
let snapshot = config.snapshot()
#expect(snapshot.string(forKey: "user-agent") == nil)
let scoped = snapshot.scoped(to: "http.client")
#expect(scoped.string(forKey: "user-agent") == "Config/1.0 (Test)")
}
}
10 changes: 5 additions & 5 deletions Tests/ConfigurationTests/MutableInMemoryProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ struct MutableInMemoryProviderTests {
let provider = makeProvider()
let config = ConfigReader(provider: provider)

config.withSnapshot { snapshot in
#expect(snapshot.bool(forKey: "bool") == true)
provider.setValue(false, forKey: "bool")
#expect(snapshot.bool(forKey: "bool") == true)
}
let snapshot = config.snapshot()
#expect(snapshot.bool(forKey: "bool") == true)
provider.setValue(false, forKey: "bool")
#expect(snapshot.bool(forKey: "bool") == true)

#expect(config.bool(forKey: "bool") == false)
}

Expand Down