refactor(code-style): one top-level type per source file
Split the six files that declared multiple sibling types under one namespace extension so each file declares exactly one top-level type: the Tagged node component types (Node, NodeKind, Scalar, Sequence, Mapping, Pair), the validation report types (Report, Source, SourceReport, BatchReport), DiagnosticKind, TokenKind, ScannerFlowContext kind, and the event-stream composer helpers. Public type names are unchanged; the shared report-rendering helpers move from file-private to internal so the split report types still reach them. Make check-namespacing.sh namespace-aware: the previous column-anchored scan was blind to types declared inside indented extension blocks, so it passed while the rule was violated. It now counts direct members of a PureYAML namespace extension and ignores nested types and private helpers.
This commit is contained in:
parent
9ae6c3baff
commit
c4f7f9dccc
@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Split multi-type source files so each file declares exactly one top-level type
|
||||
(`docs/rules/code-style.md`): the Tagged node component types, the validation
|
||||
report types, `TokenKind`, and the event-stream composer helpers each move to
|
||||
their own file. Public type names are unchanged. `scripts/check-namespacing.sh`
|
||||
is now namespace-aware and detects multiple sibling types declared inside
|
||||
`extension` blocks, which the previous column-anchored scan missed.
|
||||
- Merge-key (`<<`) override resolution now lives in a single internal helper,
|
||||
`Parsing.MergeResolution`, shared by the buffered and streaming event
|
||||
composers. Both paths previously carried an identical copy of the
|
||||
|
||||
8
Sources/Parsing/EventStreamComposedNode.swift
Normal file
8
Sources/Parsing/EventStreamComposedNode.swift
Normal file
@ -0,0 +1,8 @@
|
||||
extension PureYAML.Parsing {
|
||||
struct EventStreamComposedNode {
|
||||
var value: PureYAML.Model.Value
|
||||
var mappingKey: PureYAML.Parsing.EventComposer.ComposedMappingKey
|
||||
var mark: PureYAML.Parsing.Mark
|
||||
var sequenceItems: [EventStreamComposedNode] = []
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,4 @@
|
||||
extension PureYAML.Parsing {
|
||||
enum EventStreamComposerMode {
|
||||
case singleDocument
|
||||
case stream
|
||||
}
|
||||
|
||||
struct EventStreamComposedNode {
|
||||
var value: PureYAML.Model.Value
|
||||
var mappingKey: PureYAML.Parsing.EventComposer.ComposedMappingKey
|
||||
var mark: PureYAML.Parsing.Mark
|
||||
var sequenceItems: [EventStreamComposedNode] = []
|
||||
}
|
||||
|
||||
enum EventStreamContainerKind {
|
||||
case sequence
|
||||
case mapping
|
||||
}
|
||||
|
||||
struct EventStreamContainer {
|
||||
var kind: EventStreamContainerKind
|
||||
var anchor: String?
|
||||
var values: [EventStreamComposedNode] = []
|
||||
var mergeSources: [[PureYAML.Model.Pair]] = []
|
||||
var localPairs: [PureYAML.Model.Pair] = []
|
||||
var pendingKey: PureYAML.Parsing.EventComposer.ComposedMappingKey?
|
||||
var mark: PureYAML.Parsing.Mark
|
||||
}
|
||||
|
||||
struct EventStreamComposer {
|
||||
let scalarParser: Parser
|
||||
let mode: EventStreamComposerMode
|
||||
|
||||
6
Sources/Parsing/EventStreamComposerMode.swift
Normal file
6
Sources/Parsing/EventStreamComposerMode.swift
Normal file
@ -0,0 +1,6 @@
|
||||
extension PureYAML.Parsing {
|
||||
enum EventStreamComposerMode {
|
||||
case singleDocument
|
||||
case stream
|
||||
}
|
||||
}
|
||||
11
Sources/Parsing/EventStreamContainer.swift
Normal file
11
Sources/Parsing/EventStreamContainer.swift
Normal file
@ -0,0 +1,11 @@
|
||||
extension PureYAML.Parsing {
|
||||
struct EventStreamContainer {
|
||||
var kind: EventStreamContainerKind
|
||||
var anchor: String?
|
||||
var values: [EventStreamComposedNode] = []
|
||||
var mergeSources: [[PureYAML.Model.Pair]] = []
|
||||
var localPairs: [PureYAML.Model.Pair] = []
|
||||
var pendingKey: PureYAML.Parsing.EventComposer.ComposedMappingKey?
|
||||
var mark: PureYAML.Parsing.Mark
|
||||
}
|
||||
}
|
||||
6
Sources/Parsing/EventStreamContainerKind.swift
Normal file
6
Sources/Parsing/EventStreamContainerKind.swift
Normal file
@ -0,0 +1,6 @@
|
||||
extension PureYAML.Parsing {
|
||||
enum EventStreamContainerKind {
|
||||
case sequence
|
||||
case mapping
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,4 @@
|
||||
extension PureYAML.Parsing {
|
||||
enum ScannerFlowContextKind {
|
||||
case sequence
|
||||
case mapping
|
||||
}
|
||||
|
||||
struct ScannerFlowContext {
|
||||
var kind: ScannerFlowContextKind
|
||||
var isExpectingKey: Bool
|
||||
|
||||
6
Sources/Parsing/ScannerFlowContextKind.swift
Normal file
6
Sources/Parsing/ScannerFlowContextKind.swift
Normal file
@ -0,0 +1,6 @@
|
||||
extension PureYAML.Parsing {
|
||||
enum ScannerFlowContextKind {
|
||||
case sequence
|
||||
case mapping
|
||||
}
|
||||
}
|
||||
@ -9,100 +9,3 @@ extension PureYAML.Parsing {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PureYAML.Parsing {
|
||||
enum TokenKind: Equatable {
|
||||
case streamStart
|
||||
case streamEnd
|
||||
case documentStart
|
||||
case documentEnd
|
||||
case indent(width: Int)
|
||||
case dedent(width: Int)
|
||||
case blockEntry
|
||||
case mappingKey
|
||||
case mappingValue
|
||||
case flowSequenceStart
|
||||
case flowSequenceEnd
|
||||
case flowMappingStart
|
||||
case flowMappingEnd
|
||||
case flowEntry
|
||||
case scalar(value: String, style: ScalarStyle)
|
||||
case blockScalarHeader(style: BlockScalarStyle, chomping: BlockScalarChomping)
|
||||
case anchor(String)
|
||||
case alias(String)
|
||||
case tag(String)
|
||||
case comment(String)
|
||||
}
|
||||
}
|
||||
|
||||
extension PureYAML.Parsing.TokenKind: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .streamStart:
|
||||
"streamStart"
|
||||
case .streamEnd:
|
||||
"streamEnd"
|
||||
case .documentStart:
|
||||
"documentStart"
|
||||
case .documentEnd:
|
||||
"documentEnd"
|
||||
case let .indent(width):
|
||||
"indent width=\(width)"
|
||||
case let .dedent(width):
|
||||
"dedent width=\(width)"
|
||||
case .blockEntry:
|
||||
"blockEntry"
|
||||
case .mappingKey:
|
||||
"mappingKey"
|
||||
case .mappingValue:
|
||||
"mappingValue"
|
||||
case .flowSequenceStart:
|
||||
"flowSequenceStart"
|
||||
case .flowSequenceEnd:
|
||||
"flowSequenceEnd"
|
||||
case .flowMappingStart:
|
||||
"flowMappingStart"
|
||||
case .flowMappingEnd:
|
||||
"flowMappingEnd"
|
||||
case .flowEntry:
|
||||
"flowEntry"
|
||||
case let .scalar(value, style):
|
||||
"scalar value=\"\(escape(value))\" style=\(style)"
|
||||
case let .blockScalarHeader(style, chomping):
|
||||
if chomping == .clip {
|
||||
"blockScalarHeader style=\(style)"
|
||||
} else {
|
||||
"blockScalarHeader style=\(style) chomping=\(chomping)"
|
||||
}
|
||||
case let .anchor(value):
|
||||
"anchor name=\(value)"
|
||||
case let .alias(value):
|
||||
"alias name=\(value)"
|
||||
case let .tag(value):
|
||||
"tag value=\(value)"
|
||||
case let .comment(value):
|
||||
"comment value=\"\(escape(value))\""
|
||||
}
|
||||
}
|
||||
|
||||
private func escape(_ value: String) -> String {
|
||||
var output = ""
|
||||
for character in value {
|
||||
switch character {
|
||||
case "\\":
|
||||
output += "\\\\"
|
||||
case "\"":
|
||||
output += "\\\""
|
||||
case "\n":
|
||||
output += "\\n"
|
||||
case "\r":
|
||||
output += "\\r"
|
||||
case "\t":
|
||||
output += "\\t"
|
||||
default:
|
||||
output.append(character)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
96
Sources/Parsing/TokenKind.swift
Normal file
96
Sources/Parsing/TokenKind.swift
Normal file
@ -0,0 +1,96 @@
|
||||
extension PureYAML.Parsing {
|
||||
enum TokenKind: Equatable {
|
||||
case streamStart
|
||||
case streamEnd
|
||||
case documentStart
|
||||
case documentEnd
|
||||
case indent(width: Int)
|
||||
case dedent(width: Int)
|
||||
case blockEntry
|
||||
case mappingKey
|
||||
case mappingValue
|
||||
case flowSequenceStart
|
||||
case flowSequenceEnd
|
||||
case flowMappingStart
|
||||
case flowMappingEnd
|
||||
case flowEntry
|
||||
case scalar(value: String, style: ScalarStyle)
|
||||
case blockScalarHeader(style: BlockScalarStyle, chomping: BlockScalarChomping)
|
||||
case anchor(String)
|
||||
case alias(String)
|
||||
case tag(String)
|
||||
case comment(String)
|
||||
}
|
||||
}
|
||||
|
||||
extension PureYAML.Parsing.TokenKind: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .streamStart:
|
||||
"streamStart"
|
||||
case .streamEnd:
|
||||
"streamEnd"
|
||||
case .documentStart:
|
||||
"documentStart"
|
||||
case .documentEnd:
|
||||
"documentEnd"
|
||||
case let .indent(width):
|
||||
"indent width=\(width)"
|
||||
case let .dedent(width):
|
||||
"dedent width=\(width)"
|
||||
case .blockEntry:
|
||||
"blockEntry"
|
||||
case .mappingKey:
|
||||
"mappingKey"
|
||||
case .mappingValue:
|
||||
"mappingValue"
|
||||
case .flowSequenceStart:
|
||||
"flowSequenceStart"
|
||||
case .flowSequenceEnd:
|
||||
"flowSequenceEnd"
|
||||
case .flowMappingStart:
|
||||
"flowMappingStart"
|
||||
case .flowMappingEnd:
|
||||
"flowMappingEnd"
|
||||
case .flowEntry:
|
||||
"flowEntry"
|
||||
case let .scalar(value, style):
|
||||
"scalar value=\"\(escape(value))\" style=\(style)"
|
||||
case let .blockScalarHeader(style, chomping):
|
||||
if chomping == .clip {
|
||||
"blockScalarHeader style=\(style)"
|
||||
} else {
|
||||
"blockScalarHeader style=\(style) chomping=\(chomping)"
|
||||
}
|
||||
case let .anchor(value):
|
||||
"anchor name=\(value)"
|
||||
case let .alias(value):
|
||||
"alias name=\(value)"
|
||||
case let .tag(value):
|
||||
"tag value=\(value)"
|
||||
case let .comment(value):
|
||||
"comment value=\"\(escape(value))\""
|
||||
}
|
||||
}
|
||||
|
||||
private func escape(_ value: String) -> String {
|
||||
var output = ""
|
||||
for character in value {
|
||||
switch character {
|
||||
case "\\":
|
||||
output += "\\\\"
|
||||
case "\"":
|
||||
output += "\\\""
|
||||
case "\n":
|
||||
output += "\\n"
|
||||
case "\r":
|
||||
output += "\\r"
|
||||
case "\t":
|
||||
output += "\\t"
|
||||
default:
|
||||
output.append(character)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
18
Sources/Tagged/Tagged.Mapping.swift
Normal file
18
Sources/Tagged/Tagged.Mapping.swift
Normal file
@ -0,0 +1,18 @@
|
||||
public extension PureYAML.Tagged {
|
||||
struct Mapping: Equatable, Sendable {
|
||||
public var pairs: [Pair]
|
||||
public var tag: Tag?
|
||||
|
||||
public init(
|
||||
pairs: [Pair],
|
||||
tag: Tag? = nil,
|
||||
) {
|
||||
self.pairs = pairs
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
public subscript(_ key: String) -> Node? {
|
||||
pairs.first { $0.key == key }?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,4 @@
|
||||
public extension PureYAML.Tagged {
|
||||
enum NodeKind: String, Equatable, Sendable, CustomStringConvertible {
|
||||
case scalar
|
||||
case sequence
|
||||
case mapping
|
||||
|
||||
public var description: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
||||
/// YAML node tree that preserves explicit source tags.
|
||||
enum Node: Equatable, Sendable {
|
||||
case scalar(Scalar)
|
||||
@ -37,66 +27,4 @@ public extension PureYAML.Tagged {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Scalar: Equatable, Sendable {
|
||||
public var rawValue: String
|
||||
public var value: PureYAML.Model.Value
|
||||
public var tag: Tag?
|
||||
|
||||
public init(
|
||||
rawValue: String,
|
||||
value: PureYAML.Model.Value,
|
||||
tag: Tag? = nil,
|
||||
) {
|
||||
self.rawValue = rawValue
|
||||
self.value = value
|
||||
self.tag = tag
|
||||
}
|
||||
}
|
||||
|
||||
struct Sequence: Equatable, Sendable {
|
||||
public var values: [Node]
|
||||
public var tag: Tag?
|
||||
|
||||
public init(
|
||||
values: [Node],
|
||||
tag: Tag? = nil,
|
||||
) {
|
||||
self.values = values
|
||||
self.tag = tag
|
||||
}
|
||||
}
|
||||
|
||||
struct Mapping: Equatable, Sendable {
|
||||
public var pairs: [Pair]
|
||||
public var tag: Tag?
|
||||
|
||||
public init(
|
||||
pairs: [Pair],
|
||||
tag: Tag? = nil,
|
||||
) {
|
||||
self.pairs = pairs
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
public subscript(_ key: String) -> Node? {
|
||||
pairs.first { $0.key == key }?.value
|
||||
}
|
||||
}
|
||||
|
||||
struct Pair: Equatable, Sendable {
|
||||
public var key: String
|
||||
public var keyTag: Tag?
|
||||
public var value: Node
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
keyTag: Tag? = nil,
|
||||
value: Node,
|
||||
) {
|
||||
self.key = key
|
||||
self.keyTag = keyTag
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
Sources/Tagged/Tagged.NodeKind.swift
Normal file
11
Sources/Tagged/Tagged.NodeKind.swift
Normal file
@ -0,0 +1,11 @@
|
||||
public extension PureYAML.Tagged {
|
||||
enum NodeKind: String, Equatable, Sendable, CustomStringConvertible {
|
||||
case scalar
|
||||
case sequence
|
||||
case mapping
|
||||
|
||||
public var description: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Sources/Tagged/Tagged.Pair.swift
Normal file
17
Sources/Tagged/Tagged.Pair.swift
Normal file
@ -0,0 +1,17 @@
|
||||
public extension PureYAML.Tagged {
|
||||
struct Pair: Equatable, Sendable {
|
||||
public var key: String
|
||||
public var keyTag: Tag?
|
||||
public var value: Node
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
keyTag: Tag? = nil,
|
||||
value: Node,
|
||||
) {
|
||||
self.key = key
|
||||
self.keyTag = keyTag
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Sources/Tagged/Tagged.Scalar.swift
Normal file
17
Sources/Tagged/Tagged.Scalar.swift
Normal file
@ -0,0 +1,17 @@
|
||||
public extension PureYAML.Tagged {
|
||||
struct Scalar: Equatable, Sendable {
|
||||
public var rawValue: String
|
||||
public var value: PureYAML.Model.Value
|
||||
public var tag: Tag?
|
||||
|
||||
public init(
|
||||
rawValue: String,
|
||||
value: PureYAML.Model.Value,
|
||||
tag: Tag? = nil,
|
||||
) {
|
||||
self.rawValue = rawValue
|
||||
self.value = value
|
||||
self.tag = tag
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Sources/Tagged/Tagged.Sequence.swift
Normal file
14
Sources/Tagged/Tagged.Sequence.swift
Normal file
@ -0,0 +1,14 @@
|
||||
public extension PureYAML.Tagged {
|
||||
struct Sequence: Equatable, Sendable {
|
||||
public var values: [Node]
|
||||
public var tag: Tag?
|
||||
|
||||
public init(
|
||||
values: [Node],
|
||||
tag: Tag? = nil,
|
||||
) {
|
||||
self.values = values
|
||||
self.tag = tag
|
||||
}
|
||||
}
|
||||
}
|
||||
111
Sources/Validation/Validation.BatchReport.swift
Normal file
111
Sources/Validation/Validation.BatchReport.swift
Normal file
@ -0,0 +1,111 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Non-throwing aggregate report for validating many YAML inputs.
|
||||
struct BatchReport: Equatable, Sendable, CustomStringConvertible {
|
||||
public var sourceReports: [SourceReport]
|
||||
public var failOnWarnings: Bool
|
||||
|
||||
public init(
|
||||
sourceReports: [SourceReport],
|
||||
failOnWarnings: Bool = false,
|
||||
) {
|
||||
self.sourceReports = sourceReports
|
||||
self.failOnWarnings = failOnWarnings
|
||||
}
|
||||
|
||||
public var fileCount: Int {
|
||||
sourceReports.count
|
||||
}
|
||||
|
||||
public var validCount: Int {
|
||||
sourceReports.count(where: { !$0.invalid })
|
||||
}
|
||||
|
||||
public var invalidCount: Int {
|
||||
sourceReports.count(where: { $0.invalid })
|
||||
}
|
||||
|
||||
public var diagnosticCount: Int {
|
||||
sourceReports.reduce(0) { count, sourceReport in
|
||||
count + sourceReport.report.diagnostics.count
|
||||
}
|
||||
}
|
||||
|
||||
public var isValid: Bool {
|
||||
invalidCount == 0
|
||||
}
|
||||
|
||||
public var diagnostics: [Diagnostic] {
|
||||
sourceReports.flatMap(\.report.diagnostics)
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
diagnostics.map(\.description).joined(separator: "\n")
|
||||
}
|
||||
|
||||
public func markdownDescription(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> String {
|
||||
var lines: [String] = [
|
||||
"# \(title)",
|
||||
"",
|
||||
"- Files: \(fileCount)",
|
||||
"- Valid: \(validCount)",
|
||||
"- Invalid: \(invalidCount)",
|
||||
"- Diagnostics: \(diagnosticCount)",
|
||||
"- Warnings fail: \(failOnWarnings ? "yes" : "no")",
|
||||
"",
|
||||
]
|
||||
|
||||
for sourceReport in sourceReports {
|
||||
lines.append("## \(sourceReport.name)")
|
||||
lines.append("")
|
||||
lines.append("Status: \(sourceReport.invalid ? "invalid" : "valid")")
|
||||
lines.append("")
|
||||
|
||||
if sourceReport.report.isEmpty {
|
||||
lines.append("No diagnostics.")
|
||||
} else {
|
||||
lines.append("```text")
|
||||
lines.append(sourceReport.report.description)
|
||||
lines.append("```")
|
||||
}
|
||||
|
||||
lines.append("")
|
||||
}
|
||||
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
public func modelValue(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> PureYAML.Model.Value {
|
||||
.mapping(.init([
|
||||
.init(key: "title", value: .string(title)),
|
||||
.init(key: "summary", value: summaryModelValue),
|
||||
.init(key: "sources", value: .sequence(sourceReports.map { $0.modelValue() })),
|
||||
]))
|
||||
}
|
||||
|
||||
public func yamlDescription(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> String {
|
||||
PureYAML.dump(modelValue(title: title))
|
||||
}
|
||||
|
||||
public func jsonDescription(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> String {
|
||||
modelValue(title: title).jsonDescription + "\n"
|
||||
}
|
||||
|
||||
private var summaryModelValue: PureYAML.Model.Value {
|
||||
.mapping(.init([
|
||||
.init(key: "files", value: .int(fileCount)),
|
||||
.init(key: "valid", value: .int(validCount)),
|
||||
.init(key: "invalid", value: .int(invalidCount)),
|
||||
.init(key: "diagnostics", value: .int(diagnosticCount)),
|
||||
.init(key: "warningsFail", value: .bool(failOnWarnings)),
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,4 @@
|
||||
public extension PureYAML.Validation {
|
||||
enum DiagnosticKind: Equatable, Sendable, CustomStringConvertible {
|
||||
case parse
|
||||
case validation
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .parse:
|
||||
"parse"
|
||||
case .validation:
|
||||
"validation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A parse or validation diagnostic suitable for batch validation reports.
|
||||
struct Diagnostic: Equatable, Sendable, CustomStringConvertible {
|
||||
public var kind: DiagnosticKind
|
||||
|
||||
15
Sources/Validation/Validation.DiagnosticKind.swift
Normal file
15
Sources/Validation/Validation.DiagnosticKind.swift
Normal file
@ -0,0 +1,15 @@
|
||||
public extension PureYAML.Validation {
|
||||
enum DiagnosticKind: Equatable, Sendable, CustomStringConvertible {
|
||||
case parse
|
||||
case validation
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .parse:
|
||||
"parse"
|
||||
case .validation:
|
||||
"validation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,4 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Named YAML source used for batch validation without coupling PureYAML to file IO.
|
||||
struct Source: Equatable, Sendable {
|
||||
public var name: String
|
||||
public var yaml: String
|
||||
|
||||
public init(name: String, yaml: String) {
|
||||
self.name = name
|
||||
self.yaml = yaml
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-throwing validation report for one YAML input.
|
||||
struct Report: Equatable, Sendable, CustomStringConvertible {
|
||||
public var diagnostics: [Diagnostic]
|
||||
@ -77,141 +66,11 @@ public extension PureYAML.Validation {
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Validation result for one named source in a batch.
|
||||
struct SourceReport: Equatable, Sendable {
|
||||
public var name: String
|
||||
public var report: Report
|
||||
public var invalid: Bool
|
||||
|
||||
public init(name: String, report: Report, invalid: Bool) {
|
||||
self.name = name
|
||||
self.report = report
|
||||
self.invalid = invalid
|
||||
}
|
||||
|
||||
public func modelValue() -> PureYAML.Model.Value {
|
||||
.mapping(.init([
|
||||
.init(key: "name", value: .string(name)),
|
||||
.init(key: "status", value: .string(invalid ? "invalid" : "valid")),
|
||||
.init(key: "invalid", value: .bool(invalid)),
|
||||
.init(key: "diagnostics", value: report.modelValue()),
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-throwing aggregate report for validating many YAML inputs.
|
||||
struct BatchReport: Equatable, Sendable, CustomStringConvertible {
|
||||
public var sourceReports: [SourceReport]
|
||||
public var failOnWarnings: Bool
|
||||
|
||||
public init(
|
||||
sourceReports: [SourceReport],
|
||||
failOnWarnings: Bool = false,
|
||||
) {
|
||||
self.sourceReports = sourceReports
|
||||
self.failOnWarnings = failOnWarnings
|
||||
}
|
||||
|
||||
public var fileCount: Int {
|
||||
sourceReports.count
|
||||
}
|
||||
|
||||
public var validCount: Int {
|
||||
sourceReports.count(where: { !$0.invalid })
|
||||
}
|
||||
|
||||
public var invalidCount: Int {
|
||||
sourceReports.count(where: { $0.invalid })
|
||||
}
|
||||
|
||||
public var diagnosticCount: Int {
|
||||
sourceReports.reduce(0) { count, sourceReport in
|
||||
count + sourceReport.report.diagnostics.count
|
||||
}
|
||||
}
|
||||
|
||||
public var isValid: Bool {
|
||||
invalidCount == 0
|
||||
}
|
||||
|
||||
public var diagnostics: [Diagnostic] {
|
||||
sourceReports.flatMap(\.report.diagnostics)
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
diagnostics.map(\.description).joined(separator: "\n")
|
||||
}
|
||||
|
||||
public func markdownDescription(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> String {
|
||||
var lines: [String] = [
|
||||
"# \(title)",
|
||||
"",
|
||||
"- Files: \(fileCount)",
|
||||
"- Valid: \(validCount)",
|
||||
"- Invalid: \(invalidCount)",
|
||||
"- Diagnostics: \(diagnosticCount)",
|
||||
"- Warnings fail: \(failOnWarnings ? "yes" : "no")",
|
||||
"",
|
||||
]
|
||||
|
||||
for sourceReport in sourceReports {
|
||||
lines.append("## \(sourceReport.name)")
|
||||
lines.append("")
|
||||
lines.append("Status: \(sourceReport.invalid ? "invalid" : "valid")")
|
||||
lines.append("")
|
||||
|
||||
if sourceReport.report.isEmpty {
|
||||
lines.append("No diagnostics.")
|
||||
} else {
|
||||
lines.append("```text")
|
||||
lines.append(sourceReport.report.description)
|
||||
lines.append("```")
|
||||
}
|
||||
|
||||
lines.append("")
|
||||
}
|
||||
|
||||
return lines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
public func modelValue(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> PureYAML.Model.Value {
|
||||
.mapping(.init([
|
||||
.init(key: "title", value: .string(title)),
|
||||
.init(key: "summary", value: summaryModelValue),
|
||||
.init(key: "sources", value: .sequence(sourceReports.map { $0.modelValue() })),
|
||||
]))
|
||||
}
|
||||
|
||||
public func yamlDescription(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> String {
|
||||
PureYAML.dump(modelValue(title: title))
|
||||
}
|
||||
|
||||
public func jsonDescription(
|
||||
title: String = "PureYAML Validation Report",
|
||||
) -> String {
|
||||
modelValue(title: title).jsonDescription + "\n"
|
||||
}
|
||||
|
||||
private var summaryModelValue: PureYAML.Model.Value {
|
||||
.mapping(.init([
|
||||
.init(key: "files", value: .int(fileCount)),
|
||||
.init(key: "valid", value: .int(validCount)),
|
||||
.init(key: "invalid", value: .int(invalidCount)),
|
||||
.init(key: "diagnostics", value: .int(diagnosticCount)),
|
||||
.init(key: "warningsFail", value: .bool(failOnWarnings)),
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PureYAML.Validation.Diagnostic {
|
||||
// Internal report-rendering helpers, shared by Report and BatchReport.
|
||||
|
||||
extension PureYAML.Validation.Diagnostic {
|
||||
var modelValue: PureYAML.Model.Value {
|
||||
var pairs: [PureYAML.Model.Pair] = [
|
||||
.init(key: "kind", value: .string(kind.description)),
|
||||
@ -237,7 +96,7 @@ private extension PureYAML.Validation.Diagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
private extension PureYAML.Model.Value {
|
||||
extension PureYAML.Model.Value {
|
||||
var jsonDescription: String {
|
||||
jsonDescription(indentation: 0)
|
||||
}
|
||||
@ -262,7 +121,7 @@ private extension PureYAML.Model.Value {
|
||||
}
|
||||
}
|
||||
|
||||
private extension [PureYAML.Model.Value] {
|
||||
extension [PureYAML.Model.Value] {
|
||||
func jsonDescription(indentation: Int) -> String {
|
||||
guard !isEmpty else {
|
||||
return "[]"
|
||||
@ -276,7 +135,7 @@ private extension [PureYAML.Model.Value] {
|
||||
}
|
||||
}
|
||||
|
||||
private extension PureYAML.Model.Mapping {
|
||||
extension PureYAML.Model.Mapping {
|
||||
func jsonDescription(indentation: Int) -> String {
|
||||
guard !pairs.isEmpty else {
|
||||
return "{}"
|
||||
@ -291,7 +150,7 @@ private extension PureYAML.Model.Mapping {
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
extension String {
|
||||
static func spaces(_ count: Int) -> String {
|
||||
String(repeating: " ", count: count)
|
||||
}
|
||||
@ -325,7 +184,7 @@ private extension String {
|
||||
}
|
||||
}
|
||||
|
||||
private extension UInt32 {
|
||||
extension UInt32 {
|
||||
var jsonControlEscape: String {
|
||||
let digits = Array("0123456789abcdef")
|
||||
return "\\u" + stride(from: 12, through: 0, by: -4).map { shift in
|
||||
|
||||
12
Sources/Validation/Validation.Source.swift
Normal file
12
Sources/Validation/Validation.Source.swift
Normal file
@ -0,0 +1,12 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Named YAML source used for batch validation without coupling PureYAML to file IO.
|
||||
struct Source: Equatable, Sendable {
|
||||
public var name: String
|
||||
public var yaml: String
|
||||
|
||||
public init(name: String, yaml: String) {
|
||||
self.name = name
|
||||
self.yaml = yaml
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Sources/Validation/Validation.SourceReport.swift
Normal file
23
Sources/Validation/Validation.SourceReport.swift
Normal file
@ -0,0 +1,23 @@
|
||||
public extension PureYAML.Validation {
|
||||
/// Validation result for one named source in a batch.
|
||||
struct SourceReport: Equatable, Sendable {
|
||||
public var name: String
|
||||
public var report: Report
|
||||
public var invalid: Bool
|
||||
|
||||
public init(name: String, report: Report, invalid: Bool) {
|
||||
self.name = name
|
||||
self.report = report
|
||||
self.invalid = invalid
|
||||
}
|
||||
|
||||
public func modelValue() -> PureYAML.Model.Value {
|
||||
.mapping(.init([
|
||||
.init(key: "name", value: .string(name)),
|
||||
.init(key: "status", value: .string(invalid ? "invalid" : "valid")),
|
||||
.init(key: "invalid", value: .bool(invalid)),
|
||||
.init(key: "diagnostics", value: report.modelValue()),
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,30 @@ fi
|
||||
|
||||
FAIL=0
|
||||
while IFS= read -r f; do
|
||||
count=$(grep -cE "^(public |package |internal )?(actor|struct|enum|protocol|class|final class) [A-Z]" "$f")
|
||||
# Count non-private top-level types per file (docs/rules/code-style.md). Types
|
||||
# here are declared inside `extension <Namespace>` blocks, so a column-0 grep
|
||||
# is blind to them. This namespace-aware scan counts a type when it is either
|
||||
# declared at file scope (column 0) or as a direct member (one indent) of an
|
||||
# `extension` whose target is a PureYAML namespace. Types nested inside another
|
||||
# type, declared via `extension <Namespace>.<Type>`, or marked
|
||||
# private/fileprivate are NOT counted; they MAY co-locate.
|
||||
count=$(awk '
|
||||
BEGIN {
|
||||
split("PureYAML PureYAML.Model PureYAML.Parsing PureYAML.Emitting PureYAML.Decoding PureYAML.Encoding PureYAML.Validation PureYAML.Stream PureYAML.Tagged", parts, " ")
|
||||
for (i in parts) ns[parts[i]] = 1
|
||||
}
|
||||
/^}/ { in_ns = 0; next }
|
||||
/^(public )?extension [A-Za-z0-9_.]+ \{/ {
|
||||
target = $0
|
||||
sub(/^(public )?extension /, "", target)
|
||||
sub(/ \{.*/, "", target)
|
||||
in_ns = (target in ns) ? 1 : 0
|
||||
next
|
||||
}
|
||||
/^(public |package |internal )?(indirect |final )*(actor|struct|enum|protocol|class) [A-Z]/ { count++; next }
|
||||
in_ns && /^ (public |package |internal )?(indirect |final )*(actor|struct|enum|protocol|class) [A-Z]/ { count++ }
|
||||
END { print count + 0 }
|
||||
' "$f")
|
||||
if [ "$count" -gt 1 ]; then
|
||||
echo "namespacing: $count file-scope types in $f (one per file)" >&2
|
||||
FAIL=1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user