diff --git a/Sources/Configuration/Documentation.docc/Proposals/Proposals.md b/Sources/Configuration/Documentation.docc/Proposals/Proposals.md index c3c270a..39299cc 100644 --- a/Sources/Configuration/Documentation.docc/Proposals/Proposals.md +++ b/Sources/Configuration/Documentation.docc/Proposals/Proposals.md @@ -37,3 +37,4 @@ If you have any questions, ask in an issue on GitHub. - - +- diff --git a/Sources/Configuration/Documentation.docc/Proposals/SCO-0003.md b/Sources/Configuration/Documentation.docc/Proposals/SCO-0003.md new file mode 100644 index 0000000..5532cad --- /dev/null +++ b/Sources/Configuration/Documentation.docc/Proposals/SCO-0003.md @@ -0,0 +1,136 @@ +# SCO-0003: Allow missing files in file providers + +Add an `allowMissing` parameter to file-based providers to handle missing configuration files gracefully. + +## Overview + +- Proposal: SCO-0003 +- Author(s): [Honza Dvorsky](https://github.com/czechboy0) +- Status: **In Review** +- Issue: [apple/swift-configuration#66](https://github.com/apple/swift-configuration/issues/66) +- Implementation: + - [apple/swift-configuration#73](https://github.com/apple/swift-configuration/pull/73) +- Revisions: + - v1 - Nov 12, 2025 - Initial proposal. + +### Introduction + +Add an `allowMissing` Boolean parameter to file-based configuration providers to enable graceful handling of missing configuration files. + +### Motivation + +Applications often need to handle optional configuration files that may not exist at startup or during runtime. Currently, all file-based providers (`FileProvider`, `ReloadingFileProvider`, `DirectoryFilesProvider`, and `EnvironmentVariablesProvider` when initialized from an `.env` file) throw errors when the specified configuration file is missing, which creates several challenges: + +- Applications fail to start when optional configuration files are missing, even when they could operate with sensible defaults specified in code. +- In containerized environments or cloud deployments, configuration files may be mounted dynamically or created by other services, making their availability timing unpredictable. +- Developers must create placeholder configuration files even when working on features that don't require external configuration. + +Currently, adopters must implement workarounds such as manually checking for a file's presence before creating a file-based provider, which requires writing needless boilerplate code. + +### Proposed solution + +We propose adding an `allowMissing` parameter to the initializers of `FileProvider`, `ReloadingFileProvider`, `DirectoryFilesProvider`, and `EnvironmentVariablesProvider`. When set to `true`, missing files are treated as empty configuration sources instead of causing initialization failures. + +Key behavioral changes: + +```swift +// Current behavior - throws if config.json doesn't exist +let provider = try await FileProvider(filePath: "config.json") + +// New behavior - succeeds even if config.json doesn't exist +let provider = try await FileProvider( + filePath: "config.json", + allowMissing: true +) +``` + +The `allowMissing` parameter defaults to `false`, preserving existing behavior for backward compatibility and keeping the strict variant as the default. When `true`: + +- Missing files are treated as empty configuration (no key-value pairs). +- Reloading providers continue to work - providers detect when missing files are created, updated, and deleted. +- Malformed files still throw parsing errors regardless of the `allowMissing` setting. +- Directory provider treats missing directories as empty. + +Example usage patterns: + +```swift +// Multi-layered configuration with optional overrides +let config = ConfigReader(provider: [ + EnvironmentVariablesProvider(), + try await FileProvider( + filePath: "optional-config.json", + allowMissing: true // Won't fail if missing + ), + InMemoryProvider(data: ["fallback": "values"]) +]) + +// Reloading provider that handles dynamic file creation, updates, and deletion +let dynamicConfig = try await ReloadingFileProvider( + filePath: "/etc/dynamic/config.yaml", + allowMissing: true, + pollInterval: .seconds(5) +) +``` + +### Detailed design + +#### API additions + +All affected initializers will gain an `allowMissing` parameter: + +```swift +// FileProvider.swift +public init( + snapshotType: Snapshot.Type = Snapshot.self, + parsingOptions: Snapshot.ParsingOptions = .default, + filePath: FilePath, + allowMissing: Bool = false // <<< new +) async throws + +// ReloadingFileProvider +public convenience init( + snapshotType: Snapshot.Type = Snapshot.self, + parsingOptions: Snapshot.ParsingOptions = .default, + filePath: FilePath, + allowMissing: Bool = false, // <<< new + pollInterval: Duration = .seconds(15), + logger: Logger = Logger(label: "ReloadingFileProvider"), + metrics: any MetricsFactory = MetricsSystem.factory +) async throws + +// DirectoryFilesProvider +public init( + directoryPath: FilePath, + allowMissing: Bool = false, // <<< new + secretsSpecifier: SecretsSpecifier = .all, + arraySeparator: Character = ",", + keyEncoder: some ConfigKeyEncoder = .directoryFiles +) async throws + +// EnvironmentVariablesProvider +public init( + environmentFilePath: FilePath, + allowMissing: Bool = false, // <<< new + secretsSpecifier: SecretsSpecifier = .none, + bytesDecoder: some ConfigBytesFromStringDecoder = .base64, + arraySeparator: Character = "," +) async throws +``` + +#### Configuration keys + +When using `ConfigReader`-based initialization, a new key is supported: + +- `allowMissing` (boolean, optional, default: false): Whether to allow missing files/directories. + +### API stability + +This change is purely additive, so no existing adopters are affected. + +### Future directions + +Nothing comes to mind at the moment. + +### Alternatives considered + +Status quo - we could have kept the file-reading behavior strict, which would require adopters to write conditional logic when setting up their `ConfigReader`.