|
| 1 | +# SCO-0003: Allow missing files in file providers |
| 2 | + |
| 3 | +Add an `allowMissing` parameter to file-based providers to handle missing configuration files gracefully. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +- Proposal: SCO-0003 |
| 8 | +- Author(s): [Honza Dvorsky](https://github.com/czechboy0) |
| 9 | +- Status: **Implemented (1.0.0-alpha.1)** |
| 10 | +- Issue: [apple/swift-configuration#66](https://github.com/apple/swift-configuration/issues/66) |
| 11 | +- Implementation: |
| 12 | + - [apple/swift-configuration#73](https://github.com/apple/swift-configuration/pull/73) |
| 13 | +- Revisions: |
| 14 | + - v1 - Nov 12, 2025 - Initial proposal. |
| 15 | + |
| 16 | +### Introduction |
| 17 | + |
| 18 | +Add an `allowMissing` Boolean parameter to file-based configuration providers to enable graceful handling of missing configuration files. |
| 19 | + |
| 20 | +### Motivation |
| 21 | + |
| 22 | +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: |
| 23 | + |
| 24 | +- Applications fail to start when optional configuration files are missing, even when they could operate with sensible defaults specified in code. |
| 25 | +- In containerized environments or cloud deployments, configuration files may be mounted dynamically or created by other services, making their availability timing unpredictable. |
| 26 | +- Developers must create placeholder configuration files even when working on features that don't require external configuration. |
| 27 | + |
| 28 | +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. |
| 29 | + |
| 30 | +### Proposed solution |
| 31 | + |
| 32 | +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. |
| 33 | + |
| 34 | +Key behavioral changes: |
| 35 | + |
| 36 | +```swift |
| 37 | +// Current behavior - throws if config.json doesn't exist |
| 38 | +let provider = try await FileProvider<JSONSnapshot>(filePath: "config.json") |
| 39 | + |
| 40 | +// New behavior - succeeds even if config.json doesn't exist |
| 41 | +let provider = try await FileProvider<JSONSnapshot>( |
| 42 | + filePath: "config.json", |
| 43 | + allowMissing: true |
| 44 | +) |
| 45 | +``` |
| 46 | + |
| 47 | +The `allowMissing` parameter defaults to `false`, preserving existing behavior for backward compatibility and keeping the strict variant as the default. When `true`: |
| 48 | + |
| 49 | +- Missing files are treated as empty configuration (no key-value pairs). |
| 50 | +- Reloading providers continue to work - providers detect when missing files are created, updated, and deleted. |
| 51 | +- Malformed files still throw parsing errors regardless of the `allowMissing` setting. |
| 52 | +- Directory provider treats missing directories as empty. |
| 53 | + |
| 54 | +Example usage patterns: |
| 55 | + |
| 56 | +```swift |
| 57 | +// Multi-layered configuration with optional overrides |
| 58 | +let config = ConfigReader(provider: [ |
| 59 | + EnvironmentVariablesProvider(), |
| 60 | + try await FileProvider<JSONSnapshot>( |
| 61 | + filePath: "optional-config.json", |
| 62 | + allowMissing: true // Won't fail if missing |
| 63 | + ), |
| 64 | + InMemoryProvider(data: ["fallback": "values"]) |
| 65 | +]) |
| 66 | + |
| 67 | +// Reloading provider that handles dynamic file creation, updates, and deletion |
| 68 | +let dynamicConfig = try await ReloadingFileProvider<YAMLSnapshot>( |
| 69 | + filePath: "/etc/dynamic/config.yaml", |
| 70 | + allowMissing: true, |
| 71 | + pollInterval: .seconds(5) |
| 72 | +) |
| 73 | +``` |
| 74 | + |
| 75 | +### Detailed design |
| 76 | + |
| 77 | +#### API additions |
| 78 | + |
| 79 | +All affected initializers will gain an `allowMissing` parameter: |
| 80 | + |
| 81 | +```swift |
| 82 | +// FileProvider.swift |
| 83 | +public init( |
| 84 | + snapshotType: Snapshot.Type = Snapshot.self, |
| 85 | + parsingOptions: Snapshot.ParsingOptions = .default, |
| 86 | + filePath: FilePath, |
| 87 | + allowMissing: Bool = false // <<< new |
| 88 | +) async throws |
| 89 | + |
| 90 | +// ReloadingFileProvider |
| 91 | +public convenience init( |
| 92 | + snapshotType: Snapshot.Type = Snapshot.self, |
| 93 | + parsingOptions: Snapshot.ParsingOptions = .default, |
| 94 | + filePath: FilePath, |
| 95 | + allowMissing: Bool = false, // <<< new |
| 96 | + pollInterval: Duration = .seconds(15), |
| 97 | + logger: Logger = Logger(label: "ReloadingFileProvider"), |
| 98 | + metrics: any MetricsFactory = MetricsSystem.factory |
| 99 | +) async throws |
| 100 | + |
| 101 | +// DirectoryFilesProvider |
| 102 | +public init( |
| 103 | + directoryPath: FilePath, |
| 104 | + allowMissing: Bool = false, // <<< new |
| 105 | + secretsSpecifier: SecretsSpecifier<String, Data> = .all, |
| 106 | + arraySeparator: Character = ",", |
| 107 | + keyEncoder: some ConfigKeyEncoder = .directoryFiles |
| 108 | +) async throws |
| 109 | + |
| 110 | +// EnvironmentVariablesProvider |
| 111 | +public init( |
| 112 | + environmentFilePath: FilePath, |
| 113 | + allowMissing: Bool = false, // <<< new |
| 114 | + secretsSpecifier: SecretsSpecifier<String, String> = .none, |
| 115 | + bytesDecoder: some ConfigBytesFromStringDecoder = .base64, |
| 116 | + arraySeparator: Character = "," |
| 117 | +) async throws |
| 118 | +``` |
| 119 | + |
| 120 | +#### Configuration keys |
| 121 | + |
| 122 | +When using `ConfigReader`-based initialization, a new key is supported: |
| 123 | + |
| 124 | +- `allowMissing` (boolean, optional, default: false): Whether to allow missing files/directories. |
| 125 | + |
| 126 | +### API stability |
| 127 | + |
| 128 | +This change is purely additive, so no existing adopters are affected. |
| 129 | + |
| 130 | +### Future directions |
| 131 | + |
| 132 | +Nothing comes to mind at the moment. |
| 133 | + |
| 134 | +### Alternatives considered |
| 135 | + |
| 136 | +Status quo - we could have kept the file-reading behavior strict, which would require adopters to write conditional logic when setting up their `ConfigReader`. |
0 commit comments