diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 26a405296..36dac5f66 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -101,3 +101,32 @@ jobs: windows_6_2_enabled: true windows_nightly_next_enabled: true windows_nightly_main_enabled: true + + construct-linkage-test-matrix: + name: Construct linkage matrix + runs-on: ubuntu-latest + outputs: + linkage-test-matrix: '${{ steps.generate-matrix.outputs.linkage-test-matrix }}' + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + - id: generate-matrix + run: echo "linkage-test-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT" + env: + MATRIX_LINUX_SETUP_COMMAND: apt-get update -y && apt-get install -yq jq && git config --global --add safe.directory /swift-openapi-generator + MATRIX_LINUX_COMMAND: ./scripts/run-linkage-test.sh + MATRIX_LINUX_5_10_ENABLED: false + MATRIX_LINUX_6_0_ENABLED: false + MATRIX_LINUX_6_1_ENABLED: false + MATRIX_LINUX_NIGHTLY_NEXT_ENABLED: false + MATRIX_LINUX_NIGHTLY_MAIN_ENABLED: false + + linkage-test: + name: Linkage test + needs: construct-linkage-test-matrix + uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main + with: + name: "Linkage test" + matrix_string: '${{ needs.construct-linkage-test-matrix.outputs.linkage-test-matrix }}' \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1020e9ef3..44d2ba593 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -127,3 +127,32 @@ jobs: windows_6_2_enabled: true windows_nightly_next_enabled: true windows_nightly_main_enabled: true + + construct-linkage-test-matrix: + name: Construct linkage matrix + runs-on: ubuntu-latest + outputs: + linkage-test-matrix: '${{ steps.generate-matrix.outputs.linkage-test-matrix }}' + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + persist-credentials: false + - id: generate-matrix + run: echo "linkage-test-matrix=$(curl -s https://raw.githubusercontent.com/apple/swift-nio/main/scripts/generate_matrix.sh | bash)" >> "$GITHUB_OUTPUT" + env: + MATRIX_LINUX_SETUP_COMMAND: apt-get update -y && apt-get install -yq jq && git config --global --add safe.directory /swift-openapi-generator + MATRIX_LINUX_COMMAND: ./scripts/run-linkage-test.sh + MATRIX_LINUX_5_10_ENABLED: false + MATRIX_LINUX_6_0_ENABLED: false + MATRIX_LINUX_6_1_ENABLED: false + MATRIX_LINUX_NIGHTLY_NEXT_ENABLED: false + MATRIX_LINUX_NIGHTLY_MAIN_ENABLED: false + + linkage-test: + name: Linkage test + needs: construct-linkage-test-matrix + uses: apple/swift-nio/.github/workflows/swift_test_matrix.yml@main + with: + name: "Linkage test" + matrix_string: '${{ needs.construct-linkage-test-matrix.outputs.linkage-test-matrix }}' diff --git a/Package.swift b/Package.swift index f2705fef1..cbfce56e3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.1 //===----------------------------------------------------------------------===// // // This source file is part of the SwiftOpenAPIGenerator open source project @@ -54,7 +54,7 @@ let package = Package( // Tests-only: Runtime library linked by generated code, and also // helps keep the runtime library new enough to work with the generated // code. - .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.8.2"), + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.11.0", traits: []), .package(url: "https://github.com/apple/swift-http-types", from: "1.0.2"), ], targets: [ diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift new file mode 100644 index 000000000..f2705fef1 --- /dev/null +++ b/Package@swift-5.10.swift @@ -0,0 +1,167 @@ +// swift-tools-version:5.10 +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Foundation +import PackageDescription + +// General Swift-settings for all targets. +var swiftSettings: [SwiftSetting] = [ + // https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md + // Require `any` for existential types. + .enableUpcomingFeature("ExistentialAny"), .enableExperimentalFeature("StrictConcurrency=complete"), +] + +let package = Package( + name: "swift-openapi-generator", + platforms: [ + .macOS(.v10_15), + + // The platforms below are not currently supported for running + // the generator itself. We include them here to allow the generator + // to emit a more descriptive compiler error. + .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1), + ], + products: [ + .executable(name: "swift-openapi-generator", targets: ["swift-openapi-generator"]), + .plugin(name: "OpenAPIGenerator", targets: ["OpenAPIGenerator"]), + .plugin(name: "OpenAPIGeneratorCommand", targets: ["OpenAPIGeneratorCommand"]), + .library(name: "_OpenAPIGeneratorCore", targets: ["_OpenAPIGeneratorCore"]), + ], + dependencies: [ + + // General algorithms + .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), + .package(url: "https://github.com/apple/swift-collections", from: "1.1.4"), + + // Read OpenAPI documents + .package(url: "https://github.com/mattpolzin/OpenAPIKit", from: "3.9.0"), + .package(url: "https://github.com/jpsim/Yams", "4.0.0"..<"7.0.0"), + + // CLI Tool + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), + + // Tests-only: Runtime library linked by generated code, and also + // helps keep the runtime library new enough to work with the generated + // code. + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.8.2"), + .package(url: "https://github.com/apple/swift-http-types", from: "1.0.2"), + ], + targets: [ + + // Generator Core + .target( + name: "_OpenAPIGeneratorCore", + dependencies: [ + .product(name: "OpenAPIKit", package: "OpenAPIKit"), + .product(name: "OpenAPIKit30", package: "OpenAPIKit"), + .product(name: "OpenAPIKitCompat", package: "OpenAPIKit"), + .product(name: "Algorithms", package: "swift-algorithms"), + .product(name: "OrderedCollections", package: "swift-collections"), + .product(name: "Yams", package: "Yams"), + ], + swiftSettings: swiftSettings + ), + + // Generator Core Tests + .testTarget( + name: "OpenAPIGeneratorCoreTests", + dependencies: ["_OpenAPIGeneratorCore"], + swiftSettings: swiftSettings + ), + + // GeneratorReferenceTests + .testTarget( + name: "OpenAPIGeneratorReferenceTests", + dependencies: ["_OpenAPIGeneratorCore"], + resources: [.copy("Resources")], + swiftSettings: swiftSettings + ), + + // Common types for concrete PetstoreConsumer*Tests test targets. + .target( + name: "PetstoreConsumerTestCore", + dependencies: [ + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + .product(name: "HTTPTypes", package: "swift-http-types"), + ], + swiftSettings: swiftSettings + ), + + // PetstoreConsumerTests + // Builds and tests the reference code from GeneratorReferenceTests + // to ensure it actually works correctly at runtime. + .testTarget( + name: "PetstoreConsumerTests", + dependencies: ["PetstoreConsumerTestCore"], + swiftSettings: swiftSettings + ), + + // Test Target for swift-openapi-generator + .testTarget( + name: "OpenAPIGeneratorTests", + dependencies: [ + "_OpenAPIGeneratorCore", + // Everything except windows: https://github.com/swiftlang/swift-package-manager/issues/6367 + .target( + name: "swift-openapi-generator", + condition: .when(platforms: [.android, .linux, .macOS, .openbsd, .wasi, .custom("freebsd")]) + ), .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + resources: [.copy("Resources")], + swiftSettings: swiftSettings + ), + + // Generator CLI + .executableTarget( + name: "swift-openapi-generator", + dependencies: [ + "_OpenAPIGeneratorCore", .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + swiftSettings: swiftSettings + ), + + // Build Plugin + .plugin(name: "OpenAPIGenerator", capability: .buildTool(), dependencies: ["swift-openapi-generator"]), + + // Command Plugin + .plugin( + name: "OpenAPIGeneratorCommand", + capability: .command( + intent: .custom( + verb: "generate-code-from-openapi", + description: "Generate Swift code from an OpenAPI document." + ), + permissions: [ + .writeToPackageDirectory( + reason: "To write the generated Swift files back into the source directory of the package." + ) + ] + ), + dependencies: ["swift-openapi-generator"] + ), + ] +) + +// --- STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // +for target in package.targets { + switch target.type { + case .regular, .test, .executable: + var settings = target.swiftSettings ?? [] + // https://github.com/swiftlang/swift-evolution/blob/main/proposals/0444-member-import-visibility.md + settings.append(.enableUpcomingFeature("MemberImportVisibility")) + target.swiftSettings = settings + case .macro, .plugin, .system, .binary: () // not applicable + @unknown default: () // we don't know what to do here, do nothing + } +}// --- END: STANDARD CROSS-REPO SETTINGS DO NOT EDIT --- // diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 4a7cacef5..9868b2613 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -12,6 +12,24 @@ // //===----------------------------------------------------------------------===// +/// A top-level import statement, that may be conditional based on a compile-time condition +enum ImportStatement: Equatable, Codable { + /// Imports based on compile time condition + case conditional( + condition: Condition, + thenImportDescription: ImportDescription, + elseImportDescription: ImportDescription + ) + + /// Always imported + case always(ImportDescription) + + enum Condition: Equatable, Codable { + /// A condition on whether we can import a specific module + case canImport(String) + } +} + /// A description of an import declaration. /// /// For example: `import Foo`. @@ -678,6 +696,9 @@ indirect enum Declaration: Equatable, Codable { /// An enum case declaration. case enumCase(EnumCaseDescription) + + /// A #if canImport declaration + case canImportConditional(String, then: [Declaration], else: [Declaration]) } /// A description of a deprecation notice. @@ -1052,7 +1073,7 @@ struct FileDescription: Equatable, Codable { /// Import statements placed below the top comment, but before the code /// blocks. - var imports: [ImportDescription]? + var imports: [ImportStatement]? /// The code blocks that represent the main contents of the file. var codeBlocks: [CodeBlock] @@ -1675,6 +1696,7 @@ extension Declaration { case .protocol(let protocolDescription): return protocolDescription.accessModifier case .function(let functionDescription): return functionDescription.signature.accessModifier case .enumCase: return nil + case .canImportConditional: return nil } } set { @@ -1707,6 +1729,7 @@ extension Declaration { functionDescription.signature.accessModifier = newValue self = .function(functionDescription) case .enumCase: break + case .canImportConditional: break } } } diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift index 6d9f54685..e6d57f4db 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift @@ -145,10 +145,34 @@ struct TextBasedRenderer: RendererProtocol { } /// Renders the specified import statements. - func renderImports(_ imports: [ImportDescription]?) { (imports ?? []).forEach(renderImport) } + func renderImports(_ imports: [ImportStatement]?) { (imports ?? []).forEach(renderImport) } /// Renders a single import statement. - func renderImport(_ description: ImportDescription) { + func renderImport(_ importStatement: ImportStatement) { + switch importStatement { + case .conditional(let condition, let thenImportDescription, let elseImportDescription): + writer.writeLine("#if \(renderImportCondition(condition))") + renderImport(thenImportDescription) + writer.writeLine("#else") + renderImport(elseImportDescription) + writer.writeLine("#endif") + + case .always(let importDescription): self.renderImport(importDescription) + } + } + + /// Renders a import condition + func renderImportCondition(_ condition: ImportStatement.Condition) -> String { + switch condition { + case .canImport(let argument): "canImport(\(argument))" + } + } + + /// Renders the specified import statements. + private func renderImports(_ imports: [ImportDescription]?) { (imports ?? []).forEach(renderImport) } + + /// Renders a single import statement. + private func renderImport(_ description: ImportDescription) { func render(preconcurrency: Bool) { let spiPrefix = description.spi.map { "@_spi(\($0)) " } ?? "" let preconcurrencyPrefix = preconcurrency ? "@preconcurrency " : "" @@ -713,6 +737,8 @@ struct TextBasedRenderer: RendererProtocol { case .typealias(let typealiasDescription): renderTypealias(typealiasDescription) case .function(let functionDescription): renderFunction(functionDescription) case .enumCase(let enumCase): renderEnumCase(enumCase) + case .canImportConditional(let condition, let thenDecls, let elseDecls): + renderCanImportConditional(condition, thenDecls, elseDecls) } } @@ -836,6 +862,15 @@ struct TextBasedRenderer: RendererProtocol { writer.writeLine(line) } + /// Renders a canImport with both then and else branches + func renderCanImportConditional(_ module: String, _ thenDecls: [Declaration], _ elseDecls: [Declaration]) { + writer.writeLine("#if canImport(\(module))") + for thenDecl in thenDecls { renderDeclaration(thenDecl) } + writer.writeLine("#else") + for elseDecl in elseDecls { renderDeclaration(elseDecl) } + writer.writeLine("#endif") + } + /// Renders the specified code block item. func renderCodeBlockItem(_ description: CodeBlockItem) { switch description { diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift index 7e976a61b..fba293779 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift @@ -35,7 +35,8 @@ struct ClientFileTranslator: FileTranslator { let topComment = self.topComment let imports = - Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } + Constants.File.clientServerImports + + config.additionalImports.map { .always(ImportDescription(moduleName: $0)) } let clientMethodDecls = try OperationDescription.all(from: doc.paths, in: components, context: context) .map(translateClientMethod(_:)) @@ -67,7 +68,7 @@ struct ClientFileTranslator: FileTranslator { accessModifier: config.access, kind: .initializer, parameters: [ - .init(label: "serverURL", type: .init(TypeName.url)), + .init(label: "serverURL", type: .init(TypeName.foundationURLTypeAlias)), .init( label: "configuration", type: .member(Constants.Configuration.typeName), diff --git a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift index 6f3d1782e..713ee93ee 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift @@ -34,18 +34,29 @@ enum Constants { static let topComment: String = "Generated by swift-openapi-generator, do not modify." /// The descriptions of modules imported by every generated file. - static let imports: [ImportDescription] = [ - ImportDescription(moduleName: Constants.Import.runtime, spi: "Generated"), - ImportDescription( - moduleName: "Foundation", - moduleTypes: ["struct Foundation.URL", "struct Foundation.Data", "struct Foundation.Date"], - preconcurrency: .onOS(["Linux"]) + static let imports: [ImportStatement] = [ + .always(ImportDescription(moduleName: Constants.Import.runtime, spi: "Generated")), + .conditional( + condition: .canImport("FoundationEssentials"), + thenImportDescription: ImportDescription( + moduleName: "FoundationEssentials", + moduleTypes: [ + "struct FoundationEssentials.URL", "struct FoundationEssentials.Data", + "struct FoundationEssentials.Date", + ], + preconcurrency: .onOS(["Linux"]) + ), + elseImportDescription: ImportDescription( + moduleName: "Foundation", + moduleTypes: ["struct Foundation.URL", "struct Foundation.Data", "struct Foundation.Date"], + preconcurrency: .onOS(["Linux"]) + ) ), ] /// The descriptions of modules imported by client and server files. - static let clientServerImports: [ImportDescription] = - imports + [ImportDescription(moduleName: Constants.Import.httpTypes)] + static let clientServerImports: [ImportStatement] = + imports + [.always(ImportDescription(moduleName: Constants.Import.httpTypes))] } /// Constants related to the OpenAPI server object. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift index 93c14bac7..890c2bed1 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift @@ -90,7 +90,7 @@ extension Declaration { case .enum(let desc): return desc.name case .typealias(let desc): return desc.name case .commentable(_, let decl), .deprecated(_, let decl): return decl.name - case .variable, .extension, .protocol, .function, .enumCase: return nil + case .variable, .extension, .protocol, .function, .enumCase, .canImportConditional: return nil } } @@ -99,7 +99,7 @@ extension Declaration { switch self { case .struct, .enum: return true case .commentable(_, let decl), .deprecated(_, let decl): return decl.isBoxable - case .typealias, .variable, .extension, .protocol, .function, .enumCase: return false + case .typealias, .variable, .extension, .protocol, .function, .enumCase, .canImportConditional: return false } } @@ -136,7 +136,7 @@ extension Declaration { return values.compactMap { $0.type.referencedSchemaComponentName } default: return [] } - case .extension, .protocol, .function: return [] + case .extension, .protocol, .function, .canImportConditional: return [] } } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift index 2091a0f8c..cd7ac360c 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift @@ -33,7 +33,8 @@ struct ServerFileTranslator: FileTranslator { let topComment = self.topComment let imports = - Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } + Constants.File.clientServerImports + + config.additionalImports.map { .always(ImportDescription(moduleName: $0)) } let allOperations = try OperationDescription.all(from: doc.paths, in: components, context: context) @@ -117,7 +118,11 @@ struct ServerFileTranslator: FileTranslator { kind: .function(name: "registerHandlers"), parameters: [ .init(label: "on", name: "transport", type: .member(Constants.Server.Transport.typeName)), - .init(label: "serverURL", type: .init(TypeName.url), defaultValue: .dot("defaultOpenAPIServerURL")), + .init( + label: "serverURL", + type: .init(TypeName.foundationURLTypeAlias), + defaultValue: .dot("defaultOpenAPIServerURL") + ), .init( label: "configuration", type: .member(Constants.Configuration.typeName), diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift index ee51fbbd0..b25c8145b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/Builtins.swift @@ -32,6 +32,14 @@ extension TypeName { /// - Returns: A TypeName representing the specified type within the Foundation module. static func foundation(_ name: String) -> TypeName { TypeName(swiftKeyPath: ["Foundation", name]) } + /// Returns a type name for a type with the specified name in the + /// FoundationEssentials module. + /// - Parameter name: The name of the type. + /// - Returns: A TypeName representing the specified type within the FoundationEssentials module. + static func foundationEssentials(_ name: String) -> TypeName { + TypeName(swiftKeyPath: ["FoundationEssentials", name]) + } + /// Returns a type name for a type with the specified name in the /// OpenAPIRuntime module. /// - Parameter name: The name of the type. @@ -44,11 +52,23 @@ extension TypeName { /// - Returns: A TypeName representing the type with the given name in the HTTPTypes module. static func httpTypes(_ name: String) -> TypeName { TypeName(swiftKeyPath: [Constants.Import.httpTypes, name]) } - /// Returns the type name for the Date type. - static var date: Self { .foundation("Date") } + /// Returns the type name for the Foundation Date type. + static var foundationDate: Self { .foundation("Date") } + + /// Returns the type name for the FoundationEssentials Date type. + static var foundationEssentialsDate: Self { .foundationEssentials("Date") } + + /// Returns the type name for the alias of the current Foundation URL type. + static var foundationURLTypeAlias: Self { TypeName(swiftKeyPath: ["FoundationURL"]) } + + /// Returns the type name for the alias of the current Foundation Date type. + static var foundationDateTypeAlias: Self { TypeName(swiftKeyPath: ["FoundationDate"]) } + + /// Returns the type name for the Foundation URL type. + static var foundationURL: Self { .foundation("URL") } - /// Returns the type name for the URL type. - static var url: Self { .foundation("URL") } + /// Returns the type name for the FoundationEssentials URL type. + static var foundationEssentialsURL: Self { .foundationEssentials("URL") } /// 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 1c503ae74..36bd2b9e6 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypeAssignment/TypeMatcher.swift @@ -312,7 +312,7 @@ struct TypeMatcher { case .base64: typeName = .base64 default: switch core.format { - case .dateTime: typeName = .date + case .dateTime: typeName = .foundationDateTypeAlias default: typeName = .string } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift index ea8e6aa9a..fa2667094 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift @@ -34,7 +34,8 @@ struct TypesFileTranslator: FileTranslator { let topComment = self.topComment - let imports = Constants.File.imports + config.additionalImports.map { ImportDescription(moduleName: $0) } + let imports = + Constants.File.imports + config.additionalImports.map { .always(ImportDescription(moduleName: $0)) } let apiProtocol = try translateAPIProtocol(doc.paths) @@ -52,11 +53,43 @@ struct TypesFileTranslator: FileTranslator { topComment: topComment, imports: imports, codeBlocks: [ - .declaration(apiProtocol), .declaration(apiProtocolExtension), .declaration(serversDecl), components, - operations, + self.foundationTypealiasesCodeBlock, .declaration(apiProtocol), .declaration(apiProtocolExtension), + .declaration(serversDecl), components, operations, ] ) return StructuredSwiftRepresentation(file: .init(name: GeneratorMode.types.outputFileName, contents: typesFile)) } + + private var foundationTypealiasesCodeBlock: CodeBlock { + .declaration( + .canImportConditional( + "FoundationEssentials", + then: [ + .typealias( + accessModifier: self.config.access, + name: TypeName.foundationURLTypeAlias.fullyQualifiedSwiftName, + existingType: .init(.foundationEssentialsURL) + ), + .typealias( + accessModifier: self.config.access, + name: TypeName.foundationDateTypeAlias.fullyQualifiedSwiftName, + existingType: .init(.foundationEssentialsDate) + ), + ], + else: [ + .typealias( + accessModifier: self.config.access, + name: TypeName.foundationURLTypeAlias.fullyQualifiedSwiftName, + existingType: .init(.foundationURL) + ), + .typealias( + accessModifier: self.config.access, + name: TypeName.foundationDateTypeAlias.fullyQualifiedSwiftName, + existingType: .init(.foundationDate) + ), + ] + ) + ) + } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift index ed9808520..010fae452 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateBoxedTypes.swift @@ -55,7 +55,7 @@ extension TypesFileTranslator { return .deprecated(deprecationDescription, boxedType(declaration)) case .struct(let structDescription): return .struct(boxedStruct(structDescription)) case .enum(let enumDescription): return .enum(boxedEnum(enumDescription)) - case .variable, .extension, .typealias, .protocol, .function, .enumCase: + case .variable, .extension, .typealias, .protocol, .function, .enumCase, .canImportConditional: preconditionFailure("Unexpected boxed type: \(decl.name ?? "")") } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift index cf9927a77..635d9df29 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateServers.swift @@ -56,11 +56,11 @@ extension TypesFileTranslator { kind: .function(name: name, isStatic: true), parameters: variables.map(\.parameter), keywords: [.throws], - returnType: .identifierType(TypeName.url), + returnType: .identifierType(TypeName.foundationURLTypeAlias), body: [ .expression( .try( - .identifierType(TypeName.url) + .identifierType(TypeName.foundationURLTypeAlias) .call([ .init( label: "validatingOpenAPIServerURL", diff --git a/Tests/LinkageTest/.gitignore b/Tests/LinkageTest/.gitignore new file mode 100644 index 000000000..93969ff69 --- /dev/null +++ b/Tests/LinkageTest/.gitignore @@ -0,0 +1,2 @@ +Package.resolved +.build \ No newline at end of file diff --git a/Tests/LinkageTest/Package.swift b/Tests/LinkageTest/Package.swift new file mode 100644 index 000000000..36b25d3cc --- /dev/null +++ b/Tests/LinkageTest/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 6.2 + +import PackageDescription + +let package = Package( + name: "linkage-test", + dependencies: [ + .package(name: "swift-openapi-generator", path: "../.."), + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.11.0", traits: []), + ], + targets: [ + .executableTarget( + name: "linkageTest", + dependencies: [.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime")], + plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] + ) + ] +) diff --git a/Tests/LinkageTest/Sources/main.swift b/Tests/LinkageTest/Sources/main.swift new file mode 100644 index 000000000..37a55e582 --- /dev/null +++ b/Tests/LinkageTest/Sources/main.swift @@ -0,0 +1,17 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import OpenAPIRuntime + +try? print(Servers.Server1.url()) diff --git a/Tests/LinkageTest/Sources/openapi-generator-config.yaml b/Tests/LinkageTest/Sources/openapi-generator-config.yaml new file mode 100644 index 000000000..61a070928 --- /dev/null +++ b/Tests/LinkageTest/Sources/openapi-generator-config.yaml @@ -0,0 +1,5 @@ +generate: + - client + - types +accessModifier: package +namingStrategy: idiomatic diff --git a/Tests/LinkageTest/Sources/openapi.yaml b/Tests/LinkageTest/Sources/openapi.yaml new file mode 100644 index 000000000..b42b0335a --- /dev/null +++ b/Tests/LinkageTest/Sources/openapi.yaml @@ -0,0 +1,35 @@ +openapi: "3.1.0" +info: + title: "GreetingService" + version: "1.0.0" +servers: + - url: "https://example.com/api" + description: "Example" +paths: + /greet: + get: + tags: ["Greetings"] + operationId: "getGreeting" + parameters: + - name: "name" + required: false + in: "query" + description: "name" + schema: + type: "string" + responses: + "200": + description: "Returns a greeting" + content: + application/json: + schema: + $ref: "#/components/schemas/Greeting" +components: + schemas: + Greeting: + type: "object" + properties: + message: + type: string + required: + - message diff --git a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift index 4919a7867..c1daa02b3 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Renderer/Test_TextBasedRenderer.swift @@ -78,7 +78,7 @@ final class Test_TextBasedRenderer: XCTestCase { func testImports() throws { try _test(nil, renderedBy: TextBasedRenderer.renderImports, rendersAs: "") try _test( - [ImportDescription(moduleName: "Foo"), ImportDescription(moduleName: "Bar")], + [.always(ImportDescription(moduleName: "Foo")), .always(ImportDescription(moduleName: "Bar"))], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" import Foo @@ -86,14 +86,14 @@ final class Test_TextBasedRenderer: XCTestCase { """# ) try _test( - [ImportDescription(moduleName: "Foo", spi: "Secret")], + [.always(ImportDescription(moduleName: "Foo", spi: "Secret"))], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" @_spi(Secret) import Foo """# ) try _test( - [ImportDescription(moduleName: "Foo", preconcurrency: .onOS(["Bar", "Baz"]))], + [.always(ImportDescription(moduleName: "Foo", preconcurrency: .onOS(["Bar", "Baz"])))], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" #if os(Bar) || os(Baz) @@ -105,8 +105,8 @@ final class Test_TextBasedRenderer: XCTestCase { ) try _test( [ - ImportDescription(moduleName: "Foo", preconcurrency: .always), - ImportDescription(moduleName: "Bar", spi: "Secret", preconcurrency: .always), + .always(ImportDescription(moduleName: "Foo", preconcurrency: .always)), + .always(ImportDescription(moduleName: "Bar", spi: "Secret", preconcurrency: .always)), ], renderedBy: TextBasedRenderer.renderImports, rendersAs: #""" @@ -114,6 +114,28 @@ final class Test_TextBasedRenderer: XCTestCase { @preconcurrency @_spi(Secret) import Bar """# ) + try _test( + [ + .conditional( + condition: .canImport("MyModule"), + thenImportDescription: ImportDescription( + moduleName: "MyModule2", + spi: "Secret", + preconcurrency: .always + ), + elseImportDescription: ImportDescription(moduleName: "MyModule3") + ), .always(ImportDescription(moduleName: "Bar", spi: "Secret", preconcurrency: .always)), + ], + renderedBy: TextBasedRenderer.renderImports, + rendersAs: #""" + #if canImport(MyModule) + @preconcurrency @_spi(Secret) import MyModule2 + #else + import MyModule3 + #endif + @preconcurrency @_spi(Secret) import Bar + """# + ) } func testAccessModifiers() throws { @@ -713,7 +735,7 @@ final class Test_TextBasedRenderer: XCTestCase { try _test( .init( topComment: .inline("hi"), - imports: [.init(moduleName: "Foo")], + imports: [.always(.init(moduleName: "Foo"))], codeBlocks: [.init(comment: nil, item: .declaration(.struct(.init(name: "Bar"))))] ), renderedBy: TextBasedRenderer.renderFile, diff --git a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift index fc40833b7..341e17f19 100644 --- a/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift +++ b/Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift @@ -24,6 +24,7 @@ enum DeclKind: String, Equatable, CustomStringConvertible { case `protocol` case function case enumCase + case canImportConditional var description: String { rawValue } } @@ -116,6 +117,8 @@ extension Declaration { } return .init(name: name, kind: .function) case let .enumCase(description): return .init(name: description.name, kind: .enumCase) + case let .canImportConditional(condition, then: _, else: _): + return .init(name: condition, kind: .canImportConditional) case .commentable: fatalError("Unreachable") } } diff --git a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift index ec8aafc77..3cbf30427 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Translator/TypeAssignment/Test_TypeMatcher.swift @@ -27,7 +27,7 @@ final class Test_TypeMatcher: Test_Core { (.string, "Swift.String"), (.string(contentEncoding: .binary), "OpenAPIRuntime.HTTPBody"), (.string(contentEncoding: .base64), "OpenAPIRuntime.Base64EncodedData"), (.string(.init(format: .date), .init()), "Swift.String"), - (.string(.init(format: .dateTime), .init()), "Foundation.Date"), + (.string(.init(format: .dateTime), .init()), "FoundationDate"), (.integer, "Swift.Int"), (.integer(.init(format: .int32), .init()), "Swift.Int32"), (.integer(.init(format: .int64), .init()), "Swift.Int64"), diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index c014e0215..cd5fe0018 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1521,7 +1521,7 @@ final class SnippetBasedReferenceTests: XCTestCase { """, """ public enum Schemas { - public typealias MyDate = Foundation.Date + public typealias MyDate = FoundationDate } """ ) @@ -2288,8 +2288,8 @@ final class SnippetBasedReferenceTests: XCTestCase { case log(OpenAPIRuntime.MultipartPart) public struct metadataPayload: Sendable, Hashable { public struct bodyPayload: Codable, Hashable, Sendable { - public var createdAt: Foundation.Date - public init(createdAt: Foundation.Date) { + public var createdAt: FoundationDate + public init(createdAt: FoundationDate) { self.createdAt = createdAt } public enum CodingKeys: String, CodingKey { @@ -2433,7 +2433,7 @@ final class SnippetBasedReferenceTests: XCTestCase { """ public func registerHandlers( on transport: any ServerTransport, - serverURL: Foundation.URL = .defaultOpenAPIServerURL, + serverURL: FoundationURL = .defaultOpenAPIServerURL, configuration: Configuration = .init(), middlewares: [any ServerMiddleware] = [] ) throws { @@ -2467,7 +2467,7 @@ final class SnippetBasedReferenceTests: XCTestCase { """ public func registerHandlers( on transport: any ServerTransport, - serverURL: Foundation.URL = .defaultOpenAPIServerURL, + serverURL: FoundationURL = .defaultOpenAPIServerURL, configuration: Configuration = .init(), middlewares: [any ServerMiddleware] = [] ) throws {} @@ -5918,16 +5918,16 @@ final class SnippetBasedReferenceTests: XCTestCase { """ public enum Servers { public enum Server1 { - public static func url() throws -> Foundation.URL { - try Foundation.URL( + public static func url() throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://example.com/api", variables: [] ) } } @available(*, deprecated, renamed: "Servers.Server1.url") - public static func server1() throws -> Foundation.URL { - try Foundation.URL( + public static func server1() throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://example.com/api", variables: [] ) @@ -5950,8 +5950,8 @@ final class SnippetBasedReferenceTests: XCTestCase { """ public enum Servers { public enum Server1 { - public static func url(_protocol: Swift.String = "https") throws -> Foundation.URL { - try Foundation.URL( + public static func url(_protocol: Swift.String = "https") throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "{protocol}://example.com/api", variables: [ .init( @@ -5963,8 +5963,8 @@ final class SnippetBasedReferenceTests: XCTestCase { } } @available(*, deprecated, renamed: "Servers.Server1.url") - public static func server1(_protocol: Swift.String = "https") throws -> Foundation.URL { - try Foundation.URL( + public static func server1(_protocol: Swift.String = "https") throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "{protocol}://example.com/api", variables: [ .init( @@ -6003,8 +6003,8 @@ final class SnippetBasedReferenceTests: XCTestCase { public static func url( environment: Environment = .production, version: Swift.String = "v1" - ) throws -> Foundation.URL { - try Foundation.URL( + ) throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", variables: [ .init( @@ -6023,8 +6023,8 @@ final class SnippetBasedReferenceTests: XCTestCase { public static func server1( environment: Swift.String = "production", version: Swift.String = "v1" - ) throws -> Foundation.URL { - try Foundation.URL( + ) throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", variables: [ .init( @@ -6085,8 +6085,8 @@ final class SnippetBasedReferenceTests: XCTestCase { public static func url( environment: Environment = .production, version: Swift.String = "v1" - ) throws -> Foundation.URL { - try Foundation.URL( + ) throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", variables: [ .init( @@ -6105,8 +6105,8 @@ final class SnippetBasedReferenceTests: XCTestCase { public static func server1( environment: Swift.String = "production", version: Swift.String = "v1" - ) throws -> Foundation.URL { - try Foundation.URL( + ) throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://{environment}.example.com/api/{version}", variables: [ .init( @@ -6129,8 +6129,8 @@ final class SnippetBasedReferenceTests: XCTestCase { case sandbox case develop } - public static func url(environment: Environment = .develop) throws -> Foundation.URL { - try Foundation.URL( + public static func url(environment: Environment = .develop) throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://{environment}.api.example.com/", variables: [ .init( @@ -6142,8 +6142,8 @@ final class SnippetBasedReferenceTests: XCTestCase { } } @available(*, deprecated, renamed: "Servers.Server2.url") - public static func server2(environment: Swift.String = "develop") throws -> Foundation.URL { - try Foundation.URL( + public static func server2(environment: Swift.String = "develop") throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://{environment}.api.example.com/", variables: [ .init( @@ -6158,8 +6158,8 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } public enum Server3 { - public static func url(version: Swift.String = "v1") throws -> Foundation.URL { - try Foundation.URL( + public static func url(version: Swift.String = "v1") throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://example.com/api/{version}", variables: [ .init( @@ -6171,8 +6171,8 @@ final class SnippetBasedReferenceTests: XCTestCase { } } @available(*, deprecated, renamed: "Servers.Server3.url") - public static func server3(version: Swift.String = "v1") throws -> Foundation.URL { - try Foundation.URL( + public static func server3(version: Swift.String = "v1") throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://example.com/api/{version}", variables: [ .init( @@ -6183,16 +6183,16 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } public enum Server4 { - public static func url() throws -> Foundation.URL { - try Foundation.URL( + public static func url() throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://api.example.com/", variables: [] ) } } @available(*, deprecated, renamed: "Servers.Server4.url") - public static func server4() throws -> Foundation.URL { - try Foundation.URL( + public static func server4() throws -> FoundationURL { + try FoundationURL( validatingOpenAPIServerURL: "https://api.example.com/", variables: [] ) @@ -6683,6 +6683,12 @@ fileprivate extension Declaration { return .variable(v) case let .typealias(t): return .typealias(t) case let .enumCase(e): return .enumCase(e) + case .canImportConditional(let condition, let thenDecls, let elseDecls): + return .canImportConditional( + condition, + then: thenDecls.map(stripComments(_:)), + else: elseDecls.map(stripComments(_:)) + ) } } diff --git a/scripts/run-linkage-test.sh b/scripts/run-linkage-test.sh new file mode 100755 index 000000000..258a8ce60 --- /dev/null +++ b/scripts/run-linkage-test.sh @@ -0,0 +1,51 @@ +#!/bin/bash +##===----------------------------------------------------------------------===## +## +## This source file is part of the SwiftOpenAPIGenerator open source project +## +## Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## +set -eu + +# Validate that we're running on Linux +if [[ "$(uname -s)" != "Linux" ]]; then + echo "Error: This script must be run on Linux. Current OS: $(uname -s)" >&2 + exit 1 +fi + +echo "Running on Linux - proceeding with linkage test..." + +# Build the linkage test package +echo "Building linkage test package..." +swift build --package-path Tests/LinkageTest + +# Construct build path +build_path=$(swift build --package-path Tests/LinkageTest --show-bin-path) +binary_path=$build_path/linkageTest + +# Verify the binary exists +if [[ ! -f "$binary_path" ]]; then + echo "Error: Built binary not found at $binary_path" >&2 + exit 1 +fi + +echo "Checking linkage for binary: $binary_path" + +# Run ldd and check if libFoundation.so is linked +ldd_output=$(ldd "$binary_path") +echo "LDD output:" +echo "$ldd_output" + +if echo "$ldd_output" | grep -q "libFoundation.so"; then + echo "Error: Binary is linked against libFoundation.so - this indicates incorrect linkage. Ensure the full Foundation is not linked on Linux when default traits are disabled." >&2 + exit 1 +else + echo "Success: Binary is not linked against libFoundation.so - linkage test passed." +fi