The validation module already implemented the composable-rule idiom under its own names (Check/Issue/SubjectContext) and a value-type Validator with duplicated traversal machinery per layer. Renamed the core types to the canonical names (Validation/ValidationError/ValidationErrorCollection/ ValidationContext/BuiltinValidation/AnyValidation), converted Validator to a reference-type class with the two-tier default-rule split, and genericized it over both Subject and Document via a new Traversable protocol so the model and tagged layers share one walk engine instead of two copies. Wired the JSON-representability rule set into the same shape and closed the two missing pieces (unwrap combinator, JSON coverage/config-pin tests).
225 lines
6.7 KiB
Swift
225 lines
6.7 KiB
Swift
public extension PureYAML.Decoding {
|
|
/// Typed decoder backed by a ``PureYAML/Model/Value``.
|
|
struct Decoder: Swift.Decoder {
|
|
public let value: PureYAML.Model.Value
|
|
public let codingPath: [any CodingKey]
|
|
public let userInfo: [CodingUserInfoKey: Any]
|
|
let validatesInput: Bool
|
|
|
|
public init(
|
|
value: PureYAML.Model.Value,
|
|
codingPath: [any CodingKey] = [],
|
|
userInfo: [CodingUserInfoKey: Any] = [:],
|
|
validatesInput: Bool = true,
|
|
) {
|
|
self.value = value
|
|
self.codingPath = codingPath
|
|
self.userInfo = userInfo
|
|
self.validatesInput = validatesInput
|
|
}
|
|
|
|
public func container<Key: CodingKey>(
|
|
keyedBy _: Key.Type,
|
|
) throws -> KeyedDecodingContainer<Key> {
|
|
try validateIfNeeded()
|
|
guard case let .mapping(mapping) = value else {
|
|
throw Error.typeMismatch(
|
|
expected: "mapping",
|
|
actual: value.kindDescription,
|
|
path: PureYAML.Validation.Path(codingPath: codingPath),
|
|
)
|
|
}
|
|
|
|
return KeyedDecodingContainer(KeyedDecodingContainerImpl<Key>(
|
|
mapping: mapping,
|
|
codingPath: codingPath,
|
|
userInfo: userInfo,
|
|
validatesInput: validatesInput,
|
|
))
|
|
}
|
|
|
|
public func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
|
try validateIfNeeded()
|
|
guard case let .sequence(values) = value else {
|
|
throw Error.typeMismatch(
|
|
expected: "sequence",
|
|
actual: value.kindDescription,
|
|
path: PureYAML.Validation.Path(codingPath: codingPath),
|
|
)
|
|
}
|
|
|
|
return UnkeyedDecodingContainerImpl(
|
|
values: values,
|
|
codingPath: codingPath,
|
|
userInfo: userInfo,
|
|
validatesInput: validatesInput,
|
|
)
|
|
}
|
|
|
|
public func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
|
try validateIfNeeded()
|
|
return SingleValueContainer(
|
|
value: value,
|
|
codingPath: codingPath,
|
|
userInfo: userInfo,
|
|
validatesInput: validatesInput,
|
|
)
|
|
}
|
|
|
|
private func validateIfNeeded() throws {
|
|
if validatesInput {
|
|
try PureYAML.Decoding.validate(value, codingPath: codingPath)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PureYAML.Decoding {
|
|
static func validate(
|
|
_ value: PureYAML.Model.Value,
|
|
codingPath: [any CodingKey],
|
|
) throws {
|
|
let result = PureYAML.Validation.Validator<PureYAML.Model.Value, PureYAML.Model.Value>().collect(value)
|
|
guard !result.values.isEmpty else {
|
|
return
|
|
}
|
|
|
|
let pathPrefix = PureYAML.Validation.Path(codingPath: codingPath)
|
|
let errors = result.values.map { error in
|
|
PureYAML.Validation.ValidationError(
|
|
severity: error.severity,
|
|
reason: error.reason,
|
|
path: .init(pathPrefix.components + error.path.components),
|
|
)
|
|
}
|
|
throw PureYAML.Validation.ValidationErrorCollection(errors)
|
|
}
|
|
}
|
|
|
|
private struct SingleValueContainer: SingleValueDecodingContainer {
|
|
let value: PureYAML.Model.Value
|
|
let codingPath: [any CodingKey]
|
|
let userInfo: [CodingUserInfoKey: Any]
|
|
let validatesInput: Bool
|
|
|
|
func decodeNil() -> Bool {
|
|
value == .null
|
|
}
|
|
|
|
func decode(_: Bool.Type) throws -> Bool {
|
|
guard case let .bool(value) = value else {
|
|
throw mismatch("Bool")
|
|
}
|
|
return value
|
|
}
|
|
|
|
func decode(_: String.Type) throws -> String {
|
|
guard case let .string(value) = value else {
|
|
throw mismatch("String")
|
|
}
|
|
return value
|
|
}
|
|
|
|
func decode(_: Double.Type) throws -> Double {
|
|
switch value {
|
|
case let .double(value):
|
|
return value
|
|
case let .int(value):
|
|
return Double(value)
|
|
case .null, .bool, .string, .sequence, .mapping:
|
|
throw mismatch("Double")
|
|
}
|
|
}
|
|
|
|
func decode(_: Float.Type) throws -> Float {
|
|
try Float(decode(Double.self))
|
|
}
|
|
|
|
func decode(_: Int.Type) throws -> Int {
|
|
guard case let .int(value) = value else {
|
|
throw mismatch("Int")
|
|
}
|
|
return value
|
|
}
|
|
|
|
func decode(_ type: Int8.Type) throws -> Int8 {
|
|
try decodeInteger(type)
|
|
}
|
|
|
|
func decode(_ type: Int16.Type) throws -> Int16 {
|
|
try decodeInteger(type)
|
|
}
|
|
|
|
func decode(_ type: Int32.Type) throws -> Int32 {
|
|
try decodeInteger(type)
|
|
}
|
|
|
|
func decode(_ type: Int64.Type) throws -> Int64 {
|
|
try decodeInteger(type)
|
|
}
|
|
|
|
func decode(_ type: UInt.Type) throws -> UInt {
|
|
try decodeUnsignedInteger(type)
|
|
}
|
|
|
|
func decode(_ type: UInt8.Type) throws -> UInt8 {
|
|
try decodeUnsignedInteger(type)
|
|
}
|
|
|
|
func decode(_ type: UInt16.Type) throws -> UInt16 {
|
|
try decodeUnsignedInteger(type)
|
|
}
|
|
|
|
func decode(_ type: UInt32.Type) throws -> UInt32 {
|
|
try decodeUnsignedInteger(type)
|
|
}
|
|
|
|
func decode(_ type: UInt64.Type) throws -> UInt64 {
|
|
try decodeUnsignedInteger(type)
|
|
}
|
|
|
|
func decode<Value: Decodable>(_: Value.Type) throws -> Value {
|
|
try Value(from: PureYAML.Decoding.Decoder(
|
|
value: value,
|
|
codingPath: codingPath,
|
|
userInfo: userInfo,
|
|
validatesInput: validatesInput,
|
|
))
|
|
}
|
|
|
|
private func decodeInteger<Integer: FixedWidthInteger>(
|
|
_: Integer.Type,
|
|
) throws -> Integer {
|
|
let value = try decode(Int.self)
|
|
guard let converted = Integer(exactly: value) else {
|
|
throw integerOutOfRange(String(describing: Integer.self))
|
|
}
|
|
return converted
|
|
}
|
|
|
|
private func decodeUnsignedInteger<Integer: FixedWidthInteger>(
|
|
_: Integer.Type,
|
|
) throws -> Integer {
|
|
let value = try decode(Int.self)
|
|
guard let converted = Integer(exactly: value) else {
|
|
throw integerOutOfRange(String(describing: Integer.self))
|
|
}
|
|
return converted
|
|
}
|
|
|
|
private func mismatch(_ expected: String) -> PureYAML.Decoding.Error {
|
|
.typeMismatch(
|
|
expected: expected,
|
|
actual: value.kindDescription,
|
|
path: PureYAML.Validation.Path(codingPath: codingPath),
|
|
)
|
|
}
|
|
|
|
private func integerOutOfRange(_ type: String) -> PureYAML.Decoding.Error {
|
|
.integerOutOfRange(
|
|
type: type,
|
|
path: PureYAML.Validation.Path(codingPath: codingPath),
|
|
)
|
|
}
|
|
}
|