Browse Source

fix: improve recursive model lookup in schematics

pull/5137/head
Arman Ozak 6 years ago
parent
commit
654cdb6ea8
  1. 52
      npm/ng-packs/packages/schematics/src/commands/api/index.ts
  2. 4
      npm/ng-packs/packages/schematics/src/models/model.ts
  3. 193
      npm/ng-packs/packages/schematics/src/utils/model.ts
  4. 2
      npm/ng-packs/packages/schematics/src/utils/namespace.ts
  5. 18
      npm/ng-packs/packages/schematics/src/utils/service.ts
  6. 35
      npm/ng-packs/packages/schematics/src/utils/type.ts

52
npm/ng-packs/packages/schematics/src/commands/api/index.ts

@ -4,23 +4,21 @@ import {
branchAndMerge, branchAndMerge,
chain, chain,
move, move,
Rule,
SchematicContext, SchematicContext,
SchematicsException, SchematicsException,
Tree, Tree,
url, url,
} from '@angular-devkit/schematics'; } from '@angular-devkit/schematics';
import { Exception } from '../../enums'; import { Exception } from '../../enums';
import { Model, ServiceGeneratorParams } from '../../models'; import { ServiceGeneratorParams } from '../../models';
import { import {
applyWithOverwrite, applyWithOverwrite,
buildDefaultPath, buildDefaultPath,
createApiDefinitionReader, createApiDefinitionReader,
createControllerToServiceMapper, createControllerToServiceMapper,
createImportRefsToModelMapper, createImportRefsToModelReducer,
createImportRefToEnumMapper, createImportRefToEnumMapper,
EnumGeneratorParams, EnumGeneratorParams,
filterModelRefsToGenerate,
getEnumNamesFromImports, getEnumNamesFromImports,
interpolate, interpolate,
ModelGeneratorParams, ModelGeneratorParams,
@ -106,41 +104,29 @@ function createEnumGenerator(params: EnumGeneratorParams) {
function createModelGenerator(params: ModelGeneratorParams) { function createModelGenerator(params: ModelGeneratorParams) {
const { targetPath, serviceImports, modelImports } = params; const { targetPath, serviceImports, modelImports } = params;
const mapImportRefsToModel = createImportRefsToModelMapper(params); const reduceImportRefsToModels = createImportRefsToModelReducer(params);
const models = Object.values(serviceImports).reduce(reduceImportRefsToModels, []);
return chain(reduceImportRefsToRules(Object.values(serviceImports))); models.forEach(({ imports }) =>
imports.forEach(({ refs, path }) =>
function reduceImportRefsToRules(importRefs: string[][], models: Model[] = []): Rule[] { refs.forEach(ref => {
if (!importRefs.length) return []; if (path === '@abp/ng.core') return;
if (!modelImports[path]) return (modelImports[path] = [ref]);
const accumulatedRules = importRefs.reduce((rules: Rule[], refs) => { modelImports[path] = [...new Set([...modelImports[path], ref])];
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 rule = applyWithOverwrite(url('./files-model'), [ return chain(
models.map(model =>
applyWithOverwrite(url('./files-model'), [
applyTemplates({ applyTemplates({
...cases, ...cases,
...model, ...model,
}), }),
move(normalize(targetPath)), 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) { function createServiceGenerator(params: ServiceGeneratorParams) {

4
npm/ng-packs/packages/schematics/src/models/model.ts

@ -17,6 +17,7 @@ export type ModelOptions = Omissible<Model, 'imports' | 'interfaces'>;
export class Interface { export class Interface {
base: string | null; base: string | null;
identifier: string; identifier: string;
namespace: string;
properties: Property[] = []; properties: Property[] = [];
ref: string; ref: string;
@ -32,10 +33,11 @@ export class Property {
type: string; type: string;
default: string = ''; default: string = '';
optional: '' | '?' = ''; optional: '' | '?' = '';
refs: string[] = [];
constructor(options: PropertyOptions) { constructor(options: PropertyOptions) {
Object.assign(this, options); Object.assign(this, options);
} }
} }
export type PropertyOptions = Omissible<Property, 'default' | 'optional'>; export type PropertyOptions = Omissible<Property, 'default' | 'optional' | 'refs'>;

193
npm/ng-packs/packages/schematics/src/utils/model.ts

@ -1,14 +1,13 @@
import { strings } from '@angular-devkit/core'; import { strings } from '@angular-devkit/core';
import { Import, Interface, Model, Property, Type, TypeWithEnum } from '../models'; import { VOLO_REGEX } from '../constants';
import { isEnumImport } from './enum'; import { Interface, Model, Property, Type, TypeWithEnum } from '../models';
import { parseNamespace } from './namespace'; import { parseNamespace } from './namespace';
import { relativePathToModel } from './path'; import { relativePathToModel } from './path';
import { parseGenerics } from './tree'; import { parseGenerics } from './tree';
import { import {
createTypeParser,
createTypeSimplifier, createTypeSimplifier,
createTypesToImportsReducer, createTypesToImportsReducer,
flattenUnionTypes,
normalizeTypeAnnotations,
removeTypeModifiers, removeTypeModifiers,
} from './type'; } from './type';
@ -20,38 +19,69 @@ export interface ModelGeneratorParams {
modelImports: Record<string, string[]>; modelImports: Record<string, string[]>;
} }
export function createImportRefsToModelMapper({ solution, types }: ModelGeneratorParams) { export function createImportRefsToModelReducer(params: ModelGeneratorParams) {
const mapImportRefToInterface = createImportRefToInterfaceMapper(types); const reduceImportRefsToInterfaces = createImportRefToInterfaceReducerCreator(params);
const createImportRefToImportReducer = createImportRefToImportReducerCreator(solution, types); const createRefToImportReducer = createRefToImportReducerCreator(params);
const { solution, types } = params;
return (importRefs: string[]) => {
const namespace = parseNamespace(solution, importRefs[0]); return (models: Model[], importRefs: string[]) => {
const path = relativePathToModel(namespace, namespace); const enums: string[] = [];
const model = new Model({ namespace, path }); const interfaces = importRefs.reduce(reduceImportRefsToInterfaces, []);
const imports: Import[] = []; sortInterfaces(interfaces);
const reduceImportRefToImport = createImportRefToImportReducer(namespace, _import => { interfaces.forEach(_interface => {
if (_import.path !== model.path) return true; if (VOLO_REGEX.test(_interface.ref)) return;
_import.refs.reduce(reduceImportRefsToImportsAndInterfaces, imports); if (types[_interface.ref]!.isEnum) {
return false; 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 models.forEach(model => {
.reduce(reduceImportRefsToImportsAndInterfaces, imports) let toBeImported: TypeWithEnum[] = [];
.forEach(_import => model.imports.push(_import));
model.interfaces.forEach(_interface => {
sortInterfaces(model.interfaces); 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) { const reduceRefToImport = createRefToImportReducer(model.namespace);
if (model.interfaces.some(i => i.ref === ref)) return accumulatedImports; reduceRefToImport(model.imports, toBeImported);
});
const _interface = mapImportRefToInterface(ref); return models;
if (_interface && !types[ref].isEnum) model.interfaces.push(_interface);
return reduceImportRefToImport(accumulatedImports, ref);
}
}; };
} }
@ -59,13 +89,19 @@ function sortInterfaces(interfaces: Interface[]) {
interfaces.sort((a, b) => (a.identifier > b.identifier ? 1 : -1)); interfaces.sort((a, b) => (a.identifier > b.identifier ? 1 : -1));
} }
export function createImportRefToInterfaceMapper(types: Record<string, Type>) { export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorParams) {
const { solution, types } = params;
const parseType = createTypeParser(removeTypeModifiers);
const simplifyType = createTypeSimplifier(); const simplifyType = createTypeSimplifier();
const getIdentifier = (type: string) => removeTypeModifiers(simplifyType(type)); const getIdentifier = (type: string) => removeTypeModifiers(simplifyType(type));
return (ref: string) => { return reduceRefsToInterfaces;
function reduceRefsToInterfaces(interfaces: Interface[], ref: string) {
const typeDef = types[ref]; const typeDef = types[ref];
if (!typeDef) return; if (!typeDef) return interfaces;
const namespace = parseNamespace(solution, ref);
const identifier = (typeDef.genericArguments ?? []).reduce( const identifier = (typeDef.genericArguments ?? []).reduce(
(acc, t, i) => acc.replace(`T${i}`, t), (acc, t, i) => acc.replace(`T${i}`, t),
@ -73,74 +109,33 @@ export function createImportRefToInterfaceMapper(types: Record<string, Type>) {
); );
const base = typeDef.baseType ? getIdentifier(typeDef.baseType) : null; const base = typeDef.baseType ? getIdentifier(typeDef.baseType) : null;
const _interface = new Interface({ identifier, base, ref }); const _interface = new Interface({ identifier, base, namespace, ref });
typeDef.properties?.forEach(({ name, typeSimple }) => { typeDef.properties?.forEach(prop => {
name = strings.camelize(name); const name = strings.camelize(prop.name);
const optional = typeSimple.endsWith('?') ? '?' : ''; const optional = prop.typeSimple.endsWith('?') ? '?' : '';
const type = simplifyType(typeSimple); const type = simplifyType(prop.typeSimple);
const refs = parseType(prop.type).reduce(
_interface.properties.push(new Property({ name, optional, type })); (acc: string[], r) => acc.concat(parseGenerics(r).toGenerics()),
[],
);
_interface.properties.push(new Property({ name, optional, type, refs }));
}); });
return _interface; interfaces.push(_interface);
};
}
export function createImportRefToImportReducerCreator(
solution: string,
types: Record<string, Type>,
) {
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);
};
}
export function mergeBaseTypeWithProperties({ baseType, genericArguments, properties }: Type) { return _interface.properties
const removeGenerics = createGenericRemover(genericArguments); .reduce<string[]>((refs, prop) => {
const clearTypes = (type: string) => normalizeTypeAnnotations(removeGenerics(type)); prop.refs.forEach(type => !types[type]?.isEnum && refs.push(type));
const baseTypes = baseType ? [baseType] : []; return refs;
const propTypes = (properties ?? []).map(({ type }) => type); }, [])
.concat(base || [])
return [...baseTypes, ...propTypes].reduce(flattenUnionTypes, []).map(clearTypes); .reduce<Interface[]>(reduceRefsToInterfaces, interfaces);
} }
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(',');
});
} }
export function filterModelRefsToGenerate( export function createRefToImportReducerCreator(params: ModelGeneratorParams) {
modelImports: Record<string, string[]>, const { solution } = params;
modelsCreated: Model[], return (namespace: string) => createTypesToImportsReducer(solution, namespace);
) {
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;
}, []);
} }

2
npm/ng-packs/packages/schematics/src/utils/namespace.ts

@ -2,7 +2,7 @@ import { createTypeParser } from './type';
export function parseNamespace(solution: string, type: string) { export function parseNamespace(solution: string, type: string) {
const parseType = createTypeParser(); const parseType = createTypeParser();
let namespace = parseType(type) let namespace = parseType(type)[0]
.split('.') .split('.')
.slice(0, -1) .slice(0, -1)
.join('.'); .join('.');

18
npm/ng-packs/packages/schematics/src/utils/service.ts

@ -14,7 +14,12 @@ import {
import { sortImports } from './import'; import { sortImports } from './import';
import { parseNamespace } from './namespace'; import { parseNamespace } from './namespace';
import { parseGenerics } from './tree'; import { parseGenerics } from './tree';
import { createTypeAdapter, createTypesToImportsReducer } from './type'; import {
createTypeAdapter,
createTypeParser,
createTypesToImportsReducer,
removeTypeModifiers,
} from './type';
export function serializeParameters(parameters: Property[]) { export function serializeParameters(parameters: Property[]) {
return parameters.map(p => p.name + p.optional + ': ' + p.type + p.default, '').join(', '); return parameters.map(p => p.name + p.optional + ': ' + p.type + p.default, '').join(', ');
@ -97,14 +102,19 @@ function createActionToImportsReducer(
namespace: string, namespace: string,
) { ) {
const mapTypesToImports = createTypesToImportsReducer(solution, namespace); const mapTypesToImports = createTypesToImportsReducer(solution, namespace);
const parseType = createTypeParser(removeTypeModifiers);
return (imports: Import[], { parametersOnMethod, returnValue }: Action) => return (imports: Import[], { parametersOnMethod, returnValue }: Action) =>
mapTypesToImports( mapTypesToImports(
imports, imports,
[returnValue, ...parametersOnMethod].reduce((acc: TypeWithEnum[], param) => { [returnValue, ...parametersOnMethod].reduce((acc: TypeWithEnum[], param) => {
parseGenerics(param.type) parseType(param.type).forEach(paramType =>
.toGenerics() parseGenerics(paramType)
.forEach(type => acc.push({ type, isEnum: types[type]?.isEnum })); .toGenerics()
.forEach(type => {
if (types[type]) acc.push({ type, isEnum: types[type].isEnum });
}),
);
return acc; return acc;
}, []), }, []),

35
npm/ng-packs/packages/schematics/src/utils/type.ts

@ -1,5 +1,6 @@
import { strings } from '@angular-devkit/core'; import { strings } from '@angular-devkit/core';
import { SYSTEM_TYPES } from '../constants'; import { SYSTEM_TYPES } from '../constants';
import { VOLO_REGEX } from '../constants/volo';
import { eImportKeyword } from '../enums'; import { eImportKeyword } from '../enums';
import { Import, TypeWithEnum } from '../models'; import { Import, TypeWithEnum } from '../models';
import { parseNamespace } from './namespace'; import { parseNamespace } from './namespace';
@ -7,22 +8,28 @@ import { relativePathToEnum, relativePathToModel } from './path';
import { parseGenerics } from './tree'; import { parseGenerics } from './tree';
export function createTypeSimplifier() { 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) { export function createTypeParser(replacerFn = (t: string) => t) {
return (originalType: string) => const normalizeType = createTypeNormalizer(replacerFn);
flattenUnionTypes([], originalType)
.map(type => { return (originalType: string) => flattenUnionTypes([], originalType).map(normalizeType);
type = normalizeTypeAnnotations(type); }
type = type.replace(
/System\.([0-9A-Za-z.]+)/g, export function createTypeNormalizer(replacerFn = (t: string) => t) {
(_, match) => SYSTEM_TYPES.get(match) ?? strings.camelize(match), return (type: string) => {
); type = normalizeTypeAnnotations(type);
return replacerFn(type); return replacerFn(type);
}) };
.join(' | ');
} }
export function flattenUnionTypes(types: string[], type: string) { export function flattenUnionTypes(types: string[], type: string) {
@ -77,7 +84,7 @@ export function createTypeToImportMapper(solution: string, namespace: string) {
const modelNamespace = parseNamespace(solution, type); const modelNamespace = parseNamespace(solution, type);
const refs = [removeTypeModifiers(type)]; const refs = [removeTypeModifiers(type)];
const specifiers = [adaptType(simplifyType(refs[0]).split('<')[0])]; 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' ? '@abp/ng.core'
: isEnum : isEnum
? relativePathToEnum(namespace, modelNamespace, specifiers[0]) ? relativePathToEnum(namespace, modelNamespace, specifiers[0])

Loading…
Cancel
Save