From c4f7f9dccce8601437e8f95f5cdd15fdcbee9ae6 Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Fri, 26 Jun 2026 06:14:21 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 6 + Sources/Parsing/EventStreamComposedNode.swift | 8 + Sources/Parsing/EventStreamComposer.swift | 27 --- Sources/Parsing/EventStreamComposerMode.swift | 6 + Sources/Parsing/EventStreamContainer.swift | 11 ++ .../Parsing/EventStreamContainerKind.swift | 6 + Sources/Parsing/Scanner.FlowContext.swift | 5 - Sources/Parsing/ScannerFlowContextKind.swift | 6 + Sources/Parsing/Token.swift | 97 ----------- Sources/Parsing/TokenKind.swift | 96 +++++++++++ Sources/Tagged/Tagged.Mapping.swift | 18 ++ Sources/Tagged/Tagged.Node.swift | 72 -------- Sources/Tagged/Tagged.NodeKind.swift | 11 ++ Sources/Tagged/Tagged.Pair.swift | 17 ++ Sources/Tagged/Tagged.Scalar.swift | 17 ++ Sources/Tagged/Tagged.Sequence.swift | 14 ++ .../Validation/Validation.BatchReport.swift | 111 +++++++++++++ .../Validation/Validation.Diagnostic.swift | 14 -- .../Validation.DiagnosticKind.swift | 15 ++ Sources/Validation/Validation.Report.swift | 157 +----------------- Sources/Validation/Validation.Source.swift | 12 ++ .../Validation/Validation.SourceReport.swift | 23 +++ scripts/check-namespacing.sh | 25 ++- 23 files changed, 409 insertions(+), 365 deletions(-) create mode 100644 Sources/Parsing/EventStreamComposedNode.swift create mode 100644 Sources/Parsing/EventStreamComposerMode.swift create mode 100644 Sources/Parsing/EventStreamContainer.swift create mode 100644 Sources/Parsing/EventStreamContainerKind.swift create mode 100644 Sources/Parsing/ScannerFlowContextKind.swift create mode 100644 Sources/Parsing/TokenKind.swift create mode 100644 Sources/Tagged/Tagged.Mapping.swift create mode 100644 Sources/Tagged/Tagged.NodeKind.swift create mode 100644 Sources/Tagged/Tagged.Pair.swift create mode 100644 Sources/Tagged/Tagged.Scalar.swift create mode 100644 Sources/Tagged/Tagged.Sequence.swift create mode 100644 Sources/Validation/Validation.BatchReport.swift create mode 100644 Sources/Validation/Validation.DiagnosticKind.swift create mode 100644 Sources/Validation/Validation.Source.swift create mode 100644 Sources/Validation/Validation.SourceReport.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index bf638b2..08d595c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Sources/Parsing/EventStreamComposedNode.swift b/Sources/Parsing/EventStreamComposedNode.swift new file mode 100644 index 0000000..4e0bf83 --- /dev/null +++ b/Sources/Parsing/EventStreamComposedNode.swift @@ -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] = [] + } +} diff --git a/Sources/Parsing/EventStreamComposer.swift b/Sources/Parsing/EventStreamComposer.swift index 0bc5971..6c5636b 100644 --- a/Sources/Parsing/EventStreamComposer.swift +++ b/Sources/Parsing/EventStreamComposer.swift @@ -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 diff --git a/Sources/Parsing/EventStreamComposerMode.swift b/Sources/Parsing/EventStreamComposerMode.swift new file mode 100644 index 0000000..9a0e206 --- /dev/null +++ b/Sources/Parsing/EventStreamComposerMode.swift @@ -0,0 +1,6 @@ +extension PureYAML.Parsing { + enum EventStreamComposerMode { + case singleDocument + case stream + } +} diff --git a/Sources/Parsing/EventStreamContainer.swift b/Sources/Parsing/EventStreamContainer.swift new file mode 100644 index 0000000..ccb391c --- /dev/null +++ b/Sources/Parsing/EventStreamContainer.swift @@ -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 + } +} diff --git a/Sources/Parsing/EventStreamContainerKind.swift b/Sources/Parsing/EventStreamContainerKind.swift new file mode 100644 index 0000000..e584e98 --- /dev/null +++ b/Sources/Parsing/EventStreamContainerKind.swift @@ -0,0 +1,6 @@ +extension PureYAML.Parsing { + enum EventStreamContainerKind { + case sequence + case mapping + } +} diff --git a/Sources/Parsing/Scanner.FlowContext.swift b/Sources/Parsing/Scanner.FlowContext.swift index 7f22627..4752df3 100644 --- a/Sources/Parsing/Scanner.FlowContext.swift +++ b/Sources/Parsing/Scanner.FlowContext.swift @@ -1,9 +1,4 @@ extension PureYAML.Parsing { - enum ScannerFlowContextKind { - case sequence - case mapping - } - struct ScannerFlowContext { var kind: ScannerFlowContextKind var isExpectingKey: Bool diff --git a/Sources/Parsing/ScannerFlowContextKind.swift b/Sources/Parsing/ScannerFlowContextKind.swift new file mode 100644 index 0000000..3f7fc76 --- /dev/null +++ b/Sources/Parsing/ScannerFlowContextKind.swift @@ -0,0 +1,6 @@ +extension PureYAML.Parsing { + enum ScannerFlowContextKind { + case sequence + case mapping + } +} diff --git a/Sources/Parsing/Token.swift b/Sources/Parsing/Token.swift index 0983525..d1736ab 100644 --- a/Sources/Parsing/Token.swift +++ b/Sources/Parsing/Token.swift @@ -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 - } -} diff --git a/Sources/Parsing/TokenKind.swift b/Sources/Parsing/TokenKind.swift new file mode 100644 index 0000000..6ebefcc --- /dev/null +++ b/Sources/Parsing/TokenKind.swift @@ -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 + } +} diff --git a/Sources/Tagged/Tagged.Mapping.swift b/Sources/Tagged/Tagged.Mapping.swift new file mode 100644 index 0000000..733e47f --- /dev/null +++ b/Sources/Tagged/Tagged.Mapping.swift @@ -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 + } + } +} diff --git a/Sources/Tagged/Tagged.Node.swift b/Sources/Tagged/Tagged.Node.swift index e812dd3..518b69e 100644 --- a/Sources/Tagged/Tagged.Node.swift +++ b/Sources/Tagged/Tagged.Node.swift @@ -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 - } - } } diff --git a/Sources/Tagged/Tagged.NodeKind.swift b/Sources/Tagged/Tagged.NodeKind.swift new file mode 100644 index 0000000..65951a2 --- /dev/null +++ b/Sources/Tagged/Tagged.NodeKind.swift @@ -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 + } + } +} diff --git a/Sources/Tagged/Tagged.Pair.swift b/Sources/Tagged/Tagged.Pair.swift new file mode 100644 index 0000000..8133a6f --- /dev/null +++ b/Sources/Tagged/Tagged.Pair.swift @@ -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 + } + } +} diff --git a/Sources/Tagged/Tagged.Scalar.swift b/Sources/Tagged/Tagged.Scalar.swift new file mode 100644 index 0000000..527b1bb --- /dev/null +++ b/Sources/Tagged/Tagged.Scalar.swift @@ -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 + } + } +} diff --git a/Sources/Tagged/Tagged.Sequence.swift b/Sources/Tagged/Tagged.Sequence.swift new file mode 100644 index 0000000..01cbbb4 --- /dev/null +++ b/Sources/Tagged/Tagged.Sequence.swift @@ -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 + } + } +} diff --git a/Sources/Validation/Validation.BatchReport.swift b/Sources/Validation/Validation.BatchReport.swift new file mode 100644 index 0000000..0ffcb7a --- /dev/null +++ b/Sources/Validation/Validation.BatchReport.swift @@ -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)), + ])) + } + } +} diff --git a/Sources/Validation/Validation.Diagnostic.swift b/Sources/Validation/Validation.Diagnostic.swift index 82f16d4..630880a 100644 --- a/Sources/Validation/Validation.Diagnostic.swift +++ b/Sources/Validation/Validation.Diagnostic.swift @@ -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 diff --git a/Sources/Validation/Validation.DiagnosticKind.swift b/Sources/Validation/Validation.DiagnosticKind.swift new file mode 100644 index 0000000..5358d2a --- /dev/null +++ b/Sources/Validation/Validation.DiagnosticKind.swift @@ -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" + } + } + } +} diff --git a/Sources/Validation/Validation.Report.swift b/Sources/Validation/Validation.Report.swift index 71370d5..8a4ff6e 100644 --- a/Sources/Validation/Validation.Report.swift +++ b/Sources/Validation/Validation.Report.swift @@ -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 diff --git a/Sources/Validation/Validation.Source.swift b/Sources/Validation/Validation.Source.swift new file mode 100644 index 0000000..2c85730 --- /dev/null +++ b/Sources/Validation/Validation.Source.swift @@ -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 + } + } +} diff --git a/Sources/Validation/Validation.SourceReport.swift b/Sources/Validation/Validation.SourceReport.swift new file mode 100644 index 0000000..884334a --- /dev/null +++ b/Sources/Validation/Validation.SourceReport.swift @@ -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()), + ])) + } + } +} diff --git a/scripts/check-namespacing.sh b/scripts/check-namespacing.sh index 791bcc4..7fd1697 100755 --- a/scripts/check-namespacing.sh +++ b/scripts/check-namespacing.sh @@ -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 ` 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 .`, 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