From e4c271a4b6c8fa4ba7568a8cd935aec801583b1f Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Thu, 5 Feb 2026 22:06:04 +0100 Subject: [PATCH 1/3] fix tag duplication --- README.md | 6 +++--- .../Document/DocumentRepresentable.swift | 13 ++++++++++++- .../Example/{ => Components}/Example.swift | 0 .../Example/{ => Components}/ExampleDocument.swift | 0 .../{ => Components}/ExampleDuplicatedItem.swift | 0 .../ExampleDuplicatedItemDocument.swift | 0 .../ExampleDuplicatedItemModel+Schemas.swift | 0 .../ExampleDuplicatedItemModel.swift | 0 .../{ => Components}/ExampleMissingParentItem.swift | 0 .../ExampleMissingParentItemDocument.swift | 0 .../ExampleMissingParentItemModel+Schemas.swift | 0 .../ExampleMissingParentItemModel.swift | 0 .../{ => Components}/ExampleModel+Headers.swift | 0 .../{ => Components}/ExampleModel+Operations.swift | 0 .../{ => Components}/ExampleModel+Parameters.swift | 0 .../{ => Components}/ExampleModel+PathItems.swift | 0 .../ExampleModel+RequestBodies.swift | 0 .../{ => Components}/ExampleModel+Responses.swift | 0 .../{ => Components}/ExampleModel+Schemas.swift | 0 .../ExampleModel+SecuritySchemes.swift | 0 .../{ => Components}/ExampleModel+Tags.swift | 0 .../Example/{ => Components}/ExampleModel.swift | 0 .../{ => Example}/ExampleTestSuite.swift | 0 .../FeatherOpenAPITestSuite.swift | 6 ++++++ ...nentTests.swift => PathComponentTestSuite.swift} | 4 ++-- .../{ => Petstore}/PetstoreTestSuite.swift | 0 .../{ => Todo}/TodoTestSuite.swift | 0 27 files changed, 23 insertions(+), 6 deletions(-) rename Tests/FeatherOpenAPITests/Example/{ => Components}/Example.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleDocument.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleDuplicatedItem.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleDuplicatedItemDocument.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleDuplicatedItemModel+Schemas.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleDuplicatedItemModel.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleMissingParentItem.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleMissingParentItemDocument.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleMissingParentItemModel+Schemas.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleMissingParentItemModel.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+Headers.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+Operations.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+Parameters.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+PathItems.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+RequestBodies.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+Responses.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+Schemas.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+SecuritySchemes.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel+Tags.swift (100%) rename Tests/FeatherOpenAPITests/Example/{ => Components}/ExampleModel.swift (100%) rename Tests/FeatherOpenAPITests/{ => Example}/ExampleTestSuite.swift (100%) create mode 100644 Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift rename Tests/FeatherOpenAPITests/{PathComponentTests.swift => PathComponentTestSuite.swift} (97%) rename Tests/FeatherOpenAPITests/{ => Petstore}/PetstoreTestSuite.swift (100%) rename Tests/FeatherOpenAPITests/{ => Todo}/TodoTestSuite.swift (100%) diff --git a/README.md b/README.md index 1f4e30d..c2a2004 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ The FeatherOpenAPI library makes it easy to define OpenAPI specifications using Swift in a type-safe way. [ - ![Release: 1.0.0-beta.2](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E2-F05138) + ![Release: 1.0.0-beta.3](https://img.shields.io/badge/Release-1%2E0%2E0--beta%2E3-F05138) ]( - https://github.com/feather-framework/feather-openapi/releases/tag/1.0.0-beta.2 + https://github.com/feather-framework/feather-openapi/releases/tag/1.0.0-beta.3 ) ## Features @@ -34,7 +34,7 @@ The FeatherOpenAPI library makes it easy to define OpenAPI specifications using Use Swift Package Manager; add the dependency to your `Package.swift` file: ```swift -.package(url: "https://github.com/feather-framework/feather-openapi", exact: "1.0.0-beta.2"), +.package(url: "https://github.com/feather-framework/feather-openapi", exact: "1.0.0-beta.3"), ``` Then add `FeatherOpenAPI` to your target dependencies: diff --git a/Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift b/Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift index c75bd71..c709973 100644 --- a/Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift +++ b/Sources/FeatherOpenAPI/Document/DocumentRepresentable.swift @@ -38,7 +38,18 @@ extension DocumentRepresentable { /// Collects all tags referenced by the document. public var referencedTags: [OpenAPITagRepresentable] { - paths.values.map { $0.referencedTags }.flatMap { $0 } + var seen = Set() + return paths.values + .map { $0.referencedTags } + .flatMap { $0 } + .filter { tag in + let name = tag.openAPITag().name + if seen.contains(name) { + return false + } + seen.insert(name) + return true + } } /// Collects all security requirements referenced by the document. diff --git a/Tests/FeatherOpenAPITests/Example/Example.swift b/Tests/FeatherOpenAPITests/Example/Components/Example.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/Example.swift rename to Tests/FeatherOpenAPITests/Example/Components/Example.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDocument.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleDocument.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleDocument.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleDocument.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItem.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItem.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItem.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItem.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemDocument.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItemDocument.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemDocument.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItemDocument.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel+Schemas.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItemModel+Schemas.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel+Schemas.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItemModel+Schemas.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItemModel.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleDuplicatedItemModel.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleDuplicatedItemModel.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItem.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItem.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleMissingParentItem.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItem.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemDocument.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItemDocument.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemDocument.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItemDocument.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel+Schemas.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItemModel+Schemas.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel+Schemas.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItemModel+Schemas.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItemModel.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleMissingParentItemModel.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleMissingParentItemModel.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Headers.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Headers.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+Headers.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Headers.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Operations.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Operations.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+Operations.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Operations.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Parameters.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Parameters.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+Parameters.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Parameters.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+PathItems.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+PathItems.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+PathItems.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+PathItems.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+RequestBodies.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+RequestBodies.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+RequestBodies.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+RequestBodies.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Responses.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Responses.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+Responses.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Responses.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Schemas.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Schemas.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+Schemas.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Schemas.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+SecuritySchemes.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+SecuritySchemes.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+SecuritySchemes.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+SecuritySchemes.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel+Tags.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Tags.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel+Tags.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel+Tags.swift diff --git a/Tests/FeatherOpenAPITests/Example/ExampleModel.swift b/Tests/FeatherOpenAPITests/Example/Components/ExampleModel.swift similarity index 100% rename from Tests/FeatherOpenAPITests/Example/ExampleModel.swift rename to Tests/FeatherOpenAPITests/Example/Components/ExampleModel.swift diff --git a/Tests/FeatherOpenAPITests/ExampleTestSuite.swift b/Tests/FeatherOpenAPITests/Example/ExampleTestSuite.swift similarity index 100% rename from Tests/FeatherOpenAPITests/ExampleTestSuite.swift rename to Tests/FeatherOpenAPITests/Example/ExampleTestSuite.swift diff --git a/Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift b/Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift new file mode 100644 index 0000000..985d85e --- /dev/null +++ b/Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift @@ -0,0 +1,6 @@ +// +// FeatherOpenAPITestSuite.swift +// feather-openapi +// +// Created by Tibor Bödecs on 2026. 02. 05.. +// diff --git a/Tests/FeatherOpenAPITests/PathComponentTests.swift b/Tests/FeatherOpenAPITests/PathComponentTestSuite.swift similarity index 97% rename from Tests/FeatherOpenAPITests/PathComponentTests.swift rename to Tests/FeatherOpenAPITests/PathComponentTestSuite.swift index cb96c28..3aaa289 100644 --- a/Tests/FeatherOpenAPITests/PathComponentTests.swift +++ b/Tests/FeatherOpenAPITests/PathComponentTestSuite.swift @@ -1,5 +1,5 @@ // -// PathComponentTests.swift +// PathComponentTestSuite.swift // feather-openapi // // Created by Tibor Bödecs on 2026. 01. 22.. @@ -32,7 +32,7 @@ extension Path { } @Suite -struct PathComponentTests { +struct PathComponentTestSuite { @Test func doesNotCompile() { diff --git a/Tests/FeatherOpenAPITests/PetstoreTestSuite.swift b/Tests/FeatherOpenAPITests/Petstore/PetstoreTestSuite.swift similarity index 100% rename from Tests/FeatherOpenAPITests/PetstoreTestSuite.swift rename to Tests/FeatherOpenAPITests/Petstore/PetstoreTestSuite.swift diff --git a/Tests/FeatherOpenAPITests/TodoTestSuite.swift b/Tests/FeatherOpenAPITests/Todo/TodoTestSuite.swift similarity index 100% rename from Tests/FeatherOpenAPITests/TodoTestSuite.swift rename to Tests/FeatherOpenAPITests/Todo/TodoTestSuite.swift From 9412ce1a81b652e90123606242f4d495266baa30 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Thu, 5 Feb 2026 22:09:36 +0100 Subject: [PATCH 2/3] fix tag duplication & tests --- .../FeatherOpenAPITestSuite.swift | 63 +++++++++++++++++++ .../{Todo => }/TestObjects.swift | 60 ++++++++++++++++++ .../Todo/TodoTestSuite.swift | 54 ---------------- 3 files changed, 123 insertions(+), 54 deletions(-) rename Tests/FeatherOpenAPITests/{Todo => }/TestObjects.swift (73%) delete mode 100644 Tests/FeatherOpenAPITests/Todo/TodoTestSuite.swift diff --git a/Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift b/Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift index 985d85e..42737b7 100644 --- a/Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift +++ b/Tests/FeatherOpenAPITests/FeatherOpenAPITestSuite.swift @@ -4,3 +4,66 @@ // // Created by Tibor Bödecs on 2026. 02. 05.. // + +import OpenAPIKit +import OpenAPIKit30 +import OpenAPIKitCompat +import Testing +import Yams + +@testable import FeatherOpenAPI + +@Suite +struct FeatherOpenAPITestSuite { + + @Test + func documentDeduplicatesTagsByName() { + let document = TagDedupDocument( + info: TagDedupInfo(), + paths: TagDedupPaths().pathMap, + components: Components() + ) + + let openAPIDoc = document.openAPIDocument() + let tags = openAPIDoc.tags ?? [] + + #expect(tags.map(\.name) == ["Dogs"]) + } + + @Test + func multipleVersions() throws { + + let collection = MyPathCollection() + // collection.components.schemas.register(id: "", TodoFieldId()) + + let document = MyDocument( + info: MyInfo(), + paths: collection.pathMap, + components: collection.components, + ) + + let openAPIdoc = document.openAPIDocument() + + let encoder = YAMLEncoder() + + _ = + try openAPIdoc + .locallyDereferenced() + .resolved() + + let result = try encoder.encode(openAPIdoc) + print("---- 3.0 ----") + print(result) + + // let doc31 = openAPIdoc.convert(to: .v3_1_0) + // let result31 = try encoder.encode(doc31) + // print("---- 3.1 ----") + // print(result31) + // + // let doc32 = openAPIdoc.convert(to: .v3_2_0) + // let result32 = try encoder.encode(doc32) + // print("---- 3.2 ----") + // print(result32) + + } +} diff --git a/Tests/FeatherOpenAPITests/Todo/TestObjects.swift b/Tests/FeatherOpenAPITests/TestObjects.swift similarity index 73% rename from Tests/FeatherOpenAPITests/Todo/TestObjects.swift rename to Tests/FeatherOpenAPITests/TestObjects.swift index 6eaf712..537e7b5 100644 --- a/Tests/FeatherOpenAPITests/Todo/TestObjects.swift +++ b/Tests/FeatherOpenAPITests/TestObjects.swift @@ -189,3 +189,63 @@ struct APIKeySecurityRequirement: SecurityRequirementRepresentable { var security: any SecuritySchemeRepresentable = APIKeySecurityScheme() } + +// MARK: - + + +struct TagDedupInfo: InfoRepresentable { + var title: String { "Tag Dedup Test" } + var version: String { "1.0.0" } +} + +struct TagDedupDocument: DocumentRepresentable { + var info: OpenAPIInfoRepresentable + var paths: PathMap + var components: OpenAPIComponentsRepresentable +} + +struct TagDedupPaths: PathCollectionRepresentable { + var pathMap: PathMap { + [ + "dogs": TagDedupDogPathItem() + ] + } +} + +struct TagDedupDogPathItem: PathItemRepresentable { + var get: OperationRepresentable? { TagDedupListDogsOperation() } + var post: OperationRepresentable? { TagDedupCreateDogOperation() } +} + +struct TagDedupDogTag: TagRepresentable { + var name: String = "Dogs" + var description: String? = "Manage dogs." +} + +struct TagDedupDogSchema: StringSchemaRepresentable { + var example: String? = "Hachi" +} + +struct TagDedupDogResponse: JSONResponseRepresentable { + var description: String = "Dog response" + var schema: TagDedupDogSchema = TagDedupDogSchema() +} + +struct TagDedupListDogsOperation: OperationRepresentable { + var tags: [TagRepresentable] { [TagDedupDogTag()] } + var responseMap: ResponseMap { + [ + 200: TagDedupDogResponse().reference() + ] + } +} + +struct TagDedupCreateDogOperation: OperationRepresentable { + var tags: [TagRepresentable] { [TagDedupDogTag()] } + var responseMap: ResponseMap { + [ + 200: TagDedupDogResponse().reference() + ] + } +} + diff --git a/Tests/FeatherOpenAPITests/Todo/TodoTestSuite.swift b/Tests/FeatherOpenAPITests/Todo/TodoTestSuite.swift deleted file mode 100644 index 82d679a..0000000 --- a/Tests/FeatherOpenAPITests/Todo/TodoTestSuite.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// TodoTestSuite.swift -// feather-openapi -// -// Created by Tibor Bödecs on 2026. 01. 25.. - -import OpenAPIKit -import OpenAPIKit30 -import OpenAPIKitCompat -import Testing -import Yams - -@testable import FeatherOpenAPI - -@Suite -struct TodoTestSuite { - - @Test - func example() throws { - - let collection = MyPathCollection() - // collection.components.schemas.register(id: "", TodoFieldId()) - - let document = MyDocument( - info: MyInfo(), - paths: collection.pathMap, - components: collection.components, - ) - - let openAPIdoc = document.openAPIDocument() - - let encoder = YAMLEncoder() - - _ = - try openAPIdoc - .locallyDereferenced() - .resolved() - - let result = try encoder.encode(openAPIdoc) - print("---- 3.0 ----") - print(result) - - // let doc31 = openAPIdoc.convert(to: .v3_1_0) - // let result31 = try encoder.encode(doc31) - // print("---- 3.1 ----") - // print(result31) - // - // let doc32 = openAPIdoc.convert(to: .v3_2_0) - // let result32 = try encoder.encode(doc32) - // print("---- 3.2 ----") - // print(result32) - - } -} From d72aae1fffed249eedf4045d5ac1d77ae3f737a2 Mon Sep 17 00:00:00 2001 From: Tibor Bodecs Date: Fri, 6 Feb 2026 10:31:05 +0100 Subject: [PATCH 3/3] fix format --- Tests/FeatherOpenAPITests/TestObjects.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Tests/FeatherOpenAPITests/TestObjects.swift b/Tests/FeatherOpenAPITests/TestObjects.swift index 537e7b5..79ca84d 100644 --- a/Tests/FeatherOpenAPITests/TestObjects.swift +++ b/Tests/FeatherOpenAPITests/TestObjects.swift @@ -192,7 +192,6 @@ struct APIKeySecurityRequirement: SecurityRequirementRepresentable { // MARK: - - struct TagDedupInfo: InfoRepresentable { var title: String { "Tag Dedup Test" } var version: String { "1.0.0" } @@ -248,4 +247,3 @@ struct TagDedupCreateDogOperation: OperationRepresentable { ] } } -