Skip to content

Instantly share code, notes, and snippets.

@Malien
Last active February 22, 2026 20:25
Show Gist options
  • Select an option

  • Save Malien/59b1d764b610ae02710103798cfa09a2 to your computer and use it in GitHub Desktop.

Select an option

Save Malien/59b1d764b610ae02710103798cfa09a2 to your computer and use it in GitHub Desktop.
A POC for the typesafe API for the swift-configuration package
@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