From 485430b7f27fb9c751a6f5697e74eb5531ac7889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wieland=20Sch=C3=B6bl?= Date: Fri, 21 Feb 2020 15:31:26 +0100 Subject: [PATCH] feat: add support for @inheritTags --- .gitignore | 3 + src/mapping.ts | 174 ++++++++++++++---- .../mappings/src/inherit-tags.ts | 57 ++++++ .../mappings/src/tags-ignore-case.ts | 49 +++++ test/mapping-model/mappings/src/types.ts | 2 + test/mapping.spec.ts | 15 +- test/schema.spec.ts | 2 +- test/validate.spec.ts | 2 +- 8 files changed, 266 insertions(+), 38 deletions(-) create mode 100644 test/mapping-model/mappings/src/inherit-tags.ts create mode 100644 test/mapping-model/mappings/src/tags-ignore-case.ts diff --git a/.gitignore b/.gitignore index c07b9dc8..7d3c8b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ pids *.seed *.pid.lock +# Schema generation data +Diagram-*.svg + # Directory for instrumented libs generated by jscoverage/JSCover lib-cov diff --git a/src/mapping.ts b/src/mapping.ts index caad870b..16bc2e1e 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -50,11 +50,13 @@ let aggregations: AggregationSchema = {}; const indexableTag = 'indexable'; const aggregatableTag = 'aggregatable'; const aggregatableTagParameterGlobal = 'global'; +const inheritTagsName = 'inherittags'; // clamp printed object to 1000 chars to keep error messages readable const maxErrorObjectChars = 1000; -let ignoredTagsList = ['indexable', 'validatable']; +let ignoredTagsList = ['indexable', 'validatable', inheritTagsName]; +let inheritTagsMap: { [path: string]: CommentTag[]; } = {}; /** * Gets all interfaces that have an @indexable tag @@ -107,8 +109,7 @@ function composeErrorMessage(path: string, topTypeName: string, typeName: string errors.push(error); if (showErrors) { // tslint:disable-next-line:no-floating-promises - Logger.error(error) - .then(); + void Logger.error(error); } } @@ -139,7 +140,8 @@ function trimString(value: string, maxLength: number): string { function getReflectionGeneric(type: ReferenceType, out: Map, topTypeName: string, - path: string, tags: CommentTag[]): Map { + path: string, + tags: CommentTag[]): Map { if (typeof type.typeArguments !== 'undefined' && type.reflection instanceof DeclarationReflection && typeof type.reflection.typeParameters !== 'undefined') { @@ -177,8 +179,8 @@ function getReflectionGeneric(type: ReferenceType, */ function handleExternalType(ref: ReferenceType, generics: Map, path: string, topTypeName: string, tags: CommentTag[]): ElasticsearchValue { - for (const premap in premaps) { - if (premap === ref.name && premaps.hasOwnProperty(premap)) { + for (const premap of Object.keys(premaps)) { + if (premap === ref.name) { return readFieldTags(premaps[premap], path, topTypeName, tags); } } @@ -250,7 +252,8 @@ function handleDeclarationReflection(decl: DeclarationReflection, mapping: handleType( decl.indexSignature.type, new Map(generics), path, topTypeName, - getCommentTags(decl.indexSignature)), + getCommentTags(decl.indexSignature, path, topTypeName), + ), match: '*', match_mapping_type: '*', path_match: `${path}*`, @@ -260,7 +263,7 @@ function handleDeclarationReflection(decl: DeclarationReflection, } if (decl.kindString === 'Enumeration') { - return readTypeTags('string', path, topTypeName, getCommentTags(decl, inheritedTags)); + return readTypeTags('string', path, topTypeName, getCommentTags(decl, path, topTypeName, inheritedTags)); } // check all the children, so in this case we are dealing with an OBJECT @@ -272,29 +275,35 @@ function handleDeclarationReflection(decl: DeclarationReflection, } } else if (decl.type instanceof Type) { // if the object is a type, so we are dealing with a PROPERTY // get inherited tags - return handleType(decl.type, new Map(generics), path, topTypeName, getCommentTags(decl)); + return handleType(decl.type, new Map(generics), path, topTypeName, getCommentTags(decl, path, topTypeName)); } else if (decl.kindString === 'Enumeration member') { - return readTypeTags(typeof decl.defaultValue, path, topTypeName, getCommentTags(decl, inheritedTags)); + return readTypeTags(typeof decl.defaultValue, path, topTypeName, + getCommentTags(decl, path, topTypeName, inheritedTags)); } if (empty) { composeErrorMessage(path, topTypeName, 'object', decl.name, 'Empty object'); } - return readFieldTags(out, path, topTypeName, getCommentTags(decl)); + return readFieldTags(out, path, topTypeName, getCommentTags(decl, path, topTypeName)); } /** * Reads all comment tags, including inherited ones * * @param decl the DeclarationReflection to read the tags from + * @param path the path on which the comments lie + * @param topTypeName the name of the SCThingType * @param inheritedTags any tags that might have been inherited by a parent * @param breakId the id of the previous reflection to prevent infinite recursion in some cases */ -function getCommentTags(decl: DeclarationReflection | SignatureReflection, - inheritedTags: CommentTag[] = [], - // tslint:disable-next-line:no-unnecessary-initializer - breakId: number | undefined = undefined, +function getCommentTags( + decl: DeclarationReflection | SignatureReflection, + path: string, + topTypeName: string, + inheritedTags: CommentTag[] = [], + // tslint:disable-next-line:no-unnecessary-initializer + breakId: number | undefined = undefined, ): CommentTag[] { if (decl.id === breakId) { return []; @@ -303,15 +312,52 @@ function getCommentTags(decl: DeclarationReflection | SignatureReflection, let out: CommentTag[] = decl.comment instanceof Comment ? typeof decl.comment.tags !== 'undefined' ? decl.comment.tags : inheritedTags : inheritedTags; if (decl.overwrites instanceof ReferenceType && decl.overwrites.reflection instanceof DeclarationReflection) { - out = arrayPriorityJoin(getCommentTags(decl.overwrites.reflection, inheritedTags, decl.id), out); + out = arrayPriorityJoin( + getCommentTags(decl.overwrites.reflection, path, topTypeName, inheritedTags, decl.id), out); } if (decl.inheritedFrom instanceof ReferenceType && decl.inheritedFrom.reflection instanceof DeclarationReflection) { - out = arrayPriorityJoin(getCommentTags(decl.inheritedFrom.reflection, inheritedTags, decl.id), out); + out = arrayPriorityJoin( + getCommentTags(decl.inheritedFrom.reflection, path, topTypeName, inheritedTags, decl.id), out); + } + + saveCommentTags(out, path, topTypeName); + const inheritTag = out.find(((value) => value.tagName === inheritTagsName)); + if (typeof inheritTag !== 'undefined') { + out = arrayPriorityJoin(out, retrieveCommentTags(inheritTag.text.trim(), path, topTypeName)); } return out; } +/** + * Saves all comment tags to the map + * + * @param tags all tags to be saved (@see and @[inheritTags] will be stripped) + * @param path the path of field + * @param topTypeName the name of the SCThingType + */ +function saveCommentTags(tags: CommentTag[], path: string, topTypeName: string) { + inheritTagsMap[`${topTypeName}::${path.substr(0, path.length - 1)}`] = + tags.filter(((value) => value.tagName !== 'see' && value.tagName !== inheritTagsName)); +} + +/** + * Retrieves any saved tags + * + * @param path the path to the original field + * @param currentPath the current path to the object we are in + * @param topTypeName the name of the SCThingType + */ +function retrieveCommentTags(path: string, currentPath: string, topTypeName: string): CommentTag[] { + if (!(path in inheritTagsMap)) { + composeErrorMessage(currentPath, topTypeName, path, 'Comment', 'Referenced path to tags does not exist!'); + + return []; + } + + return inheritTagsMap[path]; +} + /** * Joins two arrays of CommentTags, but overrides all original CommentTags with the same tagName * @@ -322,9 +368,7 @@ function arrayPriorityJoin(originals: CommentTag[], overrider: CommentTag[]): Co const out: CommentTag[] = overrider; originals.forEach((original) => { - const result = overrider.find((element) => { - return original.tagName === element.tagName; - }); + const result = overrider.find((element) => original.tagName === element.tagName); // no support for multiple tags with the same name if (!(result instanceof CommentTag)) { @@ -579,6 +623,31 @@ function readTypeTags(type: string, path: string, topTypeName: string, tags: Com return out; } +/** + * Reset the state + * + * This is kind of a suboptimal solution and should be changed in the future. + * https://gitlab.com/openstapps/core-tools/-/issues/49 + * + * @param resetInheritTags whether inherited tags should be reset as well + */ +function reset(resetInheritTags = true) { + errors = []; + dynamicTemplates = []; + aggregations = { + '@all': { + aggs: {}, + filter: { + match_all: {}, + }, + }, + }; + + if (resetInheritTags) { + inheritTagsMap = {}; + } +} + /** * Takes a project reflection and generates an ElasticsearchTemplate from it * @@ -597,24 +666,63 @@ export function generateTemplate(projectReflection: ProjectReflection, interfaceFilter: string[] = []): // tslint:disable-next-line:completed-docs { aggregations: AggregationSchema; errors: string[]; mappings: ElasticsearchTemplateCollection; } { - errors = []; - aggregations = { - '@all': { - aggs: {}, - filter: { - match_all: {}, - }, - }, - }; + reset(); + showErrors = showErrorOutput; - ignoredTagsList = ['indexable', 'validatable']; + ignoredTagsList = ['indexable', 'validatable', inheritTagsName]; ignoredTagsList.push.apply(ignoredTagsList, ignoredTags); const indexableInterfaces = getAllIndexableInterfaces(projectReflection); const out: ElasticsearchTemplateCollection = {}; + for (const _interface of indexableInterfaces) { + // TODO: lots of duplicate code, this all needs to be changed https://gitlab.com/openstapps/core-tools/-/issues/49 + if (!Array.isArray(_interface.children) || _interface.children.length === 0) { + throw new Error('Interface needs at least some properties to be indexable'); + } + + const typeObject = _interface.children.find((declarationReflection) => { + return declarationReflection.name === 'type'; + }); + + if (typeof typeObject === 'undefined' || typeof typeObject.type === 'undefined') { + throw new Error('Interface needs a type to be indexable'); + } + + let typeName = 'INVALID_TYPE'; + if (typeObject.type instanceof ReferenceType) { + if (typeObject.type.reflection instanceof DeclarationReflection + && typeof typeObject.type.reflection.defaultValue === 'string') { + typeName = typeObject.type.reflection.defaultValue.replace('"', '') + .replace('"', ''); + } else { + // tslint:disable-next-line:no-floating-promises + void Logger.error('Your input files seem to be incorrect, or there is a major bug in the mapping generator.'); + } + } else if (typeObject.type instanceof StringLiteralType) { + Logger.warn(`The interface ${_interface.name} uses a string literal as type, please use SCThingType.`); + typeName = typeObject.type.value; + } else { + // tslint:disable-next-line:no-floating-promises + void Logger.error(`The interface ${_interface.name} is required to use an SCThingType as a type, please do so.`); + } + // init aggregation schema for type + aggregations[typeName] = { + aggs: {}, + filter: { + type: { + value: typeName, + }, + }, + }; + handleDeclarationReflection(_interface, new Map(), '', typeName); + } + + // second traversal + reset(false); + for (const _interface of indexableInterfaces) { if (!Array.isArray(_interface.children) || _interface.children.length === 0) { throw new Error('Interface needs at least some properties to be indexable'); @@ -636,16 +744,14 @@ export function generateTemplate(projectReflection: ProjectReflection, .replace('"', ''); } else { // tslint:disable-next-line:no-floating-promises - Logger.error('Your input files seem to be incorrect, or there is a major bug in the mapping generator.') - .then(); + void Logger.error('Your input files seem to be incorrect, or there is a major bug in the mapping generator.'); } } else if (typeObject.type instanceof StringLiteralType) { Logger.warn(`The interface ${_interface.name} uses a string literal as type, please use SCThingType.`); typeName = typeObject.type.value; } else { // tslint:disable-next-line:no-floating-promises - Logger.error(`The interface ${_interface.name} is required to use an SCThingType as a type, please do so.`) - .then(); + void Logger.error(`The interface ${_interface.name} is required to use an SCThingType as a type, please do so.`); } // filter out diff --git a/test/mapping-model/mappings/src/inherit-tags.ts b/test/mapping-model/mappings/src/inherit-tags.ts new file mode 100644 index 00000000..25243ba1 --- /dev/null +++ b/test/mapping-model/mappings/src/inherit-tags.ts @@ -0,0 +1,57 @@ +// tslint:disable +/* + * Copyright (C) 2020 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {ElasticsearchDataType} from '../../../../src/mappings/definitions/typemap'; +import {MapAggTestOptions} from '../../MapAggTestOptions'; +import {ThingType} from './types'; + +/** + * @indexable + */ +export interface InheritTags { + /** + * @inheritTags inherit tags::bar.baz + */ + foo: number, + + bar: { + /** + * @float + */ + baz: number + } + + type: ThingType.InheritTags; +} + +export const inheritTagsTest: MapAggTestOptions = { + name: ThingType.InheritTags, + map: { + maps: { + foo: { + type: ElasticsearchDataType.float + }, + bar: { + dynamic: 'strict', + properties: { + baz: { + type: ElasticsearchDataType.float + } + } + }, + } + } +}; diff --git a/test/mapping-model/mappings/src/tags-ignore-case.ts b/test/mapping-model/mappings/src/tags-ignore-case.ts new file mode 100644 index 00000000..e3866601 --- /dev/null +++ b/test/mapping-model/mappings/src/tags-ignore-case.ts @@ -0,0 +1,49 @@ +import {ThingType} from './types'; +import {MapAggTestOptions} from '../../MapAggTestOptions'; +import {ElasticsearchDataType} from '../../../../src/mappings/definitions/typemap'; + +/** + * @indexable + */ +export interface TagsIgnoreCase { + /** + * @inheritTags inherit tags::bar.baz + */ + camelCase: number, + + /** + * @inherittags inherit tags::bar.baz + */ + lowerCase: number, + + bar: { + /** + * @float + */ + baz: number + } + + type: ThingType.TagsIgnoreCase; +} + +export const tagsIgnoreCaseTest: MapAggTestOptions = { + name: ThingType.TagsIgnoreCase, + map: { + maps: { + camelCase: { + type: ElasticsearchDataType.float + }, + lowerCase: { + type: ElasticsearchDataType.float + }, + bar: { + dynamic: 'strict', + properties: { + baz: { + type: ElasticsearchDataType.float + } + } + }, + } + } +}; diff --git a/test/mapping-model/mappings/src/types.ts b/test/mapping-model/mappings/src/types.ts index 387bf24b..7fd20df2 100644 --- a/test/mapping-model/mappings/src/types.ts +++ b/test/mapping-model/mappings/src/types.ts @@ -35,4 +35,6 @@ export enum ThingType { PairedTags = 'paired tags', FilterableTag = 'filterable tag', AnyUnknown = 'any unknown', + InheritTags = 'inherit tags', + TagsIgnoreCase = 'tags ignore case', } diff --git a/test/mapping.spec.ts b/test/mapping.spec.ts index 15b1ced7..2d3b8318 100644 --- a/test/mapping.spec.ts +++ b/test/mapping.spec.ts @@ -1,4 +1,3 @@ -// tslint:disable /* * Copyright (C) 2020 StApps * This program is free software: you can redistribute it and/or modify it @@ -16,6 +15,7 @@ import {Logger} from '@openstapps/logger'; import {slow, suite, test, timeout} from 'mocha-typescript'; import {MapAggTest} from './mapping-model/MapAggTest'; +import {inheritTagsTest} from './mapping-model/mappings/src/inherit-tags'; import {mapExplicitTypesTest} from './mapping-model/mappings/src/map-explicit-types'; import {doubleTypeConflictTest} from './mapping-model/mappings/src/double-type-conflict'; import {incompatibleTypeTest} from './mapping-model/mappings/src/incompatible-type'; @@ -34,6 +34,7 @@ import {inheritedPropertyTest} from './mapping-model/mappings/src/inherited-prop import {pairedTagsTest} from './mapping-model/mappings/src/paired-tags'; import {filterableTagTest} from './mapping-model/mappings/src/filterable-tag'; import {anyUnknownTest} from './mapping-model/mappings/src/any-unknown'; +import {tagsIgnoreCaseTest} from './mapping-model/mappings/src/tags-ignore-case'; process.on('unhandledRejection', (error: unknown) => { if (error instanceof Error) { @@ -66,6 +67,11 @@ export class MappingSpec { magAppInstance.testInterfaceAgainstPath(inheritedPropertyTest); } + @test + async 'Tags should ignore case'() { + magAppInstance.testInterfaceAgainstPath(tagsIgnoreCaseTest); + } + @test async 'Emums should work'() { // Known issue: Enums only use text @@ -74,7 +80,7 @@ export class MappingSpec { } @test - 'Sortable tag should work'() { + async 'Sortable tag should work'() { magAppInstance.testInterfaceAgainstPath(sortableTagTest); } @@ -85,6 +91,11 @@ export class MappingSpec { this.testInterfaceAgainstPath(typeWrapperInheritanceTest); }*/ + @test + async 'Inherit tags tag should work'() { + magAppInstance.testInterfaceAgainstPath(inheritTagsTest); + } + @test async 'Object union types should work'() { magAppInstance.testInterfaceAgainstPath(objectUnionTest); diff --git a/test/schema.spec.ts b/test/schema.spec.ts index 8db4b88e..40255d91 100644 --- a/test/schema.spec.ts +++ b/test/schema.spec.ts @@ -25,7 +25,7 @@ process.on('unhandledRejection', (error: unknown) => { process.exit(1); }); -@suite(timeout(20000), slow(10000)) +@suite(timeout(40000), slow(10000)) export class SchemaSpec { @test async getSchema() { diff --git a/test/validate.spec.ts b/test/validate.spec.ts index 2c31ca27..09220d55 100644 --- a/test/validate.spec.ts +++ b/test/validate.spec.ts @@ -36,7 +36,7 @@ const fooInstance: Foo = { type: 'Foo', }; -@suite(timeout(15000), slow(5000)) +@suite(timeout(40000), slow(5000)) export class SchemaSpec { static converter: Converter; static schema: Schema;