diff --git a/npm/ng-packs/packages/schematics/src/constants/volo.ts b/npm/ng-packs/packages/schematics/src/constants/volo.ts index 12aa166d66..00fde72c79 100644 --- a/npm/ng-packs/packages/schematics/src/constants/volo.ts +++ b/npm/ng-packs/packages/schematics/src/constants/volo.ts @@ -1,22 +1 @@ -import { Interface, Property } from '../models'; - export const VOLO_REGEX = /^Volo\.Abp\.(Application\.Dtos|ObjectExtending)/; - -export const VOLO_NAME_VALUE = new Interface({ - base: null, - identifier: 'NameValue', - ref: 'Volo.Abp.NameValue', - namespace: 'Volo.Abp', - properties: [ - new Property({ - name: 'name', - type: 'string', - refs: ['System.String'], - }), - new Property({ - name: 'value', - type: 'T', - refs: ['T'], - }), - ], -}); diff --git a/npm/ng-packs/packages/schematics/src/models/model.ts b/npm/ng-packs/packages/schematics/src/models/model.ts index 6c7feb318d..048c08a1b0 100644 --- a/npm/ng-packs/packages/schematics/src/models/model.ts +++ b/npm/ng-packs/packages/schematics/src/models/model.ts @@ -1,43 +1,97 @@ import { Import } from './import'; -import { Omissible } from './util'; +import { Options } from './util'; export class Model { - imports: Import[] = []; - interfaces: Interface[] = []; - namespace: string; - path: string; + readonly imports: Import[] = []; + readonly interfaces: Interface[] = []; + readonly namespace: string; + readonly path: string; constructor(options: ModelOptions) { Object.assign(this, options); } } -export type ModelOptions = Omissible; +export type ModelOptions = Options; export class Interface { - base: string | null; - identifier: string; - namespace: string; - properties: Property[] = []; - ref: string; + readonly base: string | null; + readonly identifier: string; + readonly namespace: string; + readonly generics: Generic[] = []; + readonly properties: Property[] = []; + readonly ref: string; constructor(options: InterfaceOptions) { Object.assign(this, options); } } -export type InterfaceOptions = Omissible; +export type InterfaceOptions = Options; -export class Property { - name: string; - type: string; - default = ''; - optional: '' | '?' = ''; - refs: string[] = []; +abstract class TypeRef { + readonly refs: string[] = []; - constructor(options: PropertyOptions) { + protected _type = ''; + get type() { + return this._type; + } + set type(value: string) { + if (!value) return; + this._type = value; + } + + protected _default = ''; + get default() { + return this._default; + } + set default(value: string) { + if (!value) return; + this._default = ` = ${value}`; + } + + constructor(options: TypeRefOptions) { Object.assign(this, options); } + + setDefault(value: string) { + this.default = value; + } + + setType(value: string) { + this.type = value; + } +} + +type TypeRefOptionalKeys = 'default' | 'refs'; +type TypeRefOptions = Options; + +export class Generic extends TypeRef { + constructor(options: GenericOptions) { + super(options); + } +} + +export type GenericOptions = Options; + +export class Property extends TypeRef { + readonly name: string; + private _optional: '' | '?' = ''; + get optional() { + return this.default ? '' : this._optional; + } + + set optional(value: '' | '?') { + this._optional = value; + } + + constructor(options: PropertyOptions) { + super(options); + } + + setOptional(isOptional: boolean) { + this.optional = isOptional ? '?' : ''; + } } -export type PropertyOptions = Omissible; +export type PropertyOptions = Options; diff --git a/npm/ng-packs/packages/schematics/src/models/util.ts b/npm/ng-packs/packages/schematics/src/models/util.ts index 5ff732ccfe..0588ed7d5b 100644 --- a/npm/ng-packs/packages/schematics/src/models/util.ts +++ b/npm/ng-packs/packages/schematics/src/models/util.ts @@ -1,2 +1,16 @@ // Omissible (given keys will become optional) export type Omissible = Partial> & Omit; + +// ExcludeKeys (keys will be excluded based on their type) +type ExcludeKeys = Exclude< + { + [Key in keyof Type]: Type[Key] extends Excluded ? never : Key; + }[keyof Type], + never +>; + +// tslint:disable-next-line: ban-types +type ExcludeMethods = Pick>; + +// Options (methods will be omitted, given keys will become optional) +export type Options> = Omissible, K>; diff --git a/npm/ng-packs/packages/schematics/src/utils/generics.ts b/npm/ng-packs/packages/schematics/src/utils/generics.ts new file mode 100644 index 0000000000..8dc7fb2629 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/generics.ts @@ -0,0 +1,100 @@ +import { Generic } from '../models'; + +export class GenericsCollector { + private _generics: Generic[] = []; + get generics() { + return this._generics; + } + + apply = (value: string, index: number) => { + const generic = this.get(index); + if (generic) { + if (!generic.type) generic.setType(value); + return value + generic.default; + } + + return value; + }; + + constructor(private getTypeIdentifier = (type: string) => type) {} + + private createGeneric(type: string, ref: string, defaultValue: string) { + const _default = this.getTypeIdentifier(defaultValue); + const refs = [generateRefWithPlaceholders(ref)]; + const generic = new Generic({ type, default: _default, refs }); + return generic; + } + + private register(index: number, generic: Generic) { + const existing = this.get(index); + if (existing) { + existing.setDefault(generic.default); + existing.setType(generic.type); + } else this.set(index, generic); + } + + collect(generics: string[], genericArguments: string[]) { + generics.forEach((ref, i) => { + const generic = this.createGeneric( + genericArguments[i], + ref, + genericArguments.includes(ref) ? '' : ref, + ); + this.register(i, generic); + }); + } + + get(index: number) { + return this.generics[index]; + } + + set(index: number, value: Generic) { + this.generics[index] = value; + } + + reset() { + this._generics = []; + } +} + +export function generateRefWithPlaceholders(sourceType: string) { + let { identifier, generics } = extractGenerics(sourceType); + + identifier = identifier; + generics = generics.map((_, i) => `T${i}`); + + return generics.length ? `${identifier}<${generics}>` : identifier; +} + +export function extractSimpleGenerics(sourceType: string) { + const { identifier, generics } = extractGenerics(sourceType); + + return { + identifier: getLastSegment(identifier), + generics: generics.map(getLastSegment), + }; +} + +export function extractGenerics(sourceType: string) { + const regex = /(?[^<]+)(<(?.+)>)?/g; + const { identifier = '', generics = '' } = regex.exec(sourceType)?.groups ?? {}; + + return { + identifier, + generics: generics.split(/,\s*/).filter(Boolean), + }; +} + +function getLastSegment(str: string) { + return str.split('.').pop()!; +} + +export function replacePlaceholdersWithGenerics( + type: string, + generics: string[], + genericsCollector: GenericsCollector, +) { + return generics + .map(genericsCollector.apply) + .reduce((acc, v, i) => acc.replace(new RegExp(`([<, ])T${i}([,>])`, 'g'), `$1${v}$2`), type); +} diff --git a/npm/ng-packs/packages/schematics/src/utils/index.ts b/npm/ng-packs/packages/schematics/src/utils/index.ts index 178920dccc..e6df05b2e2 100644 --- a/npm/ng-packs/packages/schematics/src/utils/index.ts +++ b/npm/ng-packs/packages/schematics/src/utils/index.ts @@ -5,6 +5,7 @@ export * from './barrel'; export * from './common'; export * from './enum'; export * from './file'; +export * from './generics'; export * from './import'; export * from './model'; export * from './namespace'; diff --git a/npm/ng-packs/packages/schematics/src/utils/model.ts b/npm/ng-packs/packages/schematics/src/utils/model.ts index fc8dde37c1..49d3c0fcf5 100644 --- a/npm/ng-packs/packages/schematics/src/utils/model.ts +++ b/npm/ng-packs/packages/schematics/src/utils/model.ts @@ -1,5 +1,11 @@ -import { VOLO_NAME_VALUE, VOLO_REGEX } from '../constants'; +import { VOLO_REGEX } from '../constants'; import { Interface, Model, Property, PropertyDef, Type, TypeWithEnum } from '../models'; +import { + extractGenerics, + generateRefWithPlaceholders, + GenericsCollector, + replacePlaceholdersWithGenerics, +} from './generics'; import { parseNamespace } from './namespace'; import { relativePathToModel } from './path'; import { camel } from './text'; @@ -8,6 +14,7 @@ import { createTypeParser, createTypeSimplifier, createTypesToImportsReducer, + extendsSelf, removeTypeModifiers, } from './type'; @@ -27,6 +34,7 @@ export function createImportRefsToModelReducer(params: ModelGeneratorParams) { return (models: Model[], importRefs: string[]) => { const enums: string[] = []; const interfaces = importRefs.reduce(reduceImportRefsToInterfaces, []); + sortInterfaces(interfaces); interfaces.forEach(_interface => { @@ -40,12 +48,9 @@ export function createImportRefsToModelReducer(params: ModelGeneratorParams) { const index = models.findIndex(m => m.namespace === _interface.namespace); if (index > -1) { if (models[index].interfaces.some(i => i.identifier === _interface.identifier)) return; - if (_interface.ref.startsWith(VOLO_NAME_VALUE.ref)) return; models[index].interfaces.push(_interface); } else { - if (_interface.ref.startsWith(VOLO_NAME_VALUE.ref)) _interface = VOLO_NAME_VALUE; - const { namespace } = _interface; models.push( @@ -70,7 +75,7 @@ export function createImportRefsToModelReducer(params: ModelGeneratorParams) { isEnum: false, }); - _interface.properties.forEach(prop => { + [..._interface.properties, ..._interface.generics].forEach(prop => { prop.refs.forEach(ref => { const propType = types[ref]; if (!propType) return; @@ -100,38 +105,50 @@ export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorP const parseType = createTypeParser(removeTypeModifiers); const simplifyType = createTypeSimplifier(); const getIdentifier = (type: string) => removeTypeModifiers(simplifyType(type)); + const genericsCollector = new GenericsCollector(getIdentifier); return reduceRefsToInterfaces; - function reduceRefsToInterfaces(interfaces: Interface[], ref: string) { + function reduceRefsToInterfaces(interfaces: Interface[], ref: string): Interface[] { const typeDef = types[ref]; if (!typeDef) return interfaces; const namespace = parseNamespace(solution, ref); - const identifier = (typeDef.genericArguments ?? []).reduce( - (acc, t, i) => acc.replace(`T${i}`, t), - getIdentifier(ref), - ); + let { baseType: base, genericArguments } = typeDef; + genericArguments = genericArguments || []; + let identifier = getIdentifier(ref); + identifier = replacePlaceholdersWithGenerics(identifier, genericArguments, genericsCollector); + + if (base) { + if (extendsSelf(ref, base)) { + genericsCollector.collect(extractGenerics(base).generics, genericArguments); + return reduceRefsToInterfaces(interfaces, generateRefWithPlaceholders(base)); + } else { + base = getIdentifier(base); + } + } - const base = typeDef.baseType ? getIdentifier(typeDef.baseType) : null; - const _interface = new Interface({ identifier, base, namespace, ref }); + const { generics } = genericsCollector; + const _interface = new Interface({ identifier, base, namespace, ref, generics }); + genericsCollector.reset(); typeDef.properties?.forEach(prop => { const name = camel(prop.name); - const optional = isOptionalProperty(prop) ? '?' : ''; const type = simplifyType(prop.typeSimple); const refs = parseType(prop.type).reduce( (acc: string[], r) => acc.concat(parseGenerics(r).toGenerics()), [], ); + const property = new Property({ name, type, refs }); + property.setOptional(isOptionalProperty(prop)); - _interface.properties.push(new Property({ name, optional, type, refs })); + _interface.properties.push(property); }); interfaces.push(_interface); - return _interface.properties + return [..._interface.properties, ..._interface.generics] .reduce((refs, prop) => { prop.refs.forEach(type => { if (types[type]?.isEnum) return; @@ -142,7 +159,7 @@ export function createImportRefToInterfaceReducerCreator(params: ModelGeneratorP return refs; }, []) .concat(base ? parseGenerics(typeDef.baseType!).toGenerics() : []) - .reduce(reduceRefsToInterfaces, interfaces); + .reduce(reduceRefsToInterfaces, interfaces); } } diff --git a/npm/ng-packs/packages/schematics/src/utils/service.ts b/npm/ng-packs/packages/schematics/src/utils/service.ts index b8a2d692b2..70655c821d 100644 --- a/npm/ng-packs/packages/schematics/src/utils/service.ts +++ b/npm/ng-packs/packages/schematics/src/utils/service.ts @@ -83,8 +83,8 @@ export function createActionToSignatureMapper() { signature.parameters = action.parametersOnMethod.map(p => { const type = adaptType(p.typeSimple); const parameter = new Property({ name: p.name, type }); - if (p.defaultValue) parameter.default = ` = ${p.defaultValue}`; - else if (p.isOptional) parameter.optional = '?'; + parameter.setDefault(p.defaultValue); + parameter.setOptional(p.isOptional); return parameter; }); diff --git a/npm/ng-packs/packages/schematics/src/utils/type.ts b/npm/ng-packs/packages/schematics/src/utils/type.ts index 781794c463..a087392d40 100644 --- a/npm/ng-packs/packages/schematics/src/utils/type.ts +++ b/npm/ng-packs/packages/schematics/src/utils/type.ts @@ -1,7 +1,7 @@ -import { SYSTEM_TYPES } from '../constants'; -import { VOLO_REGEX } from '../constants/volo'; +import { SYSTEM_TYPES, VOLO_REGEX } from '../constants'; import { eImportKeyword } from '../enums'; import { Import, TypeWithEnum } from '../models'; +import { extractSimpleGenerics } from './generics'; import { parseNamespace } from './namespace'; import { relativePathToEnum, relativePathToModel } from './path'; import { parseGenerics } from './tree'; @@ -15,14 +15,9 @@ export function createTypeSimplifier() { type = /any.+)<.*(?<=[\.<])(?.+)>/gm); - const { generic, genericType } = regexp.exec(type)?.groups ?? {}; + const { identifier, generics } = extractSimpleGenerics(type); - return generic - ? generic === 'any' - ? 'any' - : `${generic}<${genericType}>` - : type.split('.').pop()!; + return generics.length ? `${identifier}<${generics.join(', ')}>` : identifier; }); return (type: string) => { @@ -40,9 +35,7 @@ export function createTypeParser(replacerFn = (t: string) => t) { export function createTypeNormalizer(replacerFn = (t: string) => t) { return (type: string) => { - type = normalizeTypeAnnotations(type); - - return replacerFn(type); + return replacerFn(normalizeTypeAnnotations(type)); }; } @@ -56,8 +49,7 @@ export function flattenDictionaryTypes(types: string[], type: string) { } export function normalizeTypeAnnotations(type: string) { - type = type.replace(/\[(.+)+\]/g, '$1[]'); - return type.replace(/\?/g, ''); + return type.replace(/\[(.+)+\]/g, '$1[]').replace(/\?/g, ''); } export function removeGenerics(type: string) { @@ -115,3 +107,8 @@ export function createTypeAdapter() { const simplifyType = createTypeSimplifier(); return (type: string) => parseGenerics(type, node => simplifyType(node.data)).toString(); } + +// naming here is depictive only +export function extendsSelf(type: string, base: string) { + return removeGenerics(base) === removeGenerics(type); +}