Archived
1
0
Fork 0
This repository has been archived on 2024-02-06. You can view files and clone it, but cannot push or open issues or pull requests.
api-ts-gen/lib/OpenAPI.ts
supmiku39 87615616d6 fix Partial constructor, enhance error msg
add more info to BadValueError
  special message on undefined and null
  trace the location where the error is thrown
add int32 STP
fix STP null argument bug
2020-04-08 20:17:57 +09:00

299 lines
8.6 KiB
TypeScript

import {StrictTypeParser as STP} from './utils/StrictTypeParser';
const warn = (x: any) => console.warn('\x1b[1;33mWarning: '+x+'\x1b[0m');
/* ==== type declaration ==== */
export interface OpenAPI {
paths: Paths;
components?: Components;
}
// path
interface Paths {
[path: string]: PathItem
}
interface PathItem {
get?: Operation;
put?: Operation;
post?: Operation;
delete?: Operation;
patch?: Operation;
[_: string]: any;
}
type EMethod = 'get' | 'put' | 'post' | 'delete' | 'patch';
const ELMethod: Array<EMethod> = ['get', 'put', 'post', 'delete', 'patch'];
interface Operation {
responses: Responses;
parameters?: Parameter[];
requestBody?: RequestBody;
operationId?: string;
}
// response
interface Responses {
[status: string]: Response // | Reference;
}
interface Response {
// headers?: Header;
content?: TMediaTypes;
}
type TMediaTypes = {[contentType: string]: MediaType};
interface MediaType {
schema?: Schema | Reference;
example?: any;
examples?: {[_: string]: object};
}
// parameter
interface Parameter {
name: string;
in: EParameterIn;
description?: string;
required?: boolean;
deprecated?: boolean;
style?: string;
schema?: Schema | Reference;
}
type EParameterIn = 'query' | 'header' | 'path' | 'cookie';
export const ELParameterIn: Array<EParameterIn> = [
'path', 'query', 'header', 'cookie'];
// request body
interface RequestBody {
description: string;
content: {[contentType: string]: MediaType};
required?: boolean;
}
// components
interface Components {
schemas: {[_: string]: Schema | Reference};
}
// schemeType
export type Schemas = {[_: string]: Schema | Reference};
interface Schema {
type: string;
format?: string;
nullable?: boolean;
readOnly?: boolean;
maxSize?: number;
}
interface ArraySchema extends Schema {
items: Schema | Reference;
}
export function isArraySchema(x: any): x is ArraySchema {
return x.type === 'array';
}
interface ObjectSchema extends Schema {
properties: {[name: string]: Schema | Reference};
}
export function isObjectSchema(x: any): x is ObjectSchema {
return x.type === 'object';
}
interface Reference {
$ref: string;
maxSize?: string | number;
}
function isReference(x: any): x is Reference {
return typeof x.$ref === 'string';
}
// api
class APIFunction {
constructor(
public method: string,
public url: string,
public reqTypes: TReqTypes,
public resTypes: TResTypes,
) {}
}
type TReqTypes = {
query?: {[name: string]: SchemaType};
header?: {[name: string]: SchemaType};
path?: {[name: string]: SchemaType};
cookie?: {[name: string]: SchemaType};
body?: SchemaType;
};
type TResTypes = {[status: string]: SchemaType};
/* ==== ==== */
function mediaTypes2type(content?: TMediaTypes, required?: boolean):
SchemaType {
const media = content?.['application/json']; // TODO
if (media == null) {
if (Object.keys(content ?? {}).length > 0) {
warn('only support application/json now');
}
return new SchemaType('any', false);
}
// schema
const {schema} = media;
return new SchemaType(schema ?? 'any', required ?? false);
}
export class SchemaType {
private _typeName?: string;
get typeName(): string {
return this._typeName ??
(this._typeName = SchemaType.typeNameOf(this.schema));
}
get required(): boolean {
return this._required;
}
get maxSize(): string | number | undefined {
return this.schema.maxSize;
}
forProp(prop: string): string {
return `${prop}${this.required ? '' : '?'}: ${this.typeName}`;
}
stp(prop: string, label: string, partial: boolean=false): string {
const stp = SchemaType.gcStp(prop, this.schema, label, partial);
return (this.required ? '' : `${prop}===undefined ? undefined : `)+stp;
}
private schema: Schema | Reference;
constructor(schema: Schema | Reference | string,
private _required: boolean) {
this.schema = typeof schema === 'string' ? {type: schema} : schema;
}
static typeNameOf(schema: Schema | Reference): string {
if (isReference(schema)) {
const {$ref} = schema;
const typeName = /^#\/components\/schemas\/(\w+)$/g.exec($ref)?.[1];
if (typeName == null) {
warn(`Invalid $ref, use any instead: ${$ref}`);
return 'any';
}
return `Schemas.${typeName}`;
}
const {
type, format, nullable, readOnly,
} = schema;
let sType = type;
if (isArraySchema(schema)) {
sType = `Array<${SchemaType.typeNameOf(schema.items)}>`;
} else if (isObjectSchema(schema)) {
sType = '{';
for (const [name, sub] of Object.entries(schema.properties)) {
sType += `${name}: ${SchemaType.typeNameOf(sub)}, `;
}
sType += '}';
} else if (type === 'string') {
if (format === 'date-time') sType = 'Date';
else if (format === 'date') sType = 'FullDate';
else if (format === 'byte') sType = 'string'; // TODO Buffer
else if (format === 'binary') sType = 'string'; // TODO Buffer
else if (format) warn(`Unknown format ${format}, use string instead`);
} else if (type === 'integer') sType = 'number'; // TODO integer
if (nullable) sType = `${sType} | null`;
if (readOnly) sType = `Readonly<${sType}>`;
return sType;
}
static gcStp(para: string, schema: Schema | Reference,
label: string, partial: boolean): string {
// partial: Object only, 1 layer only
// object
if (isReference(schema)) {
const typeName = new SchemaType(schema, true).typeName;
return partial ?
`${typeName}.Partial(${para})` :
`new ${typeName}(${para})`;
}
// any
const {type, nullable, format} = schema;
let sStp;
if (type === 'any') return para;
if (isArraySchema(schema)) {
sStp = `(v, l)=>STP._Array(v, l, elm=>${
SchemaType.gcStp('elm', schema.items, `${label}[]`, false)})`;
} else if (isObjectSchema(schema)) {
sStp = '()=>({';
for (const [name, sub] of Object.entries(schema.properties)) {
sStp += `${name}: ${
SchemaType.gcStp(para+'.'+name, sub, label+'.'+name, false)}, `;
}
sStp += '})';
} else {
let t;
if (type === 'string') {
if (format === 'date-time') t = 'Date';
else if (format === 'date') t = 'FullDate';
else if (format === 'byte') t = 'byte';
else if (format === 'binary') t = 'binary';
else {
if (format) {
warn(`Unknown string format ${format}, use string instead`);
}
t = 'string';
}
} else if (type === 'integer') {
if (format === 'int32') t = 'int32';
else {
warn(`Unsupport integer format ${format}, use number instead`);
t = 'number'; // TODO int64
}
} else t = type;
if (!STP.supportTypes.includes(t)) {
warn(`Unsupport type ${type} ${format}, use any instead`);
return para;
}
sStp = `STP._${t}`;
}
// nullable
const funcName = nullable ? 'nullableParse' : 'parse';
// result
const sLabel = `'${label.replace(/'/g, '\\\'')}'`; // escape
return `STP.${funcName}(${sStp}, ${para}, ${sLabel})`;
}
}
export type APIFunctions = {[_: string]: APIFunction};
export function apiFunctionsOf(openAPI: OpenAPI): APIFunctions {
const {paths} = openAPI;
const functions: APIFunctions = {};
for (const [url, pathItem] of Object.entries(paths)) {
for (const method of ELMethod) {
const op = pathItem[method];
if (op == null) continue;
// operationId
const {
operationId, parameters, requestBody, responses,
} = op;
if (operationId == null) {
warn(`ignore operation in ${method} ${url}: ` +
'operationId should be given');
continue;
}
const name = operationId;
const reqTypes: TReqTypes = {};
const resTypes: TResTypes = {};
// reqParas
if (parameters != null) {
for (const para of parameters) {
const {
name, in: _in, required, schema,
} = para;
// add
if (reqTypes[_in] == null) reqTypes[_in] = {};
reqTypes[_in]![name] = new SchemaType(
schema ?? 'any', required ?? false);
}
}
// requestBody
if (requestBody != null) {
reqTypes.body = mediaTypes2type(
requestBody.content,
requestBody.required,
);
}
// responses
for (const [status, res] of Object.entries(responses)) {
resTypes[status] = mediaTypes2type(res.content, true);
}
// add to group
const saf = new APIFunction(method, url, reqTypes, resTypes);
functions[name] = saf;
}
}
return functions;
}