feat: add support for @inheritTags

This commit is contained in:
Wieland Schöbl
2020-02-21 15:31:26 +01:00
committed by Wieland Schöbl
parent 97f6c42407
commit 485430b7f2
8 changed files with 266 additions and 38 deletions

3
.gitignore vendored
View File

@@ -11,6 +11,9 @@ pids
*.seed
*.pid.lock
# Schema generation data
Diagram-*.svg
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

View File

@@ -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<string, ElasticsearchValue>,
topTypeName: string,
path: string, tags: CommentTag[]): Map<string, ElasticsearchValue> {
path: string,
tags: CommentTag[]): Map<string, ElasticsearchValue> {
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<string, ElasticsearchValue>,
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

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
},
}
}
};

View File

@@ -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
}
}
},
}
}
};

View File

@@ -35,4 +35,6 @@ export enum ThingType {
PairedTags = 'paired tags',
FilterableTag = 'filterable tag',
AnyUnknown = 'any unknown',
InheritTags = 'inherit tags',
TagsIgnoreCase = 'tags ignore case',
}

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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;