Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Command Plugin for generating the source code #98

Merged
merged 106 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
31cf1d5
try generator plugin
MahdiBM Jul 2, 2023
c5a404d
add new plugin to products
MahdiBM Jul 2, 2023
cfb128d
better debugging
MahdiBM Jul 2, 2023
b6e4ffe
Update plugin.swift
MahdiBM Jul 2, 2023
7fbde61
fixes
MahdiBM Jul 2, 2023
d65e0bf
correct working dir
MahdiBM Jul 2, 2023
45c8824
correct file-manipulation logic
MahdiBM Jul 2, 2023
133a3c1
better file manipulation
MahdiBM Jul 2, 2023
12fec26
generate empty files only for buildtool plugins
MahdiBM Jul 2, 2023
db96114
change directory name
MahdiBM Jul 2, 2023
7fd39a2
perform cleanup for previous builds
MahdiBM Jul 2, 2023
fbd69d1
better cleanup
MahdiBM Jul 2, 2023
d6005dc
refinements
MahdiBM Jul 2, 2023
02cda11
Update plugin.swift
MahdiBM Jul 2, 2023
7f7b33a
fixes for same-name file creations by plugins
MahdiBM Jul 2, 2023
11ee904
better configuration
MahdiBM Jul 2, 2023
0198e6c
fix arguments
MahdiBM Jul 2, 2023
7ada532
better cleanups
MahdiBM Jul 2, 2023
5460188
better cleanups
MahdiBM Jul 2, 2023
3cb098d
move plugins shared logic to shared files
MahdiBM Jul 2, 2023
42c73f3
rename and fix 'invocation-kind' arg to 'invoked-from'
MahdiBM Jul 3, 2023
253e17f
InvocationSource -> internal
MahdiBM Jul 3, 2023
31365ed
better (or worse) InvocationSource handling
MahdiBM Jul 3, 2023
617edfe
remove a lot of unneeded complexity
MahdiBM Jul 3, 2023
c381a0c
better message when running the generator
MahdiBM Jul 3, 2023
e2c085c
cleanup
MahdiBM Jul 4, 2023
ff25cba
fix passing bad arguments
MahdiBM Jul 4, 2023
ceaeb2d
even more removing unneeded complexity
MahdiBM Jul 4, 2023
9887742
refinements
MahdiBM Jul 4, 2023
8bd7363
Use existential any
MahdiBM Jul 4, 2023
ef1de07
Merge branch 'existential-any-compatibility' into generator-command-p…
MahdiBM Jul 4, 2023
e434021
move "any"s to the type names
MahdiBM Jul 11, 2023
bdcfd57
Revert "move "any"s to the type names"
MahdiBM Jul 11, 2023
8032cc6
Merge branch 'main' into generator-command-plugin
MahdiBM Jul 13, 2023
74c4e0a
Update Package.swift
MahdiBM Jul 13, 2023
d3567d9
Update Package.swift
MahdiBM Jul 13, 2023
4a762b0
fix build
MahdiBM Jul 13, 2023
0e009a0
command plugin by default run on all targets
MahdiBM Jul 13, 2023
4f934b2
Merge branch 'main' into generator-command-plugin
MahdiBM Jul 15, 2023
3b13bb4
Update Package.swift
MahdiBM Jul 15, 2023
20bdc27
fixes
MahdiBM Jul 15, 2023
9162042
more fixes
MahdiBM Jul 15, 2023
94f110c
better error handling
MahdiBM Jul 16, 2023
b9337d2
better error reporting
MahdiBM Jul 16, 2023
b2cfb69
minor refinements
MahdiBM Jul 16, 2023
3dd92f0
better errors
MahdiBM Jul 16, 2023
c14a4b9
better errors
MahdiBM Jul 16, 2023
0b92074
better arguments parsing
MahdiBM Jul 16, 2023
0d410a8
fixes
MahdiBM Jul 16, 2023
a92a4a0
better errors again
MahdiBM Jul 16, 2023
5d14ae1
Update plugin.swift
MahdiBM Jul 16, 2023
ccdf3c9
Update plugin.swift
MahdiBM Jul 16, 2023
e4b38cb
Update plugin.swift
MahdiBM Jul 16, 2023
914d5cf
Revert "Update plugin.swift"
MahdiBM Jul 16, 2023
02998c1
Revert "Update plugin.swift"
MahdiBM Jul 16, 2023
8faa974
Revert "Update plugin.swift"
MahdiBM Jul 16, 2023
5f55d7f
Revert "better errors again"
MahdiBM Jul 16, 2023
a27efce
Revert "fixes"
MahdiBM Jul 16, 2023
1f27d7d
Revert "better arguments parsing"
MahdiBM Jul 16, 2023
17ee64c
always run on all targets
MahdiBM Jul 16, 2023
39c0622
soundness
MahdiBM Jul 16, 2023
07e3163
soundness
MahdiBM Jul 16, 2023
c72516c
soundness
MahdiBM Jul 16, 2023
379abff
more soundness
MahdiBM Jul 16, 2023
62dba75
repair/add comments
MahdiBM Jul 17, 2023
d9282d6
better FileError
MahdiBM Jul 17, 2023
f7a834c
more refinements
MahdiBM Jul 17, 2023
691776a
soundness
MahdiBM Jul 17, 2023
cb70f29
FileError exclude unneeded info for conciseness
MahdiBM Jul 17, 2023
48120d5
Update Plugins/PluginsShared/PluginError.swift
MahdiBM Jul 17, 2023
125d5b5
apply some suggestions
MahdiBM Jul 17, 2023
9c95116
Merge branch 'main' into generator-command-plugin
MahdiBM Jul 17, 2023
5dbc8e5
Update PluginUtils.swift
MahdiBM Jul 17, 2023
fe01b88
get it working with passing only 1 target
MahdiBM Jul 18, 2023
6fc3ff0
run on in-package dependencies too
MahdiBM Jul 18, 2023
cb4349d
fixes
MahdiBM Jul 18, 2023
cc26a94
more refinements
MahdiBM Jul 19, 2023
8c441eb
Merge branch 'main' into generator-command-plugin
MahdiBM Jul 19, 2023
0bc0d6c
more on-point errors
MahdiBM Jul 19, 2023
a97bc8e
better errors
MahdiBM Jul 19, 2023
11ee766
more minor refinements
MahdiBM Jul 19, 2023
75b534e
soundness?
MahdiBM Jul 19, 2023
fefdde3
more soundness
MahdiBM Jul 19, 2023
80521ce
minor refinements
MahdiBM Jul 19, 2023
dd70138
Update Package.swift
MahdiBM Jul 19, 2023
74efe1d
Update Package.swift
MahdiBM Jul 19, 2023
1cffe16
Update Package.swift
MahdiBM Jul 19, 2023
3962652
partially apply requested changes
MahdiBM Jul 19, 2023
26fefd5
apply remaining requested changes
MahdiBM Jul 20, 2023
aa7a8a7
fix wrong variable
MahdiBM Jul 20, 2023
653dc4a
minor improvement
MahdiBM Jul 20, 2023
b903f54
minor refinement
MahdiBM Jul 20, 2023
0e7b1ee
soundness?
MahdiBM Jul 20, 2023
4e37d6e
minor change
MahdiBM Jul 20, 2023
8fb9775
apply logging suggestions
MahdiBM Jul 20, 2023
e9be1e7
Update plugin.swift
MahdiBM Jul 20, 2023
29b6301
try ✅ in logs
MahdiBM Jul 20, 2023
d8e5a47
Update plugin.swift
MahdiBM Jul 20, 2023
1b5286f
remove extra space
MahdiBM Jul 20, 2023
7c9cee3
swift-format to the rescue of me from itself!
MahdiBM Jul 21, 2023
d4b7756
exclude directory
MahdiBM Jul 21, 2023
a2b0ab4
another try to exclude directory from headers-check script
MahdiBM Jul 21, 2023
96398f4
headers script another try
MahdiBM Jul 21, 2023
a4f8584
fix the script itself (the check should still fail)
MahdiBM Jul 21, 2023
4b4d0f8
final soundness fix hopefully?
MahdiBM Jul 21, 2023
a611ae2
Merge branch 'main' into generator-command-plugin
czechboy0 Jul 21, 2023
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
20 changes: 20 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ let package = Package(
products: [
.executable(name: "swift-openapi-generator", targets: ["swift-openapi-generator"]),
.plugin(name: "OpenAPIGenerator", targets: ["OpenAPIGenerator"]),
.plugin(name: "OpenAPIGeneratorCommand", targets: ["OpenAPIGeneratorCommand"]),
.library(name: "_OpenAPIGeneratorCore", targets: ["_OpenAPIGeneratorCore"]),
],
dependencies: [
Expand Down Expand Up @@ -151,5 +152,24 @@ let package = Package(
"swift-openapi-generator"
]
),

// Command Plugin
.plugin(
name: "OpenAPIGeneratorCommand",
capability: .command(
intent: .custom(
verb: "generate-code-from-openapi",
description: "Generate Swift code from an OpenAPI document."
),
permissions: [
.writeToPackageDirectory(
reason: "To write the generated Swift files back into the source directory of the package."
)
]
),
dependencies: [
"swift-openapi-generator"
]
),
]
)
1 change: 1 addition & 0 deletions Plugins/OpenAPIGenerator/PluginsShared
81 changes: 15 additions & 66 deletions Plugins/OpenAPIGenerator/plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,81 +16,30 @@ import Foundation

@main
struct SwiftOpenAPIGeneratorPlugin {
enum Error: Swift.Error, CustomStringConvertible, LocalizedError {
case incompatibleTarget(targetName: String)
case noConfigFound(targetName: String)
case noDocumentFound(targetName: String)
case multiConfigFound(targetName: String, files: [Path])
case multiDocumentFound(targetName: String, files: [Path])

var description: String {
switch self {
case .incompatibleTarget(let targetName):
return
"Incompatible target called '\(targetName)'. Only Swift source targets can be used with the Swift OpenAPI generator plugin."
case .noConfigFound(let targetName):
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 .noDocumentFound(let targetName):
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 .multiConfigFound(let targetName, let files):
return
"Multiple config files found in the target named '\(targetName)', but exactly one is required. Found \(files.map(\.description).joined(separator: " "))."
case .multiDocumentFound(let targetName, let files):
return
"Multiple OpenAPI documents found in the target named '\(targetName)', but exactly one is required. Found \(files.map(\.description).joined(separator: " "))."
}
}

var errorDescription: String? {
description
}
}

private var supportedConfigFiles: Set<String> { Set(["yaml", "yml"].map { "openapi-generator-config." + $0 }) }
private var supportedDocFiles: Set<String> { Set(["yaml", "yml", "json"].map { "openapi." + $0 }) }

func createBuildCommands(
pluginWorkDirectory: PackagePlugin.Path,
tool: (String) throws -> PackagePlugin.PluginContext.Tool,
pluginWorkDirectory: Path,
tool: (String) throws -> PluginContext.Tool,
sourceFiles: FileList,
targetName: String
) throws -> [Command] {
let inputFiles = sourceFiles
let matchedConfigs = inputFiles.filter { supportedConfigFiles.contains($0.path.lastComponent) }.map(\.path)
guard matchedConfigs.count > 0 else {
throw Error.noConfigFound(targetName: targetName)
}
guard matchedConfigs.count == 1 else {
throw Error.multiConfigFound(targetName: targetName, files: matchedConfigs)
}
let config = matchedConfigs[0]
let inputs = try PluginUtils.validateInputs(
workingDirectory: pluginWorkDirectory,
tool: tool,
sourceFiles: sourceFiles,
targetName: targetName,
pluginSource: .build
)

let matchedDocs = inputFiles.filter { supportedDocFiles.contains($0.path.lastComponent) }.map(\.path)
guard matchedDocs.count > 0 else {
throw Error.noDocumentFound(targetName: targetName)
}
guard matchedDocs.count == 1 else {
throw Error.multiDocumentFound(targetName: targetName, files: matchedDocs)
}
let doc = matchedDocs[0]
let genSourcesDir = pluginWorkDirectory.appending("GeneratedSources")
let outputFiles: [Path] = GeneratorMode.allCases.map { genSourcesDir.appending($0.outputFileName) }
let outputFiles: [Path] = GeneratorMode.allCases.map { inputs.genSourcesDir.appending($0.outputFileName) }
return [
.buildCommand(
displayName: "Running swift-openapi-generator",
executable: try tool("swift-openapi-generator").path,
arguments: [
"generate", "\(doc)",
"--config", "\(config)",
"--output-directory", "\(genSourcesDir)",
"--is-plugin-invocation",
],
executable: inputs.tool.path,
arguments: inputs.arguments,
environment: [:],
inputFiles: [
config,
doc,
inputs.config,
inputs.doc,
],
outputFiles: outputFiles
)
Expand All @@ -104,7 +53,7 @@ extension SwiftOpenAPIGeneratorPlugin: BuildToolPlugin {
target: Target
) async throws -> [Command] {
guard let swiftTarget = target as? SwiftSourceModuleTarget else {
throw Error.incompatibleTarget(targetName: target.name)
throw PluginError.incompatibleTarget(name: target.name)
}
return try createBuildCommands(
pluginWorkDirectory: context.pluginWorkDirectory,
Expand Down
1 change: 1 addition & 0 deletions Plugins/OpenAPIGeneratorCommand/PluginsShared
105 changes: 105 additions & 0 deletions Plugins/OpenAPIGeneratorCommand/plugin.swift
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
MahdiBM marked this conversation as resolved.
Show resolved Hide resolved
process.environment = [:]
try process.run()
MahdiBM marked this conversation as resolved.
Show resolved Hide resolved
process.waitUntilExit()
MahdiBM marked this conversation as resolved.
Show resolved Hide resolved
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))
}
}
}
145 changes: 145 additions & 0 deletions Plugins/PluginsShared/PluginError.swift
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
}
}
1 change: 1 addition & 0 deletions Plugins/PluginsShared/PluginSource.swift
Loading