import SwiftUI struct ConfigSchemaForm: View { @Bindable var store: ChannelsStore let schema: ConfigSchemaNode let path: ConfigPath var body: some View { self.renderNode(self.schema, path: self.path) } private func renderNode(_ schema: ConfigSchemaNode, path: ConfigPath) -> AnyView { let storedValue = self.store.configValue(at: path) let value = storedValue ?? schema.explicitDefault let label = hintForPath(path, hints: store.configUiHints)?.label ?? schema.title let help = hintForPath(path, hints: store.configUiHints)?.help ?? schema.description let variants = schema.anyOf.isEmpty ? schema.oneOf : schema.anyOf if !variants.isEmpty { let nonNull = variants.filter { !$0.isNullSchema } if nonNull.count == 1, let only = nonNull.first { return self.renderNode(only, path: path) } let literals = nonNull.compactMap(\.literalValue) if !literals.isEmpty, literals.count == nonNull.count { return AnyView( VStack(alignment: .leading, spacing: 6) { if let label { Text(label).font(.callout.weight(.semibold)) } if let help { Text(help) .font(.caption) .foregroundStyle(.secondary) } Picker( "", selection: self.enumBinding( path, options: literals, defaultValue: schema.explicitDefault)) { Text("Select…").tag(-1) ForEach(literals.indices, id: \ .self) { index in Text(String(describing: literals[index])).tag(index) } } .pickerStyle(.menu) }) } } switch schema.schemaType { case "object": return AnyView( VStack(alignment: .leading, spacing: 12) { if let label { Text(label) .font(.callout.weight(.semibold)) } if let help { Text(help) .font(.caption) .foregroundStyle(.secondary) } let properties = schema.properties let sortedKeys = properties.keys.sorted { lhs, rhs in let orderA = hintForPath(path + [.key(lhs)], hints: store.configUiHints)?.order ?? 0 let orderB = hintForPath(path + [.key(rhs)], hints: store.configUiHints)?.order ?? 0 if orderA != orderB { return orderA < orderB } return lhs < rhs } ForEach(sortedKeys, id: \ .self) { key in if let child = properties[key] { self.renderNode(child, path: path + [.key(key)]) } } if schema.allowsAdditionalProperties { self.renderAdditionalProperties(schema, path: path, value: value) } }) case "array": return AnyView(self.renderArray(schema, path: path, value: value, label: label, help: help)) case "boolean": return AnyView( Toggle(isOn: self.boolBinding(path, defaultValue: schema.explicitDefault as? Bool)) { if let label { Text(label) } else { Text("Enabled") } } .help(help ?? "")) case "number", "integer": return AnyView(self.renderNumberField(schema, path: path, label: label, help: help)) case "string": return AnyView(self.renderStringField(schema, path: path, label: label, help: help)) default: return AnyView( VStack(alignment: .leading, spacing: 6) { if let label { Text(label).font(.callout.weight(.semibold)) } Text("Unsupported field type.") .font(.caption) .foregroundStyle(.secondary) }) } } @ViewBuilder private func renderStringField( _ schema: ConfigSchemaNode, path: ConfigPath, label: String?, help: String?) -> some View { let hint = hintForPath(path, hints: store.configUiHints) let placeholder = hint?.placeholder ?? "" let sensitive = hint?.sensitive ?? isSensitivePath(path) let defaultValue = schema.explicitDefault as? String VStack(alignment: .leading, spacing: 6) { if let label { Text(label).font(.callout.weight(.semibold)) } if let help { Text(help) .font(.caption) .foregroundStyle(.secondary) } if let options = schema.enumValues { Picker("", selection: self.enumBinding(path, options: options, defaultValue: schema.explicitDefault)) { Text("Select…").tag(-1) ForEach(options.indices, id: \ .self) { index in Text(String(describing: options[index])).tag(index) } } .pickerStyle(.menu) } else if sensitive { SecureField(placeholder, text: self.stringBinding(path, defaultValue: defaultValue)) .textFieldStyle(.roundedBorder) } else { TextField(placeholder, text: self.stringBinding(path, defaultValue: defaultValue)) .textFieldStyle(.roundedBorder) } } } @ViewBuilder private func renderNumberField( _ schema: ConfigSchemaNode, path: ConfigPath, label: String?, help: String?) -> some View { let defaultValue = (schema.explicitDefault as? Double) ?? (schema.explicitDefault as? Int).map(Double.init) VStack(alignment: .leading, spacing: 6) { if let label { Text(label).font(.callout.weight(.semibold)) } if let help { Text(help) .font(.caption) .foregroundStyle(.secondary) } TextField( "", text: self.numberBinding( path, isInteger: schema.schemaType == "integer", defaultValue: defaultValue)) .textFieldStyle(.roundedBorder) } } @ViewBuilder private func renderArray( _ schema: ConfigSchemaNode, path: ConfigPath, value: Any?, label: String?, help: String?) -> some View { let items = value as? [Any] ?? [] let itemSchema = schema.items VStack(alignment: .leading, spacing: 10) { if let label { Text(label).font(.callout.weight(.semibold)) } if let help { Text(help) .font(.caption) .foregroundStyle(.secondary) } ForEach(items.indices, id: \ .self) { index in HStack(alignment: .top, spacing: 8) { if let itemSchema { self.renderNode(itemSchema, path: path + [.index(index)]) } else { Text(String(describing: items[index])) } Button("Remove") { var next = items next.remove(at: index) self.store.updateConfigValue(path: path, value: next) } .buttonStyle(.bordered) .controlSize(.small) } } Button("Add") { var next = items if let itemSchema { next.append(itemSchema.defaultValue) } else { next.append("") } self.store.updateConfigValue(path: path, value: next) } .buttonStyle(.bordered) .controlSize(.small) } } @ViewBuilder private func renderAdditionalProperties( _ schema: ConfigSchemaNode, path: ConfigPath, value: Any?) -> some View { if let additionalSchema = schema.additionalProperties { let dict = value as? [String: Any] ?? [:] let reserved = Set(schema.properties.keys) let extras = dict.keys.filter { !reserved.contains($0) }.sorted() VStack(alignment: .leading, spacing: 8) { Text("Extra entries") .font(.callout.weight(.semibold)) if extras.isEmpty { Text("No extra entries yet.") .font(.caption) .foregroundStyle(.secondary) } else { ForEach(extras, id: \ .self) { key in let itemPath: ConfigPath = path + [.key(key)] HStack(alignment: .top, spacing: 8) { TextField("Key", text: self.mapKeyBinding(path: path, key: key)) .textFieldStyle(.roundedBorder) .frame(width: 160) self.renderNode(additionalSchema, path: itemPath) Button("Remove") { var next = dict next.removeValue(forKey: key) self.store.updateConfigValue(path: path, value: next) } .buttonStyle(.bordered) .controlSize(.small) } } } Button("Add") { var next = dict var index = 1 var key = "new-\(index)" while next[key] != nil { index += 1 key = "new-\(index)" } next[key] = additionalSchema.defaultValue self.store.updateConfigValue(path: path, value: next) } .buttonStyle(.bordered) .controlSize(.small) } } } private func stringBinding(_ path: ConfigPath, defaultValue: String?) -> Binding { Binding( get: { if let value = store.configValue(at: path) as? String { return value } return defaultValue ?? "" }, set: { newValue in let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) self.store.updateConfigValue(path: path, value: trimmed.isEmpty ? nil : trimmed) }) } private func boolBinding(_ path: ConfigPath, defaultValue: Bool?) -> Binding { Binding( get: { if let value = store.configValue(at: path) as? Bool { return value } return defaultValue ?? false }, set: { newValue in self.store.updateConfigValue(path: path, value: newValue) }) } private func numberBinding( _ path: ConfigPath, isInteger: Bool, defaultValue: Double?) -> Binding { Binding( get: { if let value = store.configValue(at: path) { return String(describing: value) } guard let defaultValue else { return "" } return isInteger ? String(Int(defaultValue)) : String(defaultValue) }, set: { newValue in let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { self.store.updateConfigValue(path: path, value: nil) } else if let value = Double(trimmed) { self.store.updateConfigValue(path: path, value: isInteger ? Int(value) : value) } }) } private func enumBinding( _ path: ConfigPath, options: [Any], defaultValue: Any?) -> Binding { Binding( get: { let value = self.store.configValue(at: path) ?? defaultValue guard let value else { return -1 } return options.firstIndex { option in String(describing: option) == String(describing: value) } ?? -1 }, set: { index in guard index >= 0, index < options.count else { self.store.updateConfigValue(path: path, value: nil) return } self.store.updateConfigValue(path: path, value: options[index]) }) } private func mapKeyBinding(path: ConfigPath, key: String) -> Binding { Binding( get: { key }, set: { newValue in let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } guard trimmed != key else { return } let current = self.store.configValue(at: path) as? [String: Any] ?? [:] guard current[trimmed] == nil else { return } var next = current next[trimmed] = current[key] next.removeValue(forKey: key) self.store.updateConfigValue(path: path, value: next) }) } } struct ChannelConfigForm: View { @Bindable var store: ChannelsStore let channelId: String var body: some View { if self.store.configSchemaLoading { ProgressView().controlSize(.small) } else if let schema = store.channelConfigSchema(for: channelId) { ConfigSchemaForm(store: self.store, schema: schema, path: [.key("channels"), .key(self.channelId)]) } else { Text("Schema unavailable for this channel.") .font(.caption) .foregroundStyle(.secondary) } } }