From 654cdb6ea8c1ac8b8bc299428c9d1b97c9344f30 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Fri, 21 Aug 2020 05:51:08 +0300 Subject: [PATCH] fix: improve recursive model lookup in schematics --- .../schematics/src/commands/api/index.ts | 52 ++--- .../packages/schematics/src/models/model.ts | 4 +- .../packages/schematics/src/utils/model.ts | 193 +++++++++--------- .../schematics/src/utils/namespace.ts | 2 +- .../packages/schematics/src/utils/service.ts | 18 +- .../packages/schematics/src/utils/type.ts | 35 ++-- 6 files changed, 152 insertions(+), 152 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/api/index.ts b/npm/ng-packs/packages/schematics/src/commands/api/index.ts index 1d19c8e777..688430f35f 100644 --- a/npm/ng-packs/packages/schematics/src/commands/api/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/api/index.ts @@ -4,23 +4,21 @@ import { branchAndMerge, chain, move, - Rule, SchematicContext, SchematicsException, Tree, url, } from '@angular-devkit/schematics'; import { Exception } from '../../enums'; -import { Model, ServiceGeneratorParams } from '../../models'; +import { ServiceGeneratorParams } from '../../models'; import { applyWithOverwrite, buildDefaultPath, createApiDefinitionReader, createControllerToServiceMapper, - createImportRefsToModelMapper, + createImportRefsToModelReducer, createImportRefToEnumMapper, EnumGeneratorParams, - filterModelRefsToGenerate, getEnumNamesFromImports, interpolate, ModelGeneratorParams, @@ -106,41 +104,29 @@ function createEnumGenerator(params: EnumGeneratorParams) { function createModelGenerator(params: ModelGeneratorParams) { const { targetPath, serviceImports, modelImports } = params; - const mapImportRefsToModel = createImportRefsToModelMapper(params); - - return chain(reduceImportRefsToRules(Object.values(serviceImports))); - - function reduceImportRefsToRules(importRefs: string[][], models: Model[] = []): Rule[] { - if (!importRefs.length) return []; - - const accumulatedRules = importRefs.reduce((rules: Rule[], refs) => { - const model = mapImportRefsToModel(refs); - model.imports.forEach(({ refs, path }) => - refs.forEach(ref => { - if (path === '@abp/ng.core') return; - if (!modelImports[path]) return (modelImports[path] = [ref]); - modelImports[path] = [...new Set([...modelImports[path], ref])]; - }), - ); - models.push(model); + const reduceImportRefsToModels = createImportRefsToModelReducer(params); + const models = Object.values(serviceImports).reduce(reduceImportRefsToModels, []); + models.forEach(({ imports }) => + imports.forEach(({ refs, path }) => + refs.forEach(ref => { + if (path === '@abp/ng.core') return; + if (!modelImports[path]) return (modelImports[path] = [ref]); + modelImports[path] = [...new Set([...modelImports[path], ref])]; + }), + ), + ); - const rule = applyWithOverwrite(url('./files-model'), [ + return chain( + models.map(model => + applyWithOverwrite(url('./files-model'), [ applyTemplates({ ...cases, ...model, }), move(normalize(targetPath)), - ]); - rules.push(rule); - - return rules; - }, []); - - const refsToGenerate = filterModelRefsToGenerate(modelImports, models); - reduceImportRefsToRules(refsToGenerate).forEach(rule => accumulatedRules.push(rule)); - - return accumulatedRules; - } + ]), + ), + ); } function createServiceGenerator(params: ServiceGeneratorParams) { diff --git a/npm/ng-packs/packages/schematics/src/models/model.ts b/npm/ng-packs/packages/schematics/src/models/model.ts index 76289ef2ca..5f389fe968 100644 --- a/npm/ng-packs/packages/schematics/src/models/model.ts +++ b/npm/ng-packs/packages/schematics/src/models/model.ts @@ -17,6 +17,7 @@ export type ModelOptions = Omissible; export class Interface { base: string | null; identifier: string; + namespace: string; properties: Property[] = []; ref: string; @@ -32,10 +33,11 @@ export class Property { type: string; default: string = ''; optional: '' | '?' = ''; + refs: string[] = []; constructor(options: PropertyOptions) { Object.assign(this, options); } } -export type PropertyOptions = Omissible; +export type PropertyOptions = Omissible; diff --git a/npm/ng-packs/packages/schematics/src/utils/model.ts b/npm/ng-packs/packages/schematics/src/utils/model.ts index 6373785779..94841c615a 100644 --- a/npm/ng-packs/packages/schematics/src/utils/model.ts +++ b/npm/ng-packs/packages/schematics/src/utils/model.ts @@ -1,14 +1,13 @@ import { strings } from '@angular-devkit/core'; -import { Import, Interface, Model, Property, Type, TypeWithEnum } from '../models'; -import { isEnumImport } from './enum'; +import { VOLO_REGEX } from '../constants'; +import { Interface, Model, Property, Type, TypeWithEnum } from '../models'; import { parseNamespace } from './namespace'; import { relativePathToModel } from './path'; import { parseGenerics } from './tree'; import { + createTypeParser, createTypeSimplifier, createTypesToImportsReducer, - flattenUnionTypes, - normalizeTypeAnnotations, removeTypeModifiers, } from './type'; @@ -20,38 +19,69 @@ export interface ModelGeneratorParams { modelImports: Record; } -export function createImportRefsToModelMapper({ solution, types }: ModelGeneratorParams) { - const mapImportRefToInterface = createImportRefToInterfaceMapper(types); - const createImportRefToImportReducer = createImportRefToImportReducerCreator(solution, types); - - return (importRefs: string[]) => { - const namespace = parseNamespace(solution, importRefs[0]); - const path = relativePathToModel(namespace, namespace); - const model = new Model({ namespace, path }); - const imports: Import[] = []; - - const reduceImportRefToImport = createImportRefToImportReducer(namespace, _import => { - if (_import.path !== model.path) return true; - - _import.refs.reduce(reduceImportRefsToImportsAndInterfaces, imports); - return false; +export function createImportRefsToModelReducer(params: ModelGeneratorParams) { + const reduceImportRefsToInterfaces = createImportRefToInterfaceReducerCreator(params); + const createRefToImportReducer = createRefToImportReducerCreator(params); + const { solution, types } = params; + + return (models: Model[], importRefs: string[]) => { + const enums: string[] = []; + const interfaces = importRefs.reduce(reduceImportRefsToInterfaces, []); + sortInterfaces(interfaces); + + interfaces.forEach(_interface => { + if (VOLO_REGEX.test(_interface.ref)) return; + + if (types[_interface.ref]!.isEnum) { + if (!enums.includes(_interface.ref)) enums.push(_interface.ref); + return; + } + + const index = models.findIndex(m => m.namespace === _interface.namespace); + if (index > -1) { + if (models[index].interfaces.some(i => i.identifier === _interface.identifier)) return; + models[index].interfaces.push(_interface); + } else { + const { namespace } = _interface; + + models.push( + new Model({ + interfaces: [_interface], + namespace, + path: relativePathToModel(namespace, namespace), + }), + ); + } }); - importRefs - .reduce(reduceImportRefsToImportsAndInterfaces, imports) - .forEach(_import => model.imports.push(_import)); - - sortInterfaces(model.interfaces); + models.forEach(model => { + let toBeImported: TypeWithEnum[] = []; + + model.interfaces.forEach(_interface => { + const { baseType } = types[_interface.ref]; + if (baseType && parseNamespace(solution, baseType) !== model.namespace) + toBeImported.push({ + type: parseGenerics(baseType) + .toGenerics() + .join(''), + isEnum: false, + }); + + _interface.properties.forEach(prop => { + prop.refs.forEach(ref => { + if (parseNamespace(solution, ref) !== model.namespace) + toBeImported.push({ type: ref, isEnum: types[ref]?.isEnum }); + }); + }); + }); - return model; + if (!toBeImported.length) return; - function reduceImportRefsToImportsAndInterfaces(accumulatedImports: Import[], ref: string) { - if (model.interfaces.some(i => i.ref === ref)) return accumulatedImports; + const reduceRefToImport = createRefToImportReducer(model.namespace); + reduceRefToImport(model.imports, toBeImported); + }); - const _interface = mapImportRefToInterface(ref); - if (_interface && !types[ref].isEnum) model.interfaces.push(_interface); - return reduceImportRefToImport(accumulatedImports, ref); - } + return models; }; } @@ -59,13 +89,19 @@ function sortInterfaces(interfaces: Interface[]) { interfaces.sort((a, b) => (a.identifier > b.identifier ? 1 : -1)); } -export function createImportRefToInterfaceMapper(types: Record) { +export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorParams) { + const { solution, types } = params; + const parseType = createTypeParser(removeTypeModifiers); const simplifyType = createTypeSimplifier(); const getIdentifier = (type: string) => removeTypeModifiers(simplifyType(type)); - return (ref: string) => { + return reduceRefsToInterfaces; + + function reduceRefsToInterfaces(interfaces: Interface[], ref: string) { const typeDef = types[ref]; - if (!typeDef) return; + if (!typeDef) return interfaces; + + const namespace = parseNamespace(solution, ref); const identifier = (typeDef.genericArguments ?? []).reduce( (acc, t, i) => acc.replace(`T${i}`, t), @@ -73,74 +109,33 @@ export function createImportRefToInterfaceMapper(types: Record) { ); const base = typeDef.baseType ? getIdentifier(typeDef.baseType) : null; - const _interface = new Interface({ identifier, base, ref }); - - typeDef.properties?.forEach(({ name, typeSimple }) => { - name = strings.camelize(name); - const optional = typeSimple.endsWith('?') ? '?' : ''; - const type = simplifyType(typeSimple); - - _interface.properties.push(new Property({ name, optional, type })); + const _interface = new Interface({ identifier, base, namespace, ref }); + + typeDef.properties?.forEach(prop => { + const name = strings.camelize(prop.name); + const optional = prop.typeSimple.endsWith('?') ? '?' : ''; + const type = simplifyType(prop.typeSimple); + const refs = parseType(prop.type).reduce( + (acc: string[], r) => acc.concat(parseGenerics(r).toGenerics()), + [], + ); + + _interface.properties.push(new Property({ name, optional, type, refs })); }); - return _interface; - }; -} - -export function createImportRefToImportReducerCreator( - solution: string, - types: Record, -) { - return (namespace: string, filterFn: (imports: Import) => boolean) => { - const reduceTypesToImport = createTypesToImportsReducer(solution, namespace); - - return (imports: Import[], importRef: string) => - reduceTypesToImport( - imports, - mergeBaseTypeWithProperties(types[importRef]).reduce((acc: TypeWithEnum[], typeName) => { - parseGenerics(typeName) - .toGenerics() - .forEach(type => acc.push({ type, isEnum: types[type]?.isEnum })); - - return acc; - }, []), - ).filter(filterFn); - }; -} + interfaces.push(_interface); -export function mergeBaseTypeWithProperties({ baseType, genericArguments, properties }: Type) { - const removeGenerics = createGenericRemover(genericArguments); - const clearTypes = (type: string) => normalizeTypeAnnotations(removeGenerics(type)); - const baseTypes = baseType ? [baseType] : []; - const propTypes = (properties ?? []).map(({ type }) => type); - - return [...baseTypes, ...propTypes].reduce(flattenUnionTypes, []).map(clearTypes); -} - -export function createGenericRemover(genericArguments: string[] | null) { - if (!genericArguments) return (type: string) => type; - - return (type: string) => - genericArguments.includes(type) - ? '' - : type.replace(/<([^<>]+)>/, (_, match) => { - return match - .split(/,\s*/) - .filter((t: string) => !genericArguments.includes(t)) - .join(','); - }); + return _interface.properties + .reduce((refs, prop) => { + prop.refs.forEach(type => !types[type]?.isEnum && refs.push(type)); + return refs; + }, []) + .concat(base || []) + .reduce(reduceRefsToInterfaces, interfaces); + } } -export function filterModelRefsToGenerate( - modelImports: Record, - modelsCreated: Model[], -) { - const created = modelsCreated.map(m => m.path); - - return Object.entries(modelImports).reduce((acc: string[][], [path, refs]) => { - if (isEnumImport(path)) return acc; - if (created.includes(path)) return acc; - acc.push(refs); - return acc; - }, []); +export function createRefToImportReducerCreator(params: ModelGeneratorParams) { + const { solution } = params; + return (namespace: string) => createTypesToImportsReducer(solution, namespace); } diff --git a/npm/ng-packs/packages/schematics/src/utils/namespace.ts b/npm/ng-packs/packages/schematics/src/utils/namespace.ts index 003198adcb..8b7c1a8c66 100644 --- a/npm/ng-packs/packages/schematics/src/utils/namespace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/namespace.ts @@ -2,7 +2,7 @@ import { createTypeParser } from './type'; export function parseNamespace(solution: string, type: string) { const parseType = createTypeParser(); - let namespace = parseType(type) + let namespace = parseType(type)[0] .split('.') .slice(0, -1) .join('.'); diff --git a/npm/ng-packs/packages/schematics/src/utils/service.ts b/npm/ng-packs/packages/schematics/src/utils/service.ts index 085b57c172..b8a2d692b2 100644 --- a/npm/ng-packs/packages/schematics/src/utils/service.ts +++ b/npm/ng-packs/packages/schematics/src/utils/service.ts @@ -14,7 +14,12 @@ import { import { sortImports } from './import'; import { parseNamespace } from './namespace'; import { parseGenerics } from './tree'; -import { createTypeAdapter, createTypesToImportsReducer } from './type'; +import { + createTypeAdapter, + createTypeParser, + createTypesToImportsReducer, + removeTypeModifiers, +} from './type'; export function serializeParameters(parameters: Property[]) { return parameters.map(p => p.name + p.optional + ': ' + p.type + p.default, '').join(', '); @@ -97,14 +102,19 @@ function createActionToImportsReducer( namespace: string, ) { const mapTypesToImports = createTypesToImportsReducer(solution, namespace); + const parseType = createTypeParser(removeTypeModifiers); return (imports: Import[], { parametersOnMethod, returnValue }: Action) => mapTypesToImports( imports, [returnValue, ...parametersOnMethod].reduce((acc: TypeWithEnum[], param) => { - parseGenerics(param.type) - .toGenerics() - .forEach(type => acc.push({ type, isEnum: types[type]?.isEnum })); + parseType(param.type).forEach(paramType => + parseGenerics(paramType) + .toGenerics() + .forEach(type => { + if (types[type]) acc.push({ type, isEnum: types[type].isEnum }); + }), + ); return acc; }, []), diff --git a/npm/ng-packs/packages/schematics/src/utils/type.ts b/npm/ng-packs/packages/schematics/src/utils/type.ts index ebc669ff7f..b74cf95313 100644 --- a/npm/ng-packs/packages/schematics/src/utils/type.ts +++ b/npm/ng-packs/packages/schematics/src/utils/type.ts @@ -1,5 +1,6 @@ import { strings } from '@angular-devkit/core'; import { SYSTEM_TYPES } from '../constants'; +import { VOLO_REGEX } from '../constants/volo'; import { eImportKeyword } from '../enums'; import { Import, TypeWithEnum } from '../models'; import { parseNamespace } from './namespace'; @@ -7,22 +8,28 @@ import { relativePathToEnum, relativePathToModel } from './path'; import { parseGenerics } from './tree'; export function createTypeSimplifier() { - return createTypeParser(type => type.split('.').pop()!); + const parseType = createTypeParser(type => { + type = type.replace( + /System\.([0-9A-Za-z.]+)/g, + (_, match) => SYSTEM_TYPES.get(match) ?? strings.camelize(match), + ); + return type.split('.').pop()!; + }); + return (type: string) => parseType(type).join(' | '); } export function createTypeParser(replacerFn = (t: string) => t) { - return (originalType: string) => - flattenUnionTypes([], originalType) - .map(type => { - type = normalizeTypeAnnotations(type); - type = type.replace( - /System\.([0-9A-Za-z.]+)/g, - (_, match) => SYSTEM_TYPES.get(match) ?? strings.camelize(match), - ); - - return replacerFn(type); - }) - .join(' | '); + const normalizeType = createTypeNormalizer(replacerFn); + + return (originalType: string) => flattenUnionTypes([], originalType).map(normalizeType); +} + +export function createTypeNormalizer(replacerFn = (t: string) => t) { + return (type: string) => { + type = normalizeTypeAnnotations(type); + + return replacerFn(type); + }; } export function flattenUnionTypes(types: string[], type: string) { @@ -77,7 +84,7 @@ export function createTypeToImportMapper(solution: string, namespace: string) { const modelNamespace = parseNamespace(solution, type); const refs = [removeTypeModifiers(type)]; const specifiers = [adaptType(simplifyType(refs[0]).split('<')[0])]; - const path = /^Volo\.Abp\.(Application\.Dtos|ObjectExtending)/.test(type) + const path = VOLO_REGEX.test(type) ? '@abp/ng.core' : isEnum ? relativePathToEnum(namespace, modelNamespace, specifiers[0])