Skip to content

Commit

Permalink
Merge branch 'main' into feat/json-schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
mrlubos committed Apr 5, 2024
2 parents e2d3aa0 + ba19d0f commit 92ed4c6
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 74 deletions.
4 changes: 2 additions & 2 deletions packages/openapi-ts/src/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { type PathOrFileDescriptor, writeFileSync } from 'node:fs';
import ts from 'typescript';

import * as module from './module';
import { toString } from './utils';
import { tsNodeToString } from './utils';

export class TypeScriptFile extends Array<ts.Node> {
public write(file: PathOrFileDescriptor) {
const items = this.map(i => toString(i));
const items = this.map(i => tsNodeToString(i));
writeFileSync(file, items.join('\n'));
}
}
Expand Down
25 changes: 6 additions & 19 deletions packages/openapi-ts/src/compiler/module.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import ts from 'typescript';

import { CONFIG } from './utils';
import { ots } from './utils';

/**
* Create export all declaration. Example: `export * from './y'`
* @param module - module to export from.
* @returns ts.ExportDeclaration
*/
export const createExportAllDeclaration = (module: string) =>
ts.factory.createExportDeclaration(
undefined,
false,
undefined,
ts.factory.createStringLiteral(encodeURIComponent(module))
);
ts.factory.createExportDeclaration(undefined, false, undefined, ots.string(module));

type ImportItem = { name: string; isTypeOnly?: boolean } | string;

Expand All @@ -35,14 +30,10 @@ export const createNamedExportDeclarations = (
ts.factory.createNamedExports(
items.map(item => {
const { name, isTypeOnly = undefined } = typeof item === 'string' ? { name: item } : item;
return ts.factory.createExportSpecifier(
isAllTypes ? false : Boolean(isTypeOnly),
undefined,
encodeURIComponent(name)
);
return ots.export(name, isAllTypes ? false : Boolean(isTypeOnly));
})
),
ts.factory.createStringLiteral(encodeURIComponent(module), CONFIG.useSingleQuotes)
ots.string(module)
);
};

Expand All @@ -66,14 +57,10 @@ export const createNamedImportDeclarations = (
ts.factory.createNamedImports(
items.map(item => {
const { name, isTypeOnly = undefined } = typeof item === 'string' ? { name: item } : item;
return ts.factory.createImportSpecifier(
isAllTypes ? false : Boolean(isTypeOnly),
undefined,
ts.factory.createIdentifier(encodeURIComponent(name))
);
return ots.import(name, isAllTypes ? false : Boolean(isTypeOnly));
})
)
),
ts.factory.createStringLiteral(encodeURIComponent(module), CONFIG.useSingleQuotes)
ots.string(module)
);
};
26 changes: 23 additions & 3 deletions packages/openapi-ts/src/compiler/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,27 @@ const blankSourceFile = ts.createSourceFile('', '', CONFIG.scriptTarget, undefin
* @param node - the node to print.
* @returns string
*/
export function tsNodeToString(node: ts.Node): string {
const r = printer.printNode(ts.EmitHint.Unspecified, node, blankSourceFile);
return decodeURIComponent(r);
export function tsNodeToString(node: ts.Node, decode = true): string {
const result = printer.printNode(ts.EmitHint.Unspecified, node, blankSourceFile);
if (decode) {
return decodeURIComponent(result);
}
return result;
}

// ots for openapi-ts is helpers to reduce repetition of basic ts factory functions.
export const ots = {
export: (name: string, isTypeOnly?: boolean) =>
ts.factory.createExportSpecifier(
isTypeOnly ?? false,
undefined,
ts.factory.createIdentifier(encodeURIComponent(name))
),
import: (name: string, isTypeOnly?: boolean) =>
ts.factory.createImportSpecifier(
isTypeOnly ?? false,
undefined,
ts.factory.createIdentifier(encodeURIComponent(name))
),
string: (text: string) => ts.factory.createStringLiteral(encodeURIComponent(text), CONFIG.useSingleQuotes),
};
8 changes: 4 additions & 4 deletions packages/openapi-ts/src/utils/handlebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ const escapeComment = (value: string) =>
.replace(/\/\*/g, '*')
.replace(/\r?\n(.*)/g, (_, w) => `${EOL} * ${w.trim()}`);

export const escapeDescription = (value: string) =>
value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${');

const dataDestructure = (config: Config, operation: Operation) => {
if (config.name) {
if (config.useOptions) {
Expand Down Expand Up @@ -260,10 +263,7 @@ export const registerHandlebarHelpers = (config: Config, client: Client): void =
);

Handlebars.registerHelper('escapeComment', escapeComment);

Handlebars.registerHelper('escapeDescription', function (value: string) {
return value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '\\${');
});
Handlebars.registerHelper('escapeDescription', escapeDescription);

Handlebars.registerHelper('escapeNewline', function (value: string) {
return value.replace(/\n/g, '\\n');
Expand Down
179 changes: 137 additions & 42 deletions packages/openapi-ts/src/utils/write/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,58 +8,99 @@ import type { Model } from '../../openApi';
import type { Client } from '../../types/client';
import type { Config } from '../../types/config';
import { enumValue } from '../enum';
import type { Templates } from '../handlebars';
import { escapeDescription, type Templates } from '../handlebars';

const stringToInitializer = (value: string | string[]) => {
const initializer = Array.isArray(value)
? ts.factory.createArrayLiteralExpression(value.map(v => ts.factory.createIdentifier(v)))
: ts.factory.createIdentifier(value);
return initializer;
const valueToIdentifier = (value: string | ts.ObjectLiteralExpression) => {
if (typeof value !== 'string') {
return value;
}
return ts.factory.createIdentifier(value);
};

const addObjectProperty = (
properties: ts.PropertyAssignment[],
name: string,
value: string | string[] | ts.ObjectLiteralExpression
value: string | string[] | ts.ObjectLiteralExpression | ts.ObjectLiteralExpression[]
) => {
const initializer = !Array.isArray(value) && typeof value === 'object' ? value : stringToInitializer(value);
const initializer = Array.isArray(value)
? ts.factory.createArrayLiteralExpression(value.map(v => valueToIdentifier(v)))
: valueToIdentifier(value);
const property = ts.factory.createPropertyAssignment(name, initializer);
return [...properties, property];
};

const addPropDefault = (model: Model, properties: ts.PropertyAssignment[]) => {
if (model.default === undefined) {
return properties;
const arraySchema = (config: Config, model: Model) => {
let properties = [ts.factory.createPropertyAssignment('type', ts.factory.createStringLiteral('array'))];

if (model.link) {
properties = addObjectProperty(properties, 'contains', modelToJsonSchema(config, model.link));
} else {
properties = addObjectProperty(
properties,
'contains',
`{
type: '${model.base}',
}`
);
}

if (model.default !== undefined) {
properties = addObjectProperty(properties, 'default', String(model.default));
}
return addObjectProperty(properties, 'default', String(model.default));
};

const addPropIsNullable = (model: Model, properties: ts.PropertyAssignment[]) => {
if (!model.isNullable) {
return properties;
if (model.isNullable) {
properties = addObjectProperty(properties, 'isNullable', 'true');
}
return addObjectProperty(properties, 'isNullable', 'true');
};

const addPropIsReadOnly = (model: Model, properties: ts.PropertyAssignment[]) => {
if (!model.isReadOnly) {
return properties;
if (model.isReadOnly) {
properties = addObjectProperty(properties, 'isReadOnly', 'true');
}
return addObjectProperty(properties, 'isReadOnly', 'true');

if (model.isRequired) {
properties = addObjectProperty(properties, 'isRequired', 'true');
}

return ts.factory.createObjectLiteralExpression(properties);
};

const addPropIsRequired = (model: Model, properties: ts.PropertyAssignment[]) => {
if (!model.isRequired) {
return properties;
const compositionSchema = (config: Config, model: Model) => {
let properties = [ts.factory.createPropertyAssignment('type', ts.factory.createStringLiteral(model.export))];

if (model.description) {
properties = addObjectProperty(properties, 'description', `\`${escapeDescription(model.description)}\``);
}

if (model.properties.length) {
properties = addObjectProperty(
properties,
'contains',
model.properties.map(property => modelToJsonSchema(config, property))
);
}

if (model.default !== undefined) {
properties = addObjectProperty(properties, 'default', String(model.default));
}

if (model.isNullable) {
properties = addObjectProperty(properties, 'isNullable', 'true');
}

if (model.isReadOnly) {
properties = addObjectProperty(properties, 'isReadOnly', 'true');
}
return addObjectProperty(properties, 'isRequired', 'true');

if (model.isRequired) {
properties = addObjectProperty(properties, 'isRequired', 'true');
}

return ts.factory.createObjectLiteralExpression(properties);
};

const dictSchema = (config: Config, model: Model) => {
let properties = [ts.factory.createPropertyAssignment('type', ts.factory.createStringLiteral('dictionary'))];

if (model.link) {
// properties = addObjectProperty(properties, 'contains', exportSchema(config, model.link));
properties = addObjectProperty(properties, 'contains', modelToJsonSchema(config, model.link));
} else {
properties = addObjectProperty(
Expand All @@ -71,10 +112,21 @@ const dictSchema = (config: Config, model: Model) => {
);
}

properties = addPropDefault(model, properties);
properties = addPropIsNullable(model, properties);
properties = addPropIsReadOnly(model, properties);
properties = addPropIsRequired(model, properties);
if (model.default !== undefined) {
properties = addObjectProperty(properties, 'default', String(model.default));
}

if (model.isNullable) {
properties = addObjectProperty(properties, 'isNullable', 'true');
}

if (model.isReadOnly) {
properties = addObjectProperty(properties, 'isReadOnly', 'true');
}

if (model.isRequired) {
properties = addObjectProperty(properties, 'isRequired', 'true');
}

return ts.factory.createObjectLiteralExpression(properties);
};
Expand All @@ -90,10 +142,56 @@ const enumSchema = (config: Config, model: Model) => {
);
}

properties = addPropDefault(model, properties);
properties = addPropIsNullable(model, properties);
properties = addPropIsReadOnly(model, properties);
properties = addPropIsRequired(model, properties);
if (model.default !== undefined) {
properties = addObjectProperty(properties, 'default', String(model.default));
}

if (model.isNullable) {
properties = addObjectProperty(properties, 'isNullable', 'true');
}

if (model.isReadOnly) {
properties = addObjectProperty(properties, 'isReadOnly', 'true');
}

if (model.isRequired) {
properties = addObjectProperty(properties, 'isRequired', 'true');
}

return ts.factory.createObjectLiteralExpression(properties);
};

const interfaceSchema = (config: Config, model: Model) => {
let properties: ts.PropertyAssignment[] = [];

if (model.description) {
properties = addObjectProperty(properties, 'description', `\`${escapeDescription(model.description)}\``);
}

let props: ts.PropertyAssignment[] = [];
model.properties
.filter(property => property.name !== '[key: string]')
.forEach(property => {
props = addObjectProperty(props, property.name, modelToJsonSchema(config, property));
});
const obj = ts.factory.createObjectLiteralExpression(props);
properties = addObjectProperty(properties, 'properties', obj);

if (model.default !== undefined) {
properties = addObjectProperty(properties, 'default', String(model.default));
}

if (model.isNullable) {
properties = addObjectProperty(properties, 'isNullable', 'true');
}

if (model.isReadOnly) {
properties = addObjectProperty(properties, 'isReadOnly', 'true');
}

if (model.isRequired) {
properties = addObjectProperty(properties, 'isRequired', 'true');
}

return ts.factory.createObjectLiteralExpression(properties);
};
Expand All @@ -109,12 +207,10 @@ const modelToJsonSchema = (config: Config, model: Model) => {
case 'all-of':
case 'any-of':
case 'one-of':
// {{>schemaComposition}}
schema = 'COMP';
jsonSchema = compositionSchema(config, model);
break;
case 'array':
// {{>schemaArray}}
schema = 'ARR';
jsonSchema = arraySchema(config, model);
break;
case 'dictionary':
jsonSchema = dictSchema(config, model);
Expand All @@ -123,8 +219,7 @@ const modelToJsonSchema = (config: Config, model: Model) => {
jsonSchema = enumSchema(config, model);
break;
case 'interface':
// {{>schemaInterface}}
schema = 'INTERFACE';
jsonSchema = interfaceSchema(config, model);
break;
default:
// {{>schemaGeneric}}
Expand All @@ -151,7 +246,7 @@ const exportSchema = (config: Config, model: Model) => {
ts.NodeFlags.Const
)
);
return tsNodeToString(statement);
return tsNodeToString(statement, false);
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const $ModelWithPattern_WIP = INTERFACE as const;
export const $ModelWithPattern_WIP = { firstKey: 'string expression', secondKey: 0 } as const;

export const $ModelWithPattern = {
description: `This is a model that contains a some patterns`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './models';
export const $CompositionWithOneOfDiscriminator_WIP = { type: "one-of", description: This is a model with one property with a 'one of' relationship where the options are not $ref, contains: (export * from './models';
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { ApiError } from './core/ApiError';
export const $CompositionWithOneOfDiscriminator_WIP = { type: "one-of", description: This is a model with one property with a 'one of' relationship where the options are not $ref, contains: (export { ApiError } from './core/ApiError';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';
export { OpenAPI, type OpenAPIConfig } from './core/OpenAPI';

0 comments on commit 92ed4c6

Please sign in to comment.