Sources/Hub/Config.swift (670 lines of code) (raw):
//
// Config.swift
// swift-transformers
//
// Created by Piotr Kowalczuk on 06.03.25.
import Foundation
// MARK: - Configuration files with dynamic lookup
@dynamicMemberLookup
public struct Config: Hashable, Sendable,
ExpressibleByStringLiteral,
ExpressibleByIntegerLiteral,
ExpressibleByBooleanLiteral,
ExpressibleByFloatLiteral,
ExpressibleByDictionaryLiteral,
ExpressibleByArrayLiteral,
ExpressibleByExtendedGraphemeClusterLiteral,
CustomStringConvertible
{
public typealias Key = BinaryDistinctString
public typealias Value = Config
private let value: Data
public enum Data: Sendable {
case null
case string(BinaryDistinctString)
case integer(Int)
case boolean(Bool)
case floating(Float)
case dictionary([BinaryDistinctString: Config])
case array([Config])
case token((UInt, BinaryDistinctString))
public static func == (lhs: Data, rhs: Data) -> Bool {
switch (lhs, rhs) {
case (.null, .null):
return true
case let (.string(lhs), _):
if let rhs = rhs.string() {
return lhs == BinaryDistinctString(rhs)
}
case let (.integer(lhs), _):
if let rhs = rhs.integer() {
return lhs == rhs
}
case let (.boolean(lhs), _):
if let rhs = rhs.boolean() {
return lhs == rhs
}
case let (.floating(lhs), _):
if let rhs = rhs.floating() {
return lhs == rhs
}
case let (.dictionary(lhs), .dictionary(rhs)):
return lhs == rhs
case let (.array(lhs), .array(rhs)):
return lhs == rhs
case let (.token(lhs), .token(rhs)):
return lhs == rhs
default:
return false
}
// right hand side might be a super set of left hand side
switch rhs {
case let .string(rhs):
if let lhs = lhs.string() {
return BinaryDistinctString(lhs) == rhs
}
case let .integer(rhs):
if let lhs = lhs.integer() {
return lhs == rhs
}
case let .boolean(rhs):
if let lhs = lhs.boolean() {
return lhs == rhs
}
case let .floating(rhs):
if let lhs = lhs.floating() {
return lhs == rhs
}
default:
return false
}
return false
}
public var description: String {
switch self {
case .null:
"null"
case let .string(value):
"\"\(value)\""
case let .integer(value):
"\(value)"
case let .boolean(value):
"\(value)"
case let .floating(value):
"\(value)"
case let .array(arr):
"[\(arr)]"
case let .dictionary(val):
"{\(val)}"
case let .token(val):
"(\(val.0), \(val.1))"
}
}
public func string() -> String? {
if case let .string(val) = self {
return val.string
}
return nil
}
public func boolean() -> Bool? {
if case let .boolean(val) = self {
return val
}
if case let .integer(val) = self {
return val == 1
}
if case let .string(val) = self {
switch val.string.lowercased() {
case "true", "t", "1":
return true
case "false", "f", "0":
return false
default:
return nil
}
}
return nil
}
public func integer() -> Int? {
if case let .integer(val) = self {
return val
}
return nil
}
public func floating() -> Float? {
if case let .floating(val) = self {
return val
}
if case let .integer(val) = self {
return Float(val)
}
return nil
}
}
init() {
self.value = .null
}
public init(_ value: BinaryDistinctString) {
self.value = .string(value)
}
public init(_ value: String) {
self.init(stringLiteral: value)
}
public init(_ value: Int) {
self.init(integerLiteral: value)
}
public init(_ value: Bool) {
self.init(booleanLiteral: value)
}
public init(_ value: Float) {
self.init(floatLiteral: value)
}
public init(_ value: [Config]) {
self.value = .array(value)
}
public init(_ values: (BinaryDistinctString, Config)...) {
var dict = [BinaryDistinctString: Config]()
for (key, value) in values {
dict[key] = value
}
self.value = .dictionary(dict)
}
public init(_ value: [BinaryDistinctString: Config]) {
self.value = .dictionary(value)
}
public init(_ dictionary: [NSString: Any]) {
self.value = Config.convertToBinaryDistinctKeys(dictionary as Any).value
}
public init(_ dictionary: [String: Config]) {
self.value = Config.convertToBinaryDistinctKeys(dictionary as Any).value
}
public init(_ dictionary: [NSString: Config]) {
self.value = Config.convertToBinaryDistinctKeys(dictionary as Any).value
}
public init(_ token: (UInt, BinaryDistinctString)) {
self.value = .token(token)
}
private static func convertToBinaryDistinctKeys(_ object: Any) -> Config {
if let dict = object as? [NSString: Any] {
Config(Dictionary(uniqueKeysWithValues: dict.map { (BinaryDistinctString($0.key), convertToBinaryDistinctKeys($0.value)) }))
} else if let array = object as? [Any] {
Config(array.map { convertToBinaryDistinctKeys($0) })
} else {
switch object {
case let obj as String:
Config(obj)
case let obj as Int:
Config(obj)
case let obj as Float:
Config(obj)
case let obj as Bool:
Config(obj)
case let obj as NSNumber:
if CFNumberIsFloatType(obj) {
Config(obj.floatValue)
} else {
Config(obj.intValue)
}
case _ as NSNull:
Config()
case let obj as Config:
obj
case let obj as (UInt, String):
Config((obj.0, BinaryDistinctString(obj.1)))
default:
fatalError("unknown type: \(type(of: object)) \(object)")
}
}
}
// MARK: constructors
/// Conformance to ExpressibleByStringLiteral
public init(stringLiteral value: String) {
self.value = .string(.init(value))
}
/// Conformance to ExpressibleByIntegerLiteral
public init(integerLiteral value: Int) {
self.value = .integer(value)
}
/// Conformance to ExpressibleByBooleanLiteral
public init(booleanLiteral value: Bool) {
self.value = .boolean(value)
}
/// Conformance to ExpressibleByFloatLiteral
public init(floatLiteral value: Float) {
self.value = .floating(value)
}
public init(dictionaryLiteral elements: (BinaryDistinctString, Config)...) {
let dict = elements.reduce(into: [BinaryDistinctString: Config]()) { result, element in
result[element.0] = element.1
}
self.value = .dictionary(dict)
}
public init(arrayLiteral elements: Config...) {
self.value = .array(elements)
}
public func isNull() -> Bool {
if case .null = self.value {
return true
}
return false
}
// MARK: getters - string
public func get() -> String? {
self.string()
}
public func get(or: String) -> String? {
self.string(or: or)
}
public func string() -> String? {
self.value.string()
}
public func string(or: String) -> String {
if let val: String = self.string() {
return val
}
return or
}
public func get() -> BinaryDistinctString? {
self.binaryDistinctString()
}
public func get(or: BinaryDistinctString) -> BinaryDistinctString? {
self.binaryDistinctString(or: or)
}
public func binaryDistinctString() -> BinaryDistinctString? {
if case let .string(val) = self.value {
return val
}
return nil
}
public func binaryDistinctString(or: BinaryDistinctString) -> BinaryDistinctString {
if let val: BinaryDistinctString = self.binaryDistinctString() {
return val
}
return or
}
// MARK: getters - boolean
public func get() -> Bool? {
self.boolean()
}
public func get(or: Bool) -> Bool? {
self.boolean(or: or)
}
public func boolean() -> Bool? {
self.value.boolean()
}
public func boolean(or: Bool) -> Bool {
if let val = self.boolean() {
return val
}
return or
}
// MARK: getters - integer
public func get() -> Int? {
self.integer()
}
public func get(or: Int) -> Int? {
self.integer(or: or)
}
public func integer() -> Int? {
self.value.integer()
}
public func integer(or: Int) -> Int {
if let val = self.integer() {
return val
}
return or
}
// MARK: getters/operators - floating
public func get() -> Float? {
self.value.floating()
}
public func get(or: Float) -> Float? {
self.floating(or: or)
}
public func floating() -> Float? {
self.value.floating()
}
public func floating(or: Float) -> Float {
if let val = self.value.floating() {
return val
}
return or
}
// MARK: getters - dictionary
public func get() -> [BinaryDistinctString: Int]? {
if let dict = self.dictionary() {
return dict.reduce(into: [:]) { result, element in
if let val = element.value.value.integer() {
result[element.key] = val
}
}
}
return nil
}
public func get() -> [BinaryDistinctString: Config]? {
self.dictionary()
}
public func get(or: [BinaryDistinctString: Config]) -> [BinaryDistinctString: Config] {
self.dictionary(or: or)
}
public func toJinjaCompatible() -> Any? {
switch self.value {
case let .array(val):
return val.map { $0.toJinjaCompatible() }
case let .dictionary(val):
var result: [String: Any?] = [:]
for (key, config) in val {
result[key.string] = config.toJinjaCompatible()
}
return result
case let .boolean(val):
return val
case let .floating(val):
return val
case let .integer(val):
return val
case let .string(val):
return val.string
case let .token(val):
return [String(val.0): val.1.string] as [String: String]
case .null:
return nil
}
}
public func dictionary() -> [BinaryDistinctString: Config]? {
if case let .dictionary(val) = self.value {
return val
}
return nil
}
public func dictionary(or: [BinaryDistinctString: Config]) -> [BinaryDistinctString: Config] {
if let val = self.dictionary() {
return val
}
return or
}
// MARK: getters - array
public func get() -> [String]? {
if let arr = self.array() {
return arr.reduce(into: []) { result, element in
if let val: String = element.value.string() {
result.append(val)
}
}
}
return nil
}
public func get(or: [String]) -> [String] {
if let arr: [String] = self.get() {
return arr
}
return or
}
public func get() -> [BinaryDistinctString]? {
if let arr = self.array() {
return arr.reduce(into: []) { result, element in
if let val: BinaryDistinctString = element.binaryDistinctString() {
result.append(val)
}
}
}
return nil
}
public func get(or: [BinaryDistinctString]) -> [BinaryDistinctString] {
if let arr: [BinaryDistinctString] = self.get() {
return arr
}
return or
}
public func get() -> [Config]? {
self.array()
}
public func get(or: [Config]) -> [Config] {
self.array(or: or)
}
public func array() -> [Config]? {
if case let .array(val) = self.value {
return val
}
return nil
}
public func array(or: [Config]) -> [Config] {
if let val = self.array() {
return val
}
return or
}
// MARK: getters - token
public func get() -> (UInt, String)? {
self.token()
}
public func get(or: (UInt, String)) -> (UInt, String) {
self.token(or: or)
}
public func token() -> (UInt, String)? {
if case let .token(val) = self.value {
return (val.0, val.1.string)
}
if case let .array(arr) = self.value {
guard arr.count == 2 else {
return nil
}
guard let token = arr[0].string() else {
return nil
}
guard let id = arr[1].integer() else {
return nil
}
return (UInt(id), token)
}
return nil
}
public func token(or: (UInt, String)) -> (UInt, String) {
if let val = self.token() {
return val
}
return or
}
// MARK: subscript
public subscript(index: BinaryDistinctString) -> Config {
if let dict = self.dictionary() {
return dict[index] ?? dict[self.uncamelCase(index)] ?? Config()
}
return Config()
}
public subscript(index: Int) -> Config {
if let arr = self.array(), index >= 0, index < arr.count {
return arr[index]
}
return Config()
}
public subscript(dynamicMember member: String) -> Config? {
if let dict = self.dictionary() {
return dict[BinaryDistinctString(member)] ?? dict[self.uncamelCase(BinaryDistinctString(member))] ?? Config()
}
return nil // backward compatibility
}
public subscript(dynamicMember member: String) -> Config {
if let dict = self.dictionary() {
return dict[BinaryDistinctString(member)] ?? dict[self.uncamelCase(BinaryDistinctString(member))] ?? Config()
}
return Config()
}
func uncamelCase(_ string: BinaryDistinctString) -> BinaryDistinctString {
let scalars = string.string.unicodeScalars
var result = ""
var previousCharacterIsLowercase = false
for scalar in scalars {
if CharacterSet.uppercaseLetters.contains(scalar) {
if previousCharacterIsLowercase {
result += "_"
}
let lowercaseChar = Character(scalar).lowercased()
result += lowercaseChar
previousCharacterIsLowercase = false
} else {
result += String(scalar)
previousCharacterIsLowercase = true
}
}
return BinaryDistinctString(result)
}
public var description: String {
"\(self.value.description)"
}
}
/// Old style, deprecated getters
public extension Config {
@available(*, deprecated, message: "Use string() instead")
var stringValue: String? { string() }
@available(*, deprecated, message: "Use integer() instead")
var intValue: Int? { integer() }
@available(*, deprecated, message: "Use boolean() instead")
var boolValue: Bool? { boolean() }
@available(*, deprecated, message: "Use array() instead")
var arrayValue: [Config]? { array() }
@available(*, deprecated, message: "Use token() instead")
var tokenValue: (UInt, String)? { token() }
}
extension Config: Codable {
public init(from decoder: any Decoder) throws {
// Try decoding as a single value first (for scalars and null)
let singleValueContainer = try? decoder.singleValueContainer()
if let container = singleValueContainer {
if container.decodeNil() {
self.value = .null
return
}
do {
let intValue = try container.decode(Int.self)
self.value = .integer(intValue)
return
} catch { }
do {
let floatValue = try container.decode(Float.self)
self.value = .floating(floatValue)
return
} catch { }
do {
let boolValue = try container.decode(Bool.self)
self.value = .boolean(boolValue)
return
} catch { }
do {
let stringValue = try container.decode(String.self)
self.value = .string(.init(stringValue))
return
} catch { }
}
if let tupple = Self.decodeTuple(decoder) {
self.value = tupple
return
}
if let array = Self.decodeArray(decoder) {
self.value = array
return
}
if let dict = Self.decodeDictionary(decoder) {
self.value = dict
return
}
self.value = .null
}
private static func decodeTuple(_ decoder: Decoder) -> Data? {
let unkeyedContainer = try? decoder.unkeyedContainer()
if var container = unkeyedContainer {
if container.count == 2 {
do {
let intValue = try container.decode(UInt.self)
let stringValue = try container.decode(String.self)
return .token((intValue, .init(stringValue)))
} catch { }
}
}
return nil
}
private static func decodeArray(_ decoder: Decoder) -> Data? {
do {
if var container = try? decoder.unkeyedContainer() {
var elements: [Config] = []
while !container.isAtEnd {
let element = try container.decode(Config.self)
elements.append(element)
}
return .array(elements)
}
} catch { }
return nil
}
private static func decodeDictionary(_ decoder: Decoder) -> Data? {
do {
let container = try decoder.container(keyedBy: CodingKeys.self)
var dictionaryValues: [BinaryDistinctString: Config] = [:]
for key in container.allKeys {
let value = try container.decode(Config.self, forKey: key)
dictionaryValues[BinaryDistinctString(key.stringValue)] = value
}
return .dictionary(dictionaryValues)
} catch {
return nil
}
}
public func encode(to encoder: any Encoder) throws {
switch self.value {
case .null:
var container = encoder.singleValueContainer()
try container.encodeNil()
case let .integer(val):
var container = encoder.singleValueContainer()
try container.encode(val)
case let .floating(val):
var container = encoder.singleValueContainer()
try container.encode(val)
case let .boolean(val):
var container = encoder.singleValueContainer()
try container.encode(val)
case let .string(val):
var container = encoder.singleValueContainer()
try container.encode(val.string)
case let .dictionary(val):
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in val {
try container.encode(value, forKey: CodingKeys(stringValue: key.string)!)
}
case let .array(val):
var container = encoder.unkeyedContainer()
try container.encode(contentsOf: val)
case let .token(val):
var tupple = encoder.unkeyedContainer()
try tupple.encode(val.0)
try tupple.encode(val.1.string)
}
}
private struct CodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { nil }
init?(intValue: Int) { nil }
}
}
extension Config: Equatable {
public static func == (lhs: Config, rhs: Config) -> Bool {
lhs.value == rhs.value
}
}
extension Config.Data: Hashable {
public func hash(into hasher: inout Hasher) {
switch self {
case .null:
hasher.combine(0) // Discriminator for null
case let .string(s):
hasher.combine(1) // Discriminator for string
hasher.combine(s)
case let .integer(i):
hasher.combine(2) // Discriminator for integer
hasher.combine(i)
case let .boolean(b):
hasher.combine(3) // Discriminator for boolean
hasher.combine(b)
case let .floating(f):
hasher.combine(4) // Discriminator for floating
hasher.combine(f)
case let .dictionary(d):
hasher.combine(5) // Discriminator for dict
d.hash(into: &hasher)
case let .array(a):
hasher.combine(6) // Discriminator for array
for e in a {
e.hash(into: &hasher)
}
case let .token(a):
hasher.combine(7) // Discriminator for token
a.0.hash(into: &hasher)
a.1.hash(into: &hasher)
}
}
}
public enum ConfigError: Error {
case typeMismatch(expected: Config.Data, actual: Config.Data)
case typeConversionFailed(value: Sendable, targetType: Sendable.Type)
}