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,
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) {

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

@ -17,6 +17,7 @@ export type ModelOptions = Omissible<Model, 'imports' | 'interfaces'>;
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<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 { 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<string, string[]>;
}
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<string, Type>) {
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<string, Type>) {
);
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<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);
};
}
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<string[]>((refs, prop) => {
prop.refs.forEach(type => !types[type]?.isEnum && refs.push(type));
return refs;
}, [])
.concat(base || [])
.reduce<Interface[]>(reduceRefsToInterfaces, interfaces);
}
}
export function filterModelRefsToGenerate(
modelImports: Record<string, string[]>,
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);
}

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) {
const parseType = createTypeParser();
let namespace = parseType(type)
let namespace = parseType(type)[0]
.split('.')
.slice(0, -1)
.join('.');

18
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;
}, []),

35
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])

Loading…
Cancel
Save