From 1ed17f2aeb44e98d57e6931d706477327ac7688f Mon Sep 17 00:00:00 2001 From: Marian Theisen Date: Wed, 1 Apr 2020 10:26:22 +0200 Subject: [PATCH] wip --- .../complex-schema/endpoints/delete_user.json | 2 +- .../complex-schema/endpoints/get_groups.json | 39 ++++++++++++++++ .../complex-schema/endpoints/get_users.json | 2 +- .../complex-schema/endpoints/post_users.json | 2 +- .../with-refs/complex-schema/schema.json | 3 ++ src/cli.ts | 38 ++++++++------- src/defaults.ts | 46 +++++++++++++++---- src/generators/transfer-objects.ts | 9 +++- src/generators/util.ts | 5 +- src/index.ts | 6 ++- src/options.ts | 3 +- src/preprocessing.ts | 11 +++-- src/type-model.ts | 4 +- src/util/hyper-schema.ts | 6 +-- test/integration/integration.test.ts | 1 + 15 files changed, 136 insertions(+), 41 deletions(-) create mode 100644 examples/hyper-schema/with-refs/complex-schema/endpoints/get_groups.json diff --git a/examples/hyper-schema/with-refs/complex-schema/endpoints/delete_user.json b/examples/hyper-schema/with-refs/complex-schema/endpoints/delete_user.json index ac04322..975c8b4 100644 --- a/examples/hyper-schema/with-refs/complex-schema/endpoints/delete_user.json +++ b/examples/hyper-schema/with-refs/complex-schema/endpoints/delete_user.json @@ -4,7 +4,7 @@ "definitions": { "delete_user": { "links": [{ - "href": "/users/{id}/{id2}", + "href": "/users_api/users/{id}/{id2}", "method": "DELETE", "rel": "edit", "hrefSchema": { diff --git a/examples/hyper-schema/with-refs/complex-schema/endpoints/get_groups.json b/examples/hyper-schema/with-refs/complex-schema/endpoints/get_groups.json new file mode 100644 index 0000000..7440aa4 --- /dev/null +++ b/examples/hyper-schema/with-refs/complex-schema/endpoints/get_groups.json @@ -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" + } + } + } + } + }] + } + } +} diff --git a/examples/hyper-schema/with-refs/complex-schema/endpoints/get_users.json b/examples/hyper-schema/with-refs/complex-schema/endpoints/get_users.json index 5a57234..dded430 100644 --- a/examples/hyper-schema/with-refs/complex-schema/endpoints/get_users.json +++ b/examples/hyper-schema/with-refs/complex-schema/endpoints/get_users.json @@ -4,7 +4,7 @@ "definitions": { "get_users": { "links": [{ - "href": "/users/{id}/{id2}", + "href": "/users_api/users/{id}/{id2}", "method": "GET", "rel": "instances", "hrefSchema": { diff --git a/examples/hyper-schema/with-refs/complex-schema/endpoints/post_users.json b/examples/hyper-schema/with-refs/complex-schema/endpoints/post_users.json index 0497ceb..2f1d3c6 100644 --- a/examples/hyper-schema/with-refs/complex-schema/endpoints/post_users.json +++ b/examples/hyper-schema/with-refs/complex-schema/endpoints/post_users.json @@ -4,7 +4,7 @@ "definitions": { "post_users": { "links": [{ - "href": "/users", + "href": "/users_api/users", "method": "POST", "rel": "create", "schema": { diff --git a/examples/hyper-schema/with-refs/complex-schema/schema.json b/examples/hyper-schema/with-refs/complex-schema/schema.json index 66159d1..abcf507 100644 --- a/examples/hyper-schema/with-refs/complex-schema/schema.json +++ b/examples/hyper-schema/with-refs/complex-schema/schema.json @@ -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" }, diff --git a/src/cli.ts b/src/cli.ts index 50c24c7..8d76356 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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; @@ -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(); diff --git a/src/defaults.ts b/src/defaults.ts index cf75793..fb48530 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -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( @@ -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'; @@ -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: [], @@ -103,4 +130,5 @@ export const defaultOptions: GeneratorOptions = { buildRequestClassName: methodNameBasedRequestTypeName, buildFileName: kebapizedClassName, getTargetPath: scopeByResource, + preprocessSchema: groupIntoOneResource, }; diff --git a/src/generators/transfer-objects.ts b/src/generators/transfer-objects.ts index fd5f0e8..55d30f4 100644 --- a/src/generators/transfer-objects.ts +++ b/src/generators/transfer-objects.ts @@ -6,8 +6,15 @@ export async function generateTransferObjectClassSource( response: TransferObjectDescriptor, options: Partial, ): Promise { + // 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, ); diff --git a/src/generators/util.ts b/src/generators/util.ts index e03434b..a4aaf19 100644 --- a/src/generators/util.ts +++ b/src/generators/util.ts @@ -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, ); diff --git a/src/index.ts b/src/index.ts index 91c8f69..d934320 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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)]; }); @@ -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 { - 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(); diff --git a/src/options.ts b/src/options.ts index 032cb67..e5f25f0 100644 --- a/src/options.ts +++ b/src/options.ts @@ -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[]; @@ -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; } diff --git a/src/preprocessing.ts b/src/preprocessing.ts index 566c50d..49e9f59 100644 --- a/src/preprocessing.ts +++ b/src/preprocessing.ts @@ -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)), }; } @@ -30,6 +31,7 @@ export function buildRequestType( schema: link.schema, resource, link, + resourceKey: key, }; } @@ -47,6 +49,7 @@ export function buildResponseType( schema: link.targetSchema, resource, link, + resourceKey: key, }; } @@ -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, diff --git a/src/type-model.ts b/src/type-model.ts index fbbe6a5..a3ad08d 100644 --- a/src/type-model.ts +++ b/src/type-model.ts @@ -6,6 +6,7 @@ export interface TransferObjectDescriptor { schema: JSONSchema4; resource: HyperSchemaResource4; link: HyperSchemaLink4; + resourceKey: string; } export interface InterpolatedHref { @@ -27,7 +28,8 @@ export interface GatewayOperation { } export interface GatewayClass { - nameOfClass: string; + readonly nameOfClass: string; readonly resource: HyperSchemaResource4; + readonly key: string; readonly operations: GatewayOperation[]; } diff --git a/src/util/hyper-schema.ts b/src/util/hyper-schema.ts index afdd8b6..9543698 100644 --- a/src/util/hyper-schema.ts +++ b/src/util/hyper-schema.ts @@ -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)}`))); } diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 13e935a..9f12a82 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -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]});