Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ If you have any questions, ask in an issue on GitHub.

- <doc:SCO-NNNN>
- <doc:SCO-0001>
- <doc:SCO-0002>
119 changes: 119 additions & 0 deletions Sources/Configuration/Documentation.docc/Proposals/SCO-0002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# SCO-0002: Remove custom key decoders

Remove the custom key decoder feature to fix a flaw and simplify the project

## Overview

- Proposal: SCO-0002
- Author(s): [Honza Dvorsky](https://github.com/czechboy0)
- Status: **In Review**
- Issue: [apple/swift-configuration#70](https://github.com/apple/swift-configuration/issues/70)
- Implementation:
- [apple/swift-configuration#71](https://github.com/apple/swift-configuration/pull/71)

### Introduction

Remove the custom key decoder feature to fix a flaw and simplify the project.

### Motivation

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.

Let's consider a key with the components `["http", "client", "read", "timeout"]`, read by an HTTPClient library, used by the root App executable.

#### Default key decoder

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:

```swift
// App/main.swift
import HTTPClient

let config = ConfigReader(
provider: EnvironmentVariablesProvider()
// ✅ Uses the default dot-based key decoder.
)

// ✅ Uses scoping correctly
let client = Client(config: config.scoped(to: "http.client"))

```

```swift
// HTTPClient/Config.swift

extension Client {
public init(config: ConfigReader) {
// ✅ reads ["http", "client", "read", "timeout"]
self.timeout = config.int(forKey: "read.timeout", default: 30)
}
}
```

#### Caller-customized key decoder

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.

```swift
// App/main.swift
import HTTPClient

let config = ConfigReader(
provider: EnvironmentVariablesProvider()
// ⚠️ Customizes the key decoder
keyDecoder: SeparatorKeyDecoder(separator: ":")
)

// ✅ Uses scoping correctly with the custom delimiter
let client = Client(config: config.scoped(to: "http:client"))

```

```swift
// HTTPClient/Config.swift

extension Client {
public init(config: ConfigReader) {
// ❌ incorrectly reads ["http", "client", "read.timeout"] (last component is incorrect)
self.timeout = config.int(forKey: "read.timeout", default: 30)
}
}
```

### Proposed solution

Remove the custom key decoder feature altogether and simplify the project.

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.

Not only does this change address a major flaw, it also removes over 10k lines of code.

### Detailed design

- Make `ConfigKey` and `AbsoluteConfigKey` conform to `ExpressibleByStringLiteral` and always use a dot as the delimiter when splitting components.
- Remove any public APIs related to key decoding, such as the `ConfigKeyDecoder` protocol and all concrete implementations.
- Remove all public overloads that specialized for a String-based key.

For details, check out the [draft PR](https://github.com/apple/swift-configuration/pull/71).

### API stability

Most adopters do not need to change any code.

Only if you used a method that takes both a string key and a context parameter, you now need to construct a `ConfigKey` manually:

```diff
config.string(
- forKey: "server.timeout",
- context: ["upstream": "example.com]
+ forKey: ConfigKey("server.timeout", context: ["upstream": "example.com])
)
```

### Future directions

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.

### Alternatives considered

- 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.