Skip to content

Commit

Permalink
Merge pull request #33 from amzn/external_model
Browse files Browse the repository at this point in the history
Add the ability in the plugins to use a model defined in a separate package
  • Loading branch information
tachyonics authored Aug 4, 2022
2 parents 96fa351 + 9124670 commit ea83077
Show file tree
Hide file tree
Showing 5 changed files with 470 additions and 21 deletions.
130 changes: 124 additions & 6 deletions Plugins/SmokeFrameworkGenerateClient/plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,66 @@ import Foundation

private let targetSuffix = "Client"

enum PluginError: Error {
case unknownModelPackageDependency(packageName: String)
case unknownModelTargetDependency(packageName: String, targetName: String)
case sourceModuleTargetRequired(packageName: String, targetName: String, type: Target.Type)
case unknownModelFilePath(packageName: String, targetName: String, fileName: String)
case missingConfigFile(expectedPath: String)
case missingModelLocation(target: String)
}

@main
struct SmokeFrameworkGenerateClientPlugin: BuildToolPlugin {
struct ModelLocation: Decodable {
let modelProductDependency: String?
let modelTargetDependency: String?
let modelFilePath: String
}

struct ModelLocations: Decodable {
let `default`: ModelLocation?
let targetMap: [String: ModelLocation]

enum CodingKeys: String, CodingKey {
case `default`
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.`default` = try values.decodeIfPresent(ModelLocation.self, forKey: .default)
self.targetMap = try [String: ModelLocation].init(from: decoder)
}
}

struct SmokeFrameworkCodeGen: Decodable {
let baseName: String
let modelLocations: ModelLocations?
let modelFilePath: String? // legacy location
}

/// This plugin's implementation returns a single build command which
/// calls `SmokeFrameworkApplicationGenerate` to generate the service client.
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
// get the generator tool
let smokeFrameworkApplicationGenerateTool = try context.tool(named: "SmokeFrameworkApplicationGenerate")
let sourcesDirectory = context.pluginWorkDirectory.appending("Sources")

var baseName = target.name
if baseName.hasSuffix(targetSuffix) {
baseName = String(baseName.dropLast(targetSuffix.count))
let inputFile = context.package.directory.appending("smoke-framework-codegen.json")
let configFilePath = inputFile.string
let configFile = FileHandle(forReadingAtPath: configFilePath)

guard let configData = configFile?.readDataToEndOfFile() else {
throw PluginError.missingConfigFile(expectedPath: configFilePath)
}

let config = try JSONDecoder().decode(SmokeFrameworkCodeGen.self, from: configData)

let baseName = config.baseName

let modelFilePathOverride = try getModelFilePathOverride(target: target, config: config,
baseFilePath: context.package.directory)

let clientDirectory = sourcesDirectory.appending("\(baseName)\(targetSuffix)")

let clientFiles = ["APIGateway\(baseName)\(targetSuffix).swift",
Expand All @@ -29,8 +76,6 @@ struct SmokeFrameworkGenerateClientPlugin: BuildToolPlugin {
"Throwing\(baseName)\(targetSuffix).swift"]
let clientOutputPaths = clientFiles.map { clientDirectory.appending($0) }

let inputFile = context.package.directory.appending("smoke-framework-codegen.json")

// Specifying the input and output paths lets the build system know
// when to invoke the command.
let inputFiles = [inputFile]
Expand All @@ -40,7 +85,8 @@ struct SmokeFrameworkGenerateClientPlugin: BuildToolPlugin {
let commandArgs = [
"--base-file-path", context.package.directory.description,
"--base-output-file-path", context.pluginWorkDirectory.description,
"--generation-type", "codeGenClient"
"--generation-type", "codeGenClient",
"--model-path", modelFilePathOverride
]

// Append a command containing the information we generated.
Expand All @@ -53,4 +99,76 @@ struct SmokeFrameworkGenerateClientPlugin: BuildToolPlugin {

return [command]
}

private func getModelFilePathOverride(target: Target, config: SmokeFrameworkCodeGen,
baseFilePath: PackagePlugin.Path) throws -> String {
// find the model for the current target
let targetModelLocationOptional = config.modelLocations?.targetMap[target.name]

let modelLocation: ModelLocation
if let theModelLocation = targetModelLocationOptional {
modelLocation = theModelLocation
} else if let theModelLocation = config.modelLocations?.default {
modelLocation = theModelLocation
} else if let modelFilePath = config.modelFilePath {
modelLocation = ModelLocation(modelProductDependency: nil, modelTargetDependency: nil, modelFilePath: modelFilePath)
} else {
throw PluginError.missingModelLocation(target: target.name)
}

return try getModelFilePathOverride(target: target, modelLocation: modelLocation, baseFilePath: baseFilePath)
}

private func getModelFilePathOverride(target: Target, modelLocation: ModelLocation,
baseFilePath: PackagePlugin.Path) throws -> String {
// if the model is in a dependency
if let modelProductDependency = modelLocation.modelProductDependency {
let dependencies: [Product] = target.dependencies.compactMap { dependency in
if case .product(let product) = dependency, product.name == modelProductDependency {
return product
}

return nil
}

// if there is no such dependency
guard let modelProduct = dependencies.first else {
throw PluginError.unknownModelPackageDependency(packageName: modelProductDependency)
}

let modelTargetDependency = modelLocation.modelTargetDependency ?? modelProductDependency

let filteredTargets = modelProduct.targets.filter { $0.name == modelTargetDependency }
guard let modelTarget = filteredTargets.first else {
throw PluginError.unknownModelTargetDependency(packageName: modelProductDependency,
targetName: modelTargetDependency)
}

guard let modelTarget = modelTarget as? SourceModuleTarget else {
throw PluginError.sourceModuleTargetRequired(packageName: modelProductDependency,
targetName: modelTargetDependency,
type: type(of: modelTarget))
}

let targetDirectory: String
let rawTargetDirectory = modelTarget.directory.string
if !rawTargetDirectory.hasSuffix("/") {
targetDirectory = "\(rawTargetDirectory)/"
} else {
targetDirectory = rawTargetDirectory
}

let filteredFiles = modelTarget.sourceFiles.filter { $0.path.string.dropFirst(targetDirectory.count) == modelLocation.modelFilePath }
guard let modelFile = filteredFiles.first else {
throw PluginError.unknownModelFilePath(packageName: modelProductDependency,
targetName: modelTargetDependency,
fileName: modelLocation.modelFilePath)
}

return modelFile.path.string
}

// the model is local to the package
return baseFilePath.appending(modelLocation.modelFilePath).description
}
}
131 changes: 124 additions & 7 deletions Plugins/SmokeFrameworkGenerateHttp1/plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,65 @@ import Foundation

private let targetSuffix = "OperationsHTTP1"

enum PluginError: Error {
case unknownModelPackageDependency(packageName: String)
case unknownModelTargetDependency(packageName: String, targetName: String)
case sourceModuleTargetRequired(packageName: String, targetName: String, type: Target.Type)
case unknownModelFilePath(packageName: String, targetName: String, fileName: String)
case missingConfigFile(expectedPath: String)
case missingModelLocation(target: String)
}

@main
struct SmokeFrameworkGenerateHttp1Plugin: BuildToolPlugin {
struct ModelLocation: Decodable {
let modelProductDependency: String?
let modelTargetDependency: String?
let modelFilePath: String
}

struct ModelLocations: Decodable {
let `default`: ModelLocation?
let targetMap: [String: ModelLocation]

enum CodingKeys: String, CodingKey {
case `default`
}

init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.`default` = try values.decodeIfPresent(ModelLocation.self, forKey: .default)
self.targetMap = try [String: ModelLocation].init(from: decoder)
}
}

struct SmokeFrameworkCodeGen: Decodable {
let baseName: String
let modelLocations: ModelLocations?
let modelFilePath: String? // legacy location
}

/// This plugin's implementation returns a single build command which
/// calls `SmokeFrameworkApplicationGenerate` to generate the http1 protocol integration.
func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
// get the generator tool
let smokeFrameworkApplicationGenerateTool = try context.tool(named: "SmokeFrameworkApplicationGenerate")
let sourcesDirectory = context.pluginWorkDirectory.appending("Sources")

var baseName = target.name
if baseName.hasSuffix(targetSuffix) {
baseName = String(baseName.dropLast(targetSuffix.count))
let inputFile = context.package.directory.appending("smoke-framework-codegen.json")
let configFilePath = inputFile.string
let configFile = FileHandle(forReadingAtPath: configFilePath)

guard let configData = configFile?.readDataToEndOfFile() else {
throw PluginError.missingConfigFile(expectedPath: configFilePath)
}

let config = try JSONDecoder().decode(SmokeFrameworkCodeGen.self, from: configData)

let baseName = config.baseName

let modelFilePathOverride = try getModelFilePathOverride(target: target, config: config,
baseFilePath: context.package.directory)

let http1Directory = sourcesDirectory.appending("\(baseName)\(targetSuffix)")

Expand All @@ -24,9 +70,7 @@ struct SmokeFrameworkGenerateHttp1Plugin: BuildToolPlugin {
"\(baseName)OperationsHTTPOutput.swift",
"\(baseName)PerInvocationContextInitializerProtocol.swift"]
let http1OutputPaths = http1Files.map { http1Directory.appending($0) }

let inputFile = context.package.directory.appending("smoke-framework-codegen.json")


// Specifying the input and output paths lets the build system know
// when to invoke the command.
let inputFiles = [inputFile]
Expand All @@ -36,7 +80,8 @@ struct SmokeFrameworkGenerateHttp1Plugin: BuildToolPlugin {
let commandArgs = [
"--base-file-path", context.package.directory.description,
"--base-output-file-path", context.pluginWorkDirectory.description,
"--generation-type", "codeGenHttp1"
"--generation-type", "codeGenHttp1",
"--model-path", modelFilePathOverride
]

// Append a command containing the information we generated.
Expand All @@ -49,4 +94,76 @@ struct SmokeFrameworkGenerateHttp1Plugin: BuildToolPlugin {

return [command]
}

private func getModelFilePathOverride(target: Target, config: SmokeFrameworkCodeGen,
baseFilePath: PackagePlugin.Path) throws -> String {
// find the model for the current target
let targetModelLocationOptional = config.modelLocations?.targetMap[target.name]

let modelLocation: ModelLocation
if let theModelLocation = targetModelLocationOptional {
modelLocation = theModelLocation
} else if let theModelLocation = config.modelLocations?.default {
modelLocation = theModelLocation
} else if let modelFilePath = config.modelFilePath {
modelLocation = ModelLocation(modelProductDependency: nil, modelTargetDependency: nil, modelFilePath: modelFilePath)
} else {
throw PluginError.missingModelLocation(target: target.name)
}

return try getModelFilePathOverride(target: target, modelLocation: modelLocation, baseFilePath: baseFilePath)
}

private func getModelFilePathOverride(target: Target, modelLocation: ModelLocation,
baseFilePath: PackagePlugin.Path) throws -> String {
// if the model is in a dependency
if let modelProductDependency = modelLocation.modelProductDependency {
let dependencies: [Product] = target.dependencies.compactMap { dependency in
if case .product(let product) = dependency, product.name == modelProductDependency {
return product
}

return nil
}

// if there is no such dependency
guard let modelProduct = dependencies.first else {
throw PluginError.unknownModelPackageDependency(packageName: modelProductDependency)
}

let modelTargetDependency = modelLocation.modelTargetDependency ?? modelProductDependency

let filteredTargets = modelProduct.targets.filter { $0.name == modelTargetDependency }
guard let modelTarget = filteredTargets.first else {
throw PluginError.unknownModelTargetDependency(packageName: modelProductDependency,
targetName: modelTargetDependency)
}

guard let modelTarget = modelTarget as? SourceModuleTarget else {
throw PluginError.sourceModuleTargetRequired(packageName: modelProductDependency,
targetName: modelTargetDependency,
type: type(of: modelTarget))
}

let targetDirectory: String
let rawTargetDirectory = modelTarget.directory.string
if !rawTargetDirectory.hasSuffix("/") {
targetDirectory = "\(rawTargetDirectory)/"
} else {
targetDirectory = rawTargetDirectory
}

let filteredFiles = modelTarget.sourceFiles.filter { $0.path.string.dropFirst(targetDirectory.count) == modelLocation.modelFilePath }
guard let modelFile = filteredFiles.first else {
throw PluginError.unknownModelFilePath(packageName: modelProductDependency,
targetName: modelTargetDependency,
fileName: modelLocation.modelFilePath)
}

return modelFile.path.string
}

// the model is local to the package
return baseFilePath.appending(modelLocation.modelFilePath).description
}
}
Loading

0 comments on commit ea83077

Please sign in to comment.