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
24 changes: 23 additions & 1 deletion Sources/Configuration/AccessReporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ public protocol AccessReporter: Sendable {
public struct AccessEvent: Sendable {

/// Metadata describing the configuration access operation.
///
/// Contains information about the type of access, the key accessed, value type,
/// source location, and timestamp.
public struct Metadata: Sendable {

/// The source code location where the configuration access occurred.
/// The source code location where a configuration access occurred.
///
/// Captures the file identifier and line number for debugging and auditing purposes.
public struct SourceLocation: Sendable, CustomStringConvertible {

/// The identifier of the source file where the access occurred.
Expand All @@ -73,6 +78,9 @@ public struct AccessEvent: Sendable {
}

/// The type of configuration access operation.
///
/// Indicates whether the access was a synchronous get, asynchronous fetch,
/// or an async watch operation.
@frozen public enum AccessKind: String, Sendable {

/// A synchronous get operation that returns the current value.
Expand Down Expand Up @@ -126,6 +134,9 @@ public struct AccessEvent: Sendable {
}

/// The result of a configuration lookup from a specific provider.
///
/// Contains the provider's name and the outcome of querying that provider,
/// which can be either a successful lookup result or an error.
public struct ProviderResult: Sendable {

/// The name of the configuration provider that processed the lookup.
Expand Down Expand Up @@ -186,6 +197,17 @@ public struct AccessEvent: Sendable {
/// Use this reporter to send configuration access events to multiple destinations
/// simultaneously. Each upstream reporter receives a copy of every event in the
/// order they were provided during initialization.
///
/// ```swift
/// let fileLogger = try FileAccessLogger(filePath: "/tmp/config.log")
/// let accessLogger = AccessLogger(logger: logger)
/// let broadcaster = BroadcastingAccessReporter(upstreams: [fileLogger, accessLogger])
///
/// let config = ConfigReader(
/// provider: EnvironmentVariablesProvider(),
/// accessReporter: broadcaster
/// )
/// ```
@available(Configuration 1.0, *)
public struct BroadcastingAccessReporter: Sendable {

Expand Down
12 changes: 6 additions & 6 deletions Sources/Configuration/AccessReporters/AccessLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ import Synchronization
/// ## Log format
///
/// Each access event generates a structured log entry with metadata including:
/// - `kind`: The type of access operation (get, fetch, watch)
/// - `key`: The configuration key that was accessed
/// - `location`: The source code location where the access occurred
/// - `value`: The resolved configuration value (redacted for secrets)
/// - `counter`: An incrementing counter for tracking access frequency
/// - Provider-specific information for each provider in the hierarchy
/// - `kind`: The type of access operation (get, fetch, watch).
/// - `key`: The configuration key that was accessed.
/// - `location`: The source code location where the access occurred.
/// - `value`: The resolved configuration value (redacted for secrets).
/// - `counter`: An incrementing counter for tracking access frequency.
/// - Provider-specific information for each provider in the hierarchy.
@available(Configuration 1.0, *)
public final class AccessLogger: Sendable {

Expand Down
10 changes: 5 additions & 5 deletions Sources/Configuration/AccessReporters/FileAccessLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ import Synchronization
/// ```
///
/// The log entries include:
/// - Status emoji (✅ success, 🟡 default/nil, ❌ error)
/// - Configuration key that was accessed
/// - Resolved value (redacted for secrets)
/// - Provider that supplied the value or error information
/// - Access metadata (operation type, value type, source location, timestamp)
/// - Status emoji (✅ success, 🟡 default/nil, ❌ error).
/// - Configuration key that was accessed.
/// - Resolved value (redacted for secrets).
/// - Provider that supplied the value or error information.
/// - Access metadata (operation type, value type, source location, timestamp).
@available(Configuration 1.0, *)
public final class FileAccessLogger: Sendable {

Expand Down
24 changes: 17 additions & 7 deletions Sources/Configuration/ConfigKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,24 @@
//
//===----------------------------------------------------------------------===//

/// A configuration key that represents a relative path to a configuration value.
/// A configuration key representing a relative path to a configuration value.
///
/// Configuration keys consist of an array of string components that form a hierarchical
/// path, similar to file system paths or JSON object keys. For example, the key
/// `["http", "timeout"]` represents the value `timeout` nested underneath `http`.
/// Configuration keys consist of hierarchical string components forming paths similar to
/// file system paths or JSON object keys. For example, `["http", "timeout"]` represents
/// the `timeout` value nested under `http`.
///
/// Keys can include additional context information that some providers use to
/// refine value lookups or provide more specific results.
/// Keys support additional context information that providers can use to refine lookups
/// or provide specialized behavior.
///
/// ## Usage
///
/// Create keys using string literals, arrays, or the initializers:
///
/// ```swift
/// let key1: ConfigKey = "database.connection.timeout"
/// let key2 = ConfigKey(["api", "endpoints", "primary"])
/// let key3 = ConfigKey("server.port", context: ["environment": .string("production")])
/// ```
@available(Configuration 1.0, *)
public struct ConfigKey: Sendable {

Expand All @@ -46,7 +56,7 @@ public struct ConfigKey: Sendable {

/// Creates a new configuration key.
/// - Parameters:
/// - string: The string represenation of the key path, for example `"http.timeout"`.
/// - string: The string representation of the key path, for example `"http.timeout"`.
/// - context: Additional context information for the key.
public init(_ string: String, context: [String: ConfigContextValue] = [:]) {
self = DotSeparatorKeyDecoder.decode(string, context: context)
Expand Down
21 changes: 18 additions & 3 deletions Sources/Configuration/ConfigProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,16 @@ public protocol ConfigSnapshot: Sendable {
}

/// The result of looking up a configuration value in a provider.
///
/// Providers return this result from value lookup methods, containing both the
/// encoded key used for the lookup and the value found:
///
/// ```swift
/// let result = try provider.value(forKey: key, type: .string)
/// if let value = result.value {
/// print("Found: \(value)")
/// }
/// ```
@available(Configuration 1.0, *)
public struct LookupResult: Sendable, Equatable, Hashable {

Expand Down Expand Up @@ -435,9 +445,14 @@ public struct LookupResult: Sendable, Equatable, Hashable {

/// A configuration value that wraps content with metadata.
///
/// Configuration values include the actual content and a flag indicating whether
/// the value contains sensitive information. Secret values are protected from
/// accidental disclosure in logs and debug output.
/// Configuration values pair raw content with a flag indicating whether the value
/// contains sensitive information. Secret values are protected from accidental
/// disclosure in logs and debug output:
///
/// ```swift
/// let apiKey = ConfigValue(.string("sk-abc123"), isSecret: true)
/// print(apiKey) // Prints: [string: <REDACTED>]
/// ```
@available(Configuration 1.0, *)
public struct ConfigValue: Sendable, Equatable, Hashable {

Expand Down
14 changes: 7 additions & 7 deletions Sources/Configuration/ConfigSnapshotReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import Synchronization

/// A container type for reading config values from snapshots.
///
/// A config snapshot reader provides read-only access to config values stored in an underlying snapshot.
/// Unlike ``ConfigReader``, which can access live, changing config values from providers, a snapshot reader
/// works with a fixed, immutable snapshot of the configuration data.
/// A config snapshot reader provides read-only access to config values stored in an underlying
/// ``ConfigSnapshot``. Unlike a config reader, which can access live, changing config values
/// from providers, a snapshot reader works with a fixed, immutable snapshot of the configuration data.
///
/// ## Usage
///
/// 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:
/// Get a snapshot reader from a config reader by using the ``ConfigReader/snapshot()`` method. 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())
Expand All @@ -37,7 +37,7 @@ import Synchronization
/// let identity = MyIdentity(cert: cert, privateKey: privateKey)
/// ```
///
/// Or you can watch for snapshot updates using the ``ConfigReader/watchSnapshot(fileID:line:updatesHandler:)``:
/// Or you can watch for snapshot updates using the ``ConfigReader/watchSnapshot(fileID:line:updatesHandler:)`` method:
///
/// ```swift
/// try await config.watchSnapshot { snapshots in
Expand Down Expand Up @@ -211,7 +211,7 @@ public struct ConfigSnapshotReader: Sendable {
/// let timeout = httpConfig.int(forKey: "timeout") // Reads from "client.http.timeout" in the snapshot
/// ```
///
/// - Parameters configKey: The key to append to the current key prefix.
/// - Parameter configKey: The key to append to the current key prefix.
/// - Returns: A reader for accessing scoped values.
public func scoped(to configKey: ConfigKey)
-> ConfigSnapshotReader
Expand Down
5 changes: 2 additions & 3 deletions Sources/Configuration/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ For example, to read the timeout configuration value for an HTTP client, check o
```swift
// Environment variables consulted first, then JSON.
let primaryProvider = EnvironmentVariablesProvider()
let secondaryProvider = try await JSONProvider(
let secondaryProvider = try await FileProvider<JSONSnapshot>(
filePath: "/etc/config.json"
)
let config = ConfigReader(providers: [
Expand Down Expand Up @@ -246,8 +246,7 @@ You can also implement a custom ``ConfigProvider``.
In addition to using providers individually, you can create fallback behavior using an array of providers.
The first provider that returns a non-nil value wins.

The following example illustrates a hierarchy of provides, with environmental variables overrides winning
over command line arguments, a file at `/etc/config.json`, and in-memory defaults:
The following example shows a provider hierarchy where environment variables take precedence over command line arguments, a JSON file, and in-memory defaults:

```swift
// Create a hierarchy of providers with fallback behavior.
Expand Down
24 changes: 10 additions & 14 deletions Sources/Configuration/Documentation.docc/Guides/Best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@ Follow these principles to make your code easily configurable and composable wit

## Overview

When designing configuration for your Swift libraries and applications, following established patterns helps create
a consistent and maintainable experience for developers. These best practices ensure your configuration integrates
well with the broader Swift ecosystem.
When designing configuration for Swift libraries and applications, follow these patterns to create consistent, maintainable code that integrates well with the Swift ecosystem.

### Document configuration keys

Include comprehensive documentation about what configuration keys your library reads. For each key, document:
Include thorough documentation about what configuration keys your library reads. For each key, document:

- The key name and its hierarchical structure
- The expected data type
- Whether the key is required or optional
- Default values when applicable
- Valid value ranges or constraints
- Usage examples
- The key name and its hierarchical structure.
- The expected data type.
- Whether the key is required or optional.
- Default values when applicable.
- Valid value ranges or constraints.
- Usage examples.

```swift
public struct HTTPClientConfiguration {
Expand All @@ -38,8 +36,7 @@ public struct HTTPClientConfiguration {

### Use sensible defaults

Provide reasonable default values whenever possible to make your library work without extensive configuration.
This reduces the barrier to adoption and ensures your library works out of the box for common use cases.
Provide reasonable default values to make your library work without extensive configuration.

```swift
// Good: Provides sensible defaults
Expand Down Expand Up @@ -114,8 +111,7 @@ For more details, check out <doc:Choosing-reader-methods>.

### Validate configuration values

Consider validating configuration values and throwing meaningful errors if they're invalid. This helps
developers catch configuration issues early.
Validate configuration values and throw meaningful errors for invalid input to catch configuration issues early.

```swift
public init(config: ConfigReader) throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ Learn how to select the right method for reading configuration values based on y

## Overview

Swift Configuration provides three access patterns for retrieving configuration values, each optimized
for different use cases and performance requirements.
Swift Configuration provides three access patterns for retrieving configuration values, each optimized for different use cases and performance requirements.

The three access patterns are:

Expand Down Expand Up @@ -44,13 +43,11 @@ Use the "get" pattern when:

- Returns the currently cached value from the provider.
- No network or I/O operations occur during the call.
- Values may become stale if the underlying data source changes and the provider is either non-reloading, or
has a long reload interval.
- Values may become stale if the underlying data source changes and the provider is either non-reloading, or has a long reload interval.

### Fetch: Asynchronous fresh access

The "fetch" pattern asynchronously retrieves the most current value from the authoritative data source. This ensures
you always get up-to-date configuration, even if it requires network calls or file system access.
The "fetch" pattern asynchronously retrieves the most current value from the authoritative data source, ensuring you always get up-to-date configuration.

```swift
let config = ConfigReader(provider: remoteConfigProvider)
Expand All @@ -74,13 +71,12 @@ let dbConnectionString = try await config.fetchRequiredString(

#### When to use

Use the `fetch` pattern when:
Use the "fetch" pattern when:

- **Freshness is critical**: You need the latest configuration values.
- **Remote providers**: Using configuration services, databases, or external APIs that perform evaluation remotely.
- **Infrequent access**: Reading configuration occasionally, not in hot paths.
- **Setup operations**: Configuring long-lived resources like database connections where one-time overhead isn't
a concern, and the improved freshness is important.
- **Setup operations**: Configuring long-lived resources like database connections where one-time overhead isn't a concern, and the improved freshness is important.
- **Administrative operations**: Fetching current settings for management interfaces.

#### Behavior characteristics
Expand Down Expand Up @@ -115,8 +111,8 @@ try await config.watchInt(forKey: "http.timeout", default: 30) { updates in
Use the "watch" pattern when:

- **Dynamic configuration**: Values change during application runtime.
- **Hot reloading**: Need to update behavior without restarting the service.
- **Feature toggles**: Enabling/disabling features based on configuration changes.
- **Hot reloading**: You need to update behavior without restarting the service.
- **Feature toggles**: Enabling or disabling features based on configuration changes.
- **Resource management**: Adjusting timeouts, limits, or thresholds dynamically.
- **A/B testing**: Updating experimental parameters in real-time.

Expand Down Expand Up @@ -149,8 +145,8 @@ let context: [String: ConfigContextValue] = [
let dbConfig = try await config.fetchRequiredString(
forKey: ConfigKey(
"database.connection_string",
context: context,
)
context: context
),
isSecret: true
)

Expand All @@ -171,16 +167,19 @@ try await config.watchInt(
### Summary of performance considerations

#### Get pattern performance

- **Fastest**: No async overhead, immediate return.
- **Memory usage**: Minimal, uses cached values.
- **Best for**: Request handling, hot code paths, startup configuration.

#### Fetch pattern performance
#### Fetch pattern performance

- **Moderate**: Async overhead plus data source access time.
- **Network dependent**: Performance varies with provider implementation.
- **Best for**: Infrequent access, setup operations, administrative tasks.

#### Watch pattern performance

- **Background monitoring**: Continuous resource usage for monitoring.
- **Event-driven**: Efficient updates only when values change.
- **Best for**: Long-running services, dynamic configuration, feature toggles.
Expand Down Expand Up @@ -228,18 +227,10 @@ try await config.watchRequiredInt(forKey: "port") { updates in

### Best practices

1. **Choose based on use case**: Use "get" for performance-critical paths, "fetch" for freshness, and
"watch" for hot reloading.

2. **Handle errors appropriately**: Design error handling strategies that match your application's
resilience requirements.

3. **Use context judiciously**: Provide context when you need environment-specific or conditional
configuration values.

4. **Monitor configuration access**: Use ``AccessReporter`` to understand your application's
configuration dependencies.

1. **Choose based on use case**: Use "get" for performance-critical paths, "fetch" for freshness, and "watch" for hot reloading.
2. **Handle errors appropriately**: Design error handling strategies that match your application's resilience requirements.
3. **Use context judiciously**: Provide context when you need environment-specific or conditional configuration values.
4. **Monitor configuration access**: Use ``AccessReporter`` to understand your application's configuration dependencies.
5. **Cache wisely**: For frequently accessed values, prefer "get" over repeated "fetch" calls.

For more guidance on selecting the right reader methods for your needs, see <doc:Choosing-reader-methods>.
Expand Down
Loading
Loading