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

Add suport for readOnly properties. Add experimental flag. #203

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions lib/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ import { tsType } from './gen-utils';
* Either a request body or response content
*/
export class Content {

type: string;

constructor(
public mediaType: string,
public spec: MediaTypeObject,
public options: Options,
public openApi: OpenAPIObject) {
public openApi: OpenAPIObject,
method?: string
) {
const readOnly =
(method && ['post', 'put'].includes(method) && options.experimental) ||
false;
this.type = tsType(spec.schema, options, openApi);

if (readOnly) {
this.type = `Utils.Writable<${this.type}>`;
}
}
}
92 changes: 72 additions & 20 deletions lib/gen-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ import { OpenAPIObject, ReferenceObject, SchemaObject } from 'openapi3-ts';
import { Options } from './options';
import { Model } from './model';

export const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
export const HTTP_METHODS = [
'get',
'put',
'post',
'delete',
'options',
'head',
'patch',
'trace',
];
type SchemaOrRef = SchemaObject | ReferenceObject;

/**
Expand Down Expand Up @@ -37,7 +46,11 @@ export function qualifiedName(name: string, options: Options): string {
/**
* Returns the file to import for a given model
*/
export function modelFile(pathToModels: string, name: string, options: Options): string {
export function modelFile(
pathToModels: string,
name: string,
options: Options
): string {
let dir = pathToModels || '';
if (dir.endsWith('/')) {
dir = dir.substr(0, dir.length - 1);
Expand All @@ -47,7 +60,7 @@ export function modelFile(pathToModels: string, name: string, options: Options):
dir += `/${ns}`;
}
const file = unqualifiedName(name, options);
return dir += '/' + fileName(file);
return (dir += '/' + fileName(file));
}

/**
Expand Down Expand Up @@ -115,15 +128,23 @@ export function toBasicChars(text: string, firstNonDigit = false): string {
/**
* Returns the TypeScript comments for the given schema description, in a given indentation level
*/
export function tsComments(description: string | undefined, level: number, deprecated?: boolean) {
export function tsComments(
description: string | undefined,
level: number,
deprecated?: boolean
) {
const indent = ' '.repeat(level);
if (description == undefined || description.length === 0) {
return indent + (deprecated ? '/** @deprecated */' : '');
}
const lines = description.trim().split('\n');
let result = '\n' + indent + '/**\n';
lines.forEach(line => {
result += indent + ' *' + (line === '' ? '' : ' ' + line.replace(/\*\//g, '* / ')) + '\n';
lines.forEach((line) => {
result +=
indent +
' *' +
(line === '' ? '' : ' ' + line.replace(/\*\//g, '* / ')) +
'\n';
});
if (deprecated) {
result += indent + ' *\n' + indent + ' * @deprecated\n';
Expand All @@ -136,14 +157,18 @@ export function tsComments(description: string | undefined, level: number, depre
* Applies the prefix and suffix to a model class name
*/
export function modelClass(baseName: string, options: Options) {
return `${options.modelPrefix || ''}${typeName(baseName)}${options.modelSuffix || ''}`;
return `${options.modelPrefix || ''}${typeName(baseName)}${
options.modelSuffix || ''
}`;
}

/**
* Applies the prefix and suffix to a service class name
*/
export function serviceClass(baseName: string, options: Options) {
return `${options.servicePrefix || ''}${typeName(baseName)}${options.serviceSuffix || 'Service'}`;
return `${options.servicePrefix || ''}${typeName(baseName)}${
options.serviceSuffix || 'Service'
}`;
}

/**
Expand All @@ -160,7 +185,12 @@ export function escapeId(name: string) {
/**
* Returns the TypeScript type for the given type and options
*/
export function tsType(schemaOrRef: SchemaOrRef | undefined, options: Options, openApi: OpenAPIObject, container?: Model): string {
export function tsType(
schemaOrRef: SchemaOrRef | undefined,
options: Options,
openApi: OpenAPIObject,
container?: Model
): string {
if (!schemaOrRef) {
// No schema
return 'any';
Expand All @@ -184,9 +214,13 @@ export function tsType(schemaOrRef: SchemaOrRef | undefined, options: Options, o
const union = schema.oneOf || schema.anyOf || [];
if (union.length > 0) {
if (union.length > 1) {
return `(${union.map(u => tsType(u, options, openApi, container)).join(' | ')})`;
return `(${union
.map((u) => tsType(u, options, openApi, container))
.join(' | ')})`;
} else {
return union.map(u => tsType(u, options, openApi, container)).join(' | ');
return union
.map((u) => tsType(u, options, openApi, container))
.join(' | ');
}
}

Expand All @@ -201,7 +235,7 @@ export function tsType(schemaOrRef: SchemaOrRef | undefined, options: Options, o
const allOf = schema.allOf || [];
let intersectionType: string[] = [];
if (allOf.length > 0) {
intersectionType = allOf.map(u => tsType(u, options, openApi, container));
intersectionType = allOf.map((u) => tsType(u, options, openApi, container));
}

// An object
Expand All @@ -215,7 +249,11 @@ export function tsType(schemaOrRef: SchemaOrRef | undefined, options: Options, o
continue;
}
if ((property as SchemaObject).description) {
result += tsComments((property as SchemaObject).description, 0, (property as SchemaObject).deprecated);
result += tsComments(
(property as SchemaObject).description,
0,
(property as SchemaObject).deprecated
);
}
result += `'${propName}'`;
const propRequired = required && required.includes(propName);
Expand All @@ -229,8 +267,14 @@ export function tsType(schemaOrRef: SchemaOrRef | undefined, options: Options, o
result += `: ${propertyType};\n`;
}
if (schema.additionalProperties) {
const additionalProperties = schema.additionalProperties === true ? {} : schema.additionalProperties;
result += `[key: string]: ${tsType(additionalProperties, options, openApi, container)};\n`;
const additionalProperties =
schema.additionalProperties === true ? {} : schema.additionalProperties;
result += `[key: string]: ${tsType(
additionalProperties,
options,
openApi,
container
)};\n`;
}
result += '}';
intersectionType.push(result);
Expand All @@ -246,7 +290,7 @@ export function tsType(schemaOrRef: SchemaOrRef | undefined, options: Options, o
if (type === 'number' || type === 'integer' || type === 'boolean') {
return enumValues.join(' | ');
} else {
return enumValues.map(v => `'${jsesc(v)}'`).join(' | ');
return enumValues.map((v) => `'${jsesc(v)}'`).join(' | ');
}
}

Expand Down Expand Up @@ -291,9 +335,11 @@ export function deleteDirRecursive(dir: string) {
if (fs.existsSync(dir)) {
fs.readdirSync(dir).forEach((file: any) => {
const curPath = path.join(dir, file);
if (fs.lstatSync(curPath).isDirectory()) { // recurse
if (fs.lstatSync(curPath).isDirectory()) {
// recurse
deleteDirRecursive(curPath);
} else { // delete file
} else {
// delete file
fs.unlinkSync(curPath);
}
});
Expand All @@ -304,7 +350,11 @@ export function deleteDirRecursive(dir: string) {
/**
* Synchronizes the files from the source to the target directory. Optionally remove stale files.
*/
export function syncDirs(srcDir: string, destDir: string, removeStale: boolean): any {
export function syncDirs(
srcDir: string,
destDir: string,
removeStale: boolean
): any {
fs.ensureDirSync(destDir);
const srcFiles = fs.readdirSync(srcDir);
const destFiles = fs.readdirSync(destDir);
Expand All @@ -317,7 +367,9 @@ export function syncDirs(srcDir: string, destDir: string, removeStale: boolean):
} else {
// Read the content of both files and update if they differ
const srcContent = fs.readFileSync(srcFile, { encoding: 'utf-8' });
const destContent = fs.existsSync(destFile) ? fs.readFileSync(destFile, { encoding: 'utf-8' }) : null;
const destContent = fs.existsSync(destFile)
? fs.readFileSync(destFile, { encoding: 'utf-8' })
: null;
if (srcContent !== destContent) {
fs.writeFileSync(destFile, srcContent, { encoding: 'utf-8' });
console.debug('Wrote ' + destFile);
Expand Down
30 changes: 24 additions & 6 deletions lib/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Options } from './options';
* Stores the global variables used on generation
*/
export class Globals {

configurationClass: string;
configurationFile: string;
configurationParams: string;
Expand All @@ -15,11 +14,13 @@ export class Globals {
requestBuilderFile: string;
responseClass: string;
responseFile: string;
utilsFile?: string;
moduleClass?: string;
moduleFile?: string;
modelIndexFile?: string;
serviceIndexFile?: string;
rootUrl?: string;
experimental?: boolean;

constructor(options: Options) {
this.configurationClass = options.configuration || 'ApiConfiguration';
Expand All @@ -31,17 +32,34 @@ export class Globals {
this.requestBuilderFile = fileName(this.requestBuilderClass);
this.responseClass = options.response || 'StrictHttpResponse';
this.responseFile = fileName(this.responseClass);
this.experimental = options.experimental || false;

if (options.module !== false && options.module !== '') {
this.moduleClass = options.module === true || options.module == undefined ? 'ApiModule' : options.module;
this.moduleClass =
options.module === true || options.module == undefined
? 'ApiModule'
: options.module;
// Angular's best practices demands xxx.module.ts, not xxx-module.ts
this.moduleFile = fileName(this.moduleClass as string).replace(/\-module$/, '.module');
this.moduleFile = fileName(this.moduleClass as string).replace(
/\-module$/,
'.module'
);
}
if (options.serviceIndex !== false && options.serviceIndex !== '') {
this.serviceIndexFile = options.serviceIndex === true || options.serviceIndex == undefined ? 'services' : options.serviceIndex;
this.serviceIndexFile =
options.serviceIndex === true || options.serviceIndex == undefined
? 'services'
: options.serviceIndex;
}
if (options.modelIndex !== false && options.modelIndex !== '') {
this.modelIndexFile = options.modelIndex === true || options.modelIndex == undefined ? 'models' : options.modelIndex;
this.modelIndexFile =
options.modelIndex === true || options.modelIndex == undefined
? 'models'
: options.modelIndex;
}
}

if (this.experimental) {
this.utilsFile = 'utils';
}
}
}
44 changes: 36 additions & 8 deletions lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import { tsComments, tsType, unqualifiedName } from './gen-utils';
import { Options } from './options';
import { Property } from './property';


/**
* Context to generate a model
*/
export class Model extends GenType {

// General type
isSimple: boolean;
isEnum: boolean;
Expand All @@ -27,7 +25,15 @@ export class Model extends GenType {
properties: Property[];
additionalPropertiesType: string;

constructor(public openApi: OpenAPIObject, name: string, public schema: SchemaObject, options: Options) {
// Additional properties
readOnly: boolean;

constructor(
public openApi: OpenAPIObject,
name: string,
public schema: SchemaObject,
options: Options
) {
super(name, unqualifiedName, options);

const description = schema.description || '';
Expand All @@ -52,13 +58,17 @@ export class Model extends GenType {
this.isEnum = (this.enumValues || []).length > 0;
this.isSimple = !this.isObject && !this.isEnum;

this.readOnly = schema.readOnly || false;

if (this.isObject) {
// Object
const propertiesByName = new Map<string, Property>();
this.collectObject(schema, propertiesByName);
const sortedNames = [...propertiesByName.keys()];
sortedNames.sort();
this.properties = sortedNames.map(propName => propertiesByName.get(propName) as Property);
this.properties = sortedNames.map(
(propName) => propertiesByName.get(propName) as Property
);
} else {
// Simple / array / enum / union / intersection
this.simpleType = tsType(schema, options, openApi);
Expand All @@ -84,11 +94,15 @@ export class Model extends GenType {
return this.name === name;
}

private collectObject(schema: SchemaObject, propertiesByName: Map<string, Property>) {
private collectObject(
schema: SchemaObject,
propertiesByName: Map<string, Property>
) {
if (schema.type === 'object' || !!schema.properties) {
// An object definition
const properties = schema.properties || {};
const required = schema.required || [];
const readOnly = schema.readOnly || false;
const propNames = Object.keys(properties);
// When there are additional properties, we need an union of all types for it.
// See https://github.com/cyclosproject/ng-openapi-gen/issues/68
Expand All @@ -102,7 +116,14 @@ export class Model extends GenType {
}
};
for (const propName of propNames) {
const prop = new Property(this, propName, properties[propName], required.includes(propName), this.options, this.openApi);
const prop = new Property(
this,
propName,
properties[propName],
required.includes(propName),
this.options,
this.openApi
);
propertiesByName.set(propName, prop);
appendType(prop.type);
if (!prop.required) {
Expand All @@ -112,13 +133,20 @@ export class Model extends GenType {
if (schema.additionalProperties === true) {
this.additionalPropertiesType = 'any';
} else if (schema.additionalProperties) {
const propType = tsType(schema.additionalProperties, this.options, this.openApi);
const propType = tsType(
schema.additionalProperties,
this.options,
this.openApi
);
appendType(propType);
this.additionalPropertiesType = [...propTypes].sort().join(' | ');
}

// Aditional properties
this.readOnly = readOnly;
}
if (schema.allOf) {
schema.allOf.forEach(s => this.collectObject(s, propertiesByName));
schema.allOf.forEach((s) => this.collectObject(s, propertiesByName));
}
}
}
Loading