Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
cice committed Apr 1, 2020
1 parent c241b7e commit 1ed17f2
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"definitions": {
"delete_user": {
"links": [{
"href": "/users/{id}/{id2}",
"href": "/users_api/users/{id}/{id2}",
"method": "DELETE",
"rel": "edit",
"hrefSchema": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "http://json-schema.org/draft-04/hyper-schema",
"id": "https://example.com/users_api/endpoints/get_groups",
"definitions": {
"get_groups": {
"links": [{
"href": "/users_api/groups/{id}/{id2}",
"method": "GET",
"rel": "instances",
"hrefSchema": {
"properties": {
"id": {
"type": "integer"
},
"id2": {
"type": "integer"
}
}
},
"targetSchema": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"additionalProperties": false,
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
}
}
}
}
}]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"definitions": {
"get_users": {
"links": [{
"href": "/users/{id}/{id2}",
"href": "/users_api/users/{id}/{id2}",
"method": "GET",
"rel": "instances",
"hrefSchema": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"definitions": {
"post_users": {
"links": [{
"href": "/users",
"href": "/users_api/users",
"method": "POST",
"rel": "create",
"schema": {
Expand Down
3 changes: 3 additions & 0 deletions examples/hyper-schema/with-refs/complex-schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"get_users": {
"$ref": "https://example.com/users_api/endpoints/get_users#/definitions/get_users"
},
"get_groups": {
"$ref": "https://example.com/users_api/endpoints/get_groups#/definitions/get_groups"
},
"post_users": {
"$ref": "https://example.com/users_api/endpoints/post_users#/definitions/post_users"
},
Expand Down
38 changes: 22 additions & 16 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import * as yargs from 'yargs';
import {promises as fs} from 'fs';
import {defaultOptions} from './defaults';
import {generateGatewayFiles} from './index';

interface Arguments {
[x: string]: unknown;

_: string[];
module: string;
schema: string;
out: string;
source: string[];
}

const cliArgs = yargs
.usage('Usage: $0 generate hyperschema.json -o frontend/backend-api -m BackendApi')
.demandCommand(1)
.command('generate [schema]', 'generate api client module for given schema', (generate) => {
generate
.option('module', {
alias: 'm',
type: 'string',
description: 'Name of angular module to generate',
demandOption: true,
})
.option('out', {
alias: 'o',
type: 'string',
description: 'Output directory',
demandOption: true,
});
});
generate
.option('module', {
alias: 'm',
type: 'string',
description: 'Name of angular module to generate',
demandOption: true,
})
.option('out', {
alias: 'o',
type: 'string',
description: 'Output directory',
demandOption: true,
})
.option('source', {
alias: 's',
type: 'array',
description: 'Directories with referenced schema files',
});
});

const argv = cliArgs.argv as unknown as Arguments;

Expand All @@ -37,7 +43,7 @@ const schemaFile = argv.schema;
const outDir = argv.out;

async function run() {
await generateGatewayFiles(schemaFile, outDir, {...defaultOptions, moduleName});
await generateGatewayFiles(schemaFile, outDir, {...defaultOptions, moduleName, localSources: argv.source});
}

run();
46 changes: 37 additions & 9 deletions src/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {capitalCase, paramCase} from 'change-case';
import {JSONSchema4} from 'json-schema';
import {DEFAULT_OPTIONS} from 'json-schema-to-typescript';
import {GeneratedCode} from './generators/types';
import {GeneratorOptions} from './options';
import {HyperSchemaLink4, HyperSchemaResource4} from './types/hyper-schema';
import {HyperSchema4, HyperSchemaLink4, HyperSchemaResource4} from './types/hyper-schema';
import {getCommonHref} from './util/hyper-schema';
import {assertDefined, inspect} from './util/misc';
import {stripCommonPath, stripInterpolations} from './util/paths';
import {upperFirst} from './util/strings';
import {camelCase} from 'change-case'

export function pathToCapitalizedNameParts(path: string): string[] {
return path
.split('/')
.filter((x) => !!x)
.map((x) => capitalCase(x.replace(/[^\w-]/, '')).replace(' ', ''));
.map((x) => capitalCase(x.replace(/[^\w-]/, '')).replace(/\s/g, ''));
}

export function httpVerbAndHrefBasedMethodName(
Expand All @@ -22,20 +24,21 @@ export function httpVerbAndHrefBasedMethodName(
href: string,
): string {
const method = assertDefined(link.method, `method missing in ${inspect(link)}`);
const commonHref = getCommonHref(resource);
const commonHref = getCommonHref(resource.links);

const prefix = method.toLocaleLowerCase();
const localHref = stripCommonPath(href, commonHref);

return [prefix, ...pathToCapitalizedNameParts(localHref)].join('');
let strings = [prefix, ...pathToCapitalizedNameParts(localHref)];
return strings.join('');
}

export function commonHrefBasedClassName(resource: HyperSchemaResource4): string {
const commonHref = stripInterpolations(getCommonHref(resource));
export function commonHrefBasedClassName(resource: HyperSchemaResource4, key: string): string {
const commonHref = stripInterpolations(getCommonHref(resource.links));

const typeName = pathToCapitalizedNameParts(commonHref).join('');
let typeName = pathToCapitalizedNameParts(commonHref).join('');
if (typeName === '') {
throw `can't build type-name for href ${commonHref}`;
typeName = camelCase(key);
}

return typeName + 'Gateway';
Expand Down Expand Up @@ -74,11 +77,35 @@ export function scopeByResource(generated: GeneratedCode): string[] {
case 'code':
return [];
case 'transfer-object':
return [kebapizedClassName(commonHrefBasedClassName(generated.source.resource, generated.source.resourceKey)).replace('.gateway', '')];
case 'gateway':
return [kebapizedClassName(commonHrefBasedClassName(generated.source.resource)).replace('.gateway', '')];
return [kebapizedClassName(commonHrefBasedClassName(generated.source.resource, generated.source.key)).replace('.gateway', '')];
}
}

function collectLinks(schema: JSONSchema4 & {properties: {[p: string]: HyperSchemaResource4}}): HyperSchemaLink4[] {
return schema.properties
? Object.entries(schema.properties).flatMap(([, res]) => res.links)
: [];
}

export function noGrouping(schema: HyperSchema4): HyperSchema4 {
return schema;
}

export function groupIntoOneResource(schema: HyperSchema4): HyperSchema4 {
const links = collectLinks(schema);
const commonHref = getCommonHref(links);
return {
...schema,
properties: {
[commonHref]: {
links,
},
},
};
}

export const defaultOptions: GeneratorOptions = {
moduleName: 'ApiModule',
localSources: [],
Expand All @@ -103,4 +130,5 @@ export const defaultOptions: GeneratorOptions = {
buildRequestClassName: methodNameBasedRequestTypeName,
buildFileName: kebapizedClassName,
getTargetPath: scopeByResource,
preprocessSchema: groupIntoOneResource,
};
9 changes: 8 additions & 1 deletion src/generators/transfer-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ export async function generateTransferObjectClassSource(
response: TransferObjectDescriptor,
options: Partial<Options>,
): Promise<GeneratedTransferObject> {
// Workaround, because json-schema-to-typescript ignores given name
// if title is present.
const patchedSchema = {
...response.schema,
title: response.nameOfClass,
};

const responseTypeDef = await compile(
response.schema,
patchedSchema,
response.nameOfClass,
options,
);
Expand Down
5 changes: 4 additions & 1 deletion src/generators/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ export function formatCode(options: GeneratorOptions, code: string): string {
return format(code, {...DEFAULT_OPTIONS, ...options.json2ts});
}

export const BANNER = `/* tslint:disable */
`;

export function combineSourceWithImports(
options: GeneratorOptions,
generated: GeneratedType,
targetPath: string[],
): string {
return formatCode(
return BANNER + formatCode(
options,
generateAllImports(options, generated.dependencies, targetPath) + generated.generatedSource,
);
Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class LocalHttpResolver implements ResolverOptions {
const promises = files.map(async(file) => {
const schema = await readSchema(file);

const id = schema.id && schema.id.endsWith('#') ? schema.id.slice(0, -2) : schema.id;
const id = schema.id && schema.id.endsWith('#') ? schema.id.slice(0, -1) : schema.id;
return [id, JSON.stringify(schema)];
});

Expand Down Expand Up @@ -75,13 +75,15 @@ export async function generateGatewayFiles(

const files = await generateGateways(combined, options);
await writeOutFiles(outDir, files);

console.log(`Generated Angular gateways and ${options.moduleName} in ${outDir}`);
}

export async function generateGateways(
hyperSchema: HyperSchema4,
options: GeneratorOptions = defaultOptions,
): Promise<FileWithContent[]> {
const gatewayClasses: GatewayClass[] = Object.entries(hyperSchema.properties)
const gatewayClasses: GatewayClass[] = Object.entries(options.preprocessSchema(hyperSchema).properties)
.map(([key, resource]) => buildGatewayClass(options, resource, key));

const apiHostToken = generateApiHostInjectionToken();
Expand Down
3 changes: 2 additions & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Options} from 'json-schema-to-typescript';
import {GeneratedCode} from './generators/types';
import {HyperSchemaLink4, HyperSchemaResource4} from './types/hyper-schema';
import {HyperSchema4, HyperSchemaLink4, HyperSchemaResource4} from './types/hyper-schema';

export interface GeneratorOptions {
localSources: string[];
Expand All @@ -12,4 +12,5 @@ export interface GeneratorOptions {
buildRequestClassName(resource: HyperSchemaResource4, key: string, link: HyperSchemaLink4, simplifiedHref: string): string;
buildFileName(nameOfClass: string): string;
getTargetPath(generated: GeneratedCode): string[];
preprocessSchema(schema: HyperSchema4): HyperSchema4;
}
11 changes: 7 additions & 4 deletions src/preprocessing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function buildGatewayClass(
return {
nameOfClass: options.buildGatewayClassName(resource, key),
resource,
key,
operations: resource.links.map((link) => buildGatewayOperation(options, resource, key, link)),
};
}
Expand All @@ -30,6 +31,7 @@ export function buildRequestType(
schema: link.schema,
resource,
link,
resourceKey: key,
};
}

Expand All @@ -47,6 +49,7 @@ export function buildResponseType(
schema: link.targetSchema,
resource,
link,
resourceKey: key,
};
}

Expand All @@ -58,18 +61,18 @@ export function buildInterpolatedHref(href: string, hrefSchema?: JSONSchema4 | u
};

if(!hrefSchema || !hrefSchema.properties) {
throw `Interpolated href ${href} is missing schema`;
throw `Interpolated href ${href} is missing hrefSchema`;
}

const interpolatedArgs = Array.from(href.match(/{(\w+)}/g) || []).map((arg) => arg.replace(/[{}]/g, ''));
const interpolatedArgs = Array.from(href.match(/{([\w_]+)}/g) || []).map((arg) => arg.replace(/[{}]/g, ''));
for (let arg of interpolatedArgs) {
if(!hrefSchema.properties[arg]) {
throw `Schema for interpolated href ${href} is missing ${arg}`;
}
}

const simplifiedHref = href.replace(/{(\w+)}/g, 'by-$1');
const typescriptHref = '`' + href.replace(/({\w+})/g, '$$$1') + '`';
const simplifiedHref = href.replace(/{([\w_]+)}/g, 'by-$1'); //.replace(/[^\w-]+/g, '');
const typescriptHref = '`' + href.replace(/({[\w_]+})/g, '$$$1') + '`';

return {
href,
Expand Down
4 changes: 3 additions & 1 deletion src/type-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface TransferObjectDescriptor {
schema: JSONSchema4;
resource: HyperSchemaResource4;
link: HyperSchemaLink4;
resourceKey: string;
}

export interface InterpolatedHref {
Expand All @@ -27,7 +28,8 @@ export interface GatewayOperation {
}

export interface GatewayClass {
nameOfClass: string;
readonly nameOfClass: string;
readonly resource: HyperSchemaResource4;
readonly key: string;
readonly operations: GatewayOperation[];
}
6 changes: 3 additions & 3 deletions src/util/hyper-schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {HyperSchemaResource4} from '../types/hyper-schema';
import {HyperSchemaLink4} from '../types/hyper-schema';
import {assertDefined, inspect} from './misc';
import {getCommonPath} from './paths';

export function getCommonHref(resource: HyperSchemaResource4): string {
return getCommonPath(resource.links.map((link) => assertDefined(link.href, `href missing in ${inspect(link)}`)));
export function getCommonHref(links: HyperSchemaLink4[]): string {
return getCommonPath(links.map((link) => assertDefined(link.href, `href missing in ${inspect(link)}`)));
}
1 change: 1 addition & 0 deletions test/integration/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('Generation', () => {

await fs.rmdir(output, {recursive: true});
await fs.mkdir(output, {recursive: true});
await fs.mkdir(expectedFiles, {recursive: true});

await generateGatewayFiles(input, output, {...defaultOptions, localSources: [localSource]});

Expand Down

0 comments on commit 1ed17f2

Please sign in to comment.