-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Command Plugin for generating the source code (#98)
### Motivation This PR adds the option to use the package as a Command plugin instead of a BuildTool plugin. This benefits those who use heavy OpenAPI documents, and prefer not to have to wait for an extra round of OpenAPI code generation which can be accidentally triggered at times, for example if you clean your build folder. The whole idea of creating this Command plugin came after @czechboy0 's comment here: #96 (comment) ### Modifications Generally, add a Command plugin target to the package, plus modifying the functions etc... to match/allow this addition. ### Result There is a new Command plugin, and users can choose between the Command plugin and the BuildTool plugin at will. ### Test Plan As visible in the PR discussions below, we've done enough manual-testing of the Command plugin. --------- Co-authored-by: Honza Dvorsky <[email protected]> Co-authored-by: Honza Dvorsky <[email protected]>
- Loading branch information
1 parent
b6d82cd
commit 4198dc6
Showing
14 changed files
with
512 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../Plugins/PluginsShared |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../Plugins/PluginsShared |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 PackagePlugin | ||
import Foundation | ||
|
||
@main | ||
struct SwiftOpenAPIGeneratorPlugin { | ||
func runCommand( | ||
targetWorkingDirectory: Path, | ||
tool: (String) throws -> PluginContext.Tool, | ||
sourceFiles: FileList, | ||
targetName: String | ||
) throws { | ||
let inputs = try PluginUtils.validateInputs( | ||
workingDirectory: targetWorkingDirectory, | ||
tool: tool, | ||
sourceFiles: sourceFiles, | ||
targetName: targetName, | ||
pluginSource: .command | ||
) | ||
|
||
let toolUrl = URL(fileURLWithPath: inputs.tool.path.string) | ||
let process = Process() | ||
process.executableURL = toolUrl | ||
process.arguments = inputs.arguments | ||
process.environment = [:] | ||
try process.run() | ||
process.waitUntilExit() | ||
guard process.terminationStatus == 0 else { | ||
throw PluginError.generatorFailure(targetName: targetName) | ||
} | ||
} | ||
} | ||
|
||
extension SwiftOpenAPIGeneratorPlugin: CommandPlugin { | ||
func performCommand( | ||
context: PluginContext, | ||
arguments: [String] | ||
) async throws { | ||
let targetNameArguments = arguments.filter({ $0 != "--target" }) | ||
let targets: [Target] | ||
if targetNameArguments.isEmpty { | ||
targets = context.package.targets | ||
} else { | ||
let matchingTargets = try context.package.targets(named: targetNameArguments) | ||
let packageTargets = Set(context.package.targets.map(\.id)) | ||
let withLocalDependencies = matchingTargets.flatMap { [$0] + $0.recursiveTargetDependencies } | ||
.filter { packageTargets.contains($0.id) } | ||
let enumeratedKeyValues = withLocalDependencies.map(\.id).enumerated() | ||
.map { (key: $0.element, value: $0.offset) } | ||
let indexLookupTable = Dictionary(enumeratedKeyValues, uniquingKeysWith: { l, _ in l }) | ||
let groupedByID = Dictionary(grouping: withLocalDependencies, by: \.id) | ||
let sortedUniqueTargets = groupedByID.map(\.value[0]) | ||
.sorted { indexLookupTable[$0.id, default: 0] < indexLookupTable[$1.id, default: 0] } | ||
targets = sortedUniqueTargets | ||
} | ||
|
||
guard !targets.isEmpty else { | ||
throw PluginError.noTargetsMatchingTargetNames(targetNameArguments) | ||
} | ||
|
||
var hadASuccessfulRun = false | ||
|
||
for target in targets { | ||
print("Considering target '\(target.name)':") | ||
guard let swiftTarget = target as? SwiftSourceModuleTarget else { | ||
print("- Not a swift source module. Can't generate OpenAPI code.") | ||
continue | ||
} | ||
do { | ||
print("- Trying OpenAPI code generation.") | ||
try runCommand( | ||
targetWorkingDirectory: target.directory, | ||
tool: context.tool, | ||
sourceFiles: swiftTarget.sourceFiles, | ||
targetName: target.name | ||
) | ||
print("- ✅ OpenAPI code generation for target '\(target.name)' successfully completed.") | ||
hadASuccessfulRun = true | ||
} catch let error as PluginError { | ||
if error.isMisconfigurationError { | ||
print("- OpenAPI code generation failed with error.") | ||
throw error | ||
} else { | ||
print("- Stopping because target isn't configured for OpenAPI code generation.") | ||
} | ||
} | ||
} | ||
|
||
guard hadASuccessfulRun else { | ||
throw PluginError.noTargetsWithExpectedFiles(targetNames: targets.map(\.name)) | ||
} | ||
} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 PackagePlugin | ||
import Foundation | ||
|
||
enum PluginError: Swift.Error, CustomStringConvertible, LocalizedError { | ||
case incompatibleTarget(name: String) | ||
case generatorFailure(targetName: String) | ||
case noTargetsWithExpectedFiles(targetNames: [String]) | ||
case noTargetsMatchingTargetNames([String]) | ||
case fileErrors([FileError]) | ||
|
||
var description: String { | ||
switch self { | ||
case .incompatibleTarget(let name): | ||
return | ||
"Incompatible target called '\(name)'. Only Swift source targets can be used with the Swift OpenAPI Generator plugin." | ||
case .generatorFailure(let targetName): | ||
return "The generator failed to generate OpenAPI files for target '\(targetName)'." | ||
case .noTargetsWithExpectedFiles(let targetNames): | ||
let fileNames = FileError.Kind.allCases.map(\.name) | ||
.joined(separator: ", ", lastSeparator: " or ") | ||
let targetNames = targetNames.joined(separator: ", ", lastSeparator: " and ") | ||
return | ||
"Targets with names \(targetNames) don't contain any \(fileNames) files with expected names. See documentation for details." | ||
case .noTargetsMatchingTargetNames(let targetNames): | ||
let targetNames = targetNames.joined(separator: ", ", lastSeparator: " and ") | ||
return "Found no targets with names \(targetNames)." | ||
case .fileErrors(let fileErrors): | ||
return "Issues with required files: \(fileErrors.map(\.description).joined(separator: ", and"))." | ||
} | ||
} | ||
|
||
var errorDescription: String? { | ||
description | ||
} | ||
|
||
/// The error is definitely due to misconfiguration of a target. | ||
var isMisconfigurationError: Bool { | ||
switch self { | ||
case .incompatibleTarget: | ||
return false | ||
case .generatorFailure: | ||
return false | ||
case .noTargetsWithExpectedFiles: | ||
return false | ||
case .noTargetsMatchingTargetNames: | ||
return false | ||
case .fileErrors(let errors): | ||
return errors.isMisconfigurationError | ||
} | ||
} | ||
} | ||
|
||
struct FileError: Swift.Error, CustomStringConvertible, LocalizedError { | ||
|
||
/// The kind of the file. | ||
enum Kind: CaseIterable { | ||
/// Config file. | ||
case config | ||
/// OpenAPI document file. | ||
case document | ||
|
||
var name: String { | ||
switch self { | ||
case .config: | ||
return "config" | ||
case .document: | ||
return "OpenAPI document" | ||
} | ||
} | ||
} | ||
|
||
/// Encountered issue. | ||
enum Issue { | ||
/// File wasn't found. | ||
case noFilesFound | ||
/// More than 1 file found. | ||
case multipleFilesFound(files: [Path]) | ||
|
||
/// The error is definitely due to misconfiguration of a target. | ||
var isMisconfigurationError: Bool { | ||
switch self { | ||
case .noFilesFound: | ||
return false | ||
case .multipleFilesFound: | ||
return true | ||
} | ||
} | ||
} | ||
|
||
let targetName: String | ||
let fileKind: Kind | ||
let issue: Issue | ||
|
||
var description: String { | ||
switch fileKind { | ||
case .config: | ||
switch issue { | ||
case .noFilesFound: | ||
return | ||
"No config file found in the target named '\(targetName)'. Add a file called 'openapi-generator-config.yaml' or 'openapi-generator-config.yml' to the target's source directory. See documentation for details." | ||
case .multipleFilesFound(let files): | ||
return | ||
"Multiple config files found in the target named '\(targetName)', but exactly one is expected. Found \(files.map(\.description).joined(separator: " "))." | ||
} | ||
case .document: | ||
switch issue { | ||
case .noFilesFound: | ||
return | ||
"No OpenAPI document found in the target named '\(targetName)'. Add a file called 'openapi.yaml', 'openapi.yml' or 'openapi.json' (can also be a symlink) to the target's source directory. See documentation for details." | ||
case .multipleFilesFound(let files): | ||
return | ||
"Multiple OpenAPI documents found in the target named '\(targetName)', but exactly one is expected. Found \(files.map(\.description).joined(separator: " "))." | ||
} | ||
} | ||
} | ||
|
||
var errorDescription: String? { | ||
description | ||
} | ||
} | ||
|
||
private extension Array where Element == FileError { | ||
/// The error is definitely due to misconfiguration of a target. | ||
var isMisconfigurationError: Bool { | ||
// If errors for both files exist and none is a "Misconfiguration Error" then the | ||
// error can be related to a target that isn't supposed to be generator compatible at all. | ||
if count == FileError.Kind.allCases.count, self.allSatisfy({ !$0.issue.isMisconfigurationError }) { | ||
return false | ||
} | ||
return true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
../../Sources/swift-openapi-generator/PluginSource.swift |
Oops, something went wrong.