|
| 1 | +# SCO-0002: Remove custom key decoders |
| 2 | + |
| 3 | +Remove the custom key decoder feature to fix a flaw and simplify the project |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +- Proposal: SCO-0002 |
| 8 | +- Author(s): [Honza Dvorsky](https://github.com/czechboy0) |
| 9 | +- Status: **In Review** |
| 10 | +- Issue: [apple/swift-configuration#70](https://github.com/apple/swift-configuration/issues/70) |
| 11 | +- Implementation: |
| 12 | + - [apple/swift-configuration#71](https://github.com/apple/swift-configuration/pull/71) |
| 13 | + |
| 14 | +### Introduction |
| 15 | + |
| 16 | +Remove the custom key decoder feature to fix a flaw and simplify the project. |
| 17 | + |
| 18 | +### Motivation |
| 19 | + |
| 20 | +The custom key decoder [feature](https://swiftpackageindex.com/apple/swift-configuration/0.2.0/documentation/configuration#Custom-key-syntax) allowed using custom syntax in multi-component keys. |
| 21 | + |
| 22 | +Let's consider a key with the components `["http", "client", "read", "timeout"]`, read by an HTTPClient library, used by the root App executable. |
| 23 | + |
| 24 | +#### Default key decoder |
| 25 | + |
| 26 | +When both targets use and assume the default key decoder is used, all is well, as both rely on a single period as the delimiter in string-based keys: |
| 27 | + |
| 28 | +```swift |
| 29 | +// App/main.swift |
| 30 | +import HTTPClient |
| 31 | + |
| 32 | +let config = ConfigReader( |
| 33 | + provider: EnvironmentVariablesProvider() |
| 34 | + // ✅ Uses the default dot-based key decoder. |
| 35 | +) |
| 36 | + |
| 37 | +// ✅ Uses scoping correctly |
| 38 | +let client = Client(config: config.scoped(to: "http.client")) |
| 39 | + |
| 40 | +``` |
| 41 | + |
| 42 | +```swift |
| 43 | +// HTTPClient/Config.swift |
| 44 | + |
| 45 | +extension Client { |
| 46 | + public init(config: ConfigReader) { |
| 47 | + // ✅ reads ["http", "client", "read", "timeout"] |
| 48 | + self.timeout = config.int(forKey: "read.timeout", default: 30) |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +#### Called-customized key decoder |
| 54 | + |
| 55 | +However, if the App target customizes the key decoder to use a colon as the delimiter, it breaks the library that receives the config reader and reads an incorrect key. |
| 56 | + |
| 57 | +```swift |
| 58 | +// App/main.swift |
| 59 | +import HTTPClient |
| 60 | + |
| 61 | +let config = ConfigReader( |
| 62 | + provider: EnvironmentVariablesProvider() |
| 63 | + // ⚠️ Customizes the key decoder |
| 64 | + keyDecoder: SeparatorKeyDecoder(separator: ":") |
| 65 | +) |
| 66 | + |
| 67 | +// ✅ Uses scoping correctly with the custom delimiter |
| 68 | +let client = Client(config: config.scoped(to: "http:client")) |
| 69 | + |
| 70 | +``` |
| 71 | + |
| 72 | +```swift |
| 73 | +// HTTPClient/Config.swift |
| 74 | + |
| 75 | +extension Client { |
| 76 | + public init(config: ConfigReader) { |
| 77 | + // ❌ incorrectly reads ["http", "client", "read.timeout"] (last component is incorrect) |
| 78 | + self.timeout = config.int(forKey: "read.timeout", default: 30) |
| 79 | + } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +### Proposed solution |
| 84 | + |
| 85 | +Remove the custom key decoder feature altogether and simplify the project. |
| 86 | + |
| 87 | +This is enabled by making `ConfigKey` and `AbsoluteConfigKey` conform to `ExpressibleByStringLiteral`, which is only possible now because we can hardcode the string splitting to use the dot delimiter. This allows us to remove the explicit method overloads that took `String` for the key parameter, and we only take `ConfigKey` now. However, since `ConfigKey` is `ExpressibleByStringLiteral` now, the user experience is mostly unchanged. |
| 88 | + |
| 89 | +Not only does this change address a major flaw, it also removes over 10k lines of code. |
| 90 | + |
| 91 | +### Detailed design |
| 92 | + |
| 93 | +- Make `ConfigKey` and `AbsoluteConfigKey` conform to `ExpressibleByStringLiteral` and always use a dot as the delimiter when splitting components. |
| 94 | +- Remove any public APIs related to key decoding, such as the `ConfigKeyDecoder` protocol and all concrete implementations. |
| 95 | +- Remove all public overloads that specialized for a String-based key. |
| 96 | + |
| 97 | +For details, check out the [draft PR](https://github.com/apple/swift-configuration/pull/71). |
| 98 | + |
| 99 | +### API stability |
| 100 | + |
| 101 | +Most adopters do not need to change any code. |
| 102 | + |
| 103 | +Only if you used a method that takes both a string key and a context parameter, you now need to construct a `ConfigKey` manually: |
| 104 | + |
| 105 | +```diff |
| 106 | +config.string( |
| 107 | +- forKey: "server.timeout", |
| 108 | +- context: ["upstream": "example.com] |
| 109 | ++ forKey: ConfigKey("server.timeout", context: ["upstream": "example.com]) |
| 110 | +) |
| 111 | +``` |
| 112 | + |
| 113 | +### Future directions |
| 114 | + |
| 115 | +The feature might be added back in the future, if there's enough value in it and we can find a design that avoids the flaw. |
| 116 | + |
| 117 | +### Alternatives considered |
| 118 | + |
| 119 | +- Keep as API and document that config readers with customized key decoders must not be passed to other libraries: seemed like a suboptimal solution and would weaken the ability of the ecosystem to seamlessly integrate different libraries. |
0 commit comments