Skip to content

Commit

Permalink
Add support for filtering OpenAPI document (#319)
Browse files Browse the repository at this point in the history
### Motivation

When generating client code, Swift OpenAPI Generator generates code for
the entire OpenAPI document, even if the user only makes use of a subset
of its types and operations.

Generating code that is unused constitutes overhead for the adopter:
- The overhead of generating code for unused types and operations
- The overhead of compiling the generated code
- The overhead of unused code in the users codebase (AOT generation)

This is particularly noticeable when working with a small subset of a
large API, which can result in O(100k) lines of unused code and long
generation and compile times.

For a more detailed motivation and design, see the proposal in #303.

### Modifications

- Add document filter to the generator config.
- Run filter as a post-transition hook in the generator pipeline after
parsing the document.
- Provide a CLI command that outputs the filtered document to stdout.

### Result

Users can filter a document before code-generation. For large APIs, this
can result in >90% speedup (see proposal).

### Test Plan

- Unit tests.

---------

Signed-off-by: Si Beaumont <beaumont@apple.com>
Co-authored-by: Honza Dvorsky <honza@apple.com>
  • Loading branch information
simonjbeaumont and czechboy0 committed Oct 13, 2023
1 parent 3f8542b commit 4c8ed5c
Show file tree
Hide file tree
Showing 12 changed files with 935 additions and 6 deletions.
6 changes: 6 additions & 0 deletions Sources/_OpenAPIGeneratorCore/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,27 @@ public struct Config: Sendable {
/// Additional imports to add to each generated file.
public var additionalImports: [String]

/// Filter to apply to the OpenAPI document before generation.
public var filter: DocumentFilter?

/// Additional pre-release features to enable.
public var featureFlags: FeatureFlags

/// Creates a configuration with the specified generator mode and imports.
/// - Parameters:
/// - mode: The mode to use for generation.
/// - additionalImports: Additional imports to add to each generated file.
/// - filter: Filter to apply to the OpenAPI document before generation.
/// - featureFlags: Additional pre-release features to enable.
public init(
mode: GeneratorMode,
additionalImports: [String] = [],
filter: DocumentFilter? = nil,
featureFlags: FeatureFlags = []
) {
self.mode = mode
self.additionalImports = additionalImports
self.filter = filter
self.featureFlags = featureFlags
}
}
Expand Down
8 changes: 7 additions & 1 deletion Sources/_OpenAPIGeneratorCore/GeneratorPipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,19 @@ func makeGeneratorPipeline(
)
},
postTransitionHooks: [
{ document in
guard let documentFilter = config.filter else {
return document
}
return try documentFilter.filter(document)
},
{ doc in
let validationDiagnostics = try validator(doc, config)
for diagnostic in validationDiagnostics {
diagnostics.emit(diagnostic)
}
return doc
}
},
]
),
translateOpenAPIToStructuredSwiftStage: .init(
Expand Down
76 changes: 76 additions & 0 deletions Sources/_OpenAPIGeneratorCore/Hooks/DocumentFilter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

@preconcurrency import OpenAPIKit

/// Rules used to filter an OpenAPI document.
public struct DocumentFilter: Codable, Sendable {

/// Operations with these operation IDs will be included in the filter.
public var operations: [String]?

/// Operations tagged with these tags will be included in the filter.
public var tags: [String]?

/// These paths will be included in the filter.
public var paths: [OpenAPI.Path]?

/// These (additional) schemas will be included in the filter.
///
/// These schemas are included in addition to the transitive closure of schema dependencies of
/// the paths included in the filter.
public var schemas: [String]?

/// Create a new DocumentFilter.
///
/// - Parameters:
/// - operations: Operations with these IDs will be included in the filter.
/// - tags: Operations tagged with these tags will be included in the filter.
/// - paths: These paths will be included in the filter.
/// - schemas: These (additional) schemas will be included in the filter.
public init(
operations: [String] = [],
tags: [String] = [],
paths: [OpenAPI.Path] = [],
schemas: [String] = []
) {
self.operations = operations
self.tags = tags
self.paths = paths
self.schemas = schemas
}

/// Filter an OpenAPI document.
///
/// - Parameter document: The OpenAPI document to filter.
/// - Returns: The filtered document.
/// - Throws: If any requested document components do not exist in the original document.
/// - Throws: If any dependencies of the requested document components cannot be resolved.
public func filter(_ document: OpenAPI.Document) throws -> OpenAPI.Document {
var builder = FilteredDocumentBuilder(document: document)
for tag in tags ?? [] {
try builder.includeOperations(tagged: tag)
}
for operationID in operations ?? [] {
try builder.includeOperation(operationID: operationID)
}
for path in paths ?? [] {
try builder.includePath(path)
}
for schema in schemas ?? [] {
try builder.includeSchema(schema)
}
return try builder.filter()
}
}

0 comments on commit 4c8ed5c

Please sign in to comment.