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

[TypeScript/JavaScript] Emit all necessary info as metadata object; defer as much as possible to a runtime library #1233

Open
cspotcode opened this issue Oct 13, 2018 · 1 comment

Comments

@cspotcode
Copy link

cspotcode commented Oct 13, 2018

Description

This is related to discussion in #802 about re-thinking the various TypeScript generators as a single generator that is far more configurable.

Philosophically, I think the new generator should generate as little code as possible via the generator, delegating as much behavior as possible to a runtime that is driven by a "spec metadata" structure. This enables a better developer experience for us creating the generator and makes it easier for plugins to be written that customize the generated API's behavior.

Here's an example:

https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/typescript-fetch/api.mustache#L165-L177

if ({{paramName}} !== undefined) {
                {{#isDateTime}}
                localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any).toISOString();
                {{/isDateTime}}
                {{^isDateTime}}
                {{#isDate}}
                localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any).toISOString();
                {{/isDate}}
                {{^isDate}}
                localVarQueryParameter['{{baseName}}'] = {{paramName}};
                {{/isDate}}
                {{/isDateTime}}
            }

Instead, we should write this as plain TypeScript:

// Assume that generated code has created and passed us a `params` and `paramSpec` object
const {isDateTime, baseName, isDate} = paramSpec;
const paramValue = params[paramName];
if (paramValue !== undefined) {
                if(isDateTime) {
                localVarQueryParameter[baseName] = paramValue.toISOString();
                } else if(isDate) {
                    localVarQueryParameter[baseName] = paramValue.toISOString();
                } else {
                    localVarQueryParameter[baseName] = paramValue;
                }
            }

The mustache template's job is now much easier. It can generate something like this:

function fooOperation(param1: string, param2: Date, param3: boolean): Promise<Whatever> {
    // Delegate to our runtime, passing it a reference to operation metadata
    return invokeOperation({param1, param2, param3}, specMeta.operations.foo); // specMeta is an object containing all the metadata our runtime needs to perform API operations
}

...and then elsewhere, generate the specMeta object: (this code is not perfect but hopefully explains the idea)

export const specMeta: SpecMeta = {
    operations: {
    {{#operations}}
        {{#operation}}
        {{operationName}}: {
            parameters: [
            {{#parameters}}
            {
                paramName: '{{paramName}}',
                isDateTime: {{isDateTime}},
                isDate: {{isDate}},
                baseName: '{{baseName}}',
            },
            {{/parameters}}
            ]
        },
        {{/operation}}
    {{/operations}}
    }
}

Things the generator should generate:

  • function, class, and method signatures.
  • interfaces for all models, request bodies, and response bodies
  • metadata file (JSON or TypeScript object literal)

A few things for which the generator should not generate code:

  • Parameter validation
  • Converting parameters into an HTTP request and body
  • Invoking HTTP requests

All the necessary information should be emitted into a metadata file that is read by runtime libraries to perform the necessary tasks.

In non-JS languages, this might require messy reflection. But in JS, reflection is the norm, and its performance is not unreasonably slow compared to JS code written without reflection. (especially when we consider we're creating an API client that sends HTTP requests, not a compression algorithm)

Writing TS code in mustache templates is inconvenient because we lose the benefits of syntax highlighting and code completion. We can instead write an SDK runtime driven by the metadata emitted by openapi-generator. The metadata object can be strongly typed, so we shouldn't lose safety.

This also makes it a bit more obvious that the runtime can delegate lots of heavy lifting to third-party libraries. For example, schema validation can be implemented as a plugin that uses a third-party validator.

The generator will still be generating function, class, and method signatures for the entire API spec, which is very important to ensure a good developer experience for anyone consuming the generated API client.

EDIT: added an example of mustache template for the spec metadata object.

@fuzzzerd
Copy link
Contributor

fuzzzerd commented Aug 15, 2019

This makes a lot of sense to me. I'd like to see something like this implemented.

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

No branches or pull requests

2 participants