From 0964f1caf361552904dca9178c9e8324c3a53a35 Mon Sep 17 00:00:00 2001 From: Si Beaumont Date: Fri, 23 May 2025 14:44:38 +0100 Subject: [PATCH] Add support for configurable comments in generated files Users want the ability to add custom comments to generated files. One concrete use case is adding directives like `swift-format-ignore-file` and `swiftlint:disable all` to prevent these tools from processing generated code. - Added `additionalFileComments` property to `Config` and `UserConfig` structs - Modified `FileTranslator` to include additional comments along with the do-not-edit comment - Added CLI support with `--additional-file-comment` option - Added config tests to confirm the option propagates and the default is empty - Added snippet tests to validate the actual rendering - Updated documentation with examples and usage instructions Users can now configure additional comments to be added to generated files using either the config file or a command line option. Fixes #738. - Added unit tests for `Config` - Added snippet tests for rendering - Manual tests of generator using config and CLI on real OpenAPI doc: ```console % swift run swift-openapi-generator generate openapi-documents/petstore.yaml \ --mode types \ --output-directory test-output-cli \ --additional-file-comment "hello world" \ --additional-file-comment "testing, testing, 1, 2, 3" Build of product 'swift-openapi-generator' complete! (3.27s) Swift OpenAPI Generator is running with the following configuration: - OpenAPI document path: /Users/Si/work/code/swift-openapi-workspace/packages/swift-openapi-generator/openapi-documents/petstore.yaml - Configuration path: - Generator modes: types - Access modifier: internal - Naming strategy: defensive - Name overrides: - Feature flags: - Output file names: Types.swift - Output directory: /Users/Si/work/code/swift-openapi-workspace/packages/swift-openapi-generator/test-output-cli - Diagnostics output path: - Current directory: /Users/Si/work/code/swift-openapi-workspace/packages/swift-openapi-generator - Plugin source: - Is dry run: false - Additional imports: - Additional file comments: hello world, testing, testing, 1, 2, 3 Writing data to file Types.swift... % head -5 test-output-cli/Types.swift // Generated by swift-openapi-generator, do not modify. // hello world // testing, testing, 1, 2, 3 @_spi(Generated) import OpenAPIRuntime ``` --- Sources/_OpenAPIGeneratorCore/Config.swift | 6 +++ .../ClientTranslator/ClientTranslator.swift | 2 +- .../Translator/FileTranslator.swift | 6 +++ .../ServerTranslator/ServerTranslator.swift | 2 +- .../TypesTranslator/TypesFileTranslator.swift | 2 +- .../Articles/Configuring-the-generator.md | 13 +++++ .../GenerateOptions+runGenerator.swift | 3 ++ .../GenerateOptions.swift | 13 +++++ .../swift-openapi-generator/UserConfig.swift | 5 ++ .../Test_Config.swift | 13 +++++ .../SnippetBasedReferenceTests.swift | 48 ++++++++++++++++++- 11 files changed, 109 insertions(+), 4 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index b49537422..4c9251083 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -46,6 +46,9 @@ public struct Config: Sendable { /// Additional imports to add to each generated file. public var additionalImports: [String] + /// Additional comments to add to the top of each generated file. + public var additionalFileComments: [String] + /// Filter to apply to the OpenAPI document before generation. public var filter: DocumentFilter? @@ -68,6 +71,7 @@ public struct Config: Sendable { /// - mode: The mode to use for generation. /// - access: The access modifier to use for generated declarations. /// - additionalImports: Additional imports to add to each generated file. + /// - additionalFileComments: Additional comments to add to the top of each generated file. /// - filter: Filter to apply to the OpenAPI document before generation. /// - namingStrategy: The naming strategy to use for deriving Swift identifiers from OpenAPI identifiers. /// Defaults to `defensive`. @@ -78,6 +82,7 @@ public struct Config: Sendable { mode: GeneratorMode, access: AccessModifier, additionalImports: [String] = [], + additionalFileComments: [String] = [], filter: DocumentFilter? = nil, namingStrategy: NamingStrategy, nameOverrides: [String: String] = [:], @@ -86,6 +91,7 @@ public struct Config: Sendable { self.mode = mode self.access = access self.additionalImports = additionalImports + self.additionalFileComments = additionalFileComments self.filter = filter self.namingStrategy = namingStrategy self.nameOverrides = nameOverrides diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift index 4df9a0358..7e976a61b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift @@ -32,7 +32,7 @@ struct ClientFileTranslator: FileTranslator { let doc = parsedOpenAPI - let topComment: Comment = .inline(Constants.File.topComment) + let topComment = self.topComment let imports = Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift index 00dde5d85..6d46f0264 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/FileTranslator.swift @@ -83,6 +83,12 @@ extension FileTranslator { ) return TranslatorContext(safeNameGenerator: overridingGenerator) } + + /// Creates a top comment that includes the default "do not modify" comment + /// and any additional file comments from the configuration. + var topComment: Comment { + .inline(([Constants.File.topComment] + config.additionalFileComments).joined(separator: "\n")) + } } /// A set of configuration values for concrete file translators. diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift index 3f54d4ade..2091a0f8c 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift @@ -30,7 +30,7 @@ struct ServerFileTranslator: FileTranslator { let doc = parsedOpenAPI - let topComment: Comment = .inline(Constants.File.topComment) + let topComment = self.topComment let imports = Constants.File.clientServerImports + config.additionalImports.map { ImportDescription(moduleName: $0) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift index 3ad52eb0e..ea8e6aa9a 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift @@ -32,7 +32,7 @@ struct TypesFileTranslator: FileTranslator { let doc = parsedOpenAPI - let topComment: Comment = .inline(Constants.File.topComment) + let topComment = self.topComment let imports = Constants.File.imports + config.additionalImports.map { ImportDescription(moduleName: $0) } 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 256c8cb32..de3ffebbe 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. +- `additionalFileComments` (optional): array of strings. Each string value is a comment that will be added to the top of each generated file (after the do-not-edit comment). Useful for adding directives like `swift-format-ignore-file` or `swiftlint:disable all`. - `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. @@ -95,6 +96,18 @@ additionalImports: accessModifier: public ``` +To add file comments to exclude generated files from formatting tools: + +```yaml +generate: + - types + - client +namingStrategy: idiomatic +additionalFileComments: + - "swift-format-ignore-file" + - "swiftlint:disable all" +``` + ### 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 d91f7ad8e..cf6619f7e 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -32,6 +32,7 @@ extension _GenerateOptions { let sortedModes = try resolvedModes(config) let resolvedAccessModifier = resolvedAccessModifier(config) let resolvedAdditionalImports = resolvedAdditionalImports(config) + let resolvedAdditionalFileComments = resolvedAdditionalFileComments(config) let resolvedNamingStragy = resolvedNamingStrategy(config) let resolvedNameOverrides = resolvedNameOverrides(config) let resolvedFeatureFlags = resolvedFeatureFlags(config) @@ -40,6 +41,7 @@ extension _GenerateOptions { mode: $0, access: resolvedAccessModifier, additionalImports: resolvedAdditionalImports, + additionalFileComments: resolvedAdditionalFileComments, filter: config?.filter, namingStrategy: resolvedNamingStragy, nameOverrides: resolvedNameOverrides, @@ -67,6 +69,7 @@ extension _GenerateOptions { - Plugin source: \(pluginSource?.rawValue ?? "") - Is dry run: \(isDryRun) - Additional imports: \(resolvedAdditionalImports.isEmpty ? "" : resolvedAdditionalImports.joined(separator: ", ")) + - Additional file comments: \(resolvedAdditionalFileComments.isEmpty ? "" : resolvedAdditionalFileComments.joined(separator: ", ")) """ ) do { diff --git a/Sources/swift-openapi-generator/GenerateOptions.swift b/Sources/swift-openapi-generator/GenerateOptions.swift index 6935a1346..1cdb73bc3 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -40,6 +40,8 @@ struct _GenerateOptions: ParsableArguments { @Option(help: "Additional import to add to all generated files.") var additionalImport: [String] = [] + @Option(help: "Additional file comment to add to all generated files.") var additionalFileComment: [String] = [] + @Option(help: "Pre-release feature to enable. Options: \(FeatureFlag.prettyListing).") var featureFlag: [FeatureFlag] = [] @@ -81,6 +83,17 @@ extension _GenerateOptions { return [] } + /// Returns a list of additional file comments requested by the user. + /// - Parameter config: The configuration specified by the user. + /// - Returns: A list of additional file comments requested by the user. + func resolvedAdditionalFileComments(_ config: _UserConfig?) -> [String] { + if !additionalFileComment.isEmpty { return additionalFileComment } + if let additionalFileComments = config?.additionalFileComments, !additionalFileComments.isEmpty { + return additionalFileComments + } + return [] + } + /// Returns the naming strategy requested by the user. /// - Parameter config: The configuration specified by the user. /// - Returns: The naming strategy requestd by the user. diff --git a/Sources/swift-openapi-generator/UserConfig.swift b/Sources/swift-openapi-generator/UserConfig.swift index 239f67b09..a1711bdc3 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 comments that are added to the top of every + /// generated Swift file. + var additionalFileComments: [String]? + /// Filter to apply to the OpenAPI document before generation. var filter: DocumentFilter? @@ -51,6 +55,7 @@ struct _UserConfig: Codable { case generate case accessModifier case additionalImports + case additionalFileComments case filter case namingStrategy case nameOverrides diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift index da8c522f3..19c38bd18 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift @@ -17,4 +17,17 @@ import OpenAPIKit final class Test_Config: Test_Core { func testDefaultAccessModifier() { XCTAssertEqual(Config.defaultAccessModifier, .internal) } + func testAdditionalFileComments() { + let config = Config( + mode: .types, + access: .public, + additionalFileComments: ["swift-format-ignore-file", "swiftlint:disable all"], + namingStrategy: .defensive + ) + XCTAssertEqual(config.additionalFileComments, ["swift-format-ignore-file", "swiftlint:disable all"]) + } + func testEmptyAdditionalFileComments() { + let config = Config(mode: .types, access: .public, namingStrategy: .defensive) + XCTAssertEqual(config.additionalFileComments, []) + } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 81cdeb108..b5b563bdd 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -5862,8 +5862,42 @@ final class SnippetBasedReferenceTests: XCTestCase { """ ) } -} + func testAdditionalFileComments() throws { + let additionalFileComments = ["hello world", "foo bar baz"] + let config = Config( + mode: .types, + access: .private, + additionalImports: [], + additionalFileComments: additionalFileComments, + filter: nil, + namingStrategy: .idiomatic, + nameOverrides: [:] + ) + let translator = TypesFileTranslator( + config: config, + diagnostics: XCTestDiagnosticCollector(test: self), + components: OpenAPI.Components() + ) + let documentYAML = """ + openapi: 3.1.0 + info: + title: Minimal API + version: 1.0.0 + paths: {} + """ + let document = try YAMLDecoder().decode(OpenAPI.Document.self, from: documentYAML) + let translation = try translator.translateFile(parsedOpenAPI: document) + try XCTAssertSwiftEquivalent( + XCTUnwrap(translation.file.contents.topComment), + """ + // Generated by swift-openapi-generator, do not modify. + // hello world + // foo bar baz + """ + ) + } +} extension SnippetBasedReferenceTests { func makeTypesTranslator(openAPIDocumentYAML: String) throws -> TypesFileTranslator { let document = try YAMLDecoder().decode(OpenAPI.Document.self, from: openAPIDocumentYAML) @@ -6245,6 +6279,18 @@ private func XCTAssertSwiftEquivalent( try XCTAssertEqualWithDiff(contents, expectedSwift, file: file, line: line) } +private func XCTAssertSwiftEquivalent( + _ comment: _OpenAPIGeneratorCore.Comment, + _ expectedSwift: String, + file: StaticString = #filePath, + line: UInt = #line +) throws { + let renderer = TextBasedRenderer.default + renderer.renderComment(comment) + let contents = renderer.renderedContents() + try XCTAssertEqualWithDiff(contents, expectedSwift, file: file, line: line) +} + private func diff(expected: String, actual: String) throws -> String { let process = Process() process.executableURL = try resolveExecutable("bash")