diff --git a/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift b/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift index ca3d5682..6bbed189 100644 --- a/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift +++ b/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift @@ -28,6 +28,10 @@ public enum FeatureFlag: String, Hashable, Codable, CaseIterable, Sendable { // needs to be here for the enum to compile case empty + /// UUID support + /// + /// Enable interpretation of `type: string, format: uuid` as `Foundation.UUID` typed data. + case uuidSupport } /// A set of enabled feature flags. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 52270eb6..a91800fe 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -38,7 +38,10 @@ enum Constants { ImportDescription(moduleName: Constants.Import.runtime, spi: "Generated"), ImportDescription( moduleName: "Foundation", - moduleTypes: ["struct Foundation.URL", "struct Foundation.Data", "struct Foundation.Date"], + moduleTypes: [ + "struct Foundation.URL", "struct Foundation.Data", "struct Foundation.Date", + "struct Foundation.UUID", + ], preconcurrency: .onOS(["Linux"]) ), ] diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift index 4527bbe9..961d4727 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator+FeatureFlags.swift @@ -15,4 +15,7 @@ import OpenAPIKit extension FileTranslator { // Add helpers for reading feature flags below. + + /// A boolean value indicating whether the `uuid` format on schemas should be followed. + var supportUUIDFormat: Bool { config.featureFlags.contains(.uuidSupport) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index 4f246521..9209aa50 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -47,7 +47,9 @@ protocol FileTranslator { extension FileTranslator { /// A new context from the file translator. - var context: TranslatorContext { TranslatorContext(asSwiftSafeName: { $0.safeForSwiftCode }) } + var context: TranslatorContext { + TranslatorContext(asSwiftSafeName: { $0.safeForSwiftCode }, enableUUIDSupport: supportUUIDFormat) + } } /// A set of configuration values for concrete file translators. @@ -58,4 +60,6 @@ struct TranslatorContext { /// - Parameter string: The string to convert to be safe for Swift. /// - Returns: A Swift-safe version of the input string. var asSwiftSafeName: (String) -> String + /// A variable that indicates the presence of the `uuidSupport` feature flag. + var enableUUIDSupport: Bool } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift index ee51fbbd..e10ac046 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift @@ -50,6 +50,9 @@ extension TypeName { /// Returns the type name for the URL type. static var url: Self { .foundation("URL") } + /// Returns the type name for the UUID type. + static var uuid: Self { .foundation("UUID") } + /// Returns the type name for the DecodingError type. static var decodingError: Self { .swift("DecodingError") } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift index 1c503ae7..2f00dc92 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -313,6 +313,7 @@ struct TypeMatcher { default: switch core.format { case .dateTime: typeName = .date + case .uuid where context.enableUUIDSupport: typeName = .uuid default: typeName = .string } } diff --git a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift index a99d4d30..cfc403df 100644 --- a/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift +++ b/Tests/OpenAPIGeneratorCoreTests/TestUtilities.swift @@ -28,7 +28,7 @@ class Test_Core: XCTestCase { func makeTranslator( components: OpenAPI.Components = .noComponents, diagnostics: any DiagnosticCollector = PrintingDiagnosticCollector(), - featureFlags: FeatureFlags = [] + featureFlags: FeatureFlags = [.uuidSupport] ) -> TypesFileTranslator { makeTypesTranslator(components: components, diagnostics: diagnostics, featureFlags: featureFlags) } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift index 6b37703c..e4dd6ed3 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/Operations/Test_OperationDescription.swift @@ -144,7 +144,7 @@ final class Test_OperationDescription: Test_Core { endpoint: endpoint, pathParameters: pathItem.parameters, components: .init(), - context: .init(asSwiftSafeName: { $0 }) + context: .init(asSwiftSafeName: { $0 }, enableUUIDSupport: true) ) } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift index 6ca197d2..19d06408 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift @@ -28,6 +28,7 @@ final class Test_TypeMatcher: Test_Core { (.string(contentEncoding: .base64), "OpenAPIRuntime.Base64EncodedData"), (.string(.init(format: .date), .init()), "Swift.String"), (.string(.init(format: .dateTime), .init()), "Foundation.Date"), + (.string(.init(format: .uuid), .init()), "Foundation.UUID"), (.integer, "Swift.Int"), (.integer(.init(format: .int32), .init()), "Swift.Int32"), (.integer(.init(format: .int64), .init()), "Swift.Int64"), diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index 75b8be78..be00f4df 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -42,7 +42,7 @@ final class FileBasedReferenceTests: XCTestCase { #endif } - func testPetstore() throws { try _test(referenceProject: .init(name: .petstore)) } + func testPetstore() throws { try _test(referenceProject: .init(name: .petstore), featureFlags: [.uuidSupport]) } // MARK: - Private diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml index ac8a417d..1ec4a694 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/Docs/petstore.yaml @@ -79,6 +79,7 @@ paths: required: true schema: type: string + format: uuid My-Tracing-Header: $ref: '#/components/headers/TracingHeader' content: diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift index 75c9bf22..36b0a9a1 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift @@ -4,10 +4,12 @@ @preconcurrency import struct Foundation.URL @preconcurrency import struct Foundation.Data @preconcurrency import struct Foundation.Date +@preconcurrency import struct Foundation.UUID #else import struct Foundation.URL import struct Foundation.Data import struct Foundation.Date +import struct Foundation.UUID #endif import HTTPTypes /// Service for managing pet metadata. @@ -107,7 +109,7 @@ public struct Client: APIProtocol { My_hyphen_Response_hyphen_UUID: try converter.getRequiredHeaderFieldAsURI( in: response.headerFields, name: "My-Response-UUID", - as: Swift.String.self + as: Foundation.UUID.self ), My_hyphen_Tracing_hyphen_Header: try converter.getOptionalHeaderFieldAsURI( in: response.headerFields, diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift index 80d642b3..27ece0fb 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift @@ -4,10 +4,12 @@ @preconcurrency import struct Foundation.URL @preconcurrency import struct Foundation.Data @preconcurrency import struct Foundation.Date +@preconcurrency import struct Foundation.UUID #else import struct Foundation.URL import struct Foundation.Data import struct Foundation.Date +import struct Foundation.UUID #endif import HTTPTypes extension APIProtocol { @@ -199,7 +201,7 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol { My_hyphen_Request_hyphen_UUID: try converter.getOptionalHeaderFieldAsURI( in: request.headerFields, name: "My-Request-UUID", - as: Swift.String.self + as: Foundation.UUID.self ), accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) ) diff --git a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift index 818aff50..04470553 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Types.swift @@ -4,10 +4,12 @@ @preconcurrency import struct Foundation.URL @preconcurrency import struct Foundation.Data @preconcurrency import struct Foundation.Date +@preconcurrency import struct Foundation.UUID #else import struct Foundation.URL import struct Foundation.Data import struct Foundation.Date +import struct Foundation.UUID #endif /// A type that performs HTTP operations defined by the OpenAPI document. public protocol APIProtocol: Sendable { @@ -1820,7 +1822,7 @@ public enum Operations { /// Request identifier /// /// - Remark: Generated from `#/paths/pets/GET/header/My-Request-UUID`. - public var My_hyphen_Request_hyphen_UUID: Swift.String? + public var My_hyphen_Request_hyphen_UUID: Foundation.UUID? public var accept: [OpenAPIRuntime.AcceptHeaderContentType] /// Creates a new `Headers`. /// @@ -1828,7 +1830,7 @@ public enum Operations { /// - My_hyphen_Request_hyphen_UUID: Request identifier /// - accept: public init( - My_hyphen_Request_hyphen_UUID: Swift.String? = nil, + My_hyphen_Request_hyphen_UUID: Foundation.UUID? = nil, accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues() ) { self.My_hyphen_Request_hyphen_UUID = My_hyphen_Request_hyphen_UUID @@ -1856,7 +1858,7 @@ public enum Operations { /// Response identifier /// /// - Remark: Generated from `#/paths/pets/GET/responses/200/headers/My-Response-UUID`. - public var My_hyphen_Response_hyphen_UUID: Swift.String + public var My_hyphen_Response_hyphen_UUID: Foundation.UUID /// A description here. /// /// - Remark: Generated from `#/paths/pets/GET/responses/200/headers/My-Tracing-Header`. @@ -1867,7 +1869,7 @@ public enum Operations { /// - My_hyphen_Response_hyphen_UUID: Response identifier /// - My_hyphen_Tracing_hyphen_Header: A description here. public init( - My_hyphen_Response_hyphen_UUID: Swift.String, + My_hyphen_Response_hyphen_UUID: Foundation.UUID, My_hyphen_Tracing_hyphen_Header: Components.Headers.TracingHeader? = nil ) { self.My_hyphen_Response_hyphen_UUID = My_hyphen_Response_hyphen_UUID diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index f4811396..6cf929ba 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1462,6 +1462,36 @@ final class SnippetBasedReferenceTests: XCTestCase { """ ) } + func testComponentsSchemasUUID() throws { + try self.assertSchemasTranslation( + featureFlags: [.uuidSupport], + """ + schemas: + MyUUID: + type: string + format: uuid + """, + """ + public enum Schemas { + public typealias MyUUID = Foundation.UUID + } + """ + ) + // Without UUID support, the schema will be translated as a string + try self.assertSchemasTranslation( + """ + schemas: + MyUUID: + type: string + format: uuid + """, + """ + public enum Schemas { + public typealias MyUUID = Swift.String + } + """ + ) + } func testComponentsSchemasBase64() throws { try self.assertSchemasTranslation( diff --git a/Tests/PetstoreConsumerTests/Common.swift b/Tests/PetstoreConsumerTests/Common.swift index e635c7ad..1f74ce18 100644 --- a/Tests/PetstoreConsumerTests/Common.swift +++ b/Tests/PetstoreConsumerTests/Common.swift @@ -14,10 +14,6 @@ import XCTest import HTTPTypes -extension Operations.listPets.Output { - static var success: Self { .ok(.init(headers: .init(My_hyphen_Response_hyphen_UUID: "abcd"), body: .json([]))) } -} - extension HTTPRequest { /// Initializes an HTTP request with the specified path, HTTP method, and header fields. /// diff --git a/Tests/PetstoreConsumerTests/Test_Client.swift b/Tests/PetstoreConsumerTests/Test_Client.swift index 5b8b81fa..f7141f09 100644 --- a/Tests/PetstoreConsumerTests/Test_Client.swift +++ b/Tests/PetstoreConsumerTests/Test_Client.swift @@ -36,6 +36,8 @@ final class Test_Client: XCTestCase { } func testListPets_200() async throws { + let requestUUID = UUID(uuidString: "da6811e6-112f-494e-8bdd-7f8b2367cb66")! + let responseUUID = UUID(uuidString: "b1c601c1-8963-460b-9fe4-fda2f73da64f")! transport = .init { (request: HTTPRequest, body: HTTPBody?, baseURL: URL, operationID: String) in XCTAssertEqual(operationID, "listPets") XCTAssertEqual( @@ -44,12 +46,15 @@ final class Test_Client: XCTestCase { ) XCTAssertEqual(baseURL.absoluteString, "/api") XCTAssertEqual(request.method, .get) - XCTAssertEqual(request.headerFields, [.accept: "application/json", .init("My-Request-UUID")!: "abcd-1234"]) + XCTAssertEqual( + request.headerFields, + [.accept: "application/json", .init("My-Request-UUID")!: requestUUID.uuidString] + ) XCTAssertNil(body) return try HTTPResponse( status: .ok, headerFields: [ - .contentType: "application/json", .init("my-response-uuid")!: "abcd", + .contentType: "application/json", .init("my-response-uuid")!: responseUUID.uuidString, .init("my-tracing-header")!: "1234", ] ) @@ -67,14 +72,14 @@ final class Test_Client: XCTestCase { let response = try await client.listPets( .init( query: .init(limit: 24, habitat: .water, feeds: [.herbivore, .carnivore], since: .test), - headers: .init(My_hyphen_Request_hyphen_UUID: "abcd-1234") + headers: .init(My_hyphen_Request_hyphen_UUID: requestUUID) ) ) guard case let .ok(value) = response else { XCTFail("Unexpected response: \(response)") return } - XCTAssertEqual(value.headers.My_hyphen_Response_hyphen_UUID, "abcd") + XCTAssertEqual(value.headers.My_hyphen_Response_hyphen_UUID, responseUUID) XCTAssertEqual(value.headers.My_hyphen_Tracing_hyphen_Header, "1234") switch value.body { case .json(let pets): XCTAssertEqual(pets, [.init(id: 1, name: "Fluffz")]) diff --git a/Tests/PetstoreConsumerTests/Test_Server.swift b/Tests/PetstoreConsumerTests/Test_Server.swift index 5f6be366..36ed162d 100644 --- a/Tests/PetstoreConsumerTests/Test_Server.swift +++ b/Tests/PetstoreConsumerTests/Test_Server.swift @@ -28,15 +28,20 @@ final class Test_Server: XCTestCase { } func testListPets_200() async throws { + let requestUUID = UUID(uuidString: "da6811e6-112f-494e-8bdd-7f8b2367cb66")! + let responseUUID = UUID(uuidString: "b1c601c1-8963-460b-9fe4-fda2f73da64f")! client = .init(listPetsBlock: { input in XCTAssertEqual(input.query.limit, 24) XCTAssertEqual(input.query.habitat, .water) XCTAssertEqual(input.query.since, .test) XCTAssertEqual(input.query.feeds, [.carnivore, .herbivore]) - XCTAssertEqual(input.headers.My_hyphen_Request_hyphen_UUID, "abcd-1234") + XCTAssertEqual(input.headers.My_hyphen_Request_hyphen_UUID, requestUUID) return .ok( .init( - headers: .init(My_hyphen_Response_hyphen_UUID: "abcd", My_hyphen_Tracing_hyphen_Header: "1234"), + headers: .init( + My_hyphen_Response_hyphen_UUID: responseUUID, + My_hyphen_Tracing_hyphen_Header: "1234" + ), body: .json([.init(id: 1, name: "Fluffz")]) ) ) @@ -45,7 +50,7 @@ final class Test_Server: XCTestCase { .init( soar_path: "/api/pets?limit=24&habitat=water&feeds=carnivore&feeds=herbivore&since=\(Date.testString)", method: .get, - headerFields: [.init("My-Request-UUID")!: "abcd-1234"] + headerFields: [.init("My-Request-UUID")!: requestUUID.uuidString] ), nil, .init() @@ -54,7 +59,7 @@ final class Test_Server: XCTestCase { XCTAssertEqual( response.headerFields, [ - .init("My-Response-UUID")!: "abcd", .init("My-Tracing-Header")!: "1234", + .init("My-Response-UUID")!: responseUUID.uuidString, .init("My-Tracing-Header")!: "1234", .contentType: "application/json; charset=utf-8", .contentLength: "47", ] )