File size: 6,190 Bytes
4fc4790
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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: ".")
}