diff --git a/CHANGELOG.md b/CHANGELOG.md index 15c4c7d..10d0cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 fallback behavior, and direct value-tree validation. - Add fixture-backed literal block emission coverage for newly supported multiline content and exact quoted fallbacks. +- Add downstream-shaped document fixtures for API-style documents, service + configuration decoding, and unsupported merge-key fallback behavior. ### Changed diff --git a/README.md b/README.md index ac1ae67..733c362 100644 --- a/README.md +++ b/README.md @@ -63,26 +63,7 @@ flowchart TB #10 Typed Decoding and Encoding is complete in main. -#11 YAML Compatibility Corpus is active: - -```mermaid -flowchart TB - classDef done fill:#ddf9e4,stroke:#34c759,color:#111827 - classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827 - classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827 - classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827 - Epic11["#11 YAML Compatibility Corpus"]:::epic - Scalar24["#24 Scalar and Tag Corpus"]:::done - Collection25["#25 Collection and Anchor Corpus"]:::done - Gaps27["#27 Unsupported YAML Gaps"]:::done - Literal19["#19 Literal Block Emission"]:::done - Downstream26["#26 Downstream Document Fixtures"]:::todo - Epic11 --> Scalar24 - Scalar24 --> Collection25 - Collection25 --> Gaps27 - Gaps27 --> Literal19 - Literal19 --> Downstream26 -``` +#11 YAML Compatibility Corpus is complete in main. ## Status diff --git a/Tests/DownstreamDocumentFixtures.swift b/Tests/DownstreamDocumentFixtures.swift new file mode 100644 index 0000000..4780c10 --- /dev/null +++ b/Tests/DownstreamDocumentFixtures.swift @@ -0,0 +1,113 @@ +@testable import PureYAML + +let openAPIStyleDocumentYAML = """ +openapi: 3.1.0 +info: + title: Article API + version: 1.2.3 +servers: + - url: https://api.example.com +paths: + /articles/{id}: + get: + operationId: getArticle + parameters: + - name: id + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: Article response + content: + application/json: + schema: + $ref: "#/components/schemas/Article" +components: + schemas: + Article: + type: object + required: [id, title, tags] + properties: + id: {type: string, format: uuid} + title: + type: string + tags: + type: array + items: + type: string + summary: + type: string + nullable: true +""" + +let downstreamServiceDocumentYAML = """ +service: + name: TileDown + version: "0.4.2" + enabled: true + tags: [static, yaml] +limits: + retries: 3 + timeoutSeconds: 30 +owners: + - name: Mihaela + email: + - name: Build Bot + email: bot@example.com +metadata: + region: eu + tier: production +""" + +let downstreamServiceDocument = DownstreamServiceDocument( + service: .init( + name: "TileDown", + version: "0.4.2", + enabled: true, + tags: ["static", "yaml"], + ), + limits: .init(retries: 3, timeoutSeconds: 30), + owners: [ + .init(name: "Mihaela", email: nil), + .init(name: "Build Bot", email: "bot@example.com"), + ], + metadata: [ + "region": "eu", + "tier": "production", + ], +) + +let downstreamMergeKeyYAML = """ +defaults: &serviceDefaults {retries: 3, timeoutSeconds: 30} +services: + api: + <<: *serviceDefaults + name: API +""" + +struct DownstreamServiceDocument: Codable, Equatable { + var service: DownstreamService + var limits: DownstreamLimits + var owners: [DownstreamOwner] + var metadata: [String: String] +} + +struct DownstreamService: Codable, Equatable { + var name: String + var version: String + var enabled: Bool + var tags: [String] +} + +struct DownstreamLimits: Codable, Equatable { + var retries: Int + var timeoutSeconds: Int +} + +struct DownstreamOwner: Codable, Equatable { + var name: String + var email: String? +} diff --git a/Tests/DownstreamDocumentTests.swift b/Tests/DownstreamDocumentTests.swift new file mode 100644 index 0000000..da5ff86 --- /dev/null +++ b/Tests/DownstreamDocumentTests.swift @@ -0,0 +1,141 @@ +@testable import PureYAML +import Testing + +@Suite("Downstream Documents") +struct DownstreamDocumentTests { + @Test("Parses OpenAPI-style document fixture with exact representative paths") + func test_openAPIStyleDocumentFixture() throws { + let root = try requireRootMapping(openAPIStyleDocumentYAML) + + #expect(root.pairs.map(\.key) == ["openapi", "info", "servers", "paths", "components"]) + #expect(root["openapi"] == .string("3.1.0")) + #expect(root["swagger"] == nil) + + let info = expectMapping(root["info"], "expected info mapping") + #expect(info?["title"] == .string("Article API")) + #expect(info?["version"] == .string("1.2.3")) + + let servers = requireSequence(root["servers"], "expected servers sequence") + #expect(servers?.count == 1) + let server = expectMapping(servers?.first, "expected first server mapping") + #expect(server?["url"] == .string("https://api.example.com")) + + let paths = expectMapping(root["paths"], "expected paths mapping") + #expect(paths?["/missing"] == nil) + let articlePath = expectMapping(paths?["/articles/{id}"], "expected article path mapping") + let get = expectMapping(articlePath?["get"], "expected get operation mapping") + #expect(get?.pairs.map(\.key) == ["operationId", "parameters", "responses"]) + #expect(get?["operationId"] == .string("getArticle")) + #expect(get?["post"] == nil) + + let parameters = requireSequence(get?["parameters"], "expected parameters sequence") + #expect(parameters?.count == 1) + let parameter = expectMapping(parameters?.first, "expected parameter mapping") + #expect(parameter?["name"] == .string("id")) + #expect(parameter?["in"] == .string("path")) + #expect(parameter?["required"] == .bool(true)) + let parameterSchema = expectMapping(parameter?["schema"], "expected parameter schema mapping") + #expect(parameterSchema?["type"] == .string("string")) + #expect(parameterSchema?["format"] == .string("uuid")) + + let responses = expectMapping(get?["responses"], "expected responses mapping") + #expect(responses?["404"] == nil) + let success = expectMapping(responses?["200"], "expected success response mapping") + #expect(success?["description"] == .string("Article response")) + let content = expectMapping(success?["content"], "expected content mapping") + let mediaType = expectMapping(content?["application/json"], "expected json media type mapping") + let responseSchema = expectMapping(mediaType?["schema"], "expected response schema mapping") + #expect(responseSchema?["$ref"] == .string("#/components/schemas/Article")) + + let components = expectMapping(root["components"], "expected components mapping") + let schemas = expectMapping(components?["schemas"], "expected schemas mapping") + let article = expectMapping(schemas?["Article"], "expected Article schema mapping") + #expect(article?["type"] == .string("object")) + let required = requireSequence(article?["required"], "expected required sequence") + #expect(required == [.string("id"), .string("title"), .string("tags")]) + + let properties = expectMapping(article?["properties"], "expected properties mapping") + let tags = expectMapping(properties?["tags"], "expected tags property mapping") + #expect(tags?["type"] == .string("array")) + let tagItems = expectMapping(tags?["items"], "expected tag item mapping") + #expect(tagItems?["type"] == .string("string")) + let summary = expectMapping(properties?["summary"], "expected summary property mapping") + #expect(summary?["nullable"] == .bool(true)) + } + + @Test("Decodes downstream service configuration fixture exactly") + func test_downstreamServiceConfigurationFixture() throws { + let parsed = try PureYAML.parse(downstreamServiceDocumentYAML) + let root = expectMapping(parsed, "expected service configuration mapping") + + #expect(root?.pairs.map(\.key) == ["service", "limits", "owners", "metadata"]) + let service = expectMapping(root?["service"], "expected service mapping") + #expect(service?["name"] == .string("TileDown")) + #expect(service?["version"] == .string("0.4.2")) + #expect(service?["enabled"] == .bool(true)) + #expect(requireSequence(service?["tags"]) == [.string("static"), .string("yaml")]) + + let limits = expectMapping(root?["limits"], "expected limits mapping") + #expect(limits?["retries"] == .int(3)) + #expect(limits?["timeoutSeconds"] == .int(30)) + + let owners = requireSequence(root?["owners"], "expected owners sequence") + #expect(owners?.count == 2) + let firstOwner = expectMapping(owners?.first, "expected first owner mapping") + #expect(firstOwner?["name"] == .string("Mihaela")) + #expect(firstOwner?["email"] == .null) + let secondOwner = expectMapping(owners?.last, "expected second owner mapping") + #expect(secondOwner?["email"] == .string("bot@example.com")) + + let metadata = expectMapping(root?["metadata"], "expected metadata mapping") + #expect(metadata?["region"] == .string("eu")) + #expect(metadata?["tier"] == .string("production")) + #expect(metadata?["missing"] == nil) + + #expect(try PureYAML.decode(DownstreamServiceDocument.self, from: parsed) == downstreamServiceDocument) + #expect( + try PureYAML.decode( + DownstreamServiceDocument.self, + from: downstreamServiceDocumentYAML, + ) == downstreamServiceDocument, + ) + } + + @Test("Pins unsupported downstream merge keys as unflattened fallback values") + func test_downstreamMergeKeyFallbackFixture() throws { + let root = try requireRootMapping(downstreamMergeKeyYAML) + let defaults = expectMapping(root["defaults"], "expected defaults mapping") + let services = expectMapping(root["services"], "expected services mapping") + let api = expectMapping(services?["api"], "expected api service mapping") + let merge = expectMapping(api?["<<"], "expected unflattened merge mapping") + + #expect(root.pairs.map(\.key) == ["defaults", "services"]) + #expect(api?.pairs.map(\.key) == ["<<", "name"]) + #expect(defaults?["retries"] == .int(3)) + #expect(defaults?["timeoutSeconds"] == .int(30)) + #expect(merge == defaults) + #expect(api?["name"] == .string("API")) + #expect(api?["retries"] == nil) + #expect(api?["timeoutSeconds"] == nil) + #expect(PureYAML.Validation.Validator().collect(.mapping(root)) == .init()) + } + + func requireRootMapping(_ yaml: String) throws -> PureYAML.Model.Mapping { + guard case let .mapping(mapping) = try PureYAML.parse(yaml) else { + recordIssue("expected root mapping") + return .init() + } + return mapping + } + + func expectMapping( + _ value: PureYAML.Model.Value?, + _ message: String, + ) -> PureYAML.Model.Mapping? { + guard case let .mapping(mapping)? = value else { + recordIssue(message) + return nil + } + return mapping + } +}