From a2013a83532350fb3b7da00de4e3f4924f903e39 Mon Sep 17 00:00:00 2001 From: Nick Candello Date: Tue, 7 Jul 2026 17:47:54 -0400 Subject: [PATCH 1/5] Add multi-file generator output plumbing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Motivation Prepare the generator pipeline for file-splitting features by allowing a single generator run to carry more than one rendered Swift output, while preserving the existing single-file API and behavior for current users. ### Modifications Introduces new models/endpoints needed for multi-file plumbing: - `StructuredSwiftRepresentation.files`: lets the structured Swift stage contain one or more named Swift files before rendering. - `StructuredSwiftRepresentation.file`: keeps the existing single-file access pattern for pipeline stages that still expect exactly one structured file. - `RenderedSwiftRepresentation`: remains the representation of one rendered Swift file. - `RenderedSwiftOutputs`: represents all rendered Swift files produced by one generator pipeline run. Updates the generator pipeline so rendering still happens one file at a time. Each structured file is rendered through its own renderer instance, then the resulting files are collected into `RenderedSwiftOutputs`. Adds `runGeneratorOutputs`, which returns all generated `InMemoryOutputFile`s from a generator run. Keeps `runGenerator` as the compatibility wrapper for existing callers. It still returns a single `InMemoryOutputFile` and now asserts that the pipeline produced exactly one file before returning it. Updates the tool output path to write every file returned by `runGeneratorOutputs`. File names flow from the translator-provided `NamedFileDescription.name` through rendering to `InMemoryOutputFile.baseName`, with the existing configured `outputFileName` override still applied to the mode’s primary output file. ### Result Existing users are not regressed: - The public `runGenerator` behavior remains single-file. - Existing generator modes still produce one output file by default. - Existing configured output filenames are preserved. - Multi-file output is only exposed through the new `runGeneratorOutputs` API. This gives later file-splitting branches a dedicated multi-output path without changing the behavior of current single-output callers. --- .../GeneratorPipeline.swift | 48 +++++++++-- .../Layers/RenderedSwiftRepresentation.swift | 17 +++- .../StructuredSwiftRepresentation.swift | 21 ++++- .../runGenerator.swift | 29 ++++--- .../Test_GeneratorPipeline.swift | 79 +++++++++++++++++++ .../CompatabilityTest.swift | 8 +- .../FileBasedReferenceTests.swift | 8 +- 7 files changed, 176 insertions(+), 34 deletions(-) create mode 100644 Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift diff --git a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift index 8f4a8cf13..2f472abfe 100644 --- a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift +++ b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift @@ -18,8 +18,8 @@ import Yams /// A sequence of steps that combined represents the full end-to-end /// functionality of the generator. /// -/// The input is an in-memory OpenAPI document, and the output is an -/// in-memory generated Swift file. Which file is generated (types, client, +/// The input is an in-memory OpenAPI document, and the output is one or more +/// in-memory generated Swift files. Which files are generated (types, client, /// or server) is controlled by the generator configuration, in the translation /// stage. /// @@ -30,8 +30,8 @@ import Yams /// document into a structure Swift representation of the requested generated /// file, for one of: types, client, or server. This stage contains most of the /// OpenAPI to Swift generation code. -/// 3. Rendering: TranslatedOutput -> RenderedOutput, which converts a -/// structured Swift representation into a raw Swift file. +/// 3. Rendering: TranslatedOutput -> RenderedOutput, which converts each +/// structured Swift file into a raw Swift file. struct GeneratorPipeline { // Note: Until we have variadic generics we need to have a fixed number @@ -48,7 +48,7 @@ struct GeneratorPipeline { typealias TranslatedOutput = StructuredSwiftRepresentation /// An output of the rendering phase, usually written to disk. - typealias RenderedOutput = RenderedSwiftRepresentation + typealias RenderedOutput = RenderedSwiftOutputs /// The parsing stage. var parseOpenAPIFileStage: GeneratorPipelineStage @@ -83,7 +83,30 @@ struct GeneratorPipeline { /// - Returns: The raw contents of the generated Swift file. public func runGenerator(input: InMemoryInputFile, config: Config, diagnostics: any DiagnosticCollector) throws -> InMemoryOutputFile -{ try makeGeneratorPipeline(config: config, diagnostics: diagnostics).run(input) } +{ + let outputFiles = try runGeneratorOutputs(input: input, config: config, diagnostics: diagnostics) + guard outputFiles.count == 1, let outputFile = outputFiles.first else { + throw GenericError(message: "Expected a single generated output file, got \(outputFiles.count).") + } + return outputFile +} + +/// Runs the generator logic with the specified inputs and returns every +/// generated Swift file. +/// - Parameters: +/// - input: The raw file contents of the OpenAPI document. +/// - config: A set of configuration values for the generator. +/// - diagnostics: A collector to which the generator emits diagnostics. +/// - Throws: When encountering a non-recoverable error. For recoverable +/// issues, emits issues into the diagnostics collector. +/// - Returns: The raw contents of all generated Swift files. +public func runGeneratorOutputs( + input: InMemoryInputFile, + config: Config, + diagnostics: any DiagnosticCollector +) throws -> [InMemoryOutputFile] { + try makeGeneratorPipeline(config: config, diagnostics: diagnostics).run(input).files +} /// Creates a new pipeline instance. /// - Parameters: @@ -99,7 +122,7 @@ func makeGeneratorPipeline( parser: any ParserProtocol = YamsParser(), validator: @escaping (ParsedOpenAPIRepresentation, Config) throws -> [Diagnostic] = validateDoc, translator: any TranslatorProtocol = MultiplexTranslator(), - renderer: any RendererProtocol = TextBasedRenderer.default, + renderer: @escaping () -> any RendererProtocol = { TextBasedRenderer.default }, config: Config, diagnostics: any DiagnosticCollector ) -> GeneratorPipeline { @@ -128,7 +151,16 @@ func makeGeneratorPipeline( ), renderSwiftFilesStage: .init( preTransitionHooks: [], - transition: { input in try renderer.render(structured: input, config: config, diagnostics: diagnostics) }, + transition: { input in + let files = try input.files.map { namedFile in + try renderer().render( + structured: .init(file: namedFile), + config: config, + diagnostics: diagnostics + ) + } + return .init(files: files) + }, postTransitionHooks: [] ) ) diff --git a/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift index 872c044d7..1bda41fa9 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift @@ -20,7 +20,22 @@ public import struct Foundation.Data #endif /// An in-memory file that contains the generated Swift code. -typealias RenderedSwiftRepresentation = InMemoryOutputFile +public typealias RenderedSwiftRepresentation = InMemoryOutputFile + +/// In-memory output files emitted by rendering a generator pipeline run. +public struct RenderedSwiftOutputs: Sendable { + + /// The files emitted by a generator pipeline run. + public var files: [RenderedSwiftRepresentation] + + /// Creates rendered outputs containing one file. + /// - Parameter file: The emitted file. + public init(file: RenderedSwiftRepresentation) { self.files = [file] } + + /// Creates rendered outputs containing multiple files. + /// - Parameter files: The emitted files. + public init(files: [RenderedSwiftRepresentation]) { self.files = files } +} /// An in-memory input file that contains the raw data of an OpenAPI document. /// diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index b14212761..508f0756f 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -1087,8 +1087,25 @@ struct NamedFileDescription: Equatable, Codable { /// A file with contents made up of structured Swift code. struct StructuredSwiftRepresentation: Equatable, Codable { - /// The contents of the file. - var file: NamedFileDescription + /// The contents of the files. + var files: [NamedFileDescription] + + /// The contents of the only file in the representation. + var file: NamedFileDescription { + get { + precondition(files.count == 1, "Expected a single structured Swift file, got \(files.count).") + return files[0] + } + set { files = [newValue] } + } + + /// Creates a structured representation containing one file. + /// - Parameter file: The file emitted by the translator. + init(file: NamedFileDescription) { self.files = [file] } + + /// Creates a structured representation containing multiple files. + /// - Parameter files: The files emitted by the translator. + init(files: [NamedFileDescription]) { self.files = files } } // MARK: - Conveniences diff --git a/Sources/swift-openapi-generator/runGenerator.swift b/Sources/swift-openapi-generator/runGenerator.swift index 08b62d469..9a91165b7 100644 --- a/Sources/swift-openapi-generator/runGenerator.swift +++ b/Sources/swift-openapi-generator/runGenerator.swift @@ -88,9 +88,8 @@ extension _Tool { /// - docData: The raw contents of the OpenAPI document. /// - config: A set of configuration values for the generator. /// - outputDirectory: The directory to which the generator writes - /// the generated Swift file. - /// - outputFileName: The file name to which the generator writes - /// the generated Swift file. + /// the generated Swift files. + /// - outputFileName: The file name to use for the mode's primary output file. /// - isDryRun: A Boolean value that indicates whether this invocation should /// be a dry run. /// - diagnostics: A collector for diagnostics emitted by the generator. @@ -105,19 +104,19 @@ extension _Tool { isDryRun: Bool, diagnostics: any DiagnosticCollector ) throws { - try replaceFileContents( - inDirectory: outputDirectory, - fileName: outputFileName, - with: { - let output = try _OpenAPIGeneratorCore.runGenerator( - input: .init(absolutePath: doc, contents: docData), - config: config, - diagnostics: diagnostics - ) - return output.contents - }, - isDryRun: isDryRun + let outputs = try _OpenAPIGeneratorCore.runGeneratorOutputs( + input: .init(absolutePath: doc, contents: docData), + config: config, + diagnostics: diagnostics ) + for output in outputs { + try replaceFileContents( + inDirectory: outputDirectory, + fileName: output.baseName == config.mode.outputFileName ? outputFileName : output.baseName, + with: { output.contents }, + isDryRun: isDryRun + ) + } } /// Evaluates a closure to generate file data and writes the data to disk diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift b/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift new file mode 100644 index 000000000..d779f1086 --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import _OpenAPIGeneratorCore + +final class Test_GeneratorPipeline: Test_Core { + + func testRunGeneratorOutputsReturnsSingleFileByDefault() throws { + let source = """ + openapi: "3.1.0" + info: + title: GreetingService + version: "1.0.0" + paths: {} + """ + let input = InMemoryInputFile( + absolutePath: URL(string: "openapi.yaml")!, + contents: Data(source.utf8) + ) + let diagnostics = AccumulatingDiagnosticCollector() + let outputs = try runGeneratorOutputs( + input: input, + config: Config(mode: .types, access: .public, namingStrategy: .defensive), + diagnostics: diagnostics + ) + + XCTAssertEqual(diagnostics.diagnostics.count, 0) + XCTAssertEqual(outputs.map(\.baseName), ["Types.swift"]) + } + + func testPipelineRendersMultipleStructuredFiles() throws { + let structured = StructuredSwiftRepresentation( + files: [ + .init( + name: "First.swift", + contents: .init( + topComment: nil, + imports: nil, + codeBlocks: [.declaration(.enum(name: "First"))] + ) + ), + .init( + name: "Second.swift", + contents: .init( + topComment: nil, + imports: nil, + codeBlocks: [.declaration(.enum(name: "Second"))] + ) + ), + ] + ) + let pipeline = makeGeneratorPipeline( + parser: YamsParser(), + translator: MultiplexTranslator(), + renderer: { TextBasedRenderer.default }, + config: Config(mode: .types, access: .public, namingStrategy: .defensive), + diagnostics: AccumulatingDiagnosticCollector() + ) + let outputs = try pipeline.renderSwiftFilesStage.run(structured).files + + XCTAssertEqual(outputs.map(\.baseName), ["First.swift", "Second.swift"]) + XCTAssertTrue(String(decoding: outputs[0].contents, as: UTF8.self).contains("enum First")) + XCTAssertFalse(String(decoding: outputs[0].contents, as: UTF8.self).contains("enum Second")) + XCTAssertTrue(String(decoding: outputs[1].contents, as: UTF8.self).contains("enum Second")) + XCTAssertFalse(String(decoding: outputs[1].contents, as: UTF8.self).contains("enum First")) + } +} diff --git a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift index 1e244c233..6294d409c 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift @@ -248,7 +248,7 @@ fileprivate extension CompatibilityTest { // Run the generator. log("Generating Swift code (document size: \(documentSize))") let input = InMemoryInputFile(absolutePath: URL(string: "openapi.yaml")!, contents: documentData) - let outputs: [GeneratorPipeline.RenderedOutput] + let outputs: [InMemoryOutputFile] if compatibilityTestParallelCodegen { outputs = try await withThrowingTaskGroup(of: GeneratorPipeline.RenderedOutput.self) { group in for mode in GeneratorMode.allCases { @@ -260,15 +260,15 @@ fileprivate extension CompatibilityTest { return try assertNoThrowWithValue(generator.run(input)) } } - return try await group.reduce(into: []) { $0.append($1) } + return try await group.reduce(into: []) { $0.append(contentsOf: $1.files) } } } else { - outputs = try GeneratorMode.allCases.map { mode in + outputs = try GeneratorMode.allCases.flatMap { mode in let generator = makeGeneratorPipeline( config: Config(mode: mode, access: .public, namingStrategy: .defensive), diagnostics: diagnosticsCollector ) - return try assertNoThrowWithValue(generator.run(input)) + return try assertNoThrowWithValue(generator.run(input)).files } } XCTAssertEqual(Set(diagnosticsCollector.diagnostics.map(\.message)), expectedDiagnostics) diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index 5596602bd..cae6b5981 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -86,7 +86,9 @@ final class FileBasedReferenceTests: XCTestCase { config: referenceTest.asConfig, ignoredDiagnosticMessages: ignoredDiagnosticMessages ) - let generatedOutputSource = try generatorPipeline.run(input) + let generatedOutputSources = try generatorPipeline.run(input).files + let generatedOutputSource = try XCTUnwrap(generatedOutputSources.first) + XCTAssertEqual(generatedOutputSources.count, 1) // Write generated sources to temporary directory let generatedOutputDir = try self.temporaryDirectory() @@ -151,12 +153,10 @@ extension FileBasedReferenceTests { { let parser = YamsParser() let translator = MultiplexTranslator() - let renderer = TextBasedRenderer.default - return _OpenAPIGeneratorCore.makeGeneratorPipeline( parser: parser, translator: translator, - renderer: renderer, + renderer: { TextBasedRenderer.default }, config: config, diagnostics: XCTestDiagnosticCollector(test: self, ignoredDiagnosticMessages: ignoredDiagnosticMessages) ) From d2949a89b612bc8b2c7403a8f69e4f787add6451 Mon Sep 17 00:00:00 2001 From: Nick Candello Date: Tue, 7 Jul 2026 17:45:03 -0400 Subject: [PATCH 2/5] Add file splitting output config ### Motivation Add configuration plumbing for splitting `Types.swift` output across multiple files. This prepares the generator for file-splitting strategies without changing the default generated output. ### Modifications - Add `OutputOptions` to `Config` as the top-level home for generated-output settings. - Add `TypesOutputOptions` and `TypesFileSplittingConfig` to model `types`\-specific file splitting configuration. - Add the initial `TypesFileSplittingStrategy.namespace` strategy and `NamespaceTypesFileSplittingOptions`. - Decode the new `output.types.fileSplitting` section from YAML configuration files. - Add a --types-file-splitting command-line option so direct CLI invocations can select the file-splitting strategy. - Keep CLI option resolution structured by constructing a full `TypesFileSplittingConfig` from command-line options before merging it into resolved output options. (Not utilized yet since `namespace` splitting has no parameters. More advanced splitting configs will have knob) - Include the resolved types file splitting strategy in verbose generator output. - Document the new YAML configuration shape and matching command-line option. ### Consumer Interfaces #### Programmatic/Core API Callers that construct `_OpenAPIGeneratorCore.Config` directly can pass output options through the new `output` parameter: ```swift let config = Config( mode: .types, access: .public, namingStrategy: .defensive, output: .init( types: .init( fileSplitting: .init(strategy: .namespace) ) ) ) ``` The default remains no file splitting: ```swift let config = Config( mode: .types, access: .public, namingStrategy: .defensive ) ``` #### YAML Configuration All tool integrations that use `openapi-generator-config.yaml` can opt in through the new `output` section: ```yaml generate: - types output: types: fileSplitting: strategy: namespace ``` #### Direct CLI and Custom Build Systems Direct command-line users and build rules that shell out to the generator can use the new strategy flag: ```sh swift-openapi-generator generate path/to/openapi.yaml \ --config path/to/openapi-generator-config.yaml \ --types-file-splitting namespace \ --output-directory "$DERIVED_SOURCES_DIR" ``` The `namespace` strategy does not need additional CLI flags. Strategies that require extra inputs can add strategy-specific flags in their implementation branches and resolve them into the same `TypesFileSplittingConfig` model. ### Result Users and integration points now have a stable way to request types file splitting: - Programmatic callers use `Config(output:)`. - Config-file users use `output.types.fileSplitting`. - Direct CLI users can use `--types-file-splitting`. Existing configurations continue to default to no file splitting. --- Sources/_OpenAPIGeneratorCore/Config.swift | 8 ++- .../_OpenAPIGeneratorCore/OutputOptions.swift | 71 +++++++++++++++++++ .../Articles/Configuring-the-generator.md | 21 ++++++ .../GenerateOptions+runGenerator.swift | 5 +- .../GenerateOptions.swift | 29 ++++++++ .../swift-openapi-generator/UserConfig.swift | 4 ++ .../Test_Config.swift | 25 +++++++ .../Test_GenerateOptions.swift | 35 +++++++++ 8 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 Sources/_OpenAPIGeneratorCore/OutputOptions.swift diff --git a/Sources/_OpenAPIGeneratorCore/Config.swift b/Sources/_OpenAPIGeneratorCore/Config.swift index 74bb6f13e..cfd6a7346 100644 --- a/Sources/_OpenAPIGeneratorCore/Config.swift +++ b/Sources/_OpenAPIGeneratorCore/Config.swift @@ -68,6 +68,9 @@ public struct Config: Sendable { /// Additional pre-release features to enable. public var featureFlags: FeatureFlags + /// Options that affect generated output files. + public var output: OutputOptions + /// Creates a configuration with the specified generator mode and imports. /// - Parameters: /// - mode: The mode to use for generation. @@ -81,6 +84,7 @@ public struct Config: Sendable { /// of the naming strategy. /// - typeOverrides: A map of OpenAPI schema names to desired custom type names. /// - featureFlags: Additional pre-release features to enable. + /// - output: Options that affect generated output files. public init( mode: GeneratorMode, access: AccessModifier, @@ -90,7 +94,8 @@ public struct Config: Sendable { namingStrategy: NamingStrategy, nameOverrides: [String: String] = [:], typeOverrides: TypeOverrides = .init(), - featureFlags: FeatureFlags = [] + featureFlags: FeatureFlags = [], + output: OutputOptions = .init() ) { self.mode = mode self.access = access @@ -101,5 +106,6 @@ public struct Config: Sendable { self.nameOverrides = nameOverrides self.typeOverrides = typeOverrides self.featureFlags = featureFlags + self.output = output } } diff --git a/Sources/_OpenAPIGeneratorCore/OutputOptions.swift b/Sources/_OpenAPIGeneratorCore/OutputOptions.swift new file mode 100644 index 000000000..b8e2ff40a --- /dev/null +++ b/Sources/_OpenAPIGeneratorCore/OutputOptions.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// Configuration for generated output files. +public struct OutputOptions: Sendable, Codable, Equatable { + + /// Options that only affect `Types.swift` generation. + public var types: TypesOutputOptions? + + /// Creates output options. + /// - Parameter types: Options that only affect `Types.swift` generation. + public init(types: TypesOutputOptions? = nil) { self.types = types } +} + +/// Configuration for generated types output files. +public struct TypesOutputOptions: Sendable, Codable, Equatable { + + /// Optional configuration for splitting generated types across files. + public var fileSplitting: TypesFileSplittingConfig? + + /// Creates types output options. + /// - Parameter fileSplitting: Optional configuration for splitting generated types across files. + public init(fileSplitting: TypesFileSplittingConfig? = nil) { self.fileSplitting = fileSplitting } +} + +/// Configuration for splitting generated types across files. +public struct TypesFileSplittingConfig: Sendable, Codable, Equatable { + + /// The strategy to use when splitting generated types across files. + public var strategy: TypesFileSplittingStrategy + + /// Options for the namespace file splitting strategy. + public var namespace: NamespaceTypesFileSplittingOptions? + + /// Creates a file splitting configuration. + /// - Parameters: + /// - strategy: The strategy to use when splitting generated types across files. + /// - namespace: Options for the namespace file splitting strategy. + public init( + strategy: TypesFileSplittingStrategy, + namespace: NamespaceTypesFileSplittingOptions? = nil + ) { + self.strategy = strategy + self.namespace = namespace + } +} + +/// Options for the namespace file splitting strategy. +public struct NamespaceTypesFileSplittingOptions: Sendable, Codable, Equatable { + + /// Creates namespace file splitting options. + public init() {} +} + +/// A strategy for splitting generated types across files. +public enum TypesFileSplittingStrategy: String, Sendable, Codable, Equatable, CaseIterable { + + /// Splits generated types into a small fixed set of files by top-level namespace. + case namespace +} 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 1bce2ffa9..4d87051ce 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 @@ -48,6 +48,12 @@ The configuration file has the following keys: - `typeOverrides` (optional): Allows replacing a generated type with a custom type. - `schemas` (optional): a string to string dictionary. The key is the name of the schema, the last component of `#/components/schemas/Foo` (here, `Foo`). The value is the custom type name, such as `CustomFoo`. Check out details in [SOAR-0014](https://swiftpackageindex.com/apple/swift-openapi-generator/documentation/swift-openapi-generator/soar-0014). - `featureFlags` (optional): array of strings. Each string must be a valid feature flag to enable. For a list of currently supported feature flags, check out [FeatureFlags.swift](https://github.com/apple/swift-openapi-generator/blob/main/Sources/_OpenAPIGeneratorCore/FeatureFlags.swift). +- `output` (optional): Options that affect generated output files. + - `types` (optional): Options that only affect `types` generation. + - `fileSplitting` (optional): Splits generated types across multiple files. + - `strategy` (required): The file splitting strategy. Known values: + - `namespace`: Split generated types into a small fixed set of files by top-level namespace. + - `namespace` (optional): Options for the `namespace` strategy. ### Example config files @@ -110,6 +116,21 @@ additionalFileComments: - "swiftlint:disable all" ``` +To split generated types across multiple Swift files: + +```yaml +generate: + - types +namingStrategy: idiomatic +output: + types: + fileSplitting: + strategy: namespace + namespace: {} +``` + +The same strategy can be enabled from the command line with `--types-file-splitting namespace`. + ### 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 632dc1e6d..dd149c184 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -37,6 +37,7 @@ extension _GenerateOptions { let resolvedNameOverrides = resolvedNameOverrides(config) let resolvedTypeOverrides = resolvedTypeOverrides(config) let resolvedFeatureFlags = resolvedFeatureFlags(config) + let resolvedOutputOptions = resolvedOutputOptions(config) let configs: [Config] = sortedModes.map { .init( mode: $0, @@ -47,7 +48,8 @@ extension _GenerateOptions { namingStrategy: resolvedNamingStragy, nameOverrides: resolvedNameOverrides, typeOverrides: resolvedTypeOverrides, - featureFlags: resolvedFeatureFlags + featureFlags: resolvedFeatureFlags, + output: resolvedOutputOptions ) } let (diagnostics, finalizeDiagnostics) = preparedDiagnosticsCollector(outputPath: diagnosticsOutputPath) @@ -67,6 +69,7 @@ extension _GenerateOptions { .sorted(by: { $0.key < $1.key }) .map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", ")) - Feature flags: \(resolvedFeatureFlags.isEmpty ? "" : resolvedFeatureFlags.map(\.rawValue).joined(separator: ", ")) + - Types file splitting: \(resolvedOutputOptions.types?.fileSplitting?.strategy.rawValue ?? "") - Output file names: \(sortedModes.map(\.outputFileName).joined(separator: ", ")) - Output directory: \(outputDirectory.path) - Diagnostics output path: \(diagnosticsOutputPath?.path ?? "") diff --git a/Sources/swift-openapi-generator/GenerateOptions.swift b/Sources/swift-openapi-generator/GenerateOptions.swift index 6539fae9c..421bef1e7 100644 --- a/Sources/swift-openapi-generator/GenerateOptions.swift +++ b/Sources/swift-openapi-generator/GenerateOptions.swift @@ -48,10 +48,16 @@ struct _GenerateOptions: ParsableArguments { @Option( help: "When specified, writes out the diagnostics into a YAML file instead of emitting them to standard error." ) var diagnosticsOutputPath: URL? + + @Option( + help: + "Split generated types across files using the specified strategy. Options: \(TypesFileSplittingStrategy.prettyListing)." + ) var typesFileSplitting: TypesFileSplittingStrategy? } extension AccessModifier: ExpressibleByArgument {} extension NamingStrategy: ExpressibleByArgument {} +extension TypesFileSplittingStrategy: ExpressibleByArgument {} /// Executes a throwing operation and transforms file-not-found errors into user-friendly messages. /// @@ -151,6 +157,29 @@ extension _GenerateOptions { return config?.featureFlags ?? [] } + /// Returns the output options requested by the user. + /// - Parameter config: The configuration specified by the user. + /// - Returns: Output options requested by the user. + func resolvedOutputOptions(_ config: _UserConfig?) -> OutputOptions { + var output = config?.output ?? .init() + if let fileSplitting = resolvedTypesFileSplittingConfig() { + var typesOutput = output.types ?? .init() + typesOutput.fileSplitting = fileSplitting + output.types = typesOutput + } + return output + } + + /// Returns the types file splitting configuration requested from command-line options. + /// - Returns: Types file splitting configuration requested from command-line options. + func resolvedTypesFileSplittingConfig() -> TypesFileSplittingConfig? { + guard let typesFileSplitting else { return nil } + switch typesFileSplitting { + case .namespace: + return .init(strategy: .namespace) + } + } + /// Validates a collection of keys against a predefined set of allowed keys. /// /// - Parameter keys: A collection of keys to be validated. diff --git a/Sources/swift-openapi-generator/UserConfig.swift b/Sources/swift-openapi-generator/UserConfig.swift index f83dd71fd..540a9f0cf 100644 --- a/Sources/swift-openapi-generator/UserConfig.swift +++ b/Sources/swift-openapi-generator/UserConfig.swift @@ -51,6 +51,9 @@ struct _UserConfig: Codable { /// A set of features to explicitly enable. var featureFlags: FeatureFlags? + /// Options that affect generated output files. + var output: OutputOptions? + /// A set of raw values corresponding to the coding keys of this struct. static let codingKeysRawValues = Set(CodingKeys.allCases.map({ $0.rawValue })) @@ -64,6 +67,7 @@ struct _UserConfig: Codable { case nameOverrides case typeOverrides case featureFlags + case output } /// A container of type overrides. diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift index 19c38bd18..20cb1f75d 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift @@ -30,4 +30,29 @@ final class Test_Config: Test_Core { let config = Config(mode: .types, access: .public, namingStrategy: .defensive) XCTAssertEqual(config.additionalFileComments, []) } + + func testOutputOptionsDefaultToEmpty() { + let config = Config(mode: .types, access: .public, namingStrategy: .defensive) + XCTAssertNil(config.output.types) + } + + func testTypesFileSplittingConfig() { + let config = Config( + mode: .types, + access: .public, + namingStrategy: .defensive, + output: .init(types: .init(fileSplitting: .init(strategy: .namespace))) + ) + XCTAssertEqual(config.output.types?.fileSplitting?.strategy, .namespace) + } + + func testTypesFileSplittingConfigOptions() { + let config = TypesFileSplittingConfig( + strategy: .namespace, + namespace: .init() + ) + + XCTAssertEqual(config.strategy, .namespace) + XCTAssertEqual(config.namespace, .init()) + } } diff --git a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift index 9638fda0a..86f8e5d89 100644 --- a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift +++ b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift @@ -141,5 +141,40 @@ final class Test_GenerateOptions: XCTestCase { ) } catch { XCTFail("Expected ArgumentParser.ValidationError, but got: \(type(of: error)) - \(error)") } } + + func testLoadedConfigDecodesOutputOptions() throws { + let configURL = try makeTemporaryConfig( + """ + generate: + - types + output: + types: + fileSplitting: + strategy: namespace + namespace: {} + """ + ) + let options = try _GenerateOptions.parse(["openapi.yaml", "--config", configURL.path]) + let config = try XCTUnwrap(options.loadedConfig()) + + XCTAssertEqual(config.output?.types?.fileSplitting?.strategy, .namespace) + XCTAssertEqual(config.output?.types?.fileSplitting?.namespace, .init()) + } + + func testTypesFileSplittingOptionResolvesOutputOptions() throws { + let options = try _GenerateOptions.parse([ + "openapi.yaml", "--mode", "types", "--types-file-splitting", "namespace", + ]) + + XCTAssertEqual(options.resolvedOutputOptions(nil).types?.fileSplitting?.strategy, .namespace) + } #endif + + private func makeTemporaryConfig(_ contents: String) throws -> URL { + let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + let configURL = tempDir.appendingPathComponent("openapi-generator-config.yaml") + try contents.write(to: configURL, atomically: true, encoding: .utf8) + return configURL + } } From eb817f7c053b05211f21806ad41b5eb7faaf14cf Mon Sep 17 00:00:00 2001 From: Nick Candello Date: Mon, 13 Jul 2026 09:37:56 -0400 Subject: [PATCH 3/5] Add namespace-based types file splitting ### Motivation Add the first concrete types file-splitting strategy using the multi-output generator plumbing. The namespace strategy splits `Types.swift` into stable top-level files without parsing rendered Swift source. ### Modifications - Teach `TypesFileTranslator` to emit multiple structured Swift files when `output.types.fileSplitting.strategy` is `namespace`. - Keep the root declarations in `Types.swift`, including `APIProtocol`, the API protocol extension, and server declarations. - Move the generated `Components` namespace into `Types+Components.swift`. - Move the generated `Operations` namespace into `Types+Operations.swift`. - Add a shared `GeneratorMode` helper for constructing generated Swift file names. - Add typed output-name planning on `TypesFileSplittingConfig` for the generated namespace files. - Reuse the existing renderer multi-output path so each split file is rendered independently from its translator-provided name. - Reject file splitting for build-tool plugin invocations with a clear validation error. - Document that build-tool plugin support is not included in this first slice. - Add coverage for namespace splitting, the default unsplit behavior, and build-tool plugin rejection. ### Build-Tool Plugin Follow-Up The SwiftPM/Xcode build-tool plugin must declare generated output files before invoking the generator executable. Supporting splitting there needs a separate design so the plugin can determine the generated output set without duplicating the generator's YAML parsing logic. SwiftPM plugin targets do not get access to library dependencies transitively through executable dependencies, and adding `Yams` directly as a plugin dependency is rejected because it is a library product. For that reason, this PR intentionally keeps support focused on direct generator/command-plugin usage and leaves build-tool plugin support to a follow-up. ### Result Users can opt in to namespace-based types splitting with: ```yaml output: types: fileSplitting: strategy: namespace ``` or with: ```sh swift-openapi-generator generate openapi.yaml --mode types --types-file-splitting namespace ``` When enabled, types generation emits: ```text Types.swift Types+Components.swift Types+Operations.swift ``` When the setting is absent, generation continues to emit the existing single `Types.swift` file. --- .../_OpenAPIGeneratorCore/GeneratorMode.swift | 14 +- .../_OpenAPIGeneratorCore/OutputOptions.swift | 17 +++ .../TypesTranslator/TypesFileTranslator.swift | 26 ++++ .../Articles/Configuring-the-generator.md | 2 + .../GenerateOptions+runGenerator.swift | 5 + ...est_TypesFileTranslatorFileSplitting.swift | 134 ++++++++++++++++++ .../Test_Config.swift | 15 ++ .../Test_GenerateOptions.swift | 31 ++++ 8 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift diff --git a/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift b/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift index a0b8ebdaa..bbb9c4a52 100644 --- a/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift +++ b/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift @@ -39,15 +39,23 @@ extension GeneratorMode { /// The Swift file name including its file extension. public var outputFileName: String { switch self { - case .types: return "Types.swift" - case .client: return "Client.swift" - case .server: return "Server.swift" + case .types: return Self.outputFileName("Types") + case .client: return Self.outputFileName("Client") + case .server: return Self.outputFileName("Server") } } /// The Swift file names for all supported generator mode values. public static var allOutputFileNames: [String] { GeneratorMode.allCases.map(\.outputFileName) } + /// Returns a Swift output file name composed from the provided name components. + public static func outputFileName(_ name: String, _ extensionNames: String...) -> String { + let names = ([name] + extensionNames).map { name in + name.hasSuffix(".swift") ? String(name.dropLast(".swift".count)) : name + } + return names.joined(separator: "+") + ".swift" + } + /// Defines an order in which generators should be run. var order: Int { switch self { diff --git a/Sources/_OpenAPIGeneratorCore/OutputOptions.swift b/Sources/_OpenAPIGeneratorCore/OutputOptions.swift index b8e2ff40a..ec627da4a 100644 --- a/Sources/_OpenAPIGeneratorCore/OutputOptions.swift +++ b/Sources/_OpenAPIGeneratorCore/OutputOptions.swift @@ -69,3 +69,20 @@ public enum TypesFileSplittingStrategy: String, Sendable, Codable, Equatable, Ca /// Splits generated types into a small fixed set of files by top-level namespace. case namespace } + +extension TypesFileSplittingConfig { + + /// Returns the Swift output file names emitted by the configuration. + /// - Parameter primaryTypesFileName: The file name for the primary generated types file. + /// - Returns: The emitted Swift output file names. + public func outputFileNames(primaryTypesFileName: String) -> [String] { + switch strategy { + case .namespace: + return [ + primaryTypesFileName, + GeneratorMode.outputFileName(primaryTypesFileName, "Components"), + GeneratorMode.outputFileName(primaryTypesFileName, "Operations"), + ] + } + } +} diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift index cdf447f7b..12a9cbe8b 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift @@ -57,6 +57,32 @@ struct TypesFileTranslator: FileTranslator { ] ) + if let fileSplitting = config.output.types?.fileSplitting, fileSplitting.strategy == .namespace { + let fileNames = fileSplitting.outputFileNames(primaryTypesFileName: GeneratorMode.types.outputFileName) + return StructuredSwiftRepresentation( + files: [ + .init( + name: fileNames[0], + contents: .init( + topComment: topComment, + imports: imports, + codeBlocks: [ + .declaration(apiProtocol), .declaration(apiProtocolExtension), .declaration(serversDecl), + ] + ) + ), + .init( + name: fileNames[1], + contents: .init(topComment: topComment, imports: imports, codeBlocks: [components]) + ), + .init( + name: fileNames[2], + contents: .init(topComment: topComment, imports: imports, codeBlocks: [operations]) + ), + ] + ) + } + return StructuredSwiftRepresentation(file: .init(name: GeneratorMode.types.outputFileName, contents: typesFile)) } } 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 4d87051ce..0e98048c8 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 @@ -131,6 +131,8 @@ output: The same strategy can be enabled from the command line with `--types-file-splitting namespace`. +> Note: Types file splitting is currently supported when running the generator directly or with the command plugin. The build tool plugin does not yet support types file splitting. + ### 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 dd149c184..835fb54c4 100644 --- a/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift +++ b/Sources/swift-openapi-generator/GenerateOptions+runGenerator.swift @@ -38,6 +38,11 @@ extension _GenerateOptions { let resolvedTypeOverrides = resolvedTypeOverrides(config) let resolvedFeatureFlags = resolvedFeatureFlags(config) let resolvedOutputOptions = resolvedOutputOptions(config) + guard pluginSource != .build || resolvedOutputOptions.types?.fileSplitting == nil else { + throw ValidationError( + "Types file splitting is not supported by the build tool plugin yet. Run swift-openapi-generator directly or use the command plugin to generate split files." + ) + } let configs: [Config] = sortedModes.map { .init( mode: $0, diff --git a/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift new file mode 100644 index 000000000..1ea734ddf --- /dev/null +++ b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import _OpenAPIGeneratorCore + +final class Test_TypesFileTranslatorFileSplitting: Test_Core { + + func testNamespaceSplittingProducesRootComponentsAndOperationsFiles() throws { + let input = InMemoryInputFile( + absolutePath: URL(string: "openapi.yaml")!, + contents: Data(Self.source.utf8) + ) + let diagnostics = AccumulatingDiagnosticCollector() + let outputs = try runGeneratorOutputs( + input: input, + config: Self.splitConfig(strategy: .namespace), + diagnostics: diagnostics + ) + + XCTAssertEqual(diagnostics.diagnostics.count, 0) + XCTAssertEqual(outputs.map(\.baseName), ["Types.swift", "Types+Components.swift", "Types+Operations.swift"]) + + let outputByName = Dictionary(uniqueKeysWithValues: outputs.map { output in + (output.baseName, String(decoding: output.contents, as: UTF8.self)) + }) + let rootSource = try XCTUnwrap(outputByName["Types.swift"]) + let componentsSource = try XCTUnwrap(outputByName["Types+Components.swift"]) + let operationsSource = try XCTUnwrap(outputByName["Types+Operations.swift"]) + + XCTAssertTrue(rootSource.contains("import OpenAPIRuntime")) + XCTAssertTrue(componentsSource.contains("import OpenAPIRuntime")) + XCTAssertTrue(operationsSource.contains("import OpenAPIRuntime")) + XCTAssertTrue(componentsSource.contains("import struct Foundation.Date")) + XCTAssertTrue(operationsSource.contains("import struct Foundation.Date")) + + XCTAssertTrue(rootSource.contains("protocol APIProtocol")) + XCTAssertFalse(rootSource.contains("enum Components")) + XCTAssertFalse(rootSource.contains("enum Operations")) + + XCTAssertTrue(componentsSource.contains("enum Components")) + XCTAssertTrue(componentsSource.contains("struct User")) + XCTAssertFalse(componentsSource.contains("protocol APIProtocol")) + XCTAssertFalse(componentsSource.contains("enum Operations")) + + XCTAssertTrue(operationsSource.contains("enum Operations")) + XCTAssertFalse(operationsSource.contains("enum Components")) + XCTAssertFalse(operationsSource.contains("protocol APIProtocol")) + } + + func testNamespaceSplittingIsDisabledByDefault() throws { + let input = InMemoryInputFile( + absolutePath: URL(string: "openapi.yaml")!, + contents: Data(Self.source.utf8) + ) + let diagnostics = AccumulatingDiagnosticCollector() + let outputs = try runGeneratorOutputs( + input: input, + config: Config(mode: .types, access: .public, namingStrategy: .defensive), + diagnostics: diagnostics + ) + + XCTAssertEqual(diagnostics.diagnostics.count, 0) + XCTAssertEqual(outputs.map(\.baseName), ["Types.swift"]) + } + + private static func splitConfig(strategy: TypesFileSplittingStrategy) -> Config { + .init( + mode: .types, + access: .public, + namingStrategy: .defensive, + output: .init( + types: .init( + fileSplitting: .init( + strategy: strategy, + namespace: .init() + ) + ) + ) + ) + } + + private static let source = """ + openapi: "3.1.0" + info: + title: GreetingService + version: "1.0.0" + paths: + /users/{id}: + get: + operationId: getUser + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: A user. + headers: + X-Expires-After: + schema: + type: string + format: date-time + content: + application/json: + schema: + $ref: "#/components/schemas/User" + components: + schemas: + User: + type: object + properties: + id: + type: string + createdAt: + type: string + format: date-time + required: + - id + """ +} diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift index 20cb1f75d..9a5d3ea9b 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift @@ -55,4 +55,19 @@ final class Test_Config: Test_Core { XCTAssertEqual(config.strategy, .namespace) XCTAssertEqual(config.namespace, .init()) } + + func testGeneratorModeOutputFileNameHelper() { + XCTAssertEqual(GeneratorMode.outputFileName("Types"), "Types.swift") + XCTAssertEqual(GeneratorMode.outputFileName("Types.swift", "Components"), "Types+Components.swift") + XCTAssertEqual(GeneratorMode.outputFileName("Types.swift", "Operations.swift"), "Types+Operations.swift") + } + + func testNamespaceFileSplittingOutputFileNames() { + let config = TypesFileSplittingConfig(strategy: .namespace) + + XCTAssertEqual( + config.outputFileNames(primaryTypesFileName: "Types.swift"), + ["Types.swift", "Types+Components.swift", "Types+Operations.swift"] + ) + } } diff --git a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift index 86f8e5d89..889a734ca 100644 --- a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift +++ b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift @@ -168,6 +168,37 @@ final class Test_GenerateOptions: XCTestCase { XCTAssertEqual(options.resolvedOutputOptions(nil).types?.fileSplitting?.strategy, .namespace) } + + func testTypesFileSplittingIsRejectedForBuildToolPlugin() async throws { + let configURL = try makeTemporaryConfig( + """ + generate: + - types + output: + types: + fileSplitting: + strategy: namespace + namespace: {} + """ + ) + let options = try _GenerateOptions.parse(["openapi.yaml", "--config", configURL.path]) + + do { + try await options.runGenerator( + outputDirectory: URL(fileURLWithPath: "/tmp/generated"), + pluginSource: .build, + isDryRun: true + ) + XCTFail("Expected build tool plugin invocation to reject types file splitting") + } catch let error as ArgumentParser.ValidationError { + XCTAssertTrue( + String(describing: error).contains("Types file splitting is not supported by the build tool plugin yet"), + "Unexpected error: \(error)" + ) + } catch { + XCTFail("Expected ArgumentParser.ValidationError, but got: \(type(of: error)) - \(error)") + } + } #endif private func makeTemporaryConfig(_ contents: String) throws -> URL { From b002759c8a198806f9f9539d8bf54ee62c2cd1b3 Mon Sep 17 00:00:00 2001 From: Nick Candello Date: Wed, 15 Jul 2026 13:07:01 -0400 Subject: [PATCH 4/5] Simplify multi-file output model Resolve Simon's initial review comments: - Remove RenderedSwiftOutputs and make RenderedSwiftRepresentation the rendered file array directly. - Keep rendered-output plumbing internal instead of adding new public wrapper API. - Remove StructuredSwiftRepresentation.file and the explicit init(files:) convenience; callers now use the files storage/memberwise initializer directly. - Render one NamedFileDescription at a time via RendererProtocol.render(namedFile:), with GeneratorPipeline mapping over structured files. - Remove NamespaceTypesFileSplittingOptions and the namespace: {} YAML/test/documentation surface until there are real namespace options. - Remove the dead runGenerator outputFileName parameter and write generated outputs by output.baseName directly. --- .../GeneratorPipeline.swift | 13 ++++-------- .../Layers/RenderedSwiftRepresentation.swift | 17 +--------------- .../StructuredSwiftRepresentation.swift | 17 ---------------- .../_OpenAPIGeneratorCore/OutputOptions.swift | 20 ++----------------- .../Renderer/RendererProtocol.swift | 6 +++--- .../Renderer/TextBasedRenderer.swift | 5 ++--- .../ClientTranslator/ClientTranslator.swift | 10 ++++++---- .../ServerTranslator/ServerTranslator.swift | 16 ++++++++------- .../TypesTranslator/TypesFileTranslator.swift | 2 +- .../Articles/Configuring-the-generator.md | 1 - .../runGenerator.swift | 5 +---- ...est_TypesFileTranslatorFileSplitting.swift | 5 +---- .../Test_Config.swift | 10 ---------- .../Test_GeneratorPipeline.swift | 2 +- .../CompatabilityTest.swift | 4 ++-- .../FileBasedReferenceTests.swift | 2 +- .../SnippetBasedReferenceTests.swift | 2 +- .../Test_GenerateOptions.swift | 3 --- 18 files changed, 35 insertions(+), 105 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift index 2f472abfe..eda49c065 100644 --- a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift +++ b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift @@ -48,7 +48,7 @@ struct GeneratorPipeline { typealias TranslatedOutput = StructuredSwiftRepresentation /// An output of the rendering phase, usually written to disk. - typealias RenderedOutput = RenderedSwiftOutputs + typealias RenderedOutput = RenderedSwiftRepresentation /// The parsing stage. var parseOpenAPIFileStage: GeneratorPipelineStage @@ -105,7 +105,7 @@ public func runGeneratorOutputs( config: Config, diagnostics: any DiagnosticCollector ) throws -> [InMemoryOutputFile] { - try makeGeneratorPipeline(config: config, diagnostics: diagnostics).run(input).files + try makeGeneratorPipeline(config: config, diagnostics: diagnostics).run(input) } /// Creates a new pipeline instance. @@ -152,14 +152,9 @@ func makeGeneratorPipeline( renderSwiftFilesStage: .init( preTransitionHooks: [], transition: { input in - let files = try input.files.map { namedFile in - try renderer().render( - structured: .init(file: namedFile), - config: config, - diagnostics: diagnostics - ) + try input.files.map { namedFile in + try renderer().render(namedFile: namedFile, config: config, diagnostics: diagnostics) } - return .init(files: files) }, postTransitionHooks: [] ) diff --git a/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift index 1bda41fa9..8b1556d67 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/RenderedSwiftRepresentation.swift @@ -19,23 +19,8 @@ public import struct Foundation.URL public import struct Foundation.Data #endif -/// An in-memory file that contains the generated Swift code. -public typealias RenderedSwiftRepresentation = InMemoryOutputFile - /// In-memory output files emitted by rendering a generator pipeline run. -public struct RenderedSwiftOutputs: Sendable { - - /// The files emitted by a generator pipeline run. - public var files: [RenderedSwiftRepresentation] - - /// Creates rendered outputs containing one file. - /// - Parameter file: The emitted file. - public init(file: RenderedSwiftRepresentation) { self.files = [file] } - - /// Creates rendered outputs containing multiple files. - /// - Parameter files: The emitted files. - public init(files: [RenderedSwiftRepresentation]) { self.files = files } -} +typealias RenderedSwiftRepresentation = [InMemoryOutputFile] /// An in-memory input file that contains the raw data of an OpenAPI document. /// diff --git a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift index 508f0756f..020d1991c 100644 --- a/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift +++ b/Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift @@ -1089,23 +1089,6 @@ struct StructuredSwiftRepresentation: Equatable, Codable { /// The contents of the files. var files: [NamedFileDescription] - - /// The contents of the only file in the representation. - var file: NamedFileDescription { - get { - precondition(files.count == 1, "Expected a single structured Swift file, got \(files.count).") - return files[0] - } - set { files = [newValue] } - } - - /// Creates a structured representation containing one file. - /// - Parameter file: The file emitted by the translator. - init(file: NamedFileDescription) { self.files = [file] } - - /// Creates a structured representation containing multiple files. - /// - Parameter files: The files emitted by the translator. - init(files: [NamedFileDescription]) { self.files = files } } // MARK: - Conveniences diff --git a/Sources/_OpenAPIGeneratorCore/OutputOptions.swift b/Sources/_OpenAPIGeneratorCore/OutputOptions.swift index ec627da4a..a1f020f00 100644 --- a/Sources/_OpenAPIGeneratorCore/OutputOptions.swift +++ b/Sources/_OpenAPIGeneratorCore/OutputOptions.swift @@ -40,29 +40,13 @@ public struct TypesFileSplittingConfig: Sendable, Codable, Equatable { /// The strategy to use when splitting generated types across files. public var strategy: TypesFileSplittingStrategy - /// Options for the namespace file splitting strategy. - public var namespace: NamespaceTypesFileSplittingOptions? - /// Creates a file splitting configuration. - /// - Parameters: - /// - strategy: The strategy to use when splitting generated types across files. - /// - namespace: Options for the namespace file splitting strategy. - public init( - strategy: TypesFileSplittingStrategy, - namespace: NamespaceTypesFileSplittingOptions? = nil - ) { + /// - Parameter strategy: The strategy to use when splitting generated types across files. + public init(strategy: TypesFileSplittingStrategy) { self.strategy = strategy - self.namespace = namespace } } -/// Options for the namespace file splitting strategy. -public struct NamespaceTypesFileSplittingOptions: Sendable, Codable, Equatable { - - /// Creates namespace file splitting options. - public init() {} -} - /// A strategy for splitting generated types across files. public enum TypesFileSplittingStrategy: String, Sendable, Codable, Equatable, CaseIterable { diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/RendererProtocol.swift b/Sources/_OpenAPIGeneratorCore/Renderer/RendererProtocol.swift index c40ef2824..8ace1a36c 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/RendererProtocol.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/RendererProtocol.swift @@ -18,13 +18,13 @@ /// Rendering is the last phase of the generator pipeline. protocol RendererProtocol { - /// Renders the specified structured code into a raw Swift file. + /// Renders the specified structured Swift file into a raw Swift file. /// - Parameters: - /// - code: A structured representation of the Swift code. + /// - namedFile: A structured representation of the Swift file. /// - config: The configuration of the generator. /// - diagnostics: The collector to which to emit diagnostics. /// - Returns: A raw file with Swift contents. /// - Throws: An error if an issue occurs during rendering. - func render(structured code: StructuredSwiftRepresentation, config: Config, diagnostics: any DiagnosticCollector) + func render(namedFile: NamedFileDescription, config: Config, diagnostics: any DiagnosticCollector) throws -> InMemoryOutputFile } diff --git a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift index 1fdb035fd..518c51d28 100644 --- a/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift +++ b/Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift @@ -89,10 +89,9 @@ final class StringCodeWriter { /// to convert the provided structure code into raw string form. struct TextBasedRenderer: RendererProtocol { - func render(structured: StructuredSwiftRepresentation, config: Config, diagnostics: any DiagnosticCollector) throws - -> InMemoryOutputFile + func render(namedFile: NamedFileDescription, config: Config, diagnostics: any DiagnosticCollector) + throws -> InMemoryOutputFile { - let namedFile = structured.file renderFile(namedFile.contents) let string = writer.rendered() return InMemoryOutputFile(baseName: namedFile.name, contents: Data(string.utf8)) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift index 58798e2ee..91964de90 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ClientTranslator/ClientTranslator.swift @@ -117,10 +117,12 @@ struct ClientFileTranslator: FileTranslator { ) return StructuredSwiftRepresentation( - file: .init( - name: GeneratorMode.client.outputFileName, - contents: .init(topComment: topComment, imports: imports, codeBlocks: [.declaration(clientStructDecl)]) - ) + files: [ + .init( + name: GeneratorMode.client.outputFileName, + contents: .init(topComment: topComment, imports: imports, codeBlocks: [.declaration(clientStructDecl)]) + ) + ] ) } } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift index 427b25bbe..5890363bf 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/ServerTranslator/ServerTranslator.swift @@ -54,14 +54,16 @@ struct ServerFileTranslator: FileTranslator { ) return StructuredSwiftRepresentation( - file: .init( - name: GeneratorMode.server.outputFileName, - contents: .init( - topComment: topComment, - imports: imports, - codeBlocks: [.declaration(protocolExtensionDecl), .declaration(serverExtensionDecl)] + files: [ + .init( + name: GeneratorMode.server.outputFileName, + contents: .init( + topComment: topComment, + imports: imports, + codeBlocks: [.declaration(protocolExtensionDecl), .declaration(serverExtensionDecl)] + ) ) - ) + ] ) } diff --git a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift index 12a9cbe8b..e4fec726e 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift @@ -83,6 +83,6 @@ struct TypesFileTranslator: FileTranslator { ) } - return StructuredSwiftRepresentation(file: .init(name: GeneratorMode.types.outputFileName, contents: typesFile)) + return StructuredSwiftRepresentation(files: [.init(name: GeneratorMode.types.outputFileName, contents: typesFile)]) } } 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 0e98048c8..e97153af6 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 @@ -126,7 +126,6 @@ output: types: fileSplitting: strategy: namespace - namespace: {} ``` The same strategy can be enabled from the command line with `--types-file-splitting namespace`. diff --git a/Sources/swift-openapi-generator/runGenerator.swift b/Sources/swift-openapi-generator/runGenerator.swift index 9a91165b7..61ee7b063 100644 --- a/Sources/swift-openapi-generator/runGenerator.swift +++ b/Sources/swift-openapi-generator/runGenerator.swift @@ -56,7 +56,6 @@ extension _Tool { docData: docData, config: config, outputDirectory: outputDirectory, - outputFileName: config.mode.outputFileName, isDryRun: isDryRun, diagnostics: diagnostics ) @@ -89,7 +88,6 @@ extension _Tool { /// - config: A set of configuration values for the generator. /// - outputDirectory: The directory to which the generator writes /// the generated Swift files. - /// - outputFileName: The file name to use for the mode's primary output file. /// - isDryRun: A Boolean value that indicates whether this invocation should /// be a dry run. /// - diagnostics: A collector for diagnostics emitted by the generator. @@ -100,7 +98,6 @@ extension _Tool { docData: Data, config: Config, outputDirectory: URL, - outputFileName: String, isDryRun: Bool, diagnostics: any DiagnosticCollector ) throws { @@ -112,7 +109,7 @@ extension _Tool { for output in outputs { try replaceFileContents( inDirectory: outputDirectory, - fileName: output.baseName == config.mode.outputFileName ? outputFileName : output.baseName, + fileName: output.baseName, with: { output.contents }, isDryRun: isDryRun ) diff --git a/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift index 1ea734ddf..8dfabda2e 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift @@ -82,10 +82,7 @@ final class Test_TypesFileTranslatorFileSplitting: Test_Core { namingStrategy: .defensive, output: .init( types: .init( - fileSplitting: .init( - strategy: strategy, - namespace: .init() - ) + fileSplitting: .init(strategy: strategy) ) ) ) diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift index 9a5d3ea9b..2cbf684c8 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Test_Config.swift @@ -46,16 +46,6 @@ final class Test_Config: Test_Core { XCTAssertEqual(config.output.types?.fileSplitting?.strategy, .namespace) } - func testTypesFileSplittingConfigOptions() { - let config = TypesFileSplittingConfig( - strategy: .namespace, - namespace: .init() - ) - - XCTAssertEqual(config.strategy, .namespace) - XCTAssertEqual(config.namespace, .init()) - } - func testGeneratorModeOutputFileNameHelper() { XCTAssertEqual(GeneratorMode.outputFileName("Types"), "Types.swift") XCTAssertEqual(GeneratorMode.outputFileName("Types.swift", "Components"), "Types+Components.swift") diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift b/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift index d779f1086..8c59e9ed1 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift @@ -68,7 +68,7 @@ final class Test_GeneratorPipeline: Test_Core { config: Config(mode: .types, access: .public, namingStrategy: .defensive), diagnostics: AccumulatingDiagnosticCollector() ) - let outputs = try pipeline.renderSwiftFilesStage.run(structured).files + let outputs = try pipeline.renderSwiftFilesStage.run(structured) XCTAssertEqual(outputs.map(\.baseName), ["First.swift", "Second.swift"]) XCTAssertTrue(String(decoding: outputs[0].contents, as: UTF8.self).contains("enum First")) diff --git a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift index 6294d409c..7d52ab44b 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/CompatabilityTest.swift @@ -260,7 +260,7 @@ fileprivate extension CompatibilityTest { return try assertNoThrowWithValue(generator.run(input)) } } - return try await group.reduce(into: []) { $0.append(contentsOf: $1.files) } + return try await group.reduce(into: []) { $0.append(contentsOf: $1) } } } else { outputs = try GeneratorMode.allCases.flatMap { mode in @@ -268,7 +268,7 @@ fileprivate extension CompatibilityTest { config: Config(mode: mode, access: .public, namingStrategy: .defensive), diagnostics: diagnosticsCollector ) - return try assertNoThrowWithValue(generator.run(input)).files + return try assertNoThrowWithValue(generator.run(input)) } } XCTAssertEqual(Set(diagnosticsCollector.diagnostics.map(\.message)), expectedDiagnostics) diff --git a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift index cae6b5981..0aa3de36d 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/FileBasedReferenceTests.swift @@ -86,7 +86,7 @@ final class FileBasedReferenceTests: XCTestCase { config: referenceTest.asConfig, ignoredDiagnosticMessages: ignoredDiagnosticMessages ) - let generatedOutputSources = try generatorPipeline.run(input).files + let generatedOutputSources = try generatorPipeline.run(input) let generatedOutputSource = try XCTUnwrap(generatedOutputSources.first) XCTAssertEqual(generatedOutputSources.count, 1) diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 6ea8d60b6..0ad4ec3d9 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -6462,7 +6462,7 @@ final class SnippetBasedReferenceTests: XCTestCase { let document = try YAMLDecoder().decode(OpenAPI.Document.self, from: documentYAML) let translation = try translator.translateFile(parsedOpenAPI: document) try XCTAssertSwiftEquivalent( - XCTUnwrap(translation.file.contents.topComment), + XCTUnwrap(translation.files.first?.contents.topComment), """ // Generated by swift-openapi-generator, do not modify. // hello world diff --git a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift index 889a734ca..01c8a44a0 100644 --- a/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift +++ b/Tests/OpenAPIGeneratorTests/Test_GenerateOptions.swift @@ -151,14 +151,12 @@ final class Test_GenerateOptions: XCTestCase { types: fileSplitting: strategy: namespace - namespace: {} """ ) let options = try _GenerateOptions.parse(["openapi.yaml", "--config", configURL.path]) let config = try XCTUnwrap(options.loadedConfig()) XCTAssertEqual(config.output?.types?.fileSplitting?.strategy, .namespace) - XCTAssertEqual(config.output?.types?.fileSplitting?.namespace, .init()) } func testTypesFileSplittingOptionResolvesOutputOptions() throws { @@ -178,7 +176,6 @@ final class Test_GenerateOptions: XCTestCase { types: fileSplitting: strategy: namespace - namespace: {} """ ) let options = try _GenerateOptions.parse(["openapi.yaml", "--config", configURL.path]) From 3979529e94ac6f3abbb4e05e3b87f7df57f31e35 Mon Sep 17 00:00:00 2001 From: Nick Candello Date: Fri, 17 Jul 2026 10:48:58 -0400 Subject: [PATCH 5/5] Address multi-output generator API review feedback Remove the single-file core generator entry point so runGenerator returns the pipeline's final multi-file output directly. Also keep the filename composition helper internal and rename its variadic argument to avoid confusion with file extensions. --- .../_OpenAPIGeneratorCore/GeneratorMode.swift | 6 +++--- .../GeneratorPipeline.swift | 21 +------------------ .../runGenerator.swift | 2 +- ...est_TypesFileTranslatorFileSplitting.swift | 4 ++-- .../Test_GeneratorPipeline.swift | 4 ++-- 5 files changed, 9 insertions(+), 28 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift b/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift index bbb9c4a52..e10631312 100644 --- a/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift +++ b/Sources/_OpenAPIGeneratorCore/GeneratorMode.swift @@ -48,9 +48,9 @@ extension GeneratorMode { /// The Swift file names for all supported generator mode values. public static var allOutputFileNames: [String] { GeneratorMode.allCases.map(\.outputFileName) } - /// Returns a Swift output file name composed from the provided name components. - public static func outputFileName(_ name: String, _ extensionNames: String...) -> String { - let names = ([name] + extensionNames).map { name in + /// Returns a Swift output file name composed from the base name and type-name suffixes. + static func outputFileName(_ baseName: String, _ suffixes: String...) -> String { + let names = ([baseName] + suffixes).map { name in name.hasSuffix(".swift") ? String(name.dropLast(".swift".count)) : name } return names.joined(separator: "+") + ".swift" diff --git a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift index eda49c065..bd573cb4b 100644 --- a/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift +++ b/Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift @@ -80,27 +80,8 @@ struct GeneratorPipeline { /// - diagnostics: A collector to which the generator emits diagnostics. /// - Throws: When encountering a non-recoverable error. For recoverable /// issues, emits issues into the diagnostics collector. -/// - Returns: The raw contents of the generated Swift file. -public func runGenerator(input: InMemoryInputFile, config: Config, diagnostics: any DiagnosticCollector) throws - -> InMemoryOutputFile -{ - let outputFiles = try runGeneratorOutputs(input: input, config: config, diagnostics: diagnostics) - guard outputFiles.count == 1, let outputFile = outputFiles.first else { - throw GenericError(message: "Expected a single generated output file, got \(outputFiles.count).") - } - return outputFile -} - -/// Runs the generator logic with the specified inputs and returns every -/// generated Swift file. -/// - Parameters: -/// - input: The raw file contents of the OpenAPI document. -/// - config: A set of configuration values for the generator. -/// - diagnostics: A collector to which the generator emits diagnostics. -/// - Throws: When encountering a non-recoverable error. For recoverable -/// issues, emits issues into the diagnostics collector. /// - Returns: The raw contents of all generated Swift files. -public func runGeneratorOutputs( +public func runGenerator( input: InMemoryInputFile, config: Config, diagnostics: any DiagnosticCollector diff --git a/Sources/swift-openapi-generator/runGenerator.swift b/Sources/swift-openapi-generator/runGenerator.swift index 61ee7b063..e6b292bab 100644 --- a/Sources/swift-openapi-generator/runGenerator.swift +++ b/Sources/swift-openapi-generator/runGenerator.swift @@ -101,7 +101,7 @@ extension _Tool { isDryRun: Bool, diagnostics: any DiagnosticCollector ) throws { - let outputs = try _OpenAPIGeneratorCore.runGeneratorOutputs( + let outputs = try _OpenAPIGeneratorCore.runGenerator( input: .init(absolutePath: doc, contents: docData), config: config, diagnostics: diagnostics diff --git a/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift index 8dfabda2e..b20b8b685 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Hooks/Test_TypesFileTranslatorFileSplitting.swift @@ -23,7 +23,7 @@ final class Test_TypesFileTranslatorFileSplitting: Test_Core { contents: Data(Self.source.utf8) ) let diagnostics = AccumulatingDiagnosticCollector() - let outputs = try runGeneratorOutputs( + let outputs = try runGenerator( input: input, config: Self.splitConfig(strategy: .namespace), diagnostics: diagnostics @@ -65,7 +65,7 @@ final class Test_TypesFileTranslatorFileSplitting: Test_Core { contents: Data(Self.source.utf8) ) let diagnostics = AccumulatingDiagnosticCollector() - let outputs = try runGeneratorOutputs( + let outputs = try runGenerator( input: input, config: Config(mode: .types, access: .public, namingStrategy: .defensive), diagnostics: diagnostics diff --git a/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift b/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift index 8c59e9ed1..180107ba4 100644 --- a/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift +++ b/Tests/OpenAPIGeneratorCoreTests/Test_GeneratorPipeline.swift @@ -17,7 +17,7 @@ import XCTest final class Test_GeneratorPipeline: Test_Core { - func testRunGeneratorOutputsReturnsSingleFileByDefault() throws { + func testRunGeneratorReturnsSingleFileByDefault() throws { let source = """ openapi: "3.1.0" info: @@ -30,7 +30,7 @@ final class Test_GeneratorPipeline: Test_Core { contents: Data(source.utf8) ) let diagnostics = AccumulatingDiagnosticCollector() - let outputs = try runGeneratorOutputs( + let outputs = try runGenerator( input: input, config: Config(mode: .types, access: .public, namingStrategy: .defensive), diagnostics: diagnostics