Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Sources/_OpenAPIGeneratorCore/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -101,5 +106,6 @@ public struct Config: Sendable {
self.nameOverrides = nameOverrides
self.typeOverrides = typeOverrides
self.featureFlags = featureFlags
self.output = output
}
}
14 changes: 11 additions & 3 deletions Sources/_OpenAPIGeneratorCore/GeneratorMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 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"
}

/// Defines an order in which generators should be run.
var order: Int {
switch self {
Expand Down
28 changes: 18 additions & 10 deletions Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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
Expand Down Expand Up @@ -80,10 +80,14 @@ 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
{ try makeGeneratorPipeline(config: config, diagnostics: diagnostics).run(input) }
/// - Returns: The raw contents of all generated Swift files.
public func runGenerator(
input: InMemoryInputFile,
config: Config,
diagnostics: any DiagnosticCollector
) throws -> [InMemoryOutputFile] {
try makeGeneratorPipeline(config: config, diagnostics: diagnostics).run(input)
}

/// Creates a new pipeline instance.
/// - Parameters:
Expand All @@ -99,7 +103,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 },

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uses a fresh renderer for each file because TextBasedRenderer owns a stateful StringCodeWriter and would accumulate rendered swift for multiple file outputs

config: Config,
diagnostics: any DiagnosticCollector
) -> GeneratorPipeline {
Expand Down Expand Up @@ -128,7 +132,11 @@ func makeGeneratorPipeline(
),
renderSwiftFilesStage: .init(
preTransitionHooks: [],
transition: { input in try renderer.render(structured: input, config: config, diagnostics: diagnostics) },
transition: { input in
try input.files.map { namedFile in
try renderer().render(namedFile: namedFile, config: config, diagnostics: diagnostics)
}
},
postTransitionHooks: []
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ public import struct Foundation.URL
public import struct Foundation.Data
#endif

/// An in-memory file that contains the generated Swift code.
typealias RenderedSwiftRepresentation = InMemoryOutputFile
/// In-memory output files emitted by rendering a generator pipeline run.
typealias RenderedSwiftRepresentation = [InMemoryOutputFile]

/// An in-memory input file that contains the raw data of an OpenAPI document.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1087,8 +1087,8 @@ 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]
}

// MARK: - Conveniences
Expand Down
72 changes: 72 additions & 0 deletions Sources/_OpenAPIGeneratorCore/OutputOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//===----------------------------------------------------------------------===//
//
// 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?
Comment on lines +18 to +19

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we foresee a need to have this level of distinction in the config? Are we anticipating output options that only affect types that we wouldn't want to also apply to the client and server outputs?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think splitting client/server could be useful later for very large specs, but I see it as a separate strategy rather than this namespace strategy. Client/Server outputs are mostly flat operation methods that reference Operations types, while Types.swift owns the large generated declaration graph. So for this first slice I scoped the config under output.types to reflect the strategy we actually support today. I could also foresee some other type of output option in the future that doesn't fall within the realm of output file splitting for types.


/// 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

/// Creates a file splitting configuration.
/// - Parameter strategy: The strategy to use when splitting generated types across files.
public init(strategy: TypesFileSplittingStrategy) {
self.strategy = strategy
}
}

/// 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
}

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"),
]
}
}
}
6 changes: 3 additions & 3 deletions Sources/_OpenAPIGeneratorCore/Renderer/RendererProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
)
]
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
)
)
)
]
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,32 @@ struct TypesFileTranslator: FileTranslator {
]
)

return StructuredSwiftRepresentation(file: .init(name: GeneratorMode.types.outputFileName, contents: typesFile))
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(files: [.init(name: GeneratorMode.types.outputFileName, contents: typesFile)])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -110,6 +116,22 @@ additionalFileComments:
- "swiftlint:disable all"
```

To split generated types across multiple Swift files:

```yaml
generate:
- types
namingStrategy: idiomatic
output:
types:
fileSplitting:
strategy: namespace
```

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ extension _GenerateOptions {
let resolvedNameOverrides = resolvedNameOverrides(config)
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,
Expand All @@ -47,7 +53,8 @@ extension _GenerateOptions {
namingStrategy: resolvedNamingStragy,
nameOverrides: resolvedNameOverrides,
typeOverrides: resolvedTypeOverrides,
featureFlags: resolvedFeatureFlags
featureFlags: resolvedFeatureFlags,
output: resolvedOutputOptions
)
}
let (diagnostics, finalizeDiagnostics) = preparedDiagnosticsCollector(outputPath: diagnosticsOutputPath)
Expand All @@ -67,6 +74,7 @@ extension _GenerateOptions {
.sorted(by: { $0.key < $1.key })
.map { "\"\($0.key)\"->\"\($0.value)\"" }.joined(separator: ", "))
- Feature flags: \(resolvedFeatureFlags.isEmpty ? "<none>" : resolvedFeatureFlags.map(\.rawValue).joined(separator: ", "))
- Types file splitting: \(resolvedOutputOptions.types?.fileSplitting?.strategy.rawValue ?? "<none>")
- Output file names: \(sortedModes.map(\.outputFileName).joined(separator: ", "))
- Output directory: \(outputDirectory.path)
- Diagnostics output path: \(diagnosticsOutputPath?.path ?? "<none - logs to stderr>")
Expand Down
Loading
Loading