Skip to content

Commit

Permalink
[Usage Collection] [schema] Support spreads + canvas definition (#7…
Browse files Browse the repository at this point in the history
  • Loading branch information
afharo authored Sep 29, 2020
1 parent a3b3a4d commit 406c47a
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { SyntaxKind } from 'typescript';
import { ParsedUsageCollection } from '../ts_parser';

export const parsedSchemaDefinedWithSpreadsCollector: ParsedUsageCollection = [
'src/fixtures/telemetry_collectors/schema_defined_with_spreads_collector.ts',
{
collectorName: 'schema_defined_with_spreads',
schema: {
value: {
flat: {
type: 'keyword',
},
my_str: {
type: 'text',
},
my_objects: {
total: {
type: 'number',
},
type: {
type: 'boolean',
},
},
},
},
fetch: {
typeName: 'Usage',
typeDescriptor: {
flat: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
my_str: {
kind: SyntaxKind.StringKeyword,
type: 'StringKeyword',
},
my_objects: {
total: {
kind: SyntaxKind.NumberKeyword,
type: 'NumberKeyword',
},
type: {
kind: SyntaxKind.BooleanKeyword,
type: 'BooleanKeyword',
},
},
},
},
},
];

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('extractCollectors', () => {
const programPaths = await getProgramPaths(configs[0]);

const results = [...extractCollectors(programPaths, tsConfig)];
expect(results).toHaveLength(7);
expect(results).toHaveLength(8);
expect(results).toMatchSnapshot();
});
});
7 changes: 7 additions & 0 deletions packages/kbn-telemetry-tools/src/tools/ts_parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { parsedNestedCollector } from './__fixture__/parsed_nested_collector';
import { parsedExternallyDefinedCollector } from './__fixture__/parsed_externally_defined_collector';
import { parsedImportedUsageInterface } from './__fixture__/parsed_imported_usage_interface';
import { parsedImportedSchemaCollector } from './__fixture__/parsed_imported_schema';
import { parsedSchemaDefinedWithSpreadsCollector } from './__fixture__/parsed_schema_defined_with_spreads_collector';

export function loadFixtureProgram(fixtureName: string) {
const fixturePath = path.resolve(
Expand Down Expand Up @@ -62,6 +63,12 @@ describe('parseUsageCollection', () => {
expect(result).toEqual([parsedWorkingCollector]);
});

it('parses collector with schema defined as union of spreads', () => {
const { program, sourceFile } = loadFixtureProgram('schema_defined_with_spreads_collector');
const result = [...parseUsageCollection(sourceFile, program)];
expect(result).toEqual([parsedSchemaDefinedWithSpreadsCollector]);
});

it('parses nested collectors', () => {
const { program, sourceFile } = loadFixtureProgram('nested_collector');
const result = [...parseUsageCollection(sourceFile, program)];
Expand Down
101 changes: 61 additions & 40 deletions packages/kbn-telemetry-tools/src/tools/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,42 +100,55 @@ export function getIdentifierDeclaration(node: ts.Node) {
return getIdentifierDeclarationFromSource(node, source);
}

export function getVariableValue(node: ts.Node): string | Record<string, any> {
export function getVariableValue(node: ts.Node, program: ts.Program): string | Record<string, any> {
if (ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
}

if (ts.isObjectLiteralExpression(node)) {
return serializeObject(node);
return serializeObject(node, program);
}

if (ts.isIdentifier(node)) {
const declaration = getIdentifierDeclaration(node);
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
return getVariableValue(declaration.initializer);
return getVariableValue(declaration.initializer, program);
} else {
// Go fetch it in another file
return getIdentifierValue(node, node, program, { chaseImport: true });
}
// TODO: If this is another imported value from another file, we'll need to go fetch it like in getPropertyValue
}

throw Error(`Unsuppored Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`);
if (ts.isSpreadAssignment(node)) {
return getVariableValue(node.expression, program);
}

throw Error(
`Unsupported Node: cannot get value of node (${node.getText()}) of kind ${node.kind}`
);
}

export function serializeObject(node: ts.Node) {
export function serializeObject(node: ts.Node, program: ts.Program) {
if (!ts.isObjectLiteralExpression(node)) {
throw new Error(`Expecting Object literal Expression got ${node.getText()}`);
}

const value: Record<string, any> = {};
let value: Record<string, any> = {};
for (const property of node.properties) {
const propertyName = property.name?.getText();
const val = ts.isPropertyAssignment(property)
? getVariableValue(property.initializer, program)
: getVariableValue(property, program);

if (typeof propertyName === 'undefined') {
throw new Error(`Unable to get property name ${property.getText()}`);
}
const cleanPropertyName = propertyName.replace(/["']/g, '');
if (ts.isPropertyAssignment(property)) {
value[cleanPropertyName] = getVariableValue(property.initializer);
if (typeof val === 'object') {
value = { ...value, ...val };
} else {
throw new Error(`Unable to get property name ${property.getText()}`);
}
} else {
value[cleanPropertyName] = getVariableValue(property);
const cleanPropertyName = propertyName.replace(/["']/g, '');
value[cleanPropertyName] = val;
}
}

Expand All @@ -155,45 +168,53 @@ export function getResolvedModuleSourceFile(
return resolvedModuleSourceFile;
}

export function getPropertyValue(
export function getIdentifierValue(
node: ts.Node,
initializer: ts.Identifier,
program: ts.Program,
config: Optional<{ chaseImport: boolean }> = {}
) {
const { chaseImport = false } = config;
const identifierName = initializer.getText();
const declaration = getIdentifierDeclaration(initializer);
if (ts.isImportSpecifier(declaration)) {
if (!chaseImport) {
throw new Error(
`Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.`
);
}

if (ts.isPropertyAssignment(node)) {
const { initializer } = node;
const importedModuleName = getModuleSpecifier(declaration);

if (ts.isIdentifier(initializer)) {
const identifierName = initializer.getText();
const declaration = getIdentifierDeclaration(initializer);
if (ts.isImportSpecifier(declaration)) {
if (!chaseImport) {
throw new Error(
`Value of node ${identifierName} is imported from another file. Chasing imports is not allowed.`
);
}
const source = node.getSourceFile();
const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource);
if (!ts.isVariableDeclaration(declarationNode)) {
throw new Error(`Expected ${identifierName} to be variable declaration.`);
}
if (!declarationNode.initializer) {
throw new Error(`Expected ${identifierName} to be initialized.`);
}
const serializedObject = serializeObject(declarationNode.initializer, program);
return serializedObject;
}

const importedModuleName = getModuleSpecifier(declaration);
return getVariableValue(declaration, program);
}

const source = node.getSourceFile();
const declarationSource = getResolvedModuleSourceFile(source, program, importedModuleName);
const declarationNode = getIdentifierDeclarationFromSource(initializer, declarationSource);
if (!ts.isVariableDeclaration(declarationNode)) {
throw new Error(`Expected ${identifierName} to be variable declaration.`);
}
if (!declarationNode.initializer) {
throw new Error(`Expected ${identifierName} to be initialized.`);
}
const serializedObject = serializeObject(declarationNode.initializer);
return serializedObject;
}
export function getPropertyValue(
node: ts.Node,
program: ts.Program,
config: Optional<{ chaseImport: boolean }> = {}
) {
if (ts.isPropertyAssignment(node)) {
const { initializer } = node;

return getVariableValue(declaration);
if (ts.isIdentifier(initializer)) {
return getIdentifierValue(node, initializer, program, config);
}

return getVariableValue(initializer);
return getVariableValue(initializer, program);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ export const myCollector = makeUsageCollector<Usage>({
return { something: { count_2: 2 } };
},
schema: {
// @ts-expect-error Intentionally missing count_2
something: {
count_1: { type: 'long' }, // Intentionally missing count_2
count_1: { type: 'long' },
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CollectorSet, MakeSchemaFrom } from '../../plugins/usage_collection/server/collector';
import { loggerMock } from '../../core/server/logging/logger.mock';

const { makeUsageCollector } = new CollectorSet({
logger: loggerMock.create(),
maximumWaitTimeForAllCollectorsInS: 0,
});

interface MyObject {
total: number;
type: boolean;
}

interface Usage {
flat?: string;
my_str?: string;
my_objects: MyObject;
}

const SOME_NUMBER: number = 123;

const someSchema: MakeSchemaFrom<Pick<Usage, 'flat' | 'my_str'>> = {
flat: {
type: 'keyword',
},
my_str: {
type: 'text',
},
};

const someOtherSchema: MakeSchemaFrom<Pick<Usage, 'my_objects'>> = {
my_objects: {
total: {
type: 'number',
},
type: { type: 'boolean' },
},
};

export const myCollector = makeUsageCollector<Usage>({
type: 'schema_defined_with_spreads',
isReady: () => true,
fetch() {
const testString = '123';

return {
flat: 'hello',
my_str: testString,
my_objects: {
total: SOME_NUMBER,
type: true,
},
};
},
schema: {
...someSchema,
...someOtherSchema,
},
});
Loading

0 comments on commit 406c47a

Please sign in to comment.