diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index 6172b709..067447bf 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -32,6 +32,9 @@ public struct Config: Sendable { /// Additional imports to add to each generated file. public var additionalImports: [String] + /// Additional protocol conformances to add to the APIProtocol type + public var additionalAPIProtocols: [String] + /// Filter to apply to the OpenAPI document before generation. public var filter: DocumentFilter? @@ -49,12 +52,14 @@ public struct Config: Sendable { mode: GeneratorMode, access: AccessModifier, additionalImports: [String] = [], + additionalAPIProtocols: [String] = [], filter: DocumentFilter? = nil, featureFlags: FeatureFlags = [] ) { self.mode = mode self.access = access self.additionalImports = additionalImports + self.additionalAPIProtocols = additionalAPIProtocols self.filter = filter self.featureFlags = featureFlags } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift index 3b061854..685e7aca 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/translateAPIProtocol.swift @@ -28,7 +28,7 @@ extension TypesFileTranslator { let protocolDescription = ProtocolDescription( accessModifier: config.access, name: Constants.APIProtocol.typeName, - conformances: Constants.APIProtocol.conformances, + conformances: Constants.APIProtocol.conformances + config.additionalAPIProtocols, members: functionDecls ) let protocolComment: Comment = .doc("A type that performs HTTP operations defined by the OpenAPI document.") diff --git a/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md b/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md index cf889a85..19da7763 100644 --- a/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md +++ b/Sources/swift-openapi-generator/Documentation.docc/Articles/Configuring-the-generator.md @@ -35,6 +35,7 @@ The configuration file has the following keys: - `package`: Generated API is accessible from other modules within the same package or project. - `internal` (default): Generated API is accessible from the containing module only. - `additionalImports` (optional): array of strings. Each string value is a Swift module name. An import statement will be added to the generated source files for each module. +- `additionalAPIProtocols` (optional): array of strings. Each string value is the name of a protocol the resulting API should conform to. These protocols must be available in the scope that the API is generated. - `filter`: (optional): Filters to apply to the OpenAPI document before generation. - `operations`: Operations with these operation IDs will be included in the filter. - `tags`: Operations tagged with these tags will be included in the filter. @@ -86,6 +87,17 @@ additionalImports: accessModifier: public ``` +To use together with a mocking library, it is possible to add conformance to a custom protocol: + +```yaml +generate: + - client +additionalImports: + - APITypes +additionalAPIProtocols: + - AutoMockable +``` + ### Document filtering The generator supports filtering the OpenAPI document prior to generation, which can be useful when diff --git a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift index 9f3fe83b..4d56aa64 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -32,12 +32,14 @@ extension _GenerateOptions { let sortedModes = try resolvedModes(config) let resolvedAccessModifier = resolvedAccessModifier(config) ?? Config.defaultAccessModifier let resolvedAdditionalImports = resolvedAdditionalImports(config) + let resolvedAdditionalAPIProtocols = resolvedAdditionalAPIProtocols(config) let resolvedFeatureFlags = resolvedFeatureFlags(config) let configs: [Config] = sortedModes.map { .init( mode: $0, access: resolvedAccessModifier, additionalImports: resolvedAdditionalImports, + additionalAPIProtocols: resolvedAdditionalAPIProtocols, filter: config?.filter, featureFlags: resolvedFeatureFlags ) diff --git a/Sources/swift-openapi-generator/GenerateOptions.swift b/Sources/swift-openapi-generator/GenerateOptions.swift index 8b65db9c..52b97740 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -35,6 +35,8 @@ struct _GenerateOptions: ParsableArguments { @Option(help: "Additional import to add to all generated files.") var additionalImport: [String] = [] + @Option(help: "Additional protocol conformances to add to the APIProtocol type") var additionalAPIProtocols: [String] = [] + @Option(help: "Pre-release feature to enable. Options: \(FeatureFlag.prettyListing).") var featureFlag: [FeatureFlag] = [] @@ -83,6 +85,14 @@ extension _GenerateOptions { return [] } + func resolvedAdditionalAPIProtocols(_ config: _UserConfig?) -> [String] { + if !additionalAPIProtocols.isEmpty { return additionalAPIProtocols } + if let additionalAPIProcotols = config?.additionalAPIProtocols, !additionalAPIProcotols.isEmpty { + return additionalAPIProcotols + } + return [] + } + /// Returns a list of the feature flags requested by the user. /// - Parameter config: The configuration specified by the user. /// - Returns: A set of feature flags requested by the user. diff --git a/Sources/swift-openapi-generator/UserConfig.swift b/Sources/swift-openapi-generator/UserConfig.swift index 0ee210b7..65055418 100644 --- a/Sources/swift-openapi-generator/UserConfig.swift +++ b/Sources/swift-openapi-generator/UserConfig.swift @@ -30,6 +30,10 @@ struct _UserConfig: Codable { /// generated Swift file. var additionalImports: [String]? + /// A list of additional protocol conformances to add to the APIProtocol + /// that is generated. + var additionalAPIProtocols: [String]? + /// Filter to apply to the OpenAPI document before generation. var filter: DocumentFilter? diff --git a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift index 3f9373b9..62491c94 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Parser/Test_YamsParser.swift @@ -138,9 +138,12 @@ final class Test_YamsParser: Test_Core { additionalImports: - Foundation + + additionalAPIProtocols: + - ExampleProtocol """ let keys = try? YamsParser.extractTopLevelKeys(fromYAMLString: yaml) - XCTAssertEqual(keys, ["generate", "featureFlags", "additionalImports"]) + XCTAssertEqual(keys, ["generate", "featureFlags", "additionalImports", "additionalAPIProtocols"]) } func testExtractTopLevelKeysWithInvalidYAML() { diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index 75b8be78..841779e3 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -20,13 +20,20 @@ struct TestConfig: Encodable { var docFilePath: String var mode: GeneratorMode var additionalImports: [String]? + var additionalAPIProtocols: [String]? var featureFlags: FeatureFlags? var referenceOutputDirectory: String } extension TestConfig { var asConfig: Config { - .init(mode: mode, access: .public, additionalImports: additionalImports ?? [], featureFlags: featureFlags ?? []) + .init( + mode: mode, + access: .public, + additionalImports: additionalImports ?? [], + additionalAPIProtocols: additionalAPIProtocols ?? [], + featureFlags: featureFlags ?? [] + ) } } @@ -126,6 +133,7 @@ final class FileBasedReferenceTests: XCTestCase { docFilePath: "Docs/\(project.openAPIDocFileName)", mode: mode, additionalImports: [], + additionalAPIProtocols: [], featureFlags: featureFlags, referenceOutputDirectory: "ReferenceSources/\(project.fixtureCodeDirectoryName)" ), diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 0196c5ee..180398e0 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -2330,6 +2330,13 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testAPIProtocolConformance() throws { + try self.assertProtocolConformanceTranslation( + additionalProtocolConformances: ["ExampleProtocol"], + "public protocol APIProtocol: Sendable, ExampleProtocol {}" + ) + } + func testServerRegisterHandlers_oneOperation() throws { try self.assertServerRegisterHandlers( """ @@ -5195,13 +5202,19 @@ extension SnippetBasedReferenceTests { func makeTypesTranslator( accessModifier: AccessModifier = .public, + additionalProtocolConformances: [String] = [], featureFlags: FeatureFlags = [], ignoredDiagnosticMessages: Set = [], componentsYAML: String ) throws -> TypesFileTranslator { let components = try YAMLDecoder().decode(OpenAPI.Components.self, from: componentsYAML) return TypesFileTranslator( - config: Config(mode: .types, access: accessModifier, featureFlags: featureFlags), + config: Config( + mode: .types, + access: accessModifier, + additionalAPIProtocols: additionalProtocolConformances, + featureFlags: featureFlags + ), diagnostics: XCTestDiagnosticCollector(test: self, ignoredDiagnosticMessages: ignoredDiagnosticMessages), components: components ) @@ -5444,6 +5457,21 @@ extension SnippetBasedReferenceTests { try XCTAssertSwiftEquivalent(translation, expectedSwift, file: file, line: line) } + func assertProtocolConformanceTranslation( + additionalProtocolConformances: [String], + _ expectedSwift: String, + file: StaticString = #filePath, + line: UInt = #line + ) throws { + let translator = try makeTypesTranslator( + additionalProtocolConformances: additionalProtocolConformances, + componentsYAML: "{}" + ) + let paths: OpenAPI.PathItem.Map = .init() + let translation = try translator.translateAPIProtocol(paths) + try XCTAssertSwiftEquivalent(translation, expectedSwift, file: file, line: line) + } + func assertPathsTranslationExtension( _ pathsYAML: String, componentsYAML: String = "{}",