Boolean schema carries an explicit default; was hardcoded true. Editors expose a Default toggle in the schema panel, and boolean fields are now expandable to reach it.
267 lines
9.3 KiB
Swift
267 lines
9.3 KiB
Swift
import Foundation
|
|
|
|
public enum FeatureSchema: Sendable, Equatable {
|
|
case boolean(default: Bool)
|
|
case int(default: Int64, min: Int64?, max: Int64?)
|
|
case uint(default: UInt64, min: UInt64?, max: UInt64?)
|
|
case double(default: Double, min: Double?, max: Double?)
|
|
case string(default: String)
|
|
case regex(default: String)
|
|
case combo(choices: [ComboChoice], default: String?)
|
|
case object(fields: [ObjectField])
|
|
case array(item: ArrayItemSchema, default: [FeatureValue])
|
|
|
|
public var defaultValue: FeatureValue {
|
|
switch self {
|
|
case .boolean(let d): return .boolean(d)
|
|
case .int(let d, _, _): return .int(d)
|
|
case .uint(let d, _, _): return .uint(d)
|
|
case .double(let d, _, _): return .double(d)
|
|
case .string(let d): return .string(d)
|
|
case .regex(let d): return .regex(d)
|
|
case .combo(let choices, let d): return .string(d ?? choices.first?.id ?? "")
|
|
case .object(let fields):
|
|
let entries = fields.compactMap { field -> (String, FeatureValue)? in
|
|
guard !field.optional || field.enabledByDefault else { return nil }
|
|
return (field.id, field.schema.defaultValue)
|
|
}
|
|
return .object(Dictionary(uniqueKeysWithValues: entries))
|
|
case .array(_, let d): return .array(d)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct ComboChoice: Sendable, Equatable, Codable, Identifiable {
|
|
public var id: String
|
|
public var name: String
|
|
|
|
public init(id: String, name: String) {
|
|
self.id = id
|
|
self.name = name
|
|
}
|
|
}
|
|
|
|
public struct ObjectField: Sendable, Equatable, Codable {
|
|
public var id: String
|
|
public var name: String
|
|
public var schema: FeatureSchema
|
|
public var optional: Bool
|
|
public var enabledByDefault: Bool
|
|
|
|
public init(
|
|
id: String,
|
|
name: String,
|
|
schema: FeatureSchema,
|
|
optional: Bool = false,
|
|
enabledByDefault: Bool = true
|
|
) {
|
|
self.id = id
|
|
self.name = name
|
|
self.schema = schema
|
|
self.optional = optional
|
|
self.enabledByDefault = enabledByDefault
|
|
}
|
|
}
|
|
|
|
public enum ArrayItemSchema: Sendable, Equatable {
|
|
case boolean
|
|
case int
|
|
case uint
|
|
case double
|
|
case string
|
|
case regex
|
|
case combo(choices: [ComboChoice])
|
|
case object(fields: [ObjectField])
|
|
|
|
public var defaultValue: FeatureValue {
|
|
switch self {
|
|
case .boolean: return .boolean(false)
|
|
case .int: return .int(0)
|
|
case .uint: return .uint(0)
|
|
case .double: return .double(0)
|
|
case .string: return .string("")
|
|
case .regex: return .regex("")
|
|
case .combo(let choices): return .string(choices.first?.id ?? "")
|
|
case .object(let fields):
|
|
let entries = fields.compactMap { field -> (String, FeatureValue)? in
|
|
guard !field.optional || field.enabledByDefault else { return nil }
|
|
return (field.id, field.schema.defaultValue)
|
|
}
|
|
return .object(Dictionary(uniqueKeysWithValues: entries))
|
|
}
|
|
}
|
|
|
|
public var asFeatureSchema: FeatureSchema {
|
|
switch self {
|
|
case .boolean: return .boolean(default: false)
|
|
case .int: return .int(default: 0, min: nil, max: nil)
|
|
case .uint: return .uint(default: 0, min: nil, max: nil)
|
|
case .double: return .double(default: 0, min: nil, max: nil)
|
|
case .string: return .string(default: "")
|
|
case .regex: return .regex(default: "")
|
|
case .combo(let choices): return .combo(choices: choices, default: nil)
|
|
case .object(let fields): return .object(fields: fields)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension FeatureSchema: Codable {
|
|
private enum CodingKeys: String, CodingKey {
|
|
case kind
|
|
case `default`
|
|
case min
|
|
case max
|
|
case choices
|
|
case item
|
|
case fields
|
|
}
|
|
|
|
private enum Kind: String, Codable {
|
|
case boolean
|
|
case int
|
|
case uint
|
|
case double
|
|
case string
|
|
case regex
|
|
case combo
|
|
case object
|
|
case array
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
let kind = try c.decode(Kind.self, forKey: .kind)
|
|
switch kind {
|
|
case .boolean:
|
|
self = .boolean(default: try c.decodeIfPresent(Bool.self, forKey: .default) ?? false)
|
|
case .int:
|
|
self = .int(
|
|
default: try c.decode(Int64.self, forKey: .default),
|
|
min: try c.decodeIfPresent(Int64.self, forKey: .min),
|
|
max: try c.decodeIfPresent(Int64.self, forKey: .max)
|
|
)
|
|
case .uint:
|
|
self = .uint(
|
|
default: try c.decode(UInt64.self, forKey: .default),
|
|
min: try c.decodeIfPresent(UInt64.self, forKey: .min),
|
|
max: try c.decodeIfPresent(UInt64.self, forKey: .max)
|
|
)
|
|
case .double:
|
|
self = .double(
|
|
default: try c.decode(Double.self, forKey: .default),
|
|
min: try c.decodeIfPresent(Double.self, forKey: .min),
|
|
max: try c.decodeIfPresent(Double.self, forKey: .max)
|
|
)
|
|
case .string:
|
|
self = .string(default: try c.decode(String.self, forKey: .default))
|
|
case .regex:
|
|
self = .regex(default: try c.decode(String.self, forKey: .default))
|
|
case .combo:
|
|
self = .combo(
|
|
choices: try c.decode([ComboChoice].self, forKey: .choices),
|
|
default: try c.decodeIfPresent(String.self, forKey: .default)
|
|
)
|
|
case .object:
|
|
self = .object(fields: try c.decode([ObjectField].self, forKey: .fields))
|
|
case .array:
|
|
self = .array(
|
|
item: try c.decode(ArrayItemSchema.self, forKey: .item),
|
|
default: try c.decode([FeatureValue].self, forKey: .default)
|
|
)
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var c = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case .boolean(let d):
|
|
try c.encode(Kind.boolean, forKey: .kind)
|
|
try c.encode(d, forKey: .default)
|
|
case .int(let d, let lo, let hi):
|
|
try c.encode(Kind.int, forKey: .kind)
|
|
try c.encode(d, forKey: .default)
|
|
try c.encodeIfPresent(lo, forKey: .min)
|
|
try c.encodeIfPresent(hi, forKey: .max)
|
|
case .uint(let d, let lo, let hi):
|
|
try c.encode(Kind.uint, forKey: .kind)
|
|
try c.encode(d, forKey: .default)
|
|
try c.encodeIfPresent(lo, forKey: .min)
|
|
try c.encodeIfPresent(hi, forKey: .max)
|
|
case .double(let d, let lo, let hi):
|
|
try c.encode(Kind.double, forKey: .kind)
|
|
try c.encode(d, forKey: .default)
|
|
try c.encodeIfPresent(lo, forKey: .min)
|
|
try c.encodeIfPresent(hi, forKey: .max)
|
|
case .string(let d):
|
|
try c.encode(Kind.string, forKey: .kind)
|
|
try c.encode(d, forKey: .default)
|
|
case .regex(let d):
|
|
try c.encode(Kind.regex, forKey: .kind)
|
|
try c.encode(d, forKey: .default)
|
|
case .combo(let choices, let d):
|
|
try c.encode(Kind.combo, forKey: .kind)
|
|
try c.encode(choices, forKey: .choices)
|
|
try c.encodeIfPresent(d, forKey: .default)
|
|
case .object(let fields):
|
|
try c.encode(Kind.object, forKey: .kind)
|
|
try c.encode(fields, forKey: .fields)
|
|
case .array(let item, let d):
|
|
try c.encode(Kind.array, forKey: .kind)
|
|
try c.encode(item, forKey: .item)
|
|
try c.encode(d, forKey: .default)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension ArrayItemSchema: Codable {
|
|
private enum CodingKeys: String, CodingKey {
|
|
case kind
|
|
case choices
|
|
case fields
|
|
}
|
|
|
|
private enum Kind: String, Codable {
|
|
case boolean
|
|
case int
|
|
case uint
|
|
case double
|
|
case string
|
|
case regex
|
|
case combo
|
|
case object
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
let kind = try c.decode(Kind.self, forKey: .kind)
|
|
switch kind {
|
|
case .boolean: self = .boolean
|
|
case .int: self = .int
|
|
case .uint: self = .uint
|
|
case .double: self = .double
|
|
case .string: self = .string
|
|
case .regex: self = .regex
|
|
case .combo: self = .combo(choices: try c.decode([ComboChoice].self, forKey: .choices))
|
|
case .object: self = .object(fields: try c.decode([ObjectField].self, forKey: .fields))
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var c = encoder.container(keyedBy: CodingKeys.self)
|
|
switch self {
|
|
case .boolean: try c.encode(Kind.boolean, forKey: .kind)
|
|
case .int: try c.encode(Kind.int, forKey: .kind)
|
|
case .uint: try c.encode(Kind.uint, forKey: .kind)
|
|
case .double: try c.encode(Kind.double, forKey: .kind)
|
|
case .string: try c.encode(Kind.string, forKey: .kind)
|
|
case .regex: try c.encode(Kind.regex, forKey: .kind)
|
|
case .combo(let choices):
|
|
try c.encode(Kind.combo, forKey: .kind)
|
|
try c.encode(choices, forKey: .choices)
|
|
case .object(let fields):
|
|
try c.encode(Kind.object, forKey: .kind)
|
|
try c.encode(fields, forKey: .fields)
|
|
}
|
|
}
|
|
}
|