Skip to content

Commit 80861a0

Browse files
diegocstnpalpatim
andauthored
feat(amplify-xcode): generate JSON schema (#1080)
* amplify-xcode: generate JSON schema * Delete old CLICommandGenerateBindings.swift * feat: fix typo in command name * feat(amplify-xcode): apply suggestions from code review (spacing nit) Co-authored-by: Tim Schmelter <[email protected]> * feat(amplify-xcode): address PR comments * feat(amplify-xcode): add docs, rename CLICommandEncodable * feat(amplify-xcode): re-generate xcodeproj file * feat(amplify-xcode): re-run swiftformat * feat(amplify-xcode): generate-schema generates schema of itself * feat(amplify-xcode): add docs around schema generations * feat(amplify-xcode): add param label to clarify usage of parameters ref * feat(amplify-xcode): address PR comments Co-authored-by: Tim Schmelter <[email protected]>
1 parent d21f4be commit 80861a0

File tree

10 files changed

+2556
-2328
lines changed

10 files changed

+2556
-2328
lines changed

AmplifyTools/AmplifyXcode/AmplifyXcode.xcodeproj/project.pbxproj

Lines changed: 2326 additions & 2298 deletions
Large diffs are not rendered by default.

AmplifyTools/AmplifyXcode/AmplifyXcode.xcodeproj/xcshareddata/xcschemes/AmplifyXcode.xcscheme

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,43 +26,25 @@
2626
buildConfiguration = "Debug"
2727
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
2828
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29-
shouldUseLaunchSchemeArgsEnv = "YES"
30-
codeCoverageEnabled = "YES"
31-
onlyGenerateCoverageForSpecifiedTargets = "YES">
32-
<CodeCoverageTargets>
33-
<BuildableReference
34-
BuildableIdentifier = "primary"
35-
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeCore"
36-
BuildableName = "AmplifyXcodeCore.framework"
37-
BlueprintName = "AmplifyXcodeCore"
38-
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
39-
</BuildableReference>
40-
<BuildableReference
41-
BuildableIdentifier = "primary"
42-
BlueprintIdentifier = "AmplifyXcode::AmplifyXcode"
43-
BuildableName = "AmplifyXcode"
44-
BlueprintName = "AmplifyXcode"
45-
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
46-
</BuildableReference>
47-
</CodeCoverageTargets>
29+
shouldUseLaunchSchemeArgsEnv = "YES">
4830
<Testables>
4931
<TestableReference
5032
skipped = "NO">
5133
<BuildableReference
5234
BuildableIdentifier = "primary"
53-
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeTests"
54-
BuildableName = "AmplifyXcodeTests.xctest"
55-
BlueprintName = "AmplifyXcodeTests"
35+
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeCoreTests"
36+
BuildableName = "AmplifyXcodeCoreTests.xctest"
37+
BlueprintName = "AmplifyXcodeCoreTests"
5638
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
5739
</BuildableReference>
5840
</TestableReference>
5941
<TestableReference
6042
skipped = "NO">
6143
<BuildableReference
6244
BuildableIdentifier = "primary"
63-
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeCoreTests"
64-
BuildableName = "AmplifyXcodeCoreTests.xctest"
65-
BlueprintName = "AmplifyXcodeCoreTests"
45+
BlueprintIdentifier = "AmplifyXcode::AmplifyXcodeTests"
46+
BuildableName = "AmplifyXcodeTests.xctest"
47+
BlueprintName = "AmplifyXcodeTests"
6648
ReferencedContainer = "container:AmplifyXcode.xcodeproj">
6749
</BuildableReference>
6850
</TestableReference>

AmplifyTools/AmplifyXcode/Sources/AmplifyXcode/CLI.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@ import AmplifyXcodeCore
1818
struct AmplifyXcode: ParsableCommand {
1919
static let configuration = CommandConfiguration(
2020
abstract: "Amplify Xcode CLI",
21-
subcommands: [CLICommandImportConfig.self, CLICommandImportModels.self])
21+
subcommands: [
22+
CLICommandImportConfig.self,
23+
CLICommandImportModels.self,
24+
CLICommandGenerateJSONSchema.self
25+
])
2226
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
struct AnyCLICommandEncodable: Encodable {
11+
let name: String
12+
let abstract: String
13+
let parameters: Set<CLICommandParameter>
14+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import ArgumentParser
10+
11+
protocol CLICommandInitializable {
12+
init()
13+
}
14+
15+
private enum CLICommandCodingKeys: String, CodingKey {
16+
case commandName
17+
case abstract
18+
case parameters
19+
}
20+
21+
protocol CLICommand: Encodable, CLICommandInitializable {
22+
static var commandName: String { get }
23+
static var abstract: String { get }
24+
static var parameters: Set<CLICommandParameter> { get }
25+
}
26+
27+
extension CLICommand {
28+
func encode(to encoder: Encoder) throws {
29+
var container = encoder.container(keyedBy: CLICommandCodingKeys.self)
30+
try container.encode(Self.commandName, forKey: .commandName)
31+
try container.encode(Self.abstract, forKey: .abstract)
32+
try container.encode(Self.parameters, forKey: .parameters)
33+
}
34+
}
35+
36+
// MARK: - ParsableCommand + CLICommandEncodable
37+
38+
extension CLICommand where Self: ParsableCommand {
39+
static var commandName: String { Self.configuration.commandName! }
40+
static var abstract: String { Self.configuration.abstract }
41+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import AmplifyXcodeCore
10+
import ArgumentParser
11+
12+
/// Encodable representation of the `amplify-xcode` CLI.
13+
/// In order to get the necessary information to produce a representation of each commands,
14+
/// we instantiate them to have their params property wrappers initialized and therefore registered
15+
/// as `CLICommandParameter`s.
16+
private struct CLISchema: Encodable {
17+
let abstract = "Auto generated JSON representation of amplify-xcode CLI"
18+
var commands: [AnyCLICommandEncodable] = []
19+
20+
init() {
21+
for command in AmplifyXcode.configuration.subcommands {
22+
guard let command = command as? CLICommand.Type else {
23+
continue
24+
}
25+
_ = command.init()
26+
commands.append(AnyCLICommandEncodable(name: command.commandName,
27+
abstract: command.abstract,
28+
parameters: command.parameters))
29+
}
30+
}
31+
}
32+
33+
struct CLICommandGenerateJSONSchema: ParsableCommand, CommandExecutable, CLICommand {
34+
static var parameters = Set<CLICommandParameter>()
35+
static let configuration = CommandConfiguration(
36+
commandName: "generate-schema",
37+
abstract: "Generates a JSON description of the CLI and its commands"
38+
)
39+
40+
@Option(name: "output-path", help: "Path to save the output of generated schema file", updating: &parameters)
41+
private var outputPath: String
42+
43+
var environment: AmplifyCommandEnvironment {
44+
CommandEnvironment(basePath: outputPath, fileManager: FileManager.default)
45+
}
46+
47+
func run() throws {
48+
let schema = try JSONEncoder().encode(CLISchema())
49+
let schemaFileName = "amplify-xcode.json"
50+
let fullPath = try environment.createFile(atPath: schemaFileName,
51+
content: String(data: schema, encoding: .utf8)!)
52+
print("Schema generated at: \(fullPath)")
53+
}
54+
}

AmplifyTools/AmplifyXcode/Sources/AmplifyXcode/CLICommandImportConfig.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import ArgumentParser
1010
import AmplifyXcodeCore
1111

1212
/// CLI command invoking `CommandImportConfig`.
13-
struct CLICommandImportConfig: ParsableCommand, CommandExecutable, CLICommandReportable {
13+
struct CLICommandImportConfig: ParsableCommand, CommandExecutable, CLICommandReportable, CLICommand {
14+
static var parameters = Set<CLICommandParameter>()
1415
static let configuration = CommandConfiguration(
1516
commandName: "import-config",
1617
abstract: CommandImportConfig.description
1718
)
1819

19-
@Option(name: .shortAndLong, help: "Project base path")
20+
@Option(name: "path", help: "Project base path", updating: &parameters)
2021
private var path: String = Process().currentDirectoryPath
2122

2223
var environment: AmplifyCommandEnvironment {

AmplifyTools/AmplifyXcode/Sources/AmplifyXcode/CLICommandImportModels.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import ArgumentParser
1010
import AmplifyXcodeCore
1111

1212
/// CLI command invoking `CommandImportModels`.
13-
struct CLICommandImportModels: ParsableCommand, CommandExecutable, CLICommandReportable {
13+
struct CLICommandImportModels: ParsableCommand, CommandExecutable, CLICommandReportable, CLICommand {
14+
static var parameters = Set<CLICommandParameter>()
1415
static let configuration = CommandConfiguration(
1516
commandName: "import-models",
1617
abstract: CommandImportModels.description
1718
)
1819

19-
@Option(name: .shortAndLong, help: "Project base path")
20+
@Option(name: "path", help: "Project base path", updating: &parameters)
2021
private var path: String = Process().currentDirectoryPath
2122

2223
var environment: AmplifyCommandEnvironment {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
/// Encodable representation of a CLI command parameter.
11+
/// Commands parameters (options, flags, arguments) are declared as properties on the command type
12+
/// and annotated with `@propertyWrapper`s `@Option`, `@Flag` and `@Argument` provided by `ArgumentParser`.
13+
/// `ArgumentParser` derives parameters names from property names (i.e., an`outputPath` option becomes `--output-path`)
14+
/// making thus impossible to reliably generate a JSON representation of a command and its parameters.
15+
/// Therefore we use the following enum to keep track of each parameter and their attributes (name, type and help text).
16+
enum CLICommandParameter: Hashable {
17+
case option(name: String, type: String, help: String)
18+
case argument(name: String, type: String, help: String)
19+
case flag(name: String, type: String, help: String)
20+
21+
var kind: String {
22+
switch self {
23+
case .option:
24+
return "option"
25+
case .argument:
26+
return "argument"
27+
case .flag:
28+
return "flag"
29+
}
30+
}
31+
}
32+
33+
// MARK: - CLICommandEncodableParameter + Encodable
34+
35+
extension CLICommandParameter: Encodable {
36+
private enum CodingKeys: CodingKey {
37+
case kind
38+
case name
39+
case type
40+
case help
41+
}
42+
43+
func encode(to encoder: Encoder) throws {
44+
var container = encoder.container(keyedBy: CodingKeys.self)
45+
switch self {
46+
case .option(let name, let type, let help),
47+
.argument(let name, let type, let help),
48+
.flag(let name, let type, let help):
49+
try container.encode(name, forKey: .name)
50+
try container.encode(type, forKey: .type)
51+
try container.encode(help, forKey: .help)
52+
try container.encode(kind, forKey: .kind)
53+
}
54+
}
55+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import ArgumentParser
10+
11+
/// The following extensions on ArgumentParser commands parameters property wrappers
12+
/// help us providing a hook to generate a JSON representation of a CLI command.
13+
/// As for now `@PropertyWrappers` APIs to access enclosing type are still "private", so the passed
14+
/// `parameters` allows the property to reference an external type.
15+
/// `Argument` has been left out on purpose as we'd rather use options and flags for clarity of use.
16+
/// Also by providing these extra initializers we make parameter name explicit.
17+
extension Option where Value: ExpressibleByArgument {
18+
init(wrappedValue: Value, name: String, help: String, updating parameters: inout Set<CLICommandParameter>) {
19+
self.init(
20+
wrappedValue: wrappedValue,
21+
name: .customLong(name),
22+
parsing: .next,
23+
completion: nil,
24+
help: ArgumentHelp(help)
25+
)
26+
let type = String(describing: Value.self)
27+
parameters.insert(.option(name: name, type: type, help: help))
28+
}
29+
30+
init(name: String, help: String, updating parameters: inout Set<CLICommandParameter>) {
31+
self.init(
32+
name: .customLong(name),
33+
parsing: .next,
34+
help: ArgumentHelp(help),
35+
completion: nil
36+
)
37+
let type = String(describing: Value.self)
38+
parameters.insert(.option(name: name, type: type, help: help))
39+
}
40+
}
41+
42+
extension Flag where Value == Bool {
43+
init(wrappedValue: Value, name: String, help: String, updating parameters: inout Set<CLICommandParameter>) {
44+
self.init(wrappedValue: wrappedValue, name: .customLong(name), help: ArgumentHelp(help))
45+
let type = String(describing: Value.self)
46+
parameters.insert(.flag(name: name, type: type, help: help))
47+
}
48+
}

0 commit comments

Comments
 (0)