1. Introduction to openapi-eller 0.3.3
Documentation is still to be done. Sorry!
2. Creating Targets
2.1. Basic requirements for a conforming target
2.1.1. Getting Started
A target is the recipe for generating API code for a given platform. It may generate either a client or a server depending on its need (and the name of the target will make it obvious, assume client by default).
A target template is usually one Handlebars template, with any number of partials necessary to make a fully functional tool.
A target generator can be configured with a JSON or YAML file of key-value parameters that can be accessed from the code generation templates.
Each target’s entry point is an index.ts
file implementing the Target
type.
2.1.2. Basic API
A conforming implementation consists of at the very least:
-
A top level class—or equivalent—holding methods for each endpoint for a given service
-
A top level enum-like class—or equivalent—for holding known server endpoints, configurable for the defined variables of that server endpoint
-
At least one constructor taking a base URL as a string, and preferably another constructor accepting the enum-like class type for servers if it makes sense for the provided language
-
Each endpoint provides a return type of an Observable or a Promise/Future, preferring the most used solution for the platform, and falling back to Rx where that is not specified or is simpler
-
Generates RFC-compliant handlers for OAuth 2 where required
-
Handles all types supported by OpenAPI v3, and preferrably has a mechanism for handling well-known formats, and custom extensions for those formats
-
Generates interceptors for each type of authentication supported by OpenAPI
2.1.3. Nested Objects
OpenAPI specifications tend to have nested objects inside other objects, as is common with all JSON documents. In order to handle this effectively, depending on the placement of the object and whether or not it is anonymously defined (does not appear in the top-level schemas list), a subobject might be generated.
Some languages like Swift or Kotlin will allow nesting a class inside another class definition, however some others do not. In the case where your language does not allow nested classes, it is recommended that you generate these classes at the top level of your generated models, with the nesting generated as part of the name of the class.
For instance, if you have an object Foo
, with an anonymous field bar
which would
ordinarily generate the nested type Bar
and be accessed in Kotlin as Foo.Bar
, in
your flat language you may need to name the type Foo_Bar
or some other underutilised
delimiter in order to guarantee namespace integrity.
2.1.4. Documentation
Several targets require some set-up in order to function. Instead of generating entire demo projects, only two things are needed for fully supporting a target and assisting those people who use them:
-
At the beginning for the file, clearly mark it as generated
-
Below that, document any requirements for using dependencies, such as the version requirements, package managers required, imports to be declared in other configurations, etc. You may make this copy and pastable chunks if you desire to be super-nice.
-
The intro documentation should also include supported configuration elements from the JSON, such as
{ "es6": true }
enabling ES6 modules for ECMAScript, which would otherwise only be discoverable by reading the target source code. -
Documenting dependencies for specific elements on those elements, such as the requirement for OAuth activities in Android to specify some things in the XML manifest. This should be as copy-pastable as possible.
-
Generate the relevant documentation fields for all endpoints, models and fields. This makes the API autocompletable and discoverable in an IDE, or allow documentation to be generated with a relevant generator.
2.1.5. Reserved keywords
Each target must handle reserved keywords in a way that would be considered
acceptable for a given platform. A general approach is to append an underscore (_
)
to a variable or class name. Users have the option of providing field and class name
overrides in the configuration passed to the generator, so close enough is good
enough is the approach taken.
Even if a language supports a special way of accessing keywords, such as Swift’s backtick notation, avoid doing so. It makes actually using the API more painful than it needs to be.
2.1.6. Interceptors
All targets must implement an interceptor pattern for handling the various API key, OAuth and other header and query string mangling operations that affect all endpoints.
The basic principle is that in your language, it should be possible to provide the necessary hooks to modify the headers and query parameters to support authentication and other requirements for a given API.
Where possible, it is considered highly useful to be able to inject a logging interceptor, though with some implementations this may be difficult if not impossible. Try to design your target so that this is achievable.
2.1.7. Formatters and Types
Targets should—but are not required to—provide a mechanism for handling user-defined formats and parsing them to correct types. This is distinct from actually implementing those formats however. It should be sufficient to provide as part of the service constructors to provide parsing hook functions keyed by the type and format arguments.
In Kotlin, the @Format
annotation is used for this purpose and provides a hook.
The Gson
library can then be provided type handler implementations in the constructor
in order to make parsing those formats to the correct data types possible.
Consider for example an object of type string
with a format uint64
. It is reasonable
for this type to therefore be generated to Long
and using the @Format("int64")
annotation, causing the string to be correctly parsed into the most correct datatype.
2.1.8. Configuration
openapi-eller
supports the direct manipulation of targets using a provided config file.
Some keys are reserved for use by the generator itself (such as fieldRenames
) but may
be extended with custom keys for use by targets and are accessible within the templates
via the config
key. Examples of this in use include the ECMAScript target, where
different module export styles are used whether or not es6
or commonjs
is set to true.
At this stage, configuration is quite rudimentary. There is no support for disjoint values, and no automatically derived variables. This is an area of development going forward, and suggestions are highly welcomed.
2.1.9. Dependency policy
Until 1.0.0, this policy does not apply. Experiment at will!
All dependencies must be permissively licensed. Once a target is accepted into the tree, dependencies may not be changed except for minor version changes, except in cases of major bugs. Only in major versions will this requirement be relaxed.
5. Targets
All targets support a certain amount of customization of the end result. Those customizations are specific per target and are specified in the config file you can pass to the generator.
Configuration files are JSON or YAML files matching the relevant key-value pairs.
5.1. Kotlin
5.1.1. Target aliases
-
kotlin
5.1.2. Configuration options
-
package
(string): when available name the package -
imports
(string[]): a list of imports
5.2. Swift
5.2.1. Target aliases
-
swift
5.2.2. Configuration options
-
prefix
(string): an optional prefix to the names of classes, structs, enums, etc
5.3. TypeScript
5.3.1. Target aliases
-
typescript
-
ts
5.3.2. Configuration options
-
dev
(boolean): when true, will generate development imports and functions.