Last active
February 22, 2026 20:25
-
-
Save Malien/59b1d764b610ae02710103798cfa09a2 to your computer and use it in GitHub Desktop.
A POC for the typesafe API for the swift-configuration package
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @attached(member, names: named(ConfigProperties)) | |
| @attached(extension, conformances: ConfigShape) | |
| public macro ConfigShape() = #externalMacro(module: "TypesafeConfigurationMacros", type: "ConfigShapeMacro") | |
| @attached(peer) | |
| public macro ConfigProperty(key: String) = #externalMacro(module: "TypesafeConfigurationMacros", type: "ConfigPropertyMacro") | |
| import Configuration | |
| protocol ConfigShape { | |
| associatedtype ConfigProperties: ConfigPropertiesProtocol<Self> | |
| } | |
| protocol ConfigPropertiesProtocol<Shape> { | |
| // Reccursive "pointer" to a parent type, so that we can infer the | |
| // container type in the call to `.scoped(to:)` | |
| // | |
| // Users don't ever have to deal with, or know about the generated | |
| // ConfigProperties types. | |
| associatedtype Shape | |
| init(parentKey: String?) | |
| var parentKey: String? { get } | |
| } | |
| // @ConfigShape | |
| struct HTTPConfig: ConfigShape { | |
| var hostname: String? | |
| var port: Int = 80 | |
| // @ConfigProperty(key: "ca") | |
| var certificate: String | |
| // One would imagine this namespace being generated by a macro | |
| // from the definition above, and not by hand | |
| struct ConfigProperties: ConfigPropertiesProtocol { | |
| typealias Shape = HTTPConfig | |
| var parentKey: String? | |
| init(parentKey: String?) { | |
| self.parentKey = parentKey | |
| } | |
| let hostname = ConfigProperty.Optional<String>(key: "hostname") | |
| let port = ConfigProperty.Defaulted<Int>(key: "port", default: 80) | |
| let certificate = ConfigProperty.Required<String>(key: "ca") | |
| } | |
| } | |
| // @ConfigShape | |
| struct ApplicationConfig: ConfigShape { | |
| var stripeKey: String | |
| var http: HTTPConfig | |
| struct ConfigProperties: ConfigPropertiesProtocol { | |
| typealias Shape = ApplicationConfig | |
| var parentKey: String? | |
| init(parentKey: String?) { | |
| self.parentKey = parentKey | |
| } | |
| let stripeKey = ConfigProperty.Required<String>(key: "stripeKey") | |
| let http = HTTPConfig.ConfigProperties(parentKey: "http") | |
| } | |
| } | |
| enum ConfigProperty { | |
| struct Optional<T> { | |
| var key: String | |
| } | |
| struct Defaulted<T> { | |
| var key: String | |
| var `default`: T | |
| } | |
| struct Required<T> { | |
| var key: String | |
| } | |
| } | |
| struct TypesafeConfigReader<Source: ConfigShape> { | |
| var reader: ConfigReader | |
| var propertyDescriptors: Source.ConfigProperties | |
| init(for: Source.Type, over reader: ConfigReader) { | |
| self.reader = reader | |
| self.propertyDescriptors = Source.ConfigProperties(parentKey: nil) | |
| } | |
| init(for type: Source.Type, providers: [any ConfigProvider]) { | |
| self.init(for: type, over: ConfigReader(providers: providers)) | |
| } | |
| func get(_ keyPath: KeyPath<Source.ConfigProperties, ConfigProperty.Optional<Int>>) -> Int? { | |
| let descriptor = propertyDescriptors[keyPath: keyPath] | |
| return reader.int(forKey: descriptor.key) | |
| } | |
| func get(_ keyPath: KeyPath<Source.ConfigProperties, ConfigProperty.Defaulted<Int>>) -> Int { | |
| let descriptor = propertyDescriptors[keyPath: keyPath] | |
| return reader.int(forKey: descriptor.key, default: descriptor.default) | |
| } | |
| func get(_ keyPath: KeyPath<Source.ConfigProperties, ConfigProperty.Required<Int>>) throws -> Int { | |
| let descriptor = propertyDescriptors[keyPath: keyPath] | |
| return try reader.requiredInt(forKey: descriptor.key) | |
| } | |
| func get(_ keyPath: KeyPath<Source.ConfigProperties, ConfigProperty.Optional<String>>) -> String? { | |
| let descriptor = propertyDescriptors[keyPath: keyPath] | |
| return reader.string(forKey: descriptor.key) | |
| } | |
| func get(_ keyPath: KeyPath<Source.ConfigProperties, ConfigProperty.Defaulted<String>>) -> String { | |
| let descriptor = propertyDescriptors[keyPath: keyPath] | |
| return reader.string(forKey: descriptor.key, default: descriptor.default) | |
| } | |
| func get(_ keyPath: KeyPath<Source.ConfigProperties, ConfigProperty.Required<String>>) throws -> String { | |
| let descriptor = propertyDescriptors[keyPath: keyPath] | |
| return try reader.requiredString(forKey: descriptor.key) | |
| } | |
| // Same for double, bool, bytes, and array variants | |
| // Same for async "fetch" variants | |
| func scoped<Target: ConfigPropertiesProtocol>( | |
| to keyPath: KeyPath<Source.ConfigProperties, Target>, | |
| ) -> TypesafeConfigReader<Target.Shape> { | |
| let descriptor = propertyDescriptors[keyPath: keyPath] | |
| guard let key = descriptor.parentKey else { | |
| fatalError("Wrong conformance to the ConfigShape. Nested configs must have parentKey set") | |
| } | |
| return .init(for: Target.Shape.self, over: reader.scoped(to: key)) | |
| } | |
| } | |
| func main() throws { | |
| let app = TypesafeConfigReader(for: ApplicationConfig.self, providers: []) | |
| let stripeKey: String = try app.get(\.stripeKey) // is required | |
| let hostname: String? = app.get(\.http.hostname) // is optional | |
| let port: Int = app.get(\.http.port) // has default | |
| let http = app.scoped(to: \.http) | |
| assert(app.get(\.http.hostname) == http.get(\.hostname)) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment