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

feat(amplify-xcode): generate JSON schema #1080

Merged
merged 12 commits into from
Mar 5, 2021
Merged
4,655 changes: 2,325 additions & 2,330 deletions AmplifyTools/AmplifyXcode/AmplifyXcode.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -26,43 +26,25 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES"
onlyGenerateCoverageForSpecifiedTargets = "YES">
<CodeCoverageTargets>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeCore"
BuildableName = "AmplifyXcodeCore.framework"
BlueprintName = "AmplifyXcodeCore"
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
</BuildableReference>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AmplifyXcode::AmplifyXcode"
BuildableName = "AmplifyXcode"
BlueprintName = "AmplifyXcode"
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
</BuildableReference>
</CodeCoverageTargets>
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeTests"
BuildableName = "AmplifyXcodeTests.xctest"
BlueprintName = "AmplifyXcodeTests"
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeCoreTests"
BuildableName = "AmplifyXcodeCoreTests.xctest"
BlueprintName = "AmplifyXcodeCoreTests"
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeCoreTests"
BuildableName = "AmplifyXcodeCoreTests.xctest"
BlueprintName = "AmplifyXcodeCoreTests"
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeTests"
BuildableName = "AmplifyXcodeTests.xctest"
BlueprintName = "AmplifyXcodeTests"
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
</BuildableReference>
</TestableReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ import Foundation
struct AnyCLICommandEncodable: Encodable {
let name: String
let abstract: String
let parameters: Set<CLICommandEncodableParameter>
let parameters: Set<CLICommandParameter>
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,34 @@
import Foundation
import ArgumentParser

private enum CLICommandEncodableKeys: String, CodingKey {
protocol CLICommandInitializable {
init()
}

private enum CLICommandCodingKeys: String, CodingKey {
case commandName
case abstract
case parameters
}

protocol CLICommandEncodable: Encodable {
protocol CLICommand: Encodable, CLICommandInitializable {
static var commandName: String { get }
static var abstract: String { get }
static var paramsRegistry: CLICommandEncodableParametersRegistry { get }
init()
static var parameters: Set<CLICommandParameter> { get }
}

extension CLICommandEncodable {
extension CLICommand {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CLICommandEncodableKeys.self)
var container = encoder.container(keyedBy: CLICommandCodingKeys.self)
try container.encode(Self.commandName, forKey: .commandName)
try container.encode(Self.abstract, forKey: .abstract)
try container.encode(Self.paramsRegistry.parameters, forKey: .parameters)
try container.encode(Self.parameters, forKey: .parameters)
}
}

// MARK: - ParsableCommand + CLICommandEncodable
extension CLICommandEncodable where Self: ParsableCommand {

extension CLICommand where Self: ParsableCommand {
static var commandName: String { Self.configuration.commandName! }
static var abstract: String { Self.configuration.abstract }
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,35 @@ import Foundation
import AmplifyXcodeCore
import ArgumentParser

/// Encodable representation of the `amplify-xcode` CLI.
/// In order to get the necessary information to produce a representation of each commands,
/// we instantiate them to have their params property wrappers initialized and therefore registered
/// as `CLICommandParameter`s.
private struct CLISchema: Encodable {
let abstract = "Auto generated JSON representation of amplify-xcode CLI"
var commands: [AnyCLICommandEncodable] = []

init() {
for command in AmplifyXcode.configuration.subcommands where command != CLICommandGenerateJSONSchema.self {
if let command = command as? CLICommandEncodable.Type {
_ = command.init()
commands.append(AnyCLICommandEncodable(name: command.commandName,
abstract: command.abstract,
parameters: command.paramsRegistry.parameters))
for command in AmplifyXcode.configuration.subcommands {
guard let command = command as? CLICommand.Type else {
continue
}
_ = command.init()
commands.append(AnyCLICommandEncodable(name: command.commandName,
abstract: command.abstract,
parameters: command.parameters))
}
}
}

struct CLICommandGenerateJSONSchema: ParsableCommand, CommandExecutable {
struct CLICommandGenerateJSONSchema: ParsableCommand, CommandExecutable, CLICommand {
static var parameters: Set<CLICommandParameter> = []
static let configuration = CommandConfiguration(
commandName: "generate-schema",
abstract: "Generates a JSON description of the CLI and its commands"
)

@Option(name: .customLong("output-path"), help: "Path to save the output of generated schema file")
@Option(name: "output-path", help: "Path to save the output of generated schema file", &parameters)
private var outputPath: String

var environment: AmplifyCommandEnvironment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import ArgumentParser
import AmplifyXcodeCore

/// CLI command invoking `CommandImportConfig`.
struct CLICommandImportConfig: ParsableCommand, CommandExecutable, CLICommandReportable, CLICommandEncodable {
static var paramsRegistry = CLICommandEncodableParametersRegistry()
struct CLICommandImportConfig: ParsableCommand, CommandExecutable, CLICommandReportable, CLICommand {
static var parameters: Set<CLICommandParameter> = []
diegocstn marked this conversation as resolved.
Show resolved Hide resolved
static let configuration = CommandConfiguration(
commandName: "import-config",
abstract: CommandImportConfig.description
)

@Option(name: "path", help: "Project base path", paramsRegistry)
@Option(name: "path", help: "Project base path", &parameters)
private var path: String = Process().currentDirectoryPath

var environment: AmplifyCommandEnvironment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import ArgumentParser
import AmplifyXcodeCore

/// CLI command invoking `CommandImportModels`.
struct CLICommandImportModels: ParsableCommand, CommandExecutable, CLICommandReportable, CLICommandEncodable {
static var paramsRegistry = CLICommandEncodableParametersRegistry()
struct CLICommandImportModels: ParsableCommand, CommandExecutable, CLICommandReportable, CLICommand {
static var parameters: Set<CLICommandParameter> = []
diegocstn marked this conversation as resolved.
Show resolved Hide resolved
static let configuration = CommandConfiguration(
commandName: "import-models",
abstract: CommandImportModels.description
)

@Option(name: "path", help: "Project base path", paramsRegistry)
@Option(name: "path", help: "Project base path", &parameters)
private var path: String = Process().currentDirectoryPath

var environment: AmplifyCommandEnvironment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@

import Foundation

/// Encodable representation of CLI parameter.
enum CLICommandEncodableParameter: Hashable {
/// Encodable representation of a CLI command parameter.
/// Commands parameters (options, flags, arguments) are declared as properties on the command type
/// and annotated with `@propertyWrapper`s `@Option`, `@Flag` and `@Argument` provided by `ArgumentParser`.
/// `ArgumentParser` derives parameters names from property names (i.e., an`outputPath` option becomes `--output-path`)
/// making thus impossible to reliably generate a JSON representation of a command and its parameters.
/// Therefore we use the following enum to keep track of each parameter and their attributes (name, type and help text).
Copy link
Member

Choose a reason for hiding this comment

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

👍

enum CLICommandParameter: Hashable {
case option(name: String, type: String, help: String)
case argument(name: String, type: String, help: String)
case flag(name: String, type: String, help: String)
Expand All @@ -26,13 +31,15 @@ enum CLICommandEncodableParameter: Hashable {
}

// MARK: - CLICommandEncodableParameter + Encodable
extension CLICommandEncodableParameter: Encodable {

extension CLICommandParameter: Encodable {
private enum CodingKeys: CodingKey {
case kind
case name
case type
case help
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import ArgumentParser
/// The following extensions on ArgumentParser commands parameters property wrappers
/// help us providing a hook to generate a JSON representation of a CLI command.
/// As for now `@PropertyWrappers` APIs to access enclosing type are still "private", so the passed
/// `registry` allows the property to reference an external type.
/// `parameters` allows the property to reference an external type.
/// `Argument` has been left out on purpose as we'd rather use options and flags for clarity of use.
/// Also by providing these extra initializers we make parameter name explicit.
extension Option where Value: ExpressibleByArgument {
init(wrappedValue: Value, name: String, help: String, _ registry: CLICommandEncodableParametersRegistry) {
init(wrappedValue: Value, name: String, help: String, _ parameters: inout Set<CLICommandParameter>) {
Copy link
Member

Choose a reason for hiding this comment

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

Minor naming suggestion to clarify the intent of the parameters and why it's an inout:

Suggested change
init(wrappedValue: Value, name: String, help: String, _ parameters: inout Set<CLICommandParameter>) {
init(wrappedValue: Value, name: String, help: String, updating parameters: inout Set<CLICommandParameter>) {

Result at the call site would look like:

@Option(name: "path", help: "Project base path", updating: &parameters)
...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call, thanks!

self.init(
wrappedValue: wrappedValue,
name: .customLong(name),
Expand All @@ -23,25 +24,25 @@ extension Option where Value: ExpressibleByArgument {
help: ArgumentHelp(help)
)
let type = String(describing: Value.self)
registry.register(param: .option(name: name, type: type, help: help))
parameters.insert(.option(name: name, type: type, help: help))
}

init(name: String, help: String, _ registry: CLICommandEncodableParametersRegistry) {
init(name: String, help: String, _ parameters: inout Set<CLICommandParameter>) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
init(name: String, help: String, _ parameters: inout Set<CLICommandParameter>) {
init(name: String, help: String, updating parameters: inout Set<CLICommandParameter>) {

self.init(
name: .customLong(name),
parsing: .next,
help: ArgumentHelp(help),
completion: nil
)
let type = String(describing: Value.self)
registry.register(param: .option(name: name, type: type, help: help))
parameters.insert(.option(name: name, type: type, help: help))
}
}

extension Flag where Value == Bool {
init(wrappedValue: Value, name: String, help: String, _ registry: CLICommandEncodableParametersRegistry) {
init(wrappedValue: Value, name: String, help: String, _ parameters: inout Set<CLICommandParameter>) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
init(wrappedValue: Value, name: String, help: String, _ parameters: inout Set<CLICommandParameter>) {
init(wrappedValue: Value, name: String, help: String, updating parameters: inout Set<CLICommandParameter>) {

self.init(wrappedValue: wrappedValue, name: .customLong(name), help: ArgumentHelp(help))
let type = String(describing: Value.self)
registry.register(param: .flag(name: name, type: type, help: help))
parameters.insert(.flag(name: name, type: type, help: help))
}
}