Git Product home page Git Product logo

createapi's Introduction

Create API

Delightful code generation for OpenAPI specs for Swift written in Swift.

  • Fast: processes specs with 100K lines of YAML in less than a second
  • Smart: generates Swift code that looks like it's carefully written by hand
  • Reliable: tested on 1KK lines of publicly available OpenAPI specs producing correct code every time
  • Customizable: offers a ton of customization options

Powered by OpenAPIKit

Installation

$ mint install CreateAPI/CreateAPI
$ brew install create-api

Swift Package Plugins

Make

$ git clone https://github.com/CreateAPI/CreateAPI.git
$ cd CreateAPI
$ make install

Getting Started

You'll need an OpenAPI schema (using 3.0.x) for your API. If your schema has external references, you might also need to bundle it beforehand.

If you have never used CreateAPI before, be sure to check out our tutorial: Generating an API with CreateAPI

CreateAPI can generate complete Swift Package bundles but can also generate individual components to integrate into an existing project. Either way, you'll want to use the generate command:

$ create-api generate --help
USAGE: create-api generate <input> [--output <output>] [--config <config>] [--config-option <config-option> ...] [--verbose] [--strict] [--allow-errors] [--clean] [--watch] [--single-threaded] [--measure]

ARGUMENTS:
  <input>                 The path to the OpenAPI spec in either JSON or YAML format

OPTIONS:
  --output <output>       The directory where generated outputs are written (default: CreateAPI)
  --config <config>       The path to the generator configuration. (default: .create-api.yaml)
  --config-option <config-option>
                          Option overrides to be applied when generating.

        In scenarios where you need to customize behaviour when invoking the generator, use this option to
        specify individual overrides. For example:

        --config-option "module=MyAPIKit"
        --config-option "entities.filenameTemplate=%0DTO.swift"

        You can specify multiple --config-option arguments and the value of each one must match the
        'keyPath=value' format above where keyPath is a dot separated path to the option and value is the
        yaml/json representation of the option.

  -v, --verbose           Enables verbose log messages
  --strict                Treats warnings as errors and fails generation
  --allow-errors          Ignore errors that occur during generation and continue if possible
  -c, --clean             Removes the output directory before writing generated outputs
  --watch                 Monitor changes to both the spec and the configuration file and automatically
                          regenerate outputs
  --single-threaded       Disables parallelization
  --measure               Measure performance of individual operations and log timings
  --version               Show the version.
  -h, --help              Show help information.

To try CreateAPI out, run the following commands:

$ curl "https://petstore3.swagger.io/api/v3/openapi.json" > schema.json
$ create-api generate schema.json --config-option module=PetstoreKit --output PetstoreKit
$ cd PetstoreKit
$ swift build

There you have it, a comping Swift Package ready to be integrated with your other Swift projects!

For more information about using CreateAPI, check out the Documentation.

Projects using CreateAPI

Need some inspiration? Check out the list of projects below that are already using CreateAPI:

Are you using CreateAPI in your own open-source project? Let us know by adding it to the list above!

Contributing

We always welcome contributions from the community via Issues and Pull Requests. Please be sure to read over the contributing guidelines for more information.

createapi's People

Contributors

ainame avatar imjn avatar janc avatar kean avatar lepips avatar liamnichols avatar mattia avatar mgrider avatar philiptrauner avatar simorgh3196 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

createapi's Issues

Support documenting paths and parameters

While running create-api generate app_store_connect_api_2.0_openapi.json --module "AppStoreConnect_Swift_SDK" --output . --config .create-api.yml -s, I don't get any documentation for the files.

See for generated Entity examples: https://github.com/AvdLee/appstoreconnect-swift-sdk/tree/master/Sources/OpenAPI/Entities

Config used:

# Modifier for all generated declarations
access: public
# Add @available(*, deprecated) attributed
isAddingDeprecations: true
# Available values: ["spaces", "tabs"]
indentation: tabs
# Parses dates (e.g. "2021-09-29") using `NaiveDate` (https://github.com/CreateAPI/NaiveDate)
isNaiveDateEnabled: false
# Overrides file header comment
fileHeaderComment: "// Generated by Create API\n// https://github.com/CreateAPI/CreateAPI"

paths:
  style: rest
  # Generate response headers using https://github.com/CreateAPI/HTTPHeaders
  isGeneratingResponseHeaders: true
  # Add operation id to each request
  isAddingOperationIds: false
  # The types to import, by default uses "Get" (https://github.com/CreateAPI/Get)
  imports: []
  # Inline simple requests, like the ones with a single parameter 
  isInliningSimpleRequests: true
  # Inline simple parametesr with few arguments.
  isInliningSimpleQueryParameters: true
  # Threshold from which to start inlining query parameters
  simpleQueryParametersThreshold: 2
  # Tries to remove redundant paths
  isRemovingRedundantPaths: true
  namespace: "APIEndpoint"

comments:
  # Generate comments
  isEnabled: true
  # Generate titles
  isAddingTitles: true
  # Generate description 
  isAddingDescription: true
  # Generate examples
  isAddingExamples: true
  # Add links to the external documenatation
  isAddingExternalDocumentation: true
  # Auto-capitalizes comments
  isCapitalizationEnabled: true

Simplify `acronym` related properties

Currently there are four things related to acronyms:

  1. The default list (["url", "id", "html", "ssl", "tls", "https", "http", "dns", "ftp", "api", "uuid", "json"])
  2. isReplacingCommonAcronyms (defaults to true)
  3. addedAcronyms
  4. ignoredAcronyms

Its done like this to include a default set of acronyms, and then allow customisation by either disabling replacement, ignoring specific or adding new ones.

After thinking it through some more, this could be simplified down to a single configuration option:

/// A set of acronyms that should be uppercased if detected.
///
/// If you don't want to uppercase any acronyms, set this property to an empty array.
public var uppercaseAcronyms: [String] = ["url", "id", "html", "ssl", "tls", "https", "http", "dns", "ftp", "api", "uuid", "json"]

We provide (and document) the default set, so if the user wants to add to it, they can incorporate the set into their configuration file.

If the user doesn't want to uppercase acronyms, they can set the property to an empty array.

I can't see any reason not to do this?

Remove `isAddingOperationIds`

After #83, isAddingOperationIds brings very little value since we no longer have to generate a custom extension to set id on Request.

I think it's just easier to remove the flag and always set the id on the Request if !operationId.isEmpty?

Are there any reasons not to do this?

Custom Date handling

I have to access an API which uses non ISO date formatting. ๐Ÿ˜ข

I've searched the documentation and the code for something like:

  • provide a custom date formatter when decoding a date or date-time
  • don't decode format: date but handle it as String. So basically ignoring the type "date" as a workaorund.

I've not found anything. Is there some option I'm missing?

Thanks for your help, I really like CreateApi and Get, a very new fresh approach ๐Ÿฅ‡

Duplicate http scheme

I'm trying to use CreateAPI and Get in Swiftlane https://github.com/onmyway133/Swiftlane/blob/main/Examples/CLI/CLI/Script.swift#L61 and it seems we have duplicated https scheme. Here is the error

NSLocalizedDescription=bad URL, NSErrorFailingURLStringKey=https://https%3a%2f%2fapi.appstoreconnect.apple.com%2fv1/v1/certificates?, NSErrorFailingURLKey=https://https%3a%2f%2fapi.appstoreconnect.apple.com%2fv1/v1/certificates?, _kCFStreamErrorDomainKey=1}

And also that I provide an empty init Parameter, the path contains extra ? question mark.

Align behaviour of `output` directory and make `--clean` a little safer

While the usage of the output dir is technically consistent, it comes across a little confusing. Lets take the following examples:

This is not always the case though, because sometimes create-api will generate the nested directory for you:

create-api generate schema.json --package "FooKit" --output ./ --no-split:

$ tree .
.
โ”œโ”€โ”€ FooKit
โ”‚ย ย  โ”œโ”€โ”€ Package.swift
โ”‚ย ย  โ””โ”€โ”€ Sources
โ”‚ย ย      โ”œโ”€โ”€ Entities.swift
โ”‚ย ย      โ””โ”€โ”€ Paths.swift
โ”œโ”€โ”€ .create-api.yaml
โ””โ”€โ”€ schema.json

create-api generate schema.json --module "FooKit" --output ./ --no-split:

$ tree .
.
โ”œโ”€โ”€ Entities.swift
โ”œโ”€โ”€ Paths.swift
โ”œโ”€โ”€ .create-api.yaml
โ””โ”€โ”€ schema.json

Writing generated code to the same directory as non-generated code is probably not a great idea in general, but when using --package, it seems to align with the kind of output that you might expect since a single folder with the name of the package is written into the output directory.

This however is not the case with using --module.

Making --split the default/recommended option now means that its more common to run into scenarios where you need to clean the generated outputs in the event that an entity or path was removed from the schema (because the next generator run wouldn't overwrite it).

Clean is documented (and behaves) like so:

-c, --clean             Removes the output folder before continuing

Using --clean in the above two examples would cause the generator to unexpectedly(?) delete the entire contents of ./ including schema.json and .create-api.yaml, something I don't think that we ever want to happen.

For consistency, I propose that we align --package and --module behaviours so that the generated output always goes directly into --output. This should hopefully encourage people to always specify a subdirectory for their generated outputs instead of writing them into ./.

To demonstrate, with my proposed changes, it would be more common for people to run the following instead:

create-api generate schema.json --package "FooKit" --output ./Generated --no-split:


$ tree .
.
โ”œโ”€โ”€ Generated
โ”‚ย ย  โ”œโ”€โ”€ Package.swift
โ”‚ย ย  โ””โ”€โ”€ Sources
โ”‚ย ย      โ”œโ”€โ”€ Entities.swift
โ”‚ย ย      โ””โ”€โ”€ Paths.swift
โ”œโ”€โ”€ .create-api.yaml
โ””โ”€โ”€ schema.json

create-api generate schema.json --module "FooKit" --output ./Generated --no-split:

$ tree .
.
โ”œโ”€โ”€ Generated
โ”‚ย ย  โ”œโ”€โ”€ Entities.swift
โ”‚ย ย  โ””โ”€โ”€ Paths.swift
โ”œโ”€โ”€ .create-api.yaml
โ””โ”€โ”€ schema.json

While this helps with using --clean in these scenarios, it doesn't completely make clean safe. There are probably still times when you might set --output to ./, for example if you are pushing a package to a repository, you need Package.swift at the top level, but you might also want to commit the schema/config to keep everything in one place.

For the immediate problem of mistakenly deleting configs/schemas, we can add a check to make sure that when using --clean, the config and schema path aren't inside output.

Remove option `isAdditionalPropertiesOnByDefault`?

When renaming some config options, I noticed that isAdditionalPropertiesOnByDefault seems to be redundant.

It is currently used like so:

var additional = details.additionalProperties
if details.properties.isEmpty, options.entities.isAdditionalPropertiesOnByDefault {
additional = additional ?? .a(true)
}
guard let additional = additional else {
return nil
}

So when a schema does not define additionalProperties, if there are no other properties in the schema and isAdditionalPropertiesOnByDefault is set to true (the default), it will assume that additionalProperties is to be treated as true.

According to the OpenAPI Spec however, additionalProperties has a default value of true anyway:

  • additionalProperties - Value can be boolean or object. Inline or referenced schema MUST be of a Schema Object and not a standard JSON Schema. Consistent with JSON Schema, additionalProperties defaults to true.

https://swagger.io/specification/

This means that we shouldn't need to assume a missing additionalProperties (nil) is ambiguous because according to the specification it should be treated as true.

If I am not mistaken, we can remove the option and simplify the implementation to the following:

- var additional = details.additionalProperties 
- if details.properties.isEmpty, options.entities.isAdditionalPropertiesOnByDefault { 
-     additional = additional ?? .a(true) 
- } 
- guard let additional = additional else { 
-     return nil 
- }
+ let additional = details.additionalProperties ?? .a(true) 

The only thing I am not sure of is why we check details.properties.isEmpty. I removed the code and all tests pass so as far as I can tell, this change has no impact.

Make `ConfigOptions.access` an enum

/// Access level modifier for all generated declarations
public var access: String = "public"

The access property is currently just a string meaning that you can put anything in the config file and we'll end up generating non-functioning code.

CreateAPI will only support public or internal access levels since open can't be applied everywhere and private would just generate code that is unusable. We could model this with an enum instead.

One thing to note is that while we could specify access: internal in the config, we'd want to generate code without access level specifiers (i.e the same as an empty string) since the internal keyword itself in generated code is redundant.

For reference, we've used enums elsewhere before in ConfigOptions so the same pattern can be followed:

/// Available levels of indentation
public enum Indentation: String, Codable {
case spaces
case tabs
}
/// Change the style of indentation. Supported values:
/// - `spaces`
/// - `tabs`
public var indentation: ConfigOptions.Indentation = .spaces

๐Ÿ“ 0.1.0 Release

CreateAPI has proven to be a very stable tool since its initial prerelease and to better reflect that, we're working to move to version 0.1. In doing so, we want to take this opportunity to make a few bigger than normal changes based on how the tool has been used out in the wild.

To summarise what is going into 0.1.0:

Documentation

Up until now, there hadn't been much of a focus on documentation but that needed to change, in 0.1.0 we're writing a lot of new documentation to help new users get started with CreateAPI.

Configuration

The Configuration Options are at the heart of how you tweak your generated code and its important to make sure that these options are clear and easy to understand. As a result, we'll be making some adjustments to how these options are styled which will result in some breaking changes. A complete set of notes for migration will be provided in the final release, but the aim here is to set good foundations for future changes.

In addition, we're making it easier to figure out when your configuration file contains typos, unsupported or deprecated options:

Installing CreateAPI

The easier it is to install CreateAPI, the more likely you are to adopt it (I hope)

Get

Get recently announced the 1.0.0 release, started from CreateAPI 0.1.0, generated packages will use Get 1.0.0

Performance Optimizations

While CreateAPI has previously focused on optimising code generation, we hadn't made much effort in terms of optimising the generated code for fast build times and slim binaries. In 0.1 we're making some changes to help with this:

It's now recommended (and the default) to split your generated entities and paths into individual source files. In addition, avoiding the generation of individual CodingKey enums is recommended to remove bloat from the compiled binary with larger schemas.


As always, feedback and suggestions are welcome ๐ŸŒŸ


Release Notes (Draft)

Preview

Breaking Changes

  • Generated packages and paths now depend on Get 1.0.2 or later. If you don't use Get, your Request type must expose an initializer that matches the initializer defined in Get. For more information, see #83.

  • When generating a Swift Package, the Package.swift file and all other sources are written to the root of the --output directory instead of being nested inside a subdirectory.

  • Default output directory is now ./CreateAPI.

  • The rename.properties option now understands property names as defined in the original schema and not after applying CreateAPI transformations (such as case conversion or swifty style booleans). For more information, see #112.

  • The generator will now error if the path defined using --config did not contain a valid file (prior behaviour was to fallback to the default configuration). For more info, see #125.

  • Command Line Argument options that alter the generate output have now been moved into the configuration file and the behaviour may have also been adjusted. Please refer to the table below for more information:

    Pre 0.1 Argument Remark
    --split (-s) This is now the default behaviour. To generate entities and structs merged into a single file, use the mergeSources option in the configuration file.
    --filename-template Split into entities.filenameTemplate and paths.filenameTemplate in the configuration file.
    --entityname-template Replaced by the entities.nameTemplate config option.
    --generate Moved into the configuration file as generate. In addition, enums and package are also now valid options to consistently control the generation of all components.
    --package To generate as a package, ensure that generate is set to [entities, paths, enums, package]. To customise the module name, set the module option.
    --module To disable package generation, set generate to [entities, paths] (see above) and to customise the module name, set the module option.
    --vendor Replaced with the vendor config option.
  • Significant changes to options in the configuration file:

    Pre 0.1 Option Remark
    isUsingIntegersWithPredefinedCapacity Removed in favour of dataTypes customisation. Fixed width integers are now used by default so you must override the int32 and int64 formats if you wish to disable this.
    isNaiveDateEnabled Renamed to useNaiveDate
    isPluralizationEnabled Renamed to pluralizeProperties
    isInliningTypealiases Renamed to inlineTypealiases
    isGeneratingSwiftyBooleanPropertyNames Renamed to useSwiftyPropertyNames
    isGeneratingEnums Now part of the generate option (included by default).
    isAddingDeprecations Renamed to annotateDeprecations
    isStrippingParentNameInNestedObjects Renamed to stripParentNameInNestedObjects
    isAddingDefaultValues Renamed to includeDefaultValues
    isSortingPropertiesAlphabetically Renamed to sortPropertiesAlphabetically
    isGeneratingEncodeWithEncoder Renamed to alwaysIncludeEncodableImplementation
    isGeneratingInitWithDecoder Renamed to alwaysIncludeDecodableImplementation
    isGeneratingInitializers Renamed to includeInitializer
    isSkippingRedundantProtocols Renamed to skipRedundantProtocols
    isGeneratingIdentifiableConformance Renamed to includeIdentifiableConformance
    isRemovingRedundantPaths Renamed to removeRedundantPaths
    isMakingOptionalPatchParametersDoubleOptional Renamed to makeOptionalPatchParametersDoubleOptional
    isInliningSimpleQueryParameters Renamed to inlineSimpleQueryParameters
    isInliningSimpleRequests Renamed to inlineSimpleRequests
    isGeneratingResponseHeaders Renamed to includeResponseHeaders
    overridenResponses Renamed to overriddenResponses (double d)
    overridenBodyTypes Renamed to overriddenBodyTypes (double d)
    isSwiftLintDisabled Removed. Use fileHeaderComment to replicate similar behaviour
    isReplacingCommonAcronyms Replaced by acronyms
    addedAcronyms Replaced by acronyms
    ignoredAcronyms Replaced by acronyms
    isAdditionalPropertiesOnByDefault Now always enabled
    isAddingOperationIds Now always enabled
    isGeneratingCustomCodingKeys Replaced by optimizeCodingKeys with different default behaviour
    isInliningPropertiesFromReferencedSchemas Replaced by inlineReferencedSchemas with different default behaviour
    isGeneratingMutableClassProperties Replaced by mutableProperties
    isGeneratingMutableStructProperties Replaced by mutableProperties
    isGeneratingStructs Replaced by defaultType
    isMakingClassesFinal Replaced by defaultType
    entitiesGeneratedAsClasses Replaced by typeOverrides
    entitiesGeneratedAsStructs Replaced by typeOverrides
    access Accepted values limited to public or internal (public is the default)

Config Options Documentation

Apologies as I was a bit too late to leave a comment in #52, but I would like to propose some improvements to the ConfigOptions.md generation since I do reference it a bit.

1 - Alphabetize options within their scope (general, comments, entities, etc.), but leave sections at the end, sorted by section name.
2 - Add examples under a dropdown
3 - Add dividers to be more easy on the eyes

I'm referencing SwiftFormat's Rules.md for these changes as I personally think they present their list of options well.

This issue could have help-wanted for the examples on the PR if this is approved as a change.

Failure when empty enum is found

See the following output:

avanderlee@Antoines-MBP-2 OpenAPI % create-api generate app_store_connect_api_2.0_openapi.json --module "AppStoreConnect_Swift_SDK" --output . --config .create-api.yml -s
Generating code for app_store_connect_api_2.0_openapi.json...
WARNING: Unknown body content types: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.other("application/a-gzip"), warnings: [], parameters: [:])], defaulting to Data. Use paths.overridenBodyTypes to add support for your content types.
WARNING: Unknown body content types: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.other("application/a-gzip"), warnings: [], parameters: [:])], defaulting to Data. Use paths.overridenBodyTypes to add support for your content types.
WARNING: Unknown body content types: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.other("application/vnd.apple.xcode-metrics+json"), warnings: [], parameters: [:])], defaulting to Data. Use paths.overridenBodyTypes to add support for your content types.
WARNING: Unknown body content types: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.other("application/vnd.apple.xcode-metrics+json"), warnings: [], parameters: [:])], defaulting to Data. Use paths.overridenBodyTypes to add support for your content types.
WARNING: Unknown body content types: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.other("application/vnd.apple.diagnostic-logs+json"), warnings: [], parameters: [:])], defaulting to Data. Use paths.overridenBodyTypes to add support for your content types.
Error: Failed to generate get for Operation(tags: Optional(["SubscriptionOfferCodeOneTimeUseCodes"]), summary: nil, description: nil, externalDocs: nil, operationId: Optional("subscriptionOfferCodeOneTimeUseCodes-get_instance"), parameters: [Either(Parameter(name: "fields[subscriptionOfferCodeOneTimeUseCodes]", context: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.Context.query(required: false, allowEmptyValue: false), description: Optional("the fields to include for returned resources of type subscriptionOfferCodeOneTimeUseCodes"), deprecated: false, schemaOrContent: Either(SchemaContext(style: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.SchemaContext.Style.form, explode: false, allowReserved: false, schema: Either(JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.array(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.ArrayFormat>(format: OpenAPIKit30.JSONTypeFormat.ArrayFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: nil, defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.ArrayContext(items: Optional(OpenAPIKit30.JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.string(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.StringFormat>(format: OpenAPIKit30.JSONTypeFormat.StringFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: Optional([AnyCodable("active"), AnyCodable("createdDate"), AnyCodable("expirationDate"), AnyCodable("numberOfCodes"), AnyCodable("offerCode"), AnyCodable("values")]), defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.StringContext(maxLength: nil, _minLength: nil, pattern: nil)))), maxItems: nil, _minItems: nil, _uniqueItems: nil)))), example: nil, examples: nil)), vendorExtensions: [:])), Either(Parameter(name: "include", context: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.Context.query(required: false, allowEmptyValue: false), description: Optional("comma-separated list of relationships to include"), deprecated: false, schemaOrContent: Either(SchemaContext(style: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.SchemaContext.Style.form, explode: false, allowReserved: false, schema: Either(JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.array(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.ArrayFormat>(format: OpenAPIKit30.JSONTypeFormat.ArrayFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: nil, defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.ArrayContext(items: Optional(OpenAPIKit30.JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.string(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.StringFormat>(format: OpenAPIKit30.JSONTypeFormat.StringFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: Optional([AnyCodable("offerCode")]), defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.StringContext(maxLength: nil, _minLength: nil, pattern: nil)))), maxItems: nil, _minItems: nil, _uniqueItems: nil)))), example: nil, examples: nil)), vendorExtensions: [:])), Either(Parameter(name: "fields[subscriptionOfferCodeOneTimeUseCodeValues]", context: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.Context.query(required: false, allowEmptyValue: false), description: Optional("the fields to include for returned resources of type subscriptionOfferCodeOneTimeUseCodeValues"), deprecated: false, schemaOrContent: Either(SchemaContext(style: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.SchemaContext.Style.form, explode: false, allowReserved: false, schema: Either(JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.array(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.ArrayFormat>(format: OpenAPIKit30.JSONTypeFormat.ArrayFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: nil, defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.ArrayContext(items: Optional(OpenAPIKit30.JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.string(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.StringFormat>(format: OpenAPIKit30.JSONTypeFormat.StringFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: Optional([]), defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.StringContext(maxLength: nil, _minLength: nil, pattern: nil)))), maxItems: nil, _minItems: nil, _uniqueItems: nil)))), example: nil, examples: nil)), vendorExtensions: [:]))], requestBody: nil, responses: OpenAPIKitCore.OrderedDictionary<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode, OpenAPIKitCore.Either<OpenAPIKit30.JSONReference<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response>, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response>>(orderedKeys: [(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 400)), (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 403)), (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 404)), (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 200))], unorderedHash: [(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 400)): Either(Response(description: "Parameter error(s)", headers: nil, content: OpenAPIKitCore.OrderedDictionary<OpenAPIKitCore.OpenAPI.ContentType, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content>(orderedKeys: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:])], unorderedHash: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:]): (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content(schema: Optional(Either(internal(#/components/schemas/ErrorResponse))), example: nil, examples: nil, encoding: nil, vendorExtensions: [:])], _warnings: []), links: OpenAPIKitCore.OrderedDictionary<Swift.String, OpenAPIKitCore.Either<OpenAPIKit30.JSONReference<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>>(orderedKeys: [], unorderedHash: [:], _warnings: []), vendorExtensions: [:])), (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 403)): Either(Response(description: "Forbidden error", headers: nil, content: OpenAPIKitCore.OrderedDictionary<OpenAPIKitCore.OpenAPI.ContentType, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content>(orderedKeys: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:])], unorderedHash: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:]): (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content(schema: Optional(Either(internal(#/components/schemas/ErrorResponse))), example: nil, examples: nil, encoding: nil, vendorExtensions: [:])], _warnings: []), links: OpenAPIKitCore.OrderedDictionary<Swift.String, OpenAPIKitCore.Either<OpenAPIKit30.JSONReference<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>>(orderedKeys: [], unorderedHash: [:], _warnings: []), vendorExtensions: [:])), (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 404)): Either(Response(description: "Not found error", headers: nil, content: OpenAPIKitCore.OrderedDictionary<OpenAPIKitCore.OpenAPI.ContentType, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content>(orderedKeys: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:])], unorderedHash: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:]): (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content(schema: Optional(Either(internal(#/components/schemas/ErrorResponse))), example: nil, examples: nil, encoding: nil, vendorExtensions: [:])], _warnings: []), links: OpenAPIKitCore.OrderedDictionary<Swift.String, OpenAPIKitCore.Either<OpenAPIKit30.JSONReference<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>>(orderedKeys: [], unorderedHash: [:], _warnings: []), vendorExtensions: [:])), (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode(warnings: [], value: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Response.StatusCode.Code.status(code: 200)): Either(Response(description: "Single SubscriptionOfferCodeOneTimeUseCode", headers: nil, content: OpenAPIKitCore.OrderedDictionary<OpenAPIKitCore.OpenAPI.ContentType, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content>(orderedKeys: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:])], unorderedHash: [OpenAPIKitCore.OpenAPI.ContentType(underlyingType: OpenAPIKitCore.OpenAPI.ContentType.Builtin.json, warnings: [], parameters: [:]): (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Content(schema: Optional(Either(internal(#/components/schemas/SubscriptionOfferCodeOneTimeUseCodeResponse))), example: nil, examples: nil, encoding: nil, vendorExtensions: [:])], _warnings: []), links: OpenAPIKitCore.OrderedDictionary<Swift.String, OpenAPIKitCore.Either<OpenAPIKit30.JSONReference<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Link>>(orderedKeys: [], unorderedHash: [:], _warnings: []), vendorExtensions: [:]))], _warnings: []), callbacks: OpenAPIKitCore.OrderedDictionary<Swift.String, OpenAPIKitCore.Either<OpenAPIKit30.JSONReference<OpenAPIKitCore.OrderedDictionary<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.CallbackURL, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.PathItem>>, OpenAPIKitCore.OrderedDictionary<(extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.CallbackURL, (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.PathItem>>>(orderedKeys: [], unorderedHash: [:], _warnings: []), deprecated: false, security: nil, servers: nil, vendorExtensions: [:]). Failed to generate query parameter Parameter(name: "fields[subscriptionOfferCodeOneTimeUseCodeValues]", context: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.Context.query(required: false, allowEmptyValue: false), description: Optional("the fields to include for returned resources of type subscriptionOfferCodeOneTimeUseCodeValues"), deprecated: false, schemaOrContent: Either(SchemaContext(style: (extension in OpenAPIKit30):OpenAPIKitCore.OpenAPI.Parameter.SchemaContext.Style.form, explode: false, allowReserved: false, schema: Either(JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.array(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.ArrayFormat>(format: OpenAPIKit30.JSONTypeFormat.ArrayFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: nil, defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.ArrayContext(items: Optional(OpenAPIKit30.JSONSchema(warnings: [], value: OpenAPIKit30.JSONSchema.Schema.string(OpenAPIKit30.JSONSchema.CoreContext<OpenAPIKit30.JSONTypeFormat.StringFormat>(format: OpenAPIKit30.JSONTypeFormat.StringFormat.generic, required: true, _nullable: nil, _permissions: nil, _deprecated: nil, title: nil, description: nil, externalDocs: nil, discriminator: nil, allowedValues: Optional([]), defaultValue: nil, example: nil), OpenAPIKit30.JSONSchema.StringContext(maxLength: nil, _minLength: nil, pattern: nil)))), maxItems: nil, _minItems: nil, _uniqueItems: nil)))), example: nil, examples: nil)), vendorExtensions: [:]). Enum "FieldsSubscriptionOfferCodeOneTimeUseCodeValues" has no values

Mostly due to:

Enum "FieldsSubscriptionOfferCodeOneTimeUseCodeValues" has no values

The specification used can be found here

Add official way to opt out of using Get

Background

As it stands, to opt out of using Get, you must do three things:

  1. Use --module mode
  2. Update paths.imports to be an empty array
  3. Provide your own Request shim

While we believe that using CreateAPI with Get it the quickest and easiest option, we understand that not everybody is in a position to adopt Get. As a result, we need to provide a better way to disable use of the dependency.

Description

I can think of three scenarios:

  1. You want to use Get and you are happy with the default behaviour
  2. Your networking client already defines it's own Request in a module that should be imported
  3. You will build your networking client on top of the generated code and want a Request type generated for you

I did at first think that we should just generate Request if you opted not to use Get, but this will put some people who already import a different module defining Request in a situation where they can't use the generated code. It might be that like Get, the Request type is used for manually crafted requests too so defining it inside the CreateAPI generated package might not be appropriate.

Not sure how best to handle this...

Support custom mappings between schema formats and Swift types

Edit: differs from the original to a single global override instead of individual properties and I forgot parameter types. I think that we can start with a global override and somebody can request this fine tuning later.


Introduce a config option to override the Swift type when they are parsed and written:

# schema
Pet:
  properties:
    id:
      type: string
    tag:
      type: string
      format: uuid

# config
swiftTypeOverrides:
  UUID: String # any occurrences of UUID are replaced with String, so `Pet.tag: String`

Config name open for improvement. Obviously, a developer might override to a type that cannot be decoded from/encoded to the original type, like UUID: Int or a type that does not exist, like UUID: qwerty. So, it is a developer responsibility to make sure that overriding types can be properly decoded/encoded and that they are a proper Swift type.


This can be further enhanced with imports as we should be trivially able to set an imported type after #85 is figured out. Using NaiveDate as an example removing the useNaiveDate option, we include the package as an import in the config and then set the type override:

# schema
Pet:
  properties:
    birth_date:
      type: string
      format: date

# config
swiftTypeOverrides:
  Date: NaiveDate # any occurrences of Date are replaced with NaiveDate

Obviously, imported objects should be expected to properly decode/encode but that's an implementation detail of the imported type.

Setup a code linter

@LePips raises a good point that we should probably simplify some things around code review by setting up some basic rules with a linting tool.

I'm suggesting SwiftLint since I'm familiar with it, but open to other options too.

We should be able to run it locally and also it should run as a check on Pull Requests.

Enable `isGeneratingCustomCodingKeys` by default

Background

Changes

Generating a CodingKey conforming enum for every entity has a significant impact on the output binary size, even after optimisation and especially in larger projects.

It's much more efficient to leverage a single CodingKey type when working with generated code, which is what happens when isGeneratingCustomCodingKeys is set to false.

Essentially, this should be the default behaviour and generating custom keys should be an opt in feature (or maybe even just removed).

To ease a transition, we might well want to rename the option too:

The name isGeneratingCustomCodingKeys is also a bit poor. It should probably just spell out the behavior: isUsingRawStringsAsCodingKeys or something like that.

Simplify Entity-type and Mutable Properties in Config

We currently have two config options for entity-type generation, entities.isGeneratingStructs and entities.isMakingClassesFinal. Instead of these two flags, we condense into an enum:

entities:
  type: struct || class || finalClass

Still preserve entities.entitiesGeneratedAsClasses and entities.entitiesGeneratedAsStructs for now.

Additionally, there are two config options for property generation: entities.isGeneratingMutableClassProperties and entities.isGeneratingMutableStructProperties. Condense into a simple bool:

entities:
  mutableProperties: true || false # default true

Unstable order when reading spec from `.json` file

https://github.com/kean/CreateAPI/blob/87ba65d28a657ef7b94a758697cbf68d6075dbb9/Sources/CreateAPI/Generate.swift#L159-L160

In addition to not being thread-safe, JSONDecoder also does not appear to guarantee any explicit order of object keys.

Spec
{
  "openapi": "3.0.0",
  "info": {
    "version": "1.0.0",
    "title": "Order"
  },
  "paths": {
    "/container": {
      "get": {
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Container"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Container": {
        "type": "object",
        "required": null,
        "properties": {
          "propertyOne": {
            "type": "string"
          },
          "propertyTwo": {
            "type": "string"
          },
          "propertyThree": {
            "type": "string"
          },
          "propertyFour": {
            "type": "string"
          }
        }
      }
    }
  }
}
Output
// Generated by Create API
// https://github.com/kean/CreateAPI
//
// swiftlint:disable all

import Foundation

public struct Container: Codable {
    public var propertyFour: String?
    public var propertyThree: String?
    public var propertyOne: String?
    public var propertyTwo: String?

    public init(propertyFour: String? = nil, propertyThree: String? = nil, propertyOne: String? = nil, propertyTwo: String? = nil) {
        self.propertyFour = propertyFour
        self.propertyThree = propertyThree
        self.propertyOne = propertyOne
        self.propertyTwo = propertyTwo
    }
}

struct StringCodingKey: CodingKey, ExpressibleByStringLiteral {
    private let string: String
    private var int: Int?

    var stringValue: String { return string }

    init(string: String) {
        self.string = string
    }

    init?(stringValue: String) {
        self.string = stringValue
    }

    var intValue: Int? { return int }

    init?(intValue: Int) {
        self.string = String(describing: intValue)
        self.int = intValue
    }

    init(stringLiteral value: String) {
        self.string = value
    }
}

The easiest workaround is to convert the .json spec to a .yaml spec beforehand.

All command line arguments that alter output should be configurable in config file

Currently, we have a very vast amount of configuration options that must be included in the create-api.yaml file, but we also have some other options that also alter how the tool generates the output code that must be passed via CLI args. This includes the following:

  • -s, --split - Split output into separate files
  • --package <package> - Generates a complete package with a given name
  • --module <module> - Use the following name as a module name
  • --vendor <vendor> - Enabled vendor-specific logic (supported values: "github")
  • --generate <generate> - Specifies what to generate (default: paths, entities)
  • --filename-template <filename-template> - Example: "%0.generated.swift" will produce files with the following names: "Paths.generated.swift". (default: %0.swift)
  • --entityname-template <entityname-template> - Example: "%0Generated" will produce entities with the following names: "EntityGenerated". (default: %0)

This means that there are two sources to dig through when altering configuration, which has the potential to cause confusion. It also makes it hard to invoke create-api from multiple different places without either copying the desired arguments or wrapping the invocation in a script.

Firstly, I'd like to understand if there is a good reason that these options should be separate to the options in the configuration file?

If we agree that isn't the case, I'd like to start making them available in the configuration instead.

Unless there is a good reason to keep the arguments as overrides to configuration options, my suggestion would be to deprecate them now and remove them in a future release. Printing a warning when used until they are removed.

Remove `spaceWidth` and `indentation`?

Justification for removing SwiftLint #103 was that the developer should continue post-processing with their own linter. I think we can further follow this idea by dropping these config options and just generating with 4 space indentation (which is usual throughout the field).

Homebrew Installation

I rather prefer Homebrew to install all my packages instead over Mint and of course over manual installation for every update. Could there be a Homebrew option for installation?

Remove option `isSwiftLintDisabled`?

While it could sometimes be considered a useful feature, I have to slight concerns with it:

  1. It assumes we use SwiftLint, but what about other tools?
  2. It's more efficient to use a .swiftlint.yml configuration to setup ignore rules for the generated output directory.
  3. If we want it, we could just add it to fileHeaderComment instead.

Any other reasons to keep it? I'm suggesting that we either:

  1. Remove isSwiftLintDisabled with no replacement
  2. Remove isSwiftLintDisabled but add // swiftlint:disable all into the fileHeaderComment

Keen to hear what others think.

Can't run using Mint

$ mint install kean/CreateAPI
๐ŸŒฑ Finding latest version of CreateAPI
๐ŸŒฑ Cloning CreateAPI 0.0.1
๐ŸŒฑ Resolving package
๐ŸŒฑ  Executable product not found in CreateAPI 0.0.1
$ mint run CreateAPI create-api generate -h
๐ŸŒฑ  "CreateAPI" package not found

Are you planning on making a new release soon?

Split filename-template for both entities and paths

Using the below files, everything is freely available:

Command used:

create-api generate --output . --config createapi-config.yaml --package JellyfinAPI -s jellyfin-openapi-stable.json"

With the operations paths style I get conflicting filenames between entities and paths. I actually need to use the operations paths style because rest results in another error because the spec has a path literally called "Path" and that creates a different issue. You can simply change the paths style and generate to see the error.

Anyways, the only way to properly avoid these file conflicts is to use --entityname-template "$0Model" which properly renames all entity files and the corresponding entities, however I would not like to have every entity to be that same name due to convenience. I think a flag change for the --filename-template <filename-template> would be more appropriate to split into two flags:

  • --entity-filename-template <entity-filename-template>
  • --paths-filename-template <paths-filename-template>

These flags would create a further separation between the two types and aid in these types of conflicts. If this gets greenlit I'll be more than happy to implement to get comfortable with this project.

Unstable dependencies

Hi Alex, first of all thanks for creating this great tool.

I've successfully generated Appstore Connect APIs using your tool at https://github.com/onmyway133/AppStoreConnect
And I'm ready to include it into https://github.com/onmyway133/Swiftlane. However, Xcode complains about urlqueryencoder being unstable dependencies.

Dependencies could not be resolved because root depends on 'AppStoreConnect' 0.0.2..<1.0.0.
'AppStoreConnect' >= 0.0.2 cannot be used because no versions of 'AppStoreConnect' match the requirement 0.0.3..<1.0.0 and package 'appstoreconnect' is required using a stable-version but 'appstoreconnect' depends on an unstable-version package 'urlqueryencoder'.

I guess in the generated Package.swift, we need to specify from instead of branch?

.package(url: "https://github.com/kean/URLQueryEncoder", from: "0.2.0"),

Update documentation

I noticed that there are a few small inconsistencies with the documentation for the config in the README.md compared to the actual implantation.

Consider making `isInliningPropertiesFromReferencedSchemas` the default behaviour

Background

Context: #61 (comment)

When isInliningPropertiesFromReferencedSchemas is not enabled (the default), it generates an invalid Encodable implementation. But generally speaking, the nested objects seem to not be consistent with how other generators generate code and it leads me to think that it's not the right choice to be nesting entities.

My proposal is that we remove the isInliningPropertiesFromReferencedSchemas option and make it's behaviour the default.

Any docs on how to actually use the generated code?

I am just lost on how to actually consume my generated API.

What I've done is generate the the Entities.swift and Paths.swift drag an drop it into the project. Add Get and URLQueryEncoder via SPM. But now I'm faced with a bunch of Cannot find type 'api' in scope

image

Any info on what to do next would be nice. Thank you!

Support inclusion of custom package dependencies

Background

While it's possible to define additional import's in the generated paths or entities code, it is not yet possible to declare dependencies to those modules in the generated Package.swift.

When investigating how to give the user more control over importing Get (and I guess other default dependencies), it became a bit clearer that we needed a more powerful way to define which dependencies we want to use and what they bring to the generated code.

Se notes here: #84 (comment)

Fundamentally, I believe that we need a way to declare custom dependencies, and with that:

  • Where the dependency comes from and it's version requirements (if it needs to go into Package.swift)
  • What the dependency provides (i.e the Request type)
  • Where the custom dependency is required (i.e maybe it only needs improving into entities)

With this information, we'd able to do the following:

  1. Merge the custom dependencies with required dependencies (i.e Get) where appropriate
  2. Figure out which files need to import which module
  3. Embed each dependency in Package.swift, including the custom ones

Examples

Previous Behaviour

For matching the previous behaviour, the following config:

entities:
  imports:
  - CoreLocation

Would become this:

dependencies: 
- module: CoreLocation
  importIn:
  - entities

We can deprecate the old options and automatically backfill them during a transition period.

Package Dependencies

In the event that we also need to bring in a package to help with some additional customisations:

entities:
  protocols: 
  - DataTransferObject
dependencies:
- module: DataTransferKit
  url: https://github.com/myorg/DataTransferKit.git
  from: "0.0.2"ย # need other options too, such as branch, revision, upToNextMinor etc
  importIn:
  - entities

This will both import DataTransferKit in generated entity files for the protocol conformance, and will also include the dependency in the generated Package.swift file.

Overrides

Finally, being able to override the existing imports. For example, Get:

dependencies:
- module: MyAPIKit
  url: https://github.com/myorg/MyAPIKit.git
  branch: main
  provides:
  - Request

Instead of using importIn, this tells CreateAPI that this dependency provides the given type which would allow it to replace built-in dependencies that provide that type (Get).

Right now, the generator would just import MyAPIKit instead of Get in all Paths, but in the future we would have the potential to expand this to any type. For example:

paths:
  overriddenResponses:
    MyEntity: GenericType
dependencies:
- module: MyUtilsKit
  path: ../MyUtilsKit
  provides:
  - GenericType

And the result would be that MyUtilsKit is then imported only in specific source files that reference GenericType.

While this would be neat, I don't think this is a requested feature right now so I imagine that it would be out of scope to begin with. It just demonstrates how provides has the potential to be very flexible.

Generate servers description

OpenAPI provides a way to specify available servers and their respective URLs:

servers:
  - url: https://api.example.com/v1
    description: Production server (uses live data)
  - url: https://sandbox-api.example.com:8443/v1
    description: Sandbox server (uses test data)

CreateAPI could (optionally) generate a list of available servers:

enum Servers {
    // Sandbox server (uses test data)
    static let sandbox = URL(string: "https://sandbox-api.example.com:8443/v1")
    // Production server (uses live data)
    static let production = URL(string: "https://api.example.com/v1")
}

Usage:

import Get

func usage() {
    let client = APIClient(baseURL: Servers.sandbox)
}

This feature would be nice to have. But I'm not sure how to generate the name for the servers. There is no name property, only description that can contain arbitrary text.

Provide a way to safe decode collection items

hey,

some other open api generator tools provide a way to generate code that safely decodes elements of an array and ignore the ones that don't. This can be used in scenarios where new types are likely to be introduced in the API and yet we don't want older api clients not supporting the new types to break.

For instance, Swaggen can decode all arrays using the decodeArray which ignores elements that fail to decode.

While this works, having such a global setting which simply ignores elements of all arrays is a big assumption that should not be part of the code generation library imo: there are cases where we want some types to be decoded safely and some cases where we want them to be more strict.

I was wondering if and how CreateAPI could provide a similar functionality while remaining flexible enough.

My thoughts are that it should be up to the user of CreateAPI to

  • Decide what are the collections that can have elements which can be decoded in a safe manner.
  • What to do if an element fails to decode.

For this purpose, in our non generated code (that I'm trying to migrate to a generated code), we use a custom type to wrap the entities that are likely to fail. For instance, the following Throwable simply logs the decoding error

public struct Throwable<T: Decodable>: Decodable {
    let result: Result<T, ThrowableDecodableError>

    public init(from decoder: Decoder) throws {
        do {
            result = .success(try T(from: decoder))
        } catch let error {
            logger.info("Failed to decode: \(error)")
            result = .failure(ThrowableDecodableError())
        }
    }

    public func get() -> T? {
        return try? result.get()
    }
}

We then declare the collections properties like this

public struct Store: Decodable {
    public let pets: [Throwable<Pet>]
}

Introducing the above Throwable in CreateAPI would be again too much assumption and that's not what I'm proposing here. It's just for illustration.

However, I was thinking if it would make sense to create a setting such as arrayTypeWrappers

  arrayTypeWrappers: [
    "MyEntity": "MySettingType"
  ]

which would be a mapping of types that CreateAPI would use to simply output [MySettingType<MyEntity>] instead of
[MyEntity].

So back to my Throwable example, a setting

entities:
  arrayTypeWrappers: [
    "Pet": "Throwable"
  ]

would generate

public struct Store: Decodable {
    public let pets: [Throwable<Pet>]
}

public struct World: Decodable {
    public let stores: [Store] // nothing new here
}

Of course, the user of CreateAPI would have to provide the Throwable implementation for the code to compile

Any thoughts?

Error during `make install`

I was running make install as described in the readme and the following error showed up:

avanderlee@Antoines-MBP-2 CreateAPI % make install
swift build --disable-sandbox -c release 
warning: Usage of /Users/avanderlee/Library/org.swift.swiftpm/collections.json has been deprecated. Please delete it and use the new /Users/avanderlee/Library/org.swift.swiftpm/configuration/collections.json instead.
Building for production...
Build complete! (0.15s)
warning: Usage of /Users/avanderlee/Library/org.swift.swiftpm/collections.json has been deprecated. Please delete it and use the new /Users/avanderlee/Library/org.swift.swiftpm/configuration/collections.json instead.
mkdir -p /usr/local/bin
sudo cp -f /Users/avanderlee/Developer/GIT-Projects/Forks/CreateAPI/.build/arm64-apple-macosx/release/CreateAPI /usr/local/bin/create-api
cp: /Users/avanderlee/Developer/GIT-Projects/Forks/CreateAPI/.build/arm64-apple-macosx/release/CreateAPI: No such file or directory
make: *** [install] Error 1
avanderlee@Antoines-MBP-2 CreateAPI % 

I'm using Xcode 13.4.1 (13F100) and I'm on this machine:

CleanShot 2022-07-08 at 11 07 06@2x

POST Object ist generated as Data

Consider this OpenAPI spec.

openapi: 3.0.1
info:
  title: test
  description: ""
  contact:
    name: API Support
    url: 
    email: 
  version: "1.0"
servers:
- url: https://localhost:8080/
- url: http://localhost:8080/
paths:
  /marketplace/create-payment-intent:
    post:
      tags:
      - Marketplace
      summary: Create Payment Intent
      requestBody:
        description: MerchantItem Ids
        content:
          '*/*':
            schema:
              type: array
              items:
                type: integer
        required: true
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaymentIntent'
      security:
      - Authorization: []
      x-codegen-request-body-name: body
components:
  schemas:
    PaymentIntent:
      required:
      - clientSecret
      type: object
      properties:
        clientSecret:
          type: string
  securitySchemes:
    Authorization:
      type: apiKey
      name: Authorization
      in: header

Generating the API with Create API created following signature.

extension Paths {
    public static var createPaymentIntent: CreatePaymentIntent {
        CreatePaymentIntent(path: "/marketplace/create-payment-intent")
    }

    public struct CreatePaymentIntent {
        /// Path: `/marketplace/create-payment-intent`
        public let path: String

        /// Create Payment Intent
        public func post(_ body: Data) -> Request<PaymentIntent> {
            .post(path, body: body)
        }
    }
}

You can see that signature wants a Data object instead of [Int].

Is there any plans to add support for adding prefix and suffix for entities?

Hello Hello. I have a question.
Are there any plans to add support for adding prefix and suffix for generated entities like SwagGen does?

I have an existing project that I want to try to implement this tool and replace some entities at first and gradually replace all the entities hardcoded with the generated ones. In this case, prefix and suffix help me to integrate the generated entities easier.

If adding the feature is suited for the direction this tool is going, I am happy to make a PR for that :)

Compile time evaluation

I was wondering whether you have any insights into compile-time optimization. Since the created files can be in numbers, compile times can add up quite a bit.

I'm thinking out loud here, but things to consider:

  • Are build times faster using a single file vs. the separated file's mode?
  • Extending a namespace, e.g. enum APIEndpoint, might be less performant than making individual instances conforming to a protocol APIEndpoint

I also wonder what the effect is to export binary size. Dead code stripping might be less successful when we're extending an enum APIEndpoint (based on a Paths configuration of namespace: "APIEndpoint") compared to making the code compilation work with protocol conformance.

Curious to hear your thoughts!

Change `rename.properties` to use schema names

I observe in this comment that rename.properties requires the format of the post-processed entity name that will be attached to the final entity.

Example

The following config:

Pet:
  properties:
    example_name:
      type: string
    enabled:
      type: boolean
    source_url:
      type: String

will result in the entity:

struct Pet {
    var exampleName: String
    var isEnabled: String // due to useSwiftyPropertyNames
    var sourceURL: String // due to isReplacingCommonAcronyms
}

When a developer uses rename.properties, they are required to use the final processed entity name for the key and the value is a literal translation [1], no processing is done on the renamed property. Additionally, the developer has to know what acronyms are used and the states of the pluralizeProperties, and useSwiftyPropertyNames. This can cause a lot of problems during renaming and I omit examples of mixture of these properties because it's pretty straightforward.

rename:
  properties:
    Pet.isEnabled: somethingElse
    Pet.sourceURL: another_url # will literally become `another_url` on the entity

Change

To be consistent with include/exclude and to be more clear with how the config should be created, we should instead change rename.properties to use the schema object names for keys and values.

rename:
  properties:
    Pet.enabled: something_else
    Pet.source_url: another_url

would result in the entity:

struct Pet {
    var exampleName: String
    var somethingElse: String
    var anotherURL: String
}

  • [1] I don't think that literal translations should be an intended use of rename.properties. If a developer really really wanted to break their own convention we could implement some kind of literal renaming option that ignores the naming processing steps. I think a comment would be very clear and declarative, but I don't know how the YAML would parse that at the moment:
rename:
  properties:
    Pet.enabled: something_else
    Pet.source_url: another_url # literal

Exclude for Individual Properties

I have a very large object definition that contains itself by reference on a single property. The generator will currently turn this object into a class when I need a struct for usage in my app (just strongly prefer value types in my case). In my specific case I can just ignore the property that is causing this issue entirely, as I have done previously with editing the config file.

With the following example, I can just ignore best_friend:

    Pet:
      title: A pet title
      description: A pet description
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          example: "Buddy"
          type: string
        tag:
          type: string
        best_friend:
          $ref: "#/components/schemas/Pet"
          description: "Best friend of your pet!"
entities:
  - excludeProperties: ["Pet.best_friend"]

Will create an expected struct with no bestFriend property.

This is more of a discussion if excludeProperties is a good config name and whether the analog includeProperties should be created. In both cases, I know that I would have to check against the existing exclude/include lists to see if the object should be included in the overall generation. I think an error should be thrown in cases where usage of these flags together don't make sense. Or, we can just ignore these and leave it up to the user to have a clean config file.

entities:
  # file 1
  - include: ["some-other-object"]
  - excludeProperties: ["Pet.best_friend"] # Error as "Pet" is not included in generation
  
  # file 2
  - include: ["some-other-object"]
  - includeProperties: ["Pet.best_friend"] # Error as "Pet" is not included in generation
  
  # file 3
  - exclude: ["Pet"]
  - excludeProperties: ["Pet.best_friend"] # Error as "Pet" is not included in generation
  
  # file 4
  - exclude: ["Pet"]
  - includeProperties: ["Pet.best_friend"] # Error as "Pet" not included in generation

Or instead of a new flag we change the sections to:

entities:
  exclude:
    - objects: [...]
    - properties: [...]
  include:
    - objects: [...]
    - properties: [...]

Refactor Idea

A brainstormed refactor to include/exclude could be the following:

entities:
  # file 1
  - include:
    - Pet: # generates the "Pet" object with only the following properties
      - properties:
        - id
        - name
    - Store: # generates the "Store" object with no properties as it is empty
      - properties: 
    - Error: ... # need some way to indicate "Error" object generation with all properties included?

  # file 2
  - exclude:
    - Pet: # generates the original "Pet" object but excludes the following properties
      - properties:
        - id
        - name
    - Store: # generates the original "Store" object with no properties as it is empty
      - properties: 
    - Error: ... # need some way to indicate the "Error" object should be excluded entirely?

This refactor seems expansive but I feel a lot more declarative instead of splitting among a few lists that have to be checked against one another.

I will implement whatever option if approved.

Simplify `comments` configuration

The comments configuration is currently an object with 5 customisations expressed as Bool and 1 main isEnabled flag.

In my head, we could express this the same way with an enum/option set of values and a single commentFeatures array/set.. Something like the following:

Disable Comments:

commentFeatures: []

Everything:

commentFeatures:
- title
- description
- example
- externalDocumentation
- capitalized

Description (capitalized):

commentFeatures:
- description
- capitalized

In Swift, this would be something like the following:

public enum CommentFeature: String {
    case title, description, example, externalDocumentation, capitalized
}

public var commentFeatures: Set<CommentFeature> = [.title, .description, .example, .externalDocumentation, .capitalized]

Alternatively I'll just deswiftify the boolean properties on the existing object.

Better control for `annotateDeprecations` (formally `isAddingDeprecations`)

We have a boolean config option to enable or disable deprecation annotations, but they are added in two different ways:

  1. Properties include a /// - warning: Deprecated comment
  2. Everything else gets @available(*, deprecated, message: "Deprecated")

It looks like this happens because we read and write from properties in generated code, which would result in an undesired warning being produced by the compiler however I think we should give some more control here.

  • You might actually want deprecation annotations, even in generated code so that you can be aware of deprecated properties
  • You might not want the annotations, but would find a comment useful
  • You might not care and can just disable this flag

For that reason, it might be more appropriate to give finer control to the user:

Disable all kinds of annotations

annotateDeprecations: false
# or 
annotateDeprecations: null

Note deprecations in comments for everything (not just properties)

annotateDeprecations: comment

Add @available annotations

annotateDeprecations: availability
# or 
annotateDeprecations: true

As a use case, I can imagine that having full on annotations using @availability apis would actually be useful when we have better exclude/filtering for deprecations. For example, a developer will see the warnings in Xcode and will actively work to stop relying on deprecated paths/entities/properties and then they can go ahead and define them in their ignore config upon doing so.

Stop using swifty boolean names in configuration options

More of a question/general discussion really, but what are the thoughts regarding use of Swifty bools (isDoingSomething vs doSomething) in the configuration file?

As a personal preference, they feel a bit out of place when in a yaml/json file. But I don't know if its me being nit picky or not. They also make it a bit harder to order the options alphabetically.

For the upcoming 0.1.0 release, I'm considering that we might just switch the style but I want other opinions first.

CreateAPI strips trailing slashes from urls

Excuse my ignorance (I'm fairly new to OpenAPI), but I have an API spec that is getting parsed without errors (as near as I can tell) by CreateAPI, but the output has trailing slashes stripped from the paths.

I've attached both the relevant bit of the spec, as well as the generated path file, and as you can see the .yaml clearly specifies the url path as /api/token-auth/, but in the .swift file it's path + "/token-auth".

I did try specifying isRemovingRedundantPaths: false in the config, but that didn't seem to help.

I haven't checked, but do wonder whether this is actually an issue also present in OpenAPIKit. I'd love to hear any ideas you have about this issue!

cleaned-openapi.yaml.txt
cleaned-PathsApiTokenAuth.swift.txt

Is `Data` the correct type for properties with the `format: byte` modifier?

The type of properties with the format: byte modifier has been changed to Data in PR #25.

However, according to the Swagger documentation, they are defined as Base64-encoded characters.
So it would be correct to treat it as a String, wouldn't it?

https://swagger.io/docs/specification/data-models/data-types/#format

An optional format modifier serves as a hint at the contents and format of the string. OpenAPI defines the following built-in string formats:
...
byte โ€“ base64-encoded characters, for example, U3dhZ2dlciByb2Nrcw==

Make `--split` (`-s`) the default behaviour

Background

Description

Bundling everything into just Paths.swift and Entities.swift prevents the compiler from parallelising build tasks and results in bad compile times for larger projects.

Therefore bundling the source has little advantage compared to splitting into individual files (--split) so we should make this the default.

We're not ready to remove the option completely, likely we'll replace it with something like --no-split or --merge but we should put less emphasis on it.

The one time that I've found it useful is for predicting the output sources so that we can make efficient BuildToolPlugin's. But in the long run, I want to explore ways to break out the evaluation from the generation so that we could compute this ahead of time... Maybe..

Struct properties generated as vars

Hi @kean

first, thanks for a lot for this open source project ๐Ÿ™

I noticed that all properties of structs are generated as var. From the code I see that the isReadonly rules whether a property is a var or a let.

https://github.com/kean/CreateAPI/blob/303aecad0969368de29bcd69566f102f0cdf622b/Sources/CreateAPI/Generator/Templates.swift#L407

However, the value of isReadonly comes from here so all structs will have their properties as var
https://github.com/kean/CreateAPI/blob/303aecad0969368de29bcd69566f102f0cdf622b/Sources/CreateAPI/Generator/Generator%2BRender.swift#L32-L37

I think this could be improved by having a dedicated options.entities.mutableProperties option for it, wdyt?

Support Swift Package Plugins

Swift 5.6 introduced the concept of Build Tool Plugins. Similar to Xcode build phases, they allow us to run tools as part of the build process.

Since CreateAPI is already nicely linked into the SPM ecosystem, we're in a place where its relatively easy for us to offer a plugin that can automatically generate Paths and Entities within a given target.

We'd want to essentially enable people to create a Package.swift like this:

// swift-tools-version:5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "MyPackage",
    platforms: [
        .macOS(.v10_15)
    ],
    products: [
        .library(name: "MyLib", targets: ["MyLib"]),
        .executable(name: "my-exec", targets: ["MyExec"])
    ],
    dependencies: [
        .package(url: "https://github.com/CreateAPI/CreateAPI.git", exact: "0.5.0"),
        .package(url: "https://github.com/kean/Get.git", exact: "0.8.0")
    ],
    targets: [
        .executableTarget(name: "MyExec", dependencies: ["MyLib"]),
        .target(
            name: "MyLib",
            dependencies: ["Get"],
            exclude: [
                "create-api.yaml",
                "schema.yaml"
            ],
            plugins: [
                .plugin(name: "CreateAPIPlugin", package: "CreateAPI")
            ]
        )
    ]
)

A manifest like the one above would then be capable of building CreateAPI (and Get) as part of the build process, reading from the create-api.yaml configuration and invoking the generator executable by providing the appropriate arguments.

As a proof of concept, I created a simple initial implementation of CreateAPIPlugin:

@main
struct CreateAPIPlugin: BuildToolPlugin {
    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
        let config = target.directory.appending("create-api.yaml")
        let schema = target.directory.appending("schema.yaml")

        return [
            .buildCommand(
                displayName: "Generate with CreateAPI",
                executable: try context.tool(named: "create-api").path,
                arguments: [
                    "generate",
                    "--module", target.name,
                    "--config", config,
                    "--output", context.pluginWorkDirectory,
                    schema
                ],
                inputFiles: [
                    config,
                    schema
                ],
                outputFiles: [
                    context.pluginWorkDirectory.appending("Entities.swift"),
                    context.pluginWorkDirectory.appending("Paths.swift")
                ]
            )
        ]
    }
}

Some things to note:

  1. Xcode/SPM don't do a great job of handling errors so the developer experience is still a bit hit and miss if things go wrong
  2. When things go right, it's awesome
  3. There is no way for the users package manifest to pass configuration options into our plugin, therefore we must assume that create-api.yaml is in the target source directory (and the user must exclude it in their manifest to omit a warning)
  4. We assume the schema is also in a similar location
    • This could be improved if we could customise the location via the config file (#47)
  5. We output to context.pluginWorkDirectory, which is in .build/DerivedData
  6. We must specify the output files, meaning that to support things like --split or --generate we'd need to break some of the initial processing of the steps/schema out so that the plugin tool can gather the context that it needs. This is certainly not required initially though.

I think this has a lot of potential if I'm being honest. It makes it very trivial to just drop an OpenAPI schema into an Xcode project and be instantaneously presented with type-safe entities and path definitions, all without having to worry about any external tooling.

I'll keep exploring this area and update here as I discover more.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.