205 lines
6.0 KiB
Swift
205 lines
6.0 KiB
Swift
import Foundation
|
|
|
|
enum ConfigPathSegment: Hashable {
|
|
case key(String)
|
|
case index(Int)
|
|
}
|
|
|
|
typealias ConfigPath = [ConfigPathSegment]
|
|
|
|
struct ConfigUiHint {
|
|
let label: String?
|
|
let help: String?
|
|
let order: Double?
|
|
let advanced: Bool?
|
|
let sensitive: Bool?
|
|
let placeholder: String?
|
|
|
|
init(raw: [String: Any]) {
|
|
self.label = raw["label"] as? String
|
|
self.help = raw["help"] as? String
|
|
if let order = raw["order"] as? Double {
|
|
self.order = order
|
|
} else if let orderInt = raw["order"] as? Int {
|
|
self.order = Double(orderInt)
|
|
} else {
|
|
self.order = nil
|
|
}
|
|
self.advanced = raw["advanced"] as? Bool
|
|
self.sensitive = raw["sensitive"] as? Bool
|
|
self.placeholder = raw["placeholder"] as? String
|
|
}
|
|
}
|
|
|
|
struct ConfigSchemaNode {
|
|
let raw: [String: Any]
|
|
|
|
init?(raw: Any) {
|
|
guard let dict = raw as? [String: Any] else { return nil }
|
|
self.raw = dict
|
|
}
|
|
|
|
var title: String? { self.raw["title"] as? String }
|
|
var description: String? { self.raw["description"] as? String }
|
|
var enumValues: [Any]? { self.raw["enum"] as? [Any] }
|
|
var constValue: Any? { self.raw["const"] }
|
|
var explicitDefault: Any? { self.raw["default"] }
|
|
var requiredKeys: Set<String> {
|
|
Set((self.raw["required"] as? [String]) ?? [])
|
|
}
|
|
|
|
var typeList: [String] {
|
|
if let type = self.raw["type"] as? String { return [type] }
|
|
if let types = self.raw["type"] as? [String] { return types }
|
|
return []
|
|
}
|
|
|
|
var schemaType: String? {
|
|
let filtered = self.typeList.filter { $0 != "null" }
|
|
if let first = filtered.first { return first }
|
|
return self.typeList.first
|
|
}
|
|
|
|
var isNullSchema: Bool {
|
|
let types = self.typeList
|
|
return types.count == 1 && types.first == "null"
|
|
}
|
|
|
|
var properties: [String: ConfigSchemaNode] {
|
|
guard let props = self.raw["properties"] as? [String: Any] else { return [:] }
|
|
return props.compactMapValues { ConfigSchemaNode(raw: $0) }
|
|
}
|
|
|
|
var anyOf: [ConfigSchemaNode] {
|
|
guard let raw = self.raw["anyOf"] as? [Any] else { return [] }
|
|
return raw.compactMap { ConfigSchemaNode(raw: $0) }
|
|
}
|
|
|
|
var oneOf: [ConfigSchemaNode] {
|
|
guard let raw = self.raw["oneOf"] as? [Any] else { return [] }
|
|
return raw.compactMap { ConfigSchemaNode(raw: $0) }
|
|
}
|
|
|
|
var literalValue: Any? {
|
|
if let constValue { return constValue }
|
|
if let enumValues, enumValues.count == 1 { return enumValues[0] }
|
|
return nil
|
|
}
|
|
|
|
var items: ConfigSchemaNode? {
|
|
if let items = self.raw["items"] as? [Any], let first = items.first {
|
|
return ConfigSchemaNode(raw: first)
|
|
}
|
|
if let items = self.raw["items"] {
|
|
return ConfigSchemaNode(raw: items)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var additionalProperties: ConfigSchemaNode? {
|
|
if let additional = self.raw["additionalProperties"] as? [String: Any] {
|
|
return ConfigSchemaNode(raw: additional)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var allowsAdditionalProperties: Bool {
|
|
if let allow = self.raw["additionalProperties"] as? Bool { return allow }
|
|
return self.additionalProperties != nil
|
|
}
|
|
|
|
var defaultValue: Any {
|
|
if let value = self.raw["default"] { return value }
|
|
switch self.schemaType {
|
|
case "object":
|
|
return [String: Any]()
|
|
case "array":
|
|
return [Any]()
|
|
case "boolean":
|
|
return false
|
|
case "integer":
|
|
return 0
|
|
case "number":
|
|
return 0.0
|
|
case "string":
|
|
return ""
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func node(at path: ConfigPath) -> ConfigSchemaNode? {
|
|
var current: ConfigSchemaNode? = self
|
|
for segment in path {
|
|
guard let node = current else { return nil }
|
|
switch segment {
|
|
case let .key(key):
|
|
if node.schemaType == "object" {
|
|
if let next = node.properties[key] {
|
|
current = next
|
|
continue
|
|
}
|
|
if let additional = node.additionalProperties {
|
|
current = additional
|
|
continue
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
case .index:
|
|
guard node.schemaType == "array" else { return nil }
|
|
current = node.items
|
|
}
|
|
}
|
|
return current
|
|
}
|
|
}
|
|
|
|
func decodeUiHints(_ raw: [String: Any]) -> [String: ConfigUiHint] {
|
|
raw.reduce(into: [:]) { result, entry in
|
|
if let hint = entry.value as? [String: Any] {
|
|
result[entry.key] = ConfigUiHint(raw: hint)
|
|
}
|
|
}
|
|
}
|
|
|
|
func hintForPath(_ path: ConfigPath, hints: [String: ConfigUiHint]) -> ConfigUiHint? {
|
|
let key = pathKey(path)
|
|
if let direct = hints[key] { return direct }
|
|
let segments = key.split(separator: ".").map(String.init)
|
|
for (hintKey, hint) in hints {
|
|
guard hintKey.contains("*") else { continue }
|
|
let hintSegments = hintKey.split(separator: ".").map(String.init)
|
|
guard hintSegments.count == segments.count else { continue }
|
|
var match = true
|
|
for (index, seg) in segments.enumerated() {
|
|
let hintSegment = hintSegments[index]
|
|
if hintSegment != "*", hintSegment != seg {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
if match { return hint }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isSensitivePath(_ path: ConfigPath) -> Bool {
|
|
let key = pathKey(path).lowercased()
|
|
return key.contains("token")
|
|
|| key.contains("password")
|
|
|| key.contains("secret")
|
|
|| key.contains("apikey")
|
|
|| key.hasSuffix("key")
|
|
}
|
|
|
|
func pathKey(_ path: ConfigPath) -> String {
|
|
path.compactMap { segment -> String? in
|
|
switch segment {
|
|
case let .key(key): return key
|
|
case .index: return nil
|
|
}
|
|
}
|
|
.joined(separator: ".")
|
|
}
|