native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
4.5 kB · 100 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101import Foundationimport JSONSchema
/// A schema and its bundle-local resources. Validation never fetches a URL.nonisolated public struct WorkflowActionJSONSchema: Equatable, Sendable { public let raw: WorkflowJSONValue private let base: URL private let resources: [String: WorkflowJSONValue]
public init(_ raw: WorkflowJSONValue, path: String, files: [String: Data] = [:]) throws { guard case .object(let fields) = raw, fields["type"] == .string("object") else { throw WorkflowExpressionError.type("Action schema root must declare type: object.") } var resolver = WorkflowSchemaResources(files: files) base = URL(string: "https://pwl.invalid/bundle/")!.appending(path: path) self.raw = try resolver.resolve(raw, base: base) resources = resolver.resources guard try schema().validateAgainstMetaSchema().isValid else { throw WorkflowExpressionError.type("Action contains an invalid JSON Schema.") } for (path, resource) in resources { let schema = try Schema( rawSchema: Self.convert(resource), context: .init(dialect: .draft2020_12), baseURI: URL(string: path)!) guard try schema.validateAgainstMetaSchema().isValid else { throw WorkflowExpressionError.type("Invalid schema resource: \(path).") } } }
public func validate(_ value: WorkflowJSONValue) throws { guard case .object = value else { throw WorkflowExpressionError.type("Action value must be a JSON object.") } try WorkflowJSON.validate(value) guard try schema().validate(Self.convert(value)).isValid else { throw WorkflowExpressionError.type("Action value does not match its schema.") } }
private func schema() throws -> Schema { try Schema( rawSchema: Self.convert(raw), context: .init(dialect: .draft2020_12, remoteSchema: resources.mapValues(Self.convert)), baseURI: base) }
private static func convert(_ value: WorkflowJSONValue) throws -> JSONSchema.JSONValue { try JSONDecoder().decode(JSONSchema.JSONValue.self, from: JSONEncoder().encode(value)) }}
nonisolated private struct WorkflowSchemaResources { let files: [String: Data] var resources: [String: WorkflowJSONValue] = [:] private var loading: Set<String> = []
init(files: [String: Data]) { self.files = files }
mutating func resolve(_ raw: WorkflowJSONValue, base: URL) throws -> WorkflowJSONValue { try WorkflowJSON.validate(raw) switch raw { case .object(var fields): if fields["$id"] != nil { throw WorkflowExpressionError.type("Use bundle-relative $ref and $anchor; action schemas cannot change $id.") } for (key, value) in fields { if ["$ref", "$dynamicRef"].contains(key) { guard case .string(let reference) = value, !reference.contains(":"), !reference.hasPrefix("/"), !reference.contains("\\"), let resolved = URL(string: reference, relativeTo: base)?.absoluteURL, resolved.host == "pwl.invalid", resolved.path.hasPrefix("/bundle/") else { throw WorkflowExpressionError.type("Schema references must stay inside the bundle.") } if reference.hasPrefix("#") { continue } var document = URLComponents(url: resolved, resolvingAgainstBaseURL: true)! document.fragment = nil let documentURL = document.url! let path = String(documentURL.path.dropFirst("/bundle/".count)) guard let data = files[path] else { throw WorkflowExpressionError.missing("Schema resource '\(path)'.") } let identifier = documentURL.absoluteString if resources[identifier] == nil, loading.insert(identifier).inserted { let resource: WorkflowJSONValue if path.hasSuffix(".json") { resource = try JSONDecoder().decode(WorkflowJSONValue.self, from: data) } else { guard let text = String(data: data, encoding: .utf8) else { throw WorkflowExpressionError.type("Schema resource must be UTF-8: \(path).") } resource = try WorkflowYAMLValue.parse(text) } resources[identifier] = try resolve(resource, base: documentURL) loading.remove(identifier) } fields[key] = .string(resolved.absoluteString) } else { fields[key] = try resolve(value, base: base) } } return .object(fields) case .array(let items): return try .array(items.map { try resolve($0, base: base) }) default: return raw } }}