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

Conversation

diegocstn
Copy link
Contributor

@diegocstn diegocstn commented Feb 26, 2021

Description of changes:
This PR introduces a new amplify-xcode genenerate-schema command to generate a JSON representation of the CLI and its commands as an intermediate step to further generate necessary JavaScript/TypeScript bindings.
In order to properly generate bindings and documentation for each commands, an interface describing parameters and their usage is generated according to the following specs:

{
  name: String, // parameter name
  help: String, // brief description
  kind: "option" | "flag" | "argument", // type of parameter (--option=value, --flag, <argument>),
  type: String, // string representation of parameter type, used to generate docs
}

Based on the current set of commands defined in amplify-xcode, the generated JSON schema will look like

{
  "commands":[
     {
        "name":"import-config",
        "abstract":"Import Amplify configuration files",
        "parameters":[{ "kind":"option", "name":"path", "type":"String", "help":"Project base path"}]
     },
     {
        "name":"import-models",
        "abstract":"Import Amplify models",
        "parameters":[{ "kind":"option", "name":"path", "type":"String", "help":"Project base path"}]
     }
  ],
  "abstract":"Auto generated JSON representation of amplify-xcode CLI"
}

The following implementation is the results of a set of constraints/limitations in the ArgumentParser library and Swift runtime.

Using reflection to inspect a command type and generate a schema isn't a viable option due to the following constraints:

  • ArgumentParser derives the name used on the command line from the name of the properties annotated with @Option @Flag property wrappers by using a private String extension, so for example a property named fileOutputPath will be converted to a file-output-path parameter when used to invoke the CLI
  • trying to replicate the criteria used to derive parameters names may be inconsistent (how to handle special cases, like numbers and acronyms)
  • both @Option and @Flag are generics on their wrapped value, that makes casting and/or infer their types using reflection almost impossible
  • structs/classes used internally by ArgumentParser to parse value and/or generate help messages are private to the library and not exposed to its consumers
  • accessing the enclosing type from a property wrappers isn't "officially" supported yet

Check points:

  • Added new tests to cover change, if needed
  • All unit tests pass
  • All integration tests pass
  • Documentation update for the change if required
  • PR title conforms to conventional commit style

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@codecov
Copy link

codecov bot commented Mar 1, 2021

Codecov Report

Merging #1080 (4977270) into main (3ac0b2c) will increase coverage by 0.54%.
The diff coverage is n/a.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1080      +/-   ##
==========================================
+ Coverage   57.98%   58.53%   +0.54%     
==========================================
  Files         519      650     +131     
  Lines       14888    19094    +4206     
==========================================
+ Hits         8633    11176    +2543     
- Misses       6255     7918    +1663     
Flag Coverage Δ
API_plugin_unit_test 54.53% <ø> (?)
AWSPluginsCore 74.47% <ø> (?)
Amplify 48.04% <ø> (ø)
Analytics_plugin_unit_test 73.41% <ø> (ø)
Auth_plugin_unit_test 82.47% <ø> (ø)
DataStore_plugin_unit_test 73.06% <ø> (ø)
Predictions_plugin_unit_test 33.48% <ø> (ø)
Storage_plugin_unit_test 57.72% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
.../DataStore/Model/Internal/Schema/ModelSchema.swift 85.29% <0.00%> (-2.95%) ⬇️
...ginsCore/Auth/Provider/IAMCredentialProvider.swift 0.00% <0.00%> (ø)
.../AWSS3StorageService+GetPreSignedURLBehavior.swift 0.00% <0.00%> (ø)
...egoryPlugin/Reachability/AmplifyReachability.swift 0.00% <0.00%> (ø)
...PICategoryPluginConfiguration+EndpointConfig.swift 47.12% <0.00%> (ø)
.../RequestInterceptor/IAMURLRequestInterceptor.swift 0.00% <0.00%> (ø)
...ginsCore/Model/Decorator/PaginationDecorator.swift 87.50% <0.00%> (ø)
.../Auth/Provider/AmplifyAWSCredentialsProvider.swift 0.00% <0.00%> (ø)
...gins/Core/AWSPluginsCore/Auth/AWSAuthService.swift 35.13% <0.00%> (ø)
...in/Request/StorageUploadDataRequest+Validate.swift 100.00% <0.00%> (ø)
... and 123 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 3ac0b2c...7bb1d9d. Read the comment docs.

@diegocstn diegocstn requested a review from a team March 2, 2021 16:40
@palpatim
Copy link
Member

palpatim commented Mar 3, 2021

General question: Is there an opportunity to use an existing schema definition such as JSON Schema, Swagger, or Smithy? These are generally for service-side APIs, and I haven't investigated their applicability for a CLI use case, but it would be great if we could re-use prior art rather than creating a new standard. :D

case parameters
}

protocol CLICommandEncodable: Encodable {
Copy link
Member

Choose a reason for hiding this comment

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

Is there an existing CLICommand that it would make more sense to add Encodable conformance to? If not, "Encodable" in the protocol name seems unnecessary, unless it's the reason for the protocol's existence, as in marker protocols or something where the protocol is tightly bound to the behavior the protocol describes--see discussion at https://swift.org/documentation/api-design-guidelines/#naming

Copy link
Contributor Author

Choose a reason for hiding this comment

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

there's no existing CLICommand, the only reason for the CLICommandEncodable's existence is to have Encodable conformance -- but I'm thinking that would make sense to just have a generic CLICommand protocol and let it conform to Encodable. Thoughts?

import Foundation

/// Encodable representation of CLI parameter.
enum CLICommandEncodableParameter: Hashable {
Copy link
Member

Choose a reason for hiding this comment

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

As above, would it make more sense to add Encodable performance to an existing CLICommandParameter protocol or concrete type, rather than creating a new type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately there's no existing CLICommandParameter type as the parameters are being handled by the ArgumentParser library via property wrappers (@Option, @Flag) -- the only reason for having an extra type is to retain information about commands' parameters and use it for generating the schema

var commands: [AnyCLICommandEncodable] = []

init() {
for command in AmplifyXcode.configuration.subcommands where command != CLICommandGenerateJSONSchema.self {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't we generating a schema for the "Generate" command?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated, generate-schema command generates a schema of itself :)

protocol CLICommandEncodable: Encodable {
static var commandName: String { get }
static var abstract: String { get }
static var paramsRegistry: CLICommandEncodableParametersRegistry { get }
Copy link
Member

Choose a reason for hiding this comment

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

Do we need an additional wrapping type here, or can we just make params a Set with direct get access?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We actually don't need it, I just wanted to add a layer of abstraction and hide the underlying storage, Set. (I don't even really like the name :P )

Copy link
Member

@palpatim palpatim left a comment

Choose a reason for hiding this comment

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

Approved w/comments

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!

/// 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.

👍

}

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>) {

}
}

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>) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants