[Console] Generate url components with the new script (#163096)

## Summary
While working on https://github.com/elastic/kibana/pull/162917, I
noticed that there is a property `url_components` that is being
generated with the old script. This PR adds the same functionality to
the new script. The purpose of this property is to provide autocomplete
suggestions for dynamic parameters used in `patterns`. For example, the
URL defined as `_cluster/state/{metrics}` has a list of options allowed
for the parameter `metrics`. These options are defined in the json
specs, but unfortunately they are not always translated into concrete
types in the specification repo. In this example, `metrics` is described
as `string | string[]` in the specification repo. Because of that I
added some override files with `url_components` that will not be
available when we switch to the new script and re-generate the
definitions with it.

The logic for generating the url components is the same as for query
parameters. Most changes in this PR is extracting the code so that both
functions `generateQueryParams` and `generateUrlComponents` can share
it. The only difference between query parameters and url components:
with query parameters we keep all properties so that all parameters are
displayed in the autocomplete suggestions, for example `{ param1: '',
param2: '' }` provide information about 2 query parameters, specifically
their names. For url components, the name of the parameter is not
important, because only the values will be displayed for autocompletion.
That is why, we ignore all parameters without known values and only keep
properties like `{ param: ['value1', 'value2'] }`.
This commit is contained in:
Yulia Čech 2023-08-07 12:53:50 +02:00 committed by GitHub
parent 585d108db7
commit 25ad7a8b56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 805 additions and 546 deletions

View file

@ -6,15 +6,15 @@
* Side Public License, v 1.
*/
import { AutocompleteAvailability } from './types';
import type { EndpointDescription } from '@kbn/console-plugin/common/types';
import type { SpecificationTypes } from './types';
const DEFAULT_ENDPOINT_AVAILABILITY = true;
export const generateAvailability = (
endpoint: SpecificationTypes.Endpoint
): AutocompleteAvailability => {
const availability: AutocompleteAvailability = {
): EndpointDescription['availability'] => {
const availability: EndpointDescription['availability'] = {
stack: DEFAULT_ENDPOINT_AVAILABILITY,
serverless: DEFAULT_ENDPOINT_AVAILABILITY,
};

View file

@ -9,15 +9,16 @@
import fs from 'fs';
import Path, { join } from 'path';
import { ToolingLog } from '@kbn/tooling-log';
import type {
DefinitionUrlParams,
EndpointDefinition,
EndpointDescription,
} from '@kbn/console-plugin/common/types';
import { generateQueryParams } from './generate_query_params';
import { generateAvailability } from './generate_availability';
import type {
AutocompleteBodyParams,
AutocompleteDefinition,
AutocompleteUrlParams,
SpecificationTypes,
} from './types';
import { findTypeDefinition } from './utils';
import type { SpecificationTypes } from './types';
import { findTypeDefinition } from './helpers';
import { generateUrlComponents } from './generate_url_components';
const generateMethods = (endpoint: SpecificationTypes.Endpoint): string[] => {
// this array consists of arrays of strings
@ -42,11 +43,14 @@ const generatePatterns = (endpoint: SpecificationTypes.Endpoint): string[] => {
const generateDocumentation = (endpoint: SpecificationTypes.Endpoint): string => {
return endpoint.docUrl;
};
const generateParams = (
interface GeneratedParameters {
urlParams: DefinitionUrlParams;
urlComponents: DefinitionUrlParams;
}
const generateParameters = (
endpoint: SpecificationTypes.Endpoint,
schema: SpecificationTypes.Model
): { urlParams: AutocompleteUrlParams; bodyParams: AutocompleteBodyParams } | undefined => {
): GeneratedParameters | undefined => {
const { request } = endpoint;
if (!request) {
return;
@ -55,27 +59,22 @@ const generateParams = (
if (!requestType) {
return;
}
const urlParams = generateQueryParams(requestType as SpecificationTypes.Request, schema);
const bodyParams = generateBodyParams(requestType);
return { urlParams, bodyParams };
};
const generateBodyParams = (
requestType: SpecificationTypes.TypeDefinition
): AutocompleteBodyParams => {
return {};
const urlParams = generateQueryParams(requestType as SpecificationTypes.Request, schema);
const urlComponents = generateUrlComponents(requestType as SpecificationTypes.Request, schema);
return { urlParams, urlComponents };
};
const addParams = (
definition: AutocompleteDefinition,
params: { urlParams: AutocompleteUrlParams; bodyParams: AutocompleteBodyParams }
): AutocompleteDefinition => {
const { urlParams, bodyParams } = params;
if (urlParams && Object.keys(urlParams).length > 0) {
definition: EndpointDescription,
params: GeneratedParameters
): EndpointDescription => {
const { urlParams, urlComponents } = params;
if (Object.keys(urlParams).length > 0) {
definition.url_params = urlParams;
}
if (bodyParams && Object.keys(bodyParams).length > 0) {
definition.data_autocomplete_rules = bodyParams;
if (Object.keys(urlComponents).length > 0) {
definition.url_components = urlComponents;
}
return definition;
};
@ -83,13 +82,13 @@ const addParams = (
const generateDefinition = (
endpoint: SpecificationTypes.Endpoint,
schema: SpecificationTypes.Model
): AutocompleteDefinition => {
): EndpointDescription => {
const methods = generateMethods(endpoint);
const patterns = generatePatterns(endpoint);
const documentation = generateDocumentation(endpoint);
const availability = generateAvailability(endpoint);
let definition: AutocompleteDefinition = {};
const params = generateParams(endpoint, schema);
let definition: EndpointDescription = {};
const params = generateParameters(endpoint, schema);
if (params) {
definition = addParams(definition, params);
}
@ -117,7 +116,7 @@ export function generateConsoleDefinitions({
const { name } = endpoint;
log.info(name);
const definition = generateDefinition(endpoint, schema);
const fileContent: { [name: string]: AutocompleteDefinition } = {
const fileContent: EndpointDefinition = {
[name]: definition,
};
fs.writeFileSync(

View file

@ -8,52 +8,9 @@
import { SpecificationTypes } from './types';
import { generateQueryParams } from './generate_query_params';
import { UrlParamValue } from './types/autocomplete_definition_types';
import { getMockProperty, mockRequestType, mockSchema } from './helpers/test_helpers';
describe('generateQueryParams', () => {
const mockRequestType: SpecificationTypes.Request = {
body: { kind: 'no_body' },
kind: 'request',
name: {
name: 'TestRequest',
namespace: 'test.namespace',
},
path: [],
query: [],
specLocation: '',
};
const getMockProperty = ({
propertyName,
typeName,
serverDefault,
type,
}: {
propertyName: string;
typeName?: SpecificationTypes.TypeName;
serverDefault?: SpecificationTypes.Property['serverDefault'];
type?: SpecificationTypes.ValueOf;
}): SpecificationTypes.Property => {
return {
description: 'Description',
name: propertyName,
required: false,
serverDefault: serverDefault ?? undefined,
type: type ?? {
kind: 'instance_of',
type: typeName ?? {
name: 'string',
namespace: '_builtins',
},
},
};
};
const mockSchema: SpecificationTypes.Model = {
endpoints: [],
types: [],
};
it('iterates over attachedBehaviours', () => {
const behaviour1: SpecificationTypes.Interface = {
kind: 'interface',
@ -106,270 +63,4 @@ describe('generateQueryParams', () => {
property2: '',
});
});
it('converts builtin types', () => {
const stringProperty = getMockProperty({
propertyName: 'stringProperty',
typeName: { name: 'string', namespace: '_builtins' },
});
const numberProperty = getMockProperty({
propertyName: 'numberProperty',
typeName: { name: 'number', namespace: '_builtins' },
});
const booleanProperty = getMockProperty({
propertyName: 'booleanProperty',
typeName: { name: 'boolean', namespace: '_builtins' },
});
const requestType = {
...mockRequestType,
query: [stringProperty, numberProperty, booleanProperty],
};
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
stringProperty: '',
numberProperty: '',
booleanProperty: '__flag__',
});
});
it('adds serverDefault value if any', () => {
const propertyWithDefault = getMockProperty({
propertyName: 'propertyWithDefault',
serverDefault: 'default',
});
const requestType = { ...mockRequestType, query: [propertyWithDefault] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
propertyWithDefault: ['default'],
});
});
it('converts an enum property', () => {
const enumProperty = getMockProperty({
propertyName: 'enumProperty',
typeName: { name: 'EnumType', namespace: 'test.namespace' },
});
const enumType: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'enum1',
},
{
name: 'enum2',
},
],
name: {
name: 'EnumType',
namespace: 'test.namespace',
},
specLocation: '',
};
const requestType = { ...mockRequestType, query: [enumProperty] };
const schema = { ...mockSchema, types: [enumType] };
const urlParams = generateQueryParams(requestType, schema);
expect(urlParams).toEqual({
enumProperty: ['enum1', 'enum2'],
});
});
it('converts a type alias', () => {
const typeAliasProperty = getMockProperty({
propertyName: 'typeAliasProperty',
typeName: {
name: 'SomeTypeAlias',
namespace: 'test.namespace',
},
});
const typeAliasType: SpecificationTypes.TypeAlias = {
kind: 'type_alias',
name: {
name: 'SomeTypeAlias',
namespace: 'test.namespace',
},
specLocation: '',
type: {
kind: 'instance_of',
type: {
name: 'integer',
namespace: '_types',
},
},
};
const requestType = { ...mockRequestType, query: [typeAliasProperty] };
const schema: SpecificationTypes.Model = { ...mockSchema, types: [typeAliasType] };
const urlParams = generateQueryParams(requestType, schema);
expect(urlParams).toEqual({
typeAliasProperty: '',
});
});
it('converts a literal_value to a string', () => {
const stringProperty = getMockProperty({
propertyName: 'stringProperty',
type: { kind: 'literal_value', value: 'stringValue' },
});
const numberProperty = getMockProperty({
propertyName: 'numberProperty',
type: { kind: 'literal_value', value: 14 },
});
const booleanProperty = getMockProperty({
propertyName: 'booleanProperty',
type: { kind: 'literal_value', value: true },
});
const requestType = {
...mockRequestType,
query: [stringProperty, numberProperty, booleanProperty],
};
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
stringProperty: ['stringValue'],
numberProperty: ['14'],
booleanProperty: ['true'],
});
});
describe('converts a union_of', () => {
it('flattens the array if one of the items is converted to an array', () => {
const enumType: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'enum1',
},
{ name: 'enum2' },
],
name: { name: 'EnumType', namespace: 'test.namespace' },
specLocation: '',
};
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'EnumType',
namespace: 'test.namespace',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const schema: SpecificationTypes.Model = { ...mockSchema, types: [enumType] };
const urlParams = generateQueryParams(requestType, schema);
expect(urlParams).toEqual({
unionProperty: ['enum1', 'enum2'],
});
});
it('removes empty string from the array', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
unionProperty: [],
});
});
it('if one item is a boolean and others are empty, converts to a flag', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
{
kind: 'instance_of',
type: {
name: 'number',
namespace: '_builtins',
},
},
{
kind: 'instance_of',
type: {
name: 'boolean',
namespace: '_builtins',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
unionProperty: '__flag__',
});
});
it('if one item is an unknown type, converts it to an empty string', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'literal_value',
value: 'test',
},
{
kind: 'instance_of',
type: {
name: 'UnknownType',
namespace: 'test.namespace',
},
},
],
},
});
const requestType = { ...mockRequestType, query: [unionProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
// check that no `undefined` values are added
const value = urlParams.unionProperty as UrlParamValue[];
expect(value.length).toEqual(1);
});
});
it('converts an unknown type to an empty string', () => {
const unknownTypeProperty = getMockProperty({
propertyName: 'unknownTypeProperty',
type: {
kind: 'instance_of',
type: {
name: 'UnknownType',
namespace: 'test.namespace',
},
},
});
const requestType = { ...mockRequestType, query: [unknownTypeProperty] };
const urlParams = generateQueryParams(requestType, mockSchema);
expect(urlParams).toEqual({
unknownTypeProperty: '',
});
});
});

View file

@ -43,19 +43,15 @@
*
*/
import { UrlParamValue } from './types/autocomplete_definition_types';
import type { AutocompleteUrlParams, SpecificationTypes } from './types';
import { findTypeDefinition } from './utils';
const booleanFlagString = '__flag__';
const trueValueString = String(true);
const falseValueString = String(false);
import type { DefinitionUrlParams } from '@kbn/console-plugin/common/types';
import type { SpecificationTypes } from './types';
import { convertUrlProperties } from './helpers';
export const generateQueryParams = (
requestType: SpecificationTypes.Request,
schema: SpecificationTypes.Model
): AutocompleteUrlParams => {
let urlParams = {} as AutocompleteUrlParams;
): DefinitionUrlParams => {
let urlParams: DefinitionUrlParams = {};
const { types } = schema;
const { attachedBehaviors, query } = requestType;
// if there are any attached behaviors, iterate over each and find its type
@ -67,153 +63,13 @@ export const generateQueryParams = (
const behaviorType = foundBehavior as SpecificationTypes.Interface;
// if there are any properties in the behavior, iterate over each and add it to url params
const { properties } = behaviorType;
urlParams = convertProperties(properties, urlParams, schema);
urlParams = convertUrlProperties(properties, urlParams, schema);
}
}
}
// iterate over properties in query and add it to url params
urlParams = convertProperties(query, urlParams, schema);
urlParams = convertUrlProperties(query, urlParams, schema);
return urlParams;
};
const convertProperties = (
properties: SpecificationTypes.Property[],
urlParams: AutocompleteUrlParams,
schema: SpecificationTypes.Model
): AutocompleteUrlParams => {
for (const property of properties) {
const { name, serverDefault, type } = property;
// property has `type` which is `ValueOf`
const convertedValue = convertValueOf(type, serverDefault, schema);
urlParams[name] = convertedValue ?? '';
}
return urlParams;
};
const convertValueOf = (
valueOf: SpecificationTypes.ValueOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { kind } = valueOf;
if (kind === 'instance_of') {
return convertInstanceOf(valueOf, serverDefault, schema);
} else if (kind === 'array_of') {
return convertArrayOf(valueOf, serverDefault, schema);
} else if (kind === 'union_of') {
return convertUnionOf(valueOf, serverDefault, schema);
} else if (kind === 'literal_value') {
return convertLiteralValue(valueOf);
}
// for query params we can ignore 'dictionary_of' and 'user_defined_value'
return '';
};
const convertInstanceOf = (
type: SpecificationTypes.InstanceOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { type: typeName } = type;
const { name: propertyName, namespace } = typeName;
if (namespace === '_builtins') {
/**
* - `string`
* - `boolean`
* - `number`
* - `null` // ignore for query params
* - `void` // ignore for query params
* - `binary` // ignore for query params
*/
if (propertyName === 'boolean') {
// boolean is converted to a flag param
return booleanFlagString;
} else {
// if default value, convert to string and put in an array
return serverDefault ? [serverDefault.toString()] : '';
}
} else {
// if it's a defined type, try to convert it
const definedType = findTypeDefinition(schema, typeName);
if (definedType) {
// TypeDefinition can only be Enum or TypeAlias
if (definedType.kind === 'enum') {
return convertEnum(definedType as SpecificationTypes.Enum);
} else if (definedType.kind === 'type_alias') {
const aliasValueOf = definedType.type;
return convertValueOf(aliasValueOf, serverDefault, schema);
}
}
}
return '';
};
const convertArrayOf = (
type: SpecificationTypes.ArrayOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { value } = type;
// simply convert the value of an array item
return convertValueOf(value, serverDefault, schema);
};
const convertUnionOf = (
type: SpecificationTypes.UnionOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { items } = type;
const itemValues = new Set();
for (const item of items) {
// each item is ValueOf
const convertedValue = convertValueOf(item, serverDefault, schema);
// flatten array if needed
if (convertedValue instanceof Array) {
convertedValue.forEach((v) => itemValues.add(v));
} else itemValues.add(convertedValue);
}
// if an empty string is in values, delete it
if (itemValues.has('')) {
itemValues.delete('');
}
// if there is a flag in the values, convert it to "true" + "false"
if (itemValues.size > 1 && itemValues.has(booleanFlagString)) {
itemValues.delete(booleanFlagString);
itemValues.add(trueValueString);
itemValues.add(falseValueString);
}
// if only 2 values ("true","false"), convert back to a flag
// that can happen if the values before were ("true", "__flag__") or ("false", "__flag__")
if (
itemValues.size === 2 &&
itemValues.has(trueValueString) &&
itemValues.has(falseValueString)
) {
itemValues.clear();
itemValues.add(booleanFlagString);
}
// if only 1 element that is a flag, don't put it in an array
if (itemValues.size === 1 && itemValues.has(booleanFlagString)) {
return itemValues.values().next().value;
}
return [...itemValues] as UrlParamValue;
};
const convertLiteralValue = (type: SpecificationTypes.LiteralValue): UrlParamValue | undefined => {
// convert the value to a string
return [type.value.toString()];
};
const convertEnum = (enumDefinition: SpecificationTypes.Enum): UrlParamValue => {
const { members } = enumDefinition;
// only need the `name` property
return members.map((member) => member.name);
};

View file

@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { generateUrlComponents } from './generate_url_components';
import { getMockProperty, mockRequestType, mockSchema } from './helpers/test_helpers';
import { SpecificationTypes } from './types';
describe('generateUrlComponents', () => {
it('generates url components from path', () => {
const urlComponentProperty1 = getMockProperty({
propertyName: 'property1',
typeName: { name: 'EnumType1', namespace: 'test.namespace' },
});
const enumType1: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'value1',
},
{
name: 'value2',
},
],
name: {
name: 'EnumType1',
namespace: 'test.namespace',
},
specLocation: '',
};
const urlComponentProperty2 = getMockProperty({
propertyName: 'property2',
typeName: { name: 'EnumType2', namespace: 'test.namespace' },
});
const enumType2: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'anotherValue1',
},
{
name: 'anotherValue2',
},
],
name: {
name: 'EnumType2',
namespace: 'test.namespace',
},
specLocation: '',
};
const requestType: SpecificationTypes.Request = {
...mockRequestType,
path: [urlComponentProperty1, urlComponentProperty2],
};
const schema: SpecificationTypes.Model = { ...mockSchema, types: [enumType1, enumType2] };
const urlComponents = generateUrlComponents(requestType, schema);
expect(urlComponents).toEqual({
property1: ['value1', 'value2'],
property2: ['anotherValue1', 'anotherValue2'],
});
});
it('removes url components without any values (empty string)', () => {
const requestType: SpecificationTypes.Request = {
...mockRequestType,
path: [getMockProperty({ propertyName: 'emptyStringProperty' })],
};
const urlComponents = generateUrlComponents(requestType, mockSchema);
expect(urlComponents).toEqual({});
});
it('removes url components without any values (empty array)', () => {
const emptyArrayProperty = getMockProperty({
propertyName: 'emptyArrayProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
{
kind: 'array_of',
value: {
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
},
],
},
});
const requestType: SpecificationTypes.Request = {
...mockRequestType,
path: [emptyArrayProperty],
};
const urlComponents = generateUrlComponents(requestType, mockSchema);
expect(urlComponents).toEqual({});
});
});

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { DefinitionUrlParams } from '@kbn/console-plugin/common/types';
import type { SpecificationTypes } from './types';
import { convertUrlProperties } from './helpers';
export const generateUrlComponents = (
request: SpecificationTypes.Request,
schema: SpecificationTypes.Model
): DefinitionUrlParams => {
let urlComponents: DefinitionUrlParams = {};
const { path } = request;
urlComponents = convertUrlProperties(path, urlComponents, schema);
// remove empty strings and empty arrays
Object.entries(urlComponents).forEach(([paramName, paramValue]) => {
if (!paramValue || (paramValue as []).length === 0) {
delete urlComponents[paramName];
}
});
return urlComponents;
};

View file

@ -0,0 +1,272 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getMockProperty, mockSchema } from './test_helpers';
import { convertUrlProperties } from './convert_url_properties';
import { SpecificationTypes } from '../types';
describe('convertUrlProperties', () => {
it('converts builtin types', () => {
const stringProperty = getMockProperty({
propertyName: 'stringProperty',
typeName: { name: 'string', namespace: '_builtins' },
});
const numberProperty = getMockProperty({
propertyName: 'numberProperty',
typeName: { name: 'number', namespace: '_builtins' },
});
const booleanProperty = getMockProperty({
propertyName: 'booleanProperty',
typeName: { name: 'boolean', namespace: '_builtins' },
});
const urlParams = convertUrlProperties(
[stringProperty, numberProperty, booleanProperty],
{},
mockSchema
);
expect(urlParams).toEqual({
stringProperty: '',
numberProperty: '',
booleanProperty: '__flag__',
});
});
it('adds serverDefault value if any', () => {
const propertyWithDefault = getMockProperty({
propertyName: 'propertyWithDefault',
serverDefault: 'default',
});
const urlParams = convertUrlProperties([propertyWithDefault], {}, mockSchema);
expect(urlParams).toEqual({
propertyWithDefault: ['default'],
});
});
it('converts an enum property', () => {
const enumProperty = getMockProperty({
propertyName: 'enumProperty',
typeName: { name: 'EnumType', namespace: 'test.namespace' },
});
const enumType: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'enum1',
},
{
name: 'enum2',
},
],
name: {
name: 'EnumType',
namespace: 'test.namespace',
},
specLocation: '',
};
const schema = { ...mockSchema, types: [enumType] };
const urlParams = convertUrlProperties([enumProperty], {}, schema);
expect(urlParams).toEqual({
enumProperty: ['enum1', 'enum2'],
});
});
it('converts a type alias', () => {
const typeAliasProperty = getMockProperty({
propertyName: 'typeAliasProperty',
typeName: {
name: 'SomeTypeAlias',
namespace: 'test.namespace',
},
});
const typeAliasType: SpecificationTypes.TypeAlias = {
kind: 'type_alias',
name: {
name: 'SomeTypeAlias',
namespace: 'test.namespace',
},
specLocation: '',
type: {
kind: 'instance_of',
type: {
name: 'integer',
namespace: '_types',
},
},
};
const schema: SpecificationTypes.Model = { ...mockSchema, types: [typeAliasType] };
const urlParams = convertUrlProperties([typeAliasProperty], {}, schema);
expect(urlParams).toEqual({
typeAliasProperty: '',
});
});
it('converts a literal_value to a string', () => {
const stringProperty = getMockProperty({
propertyName: 'stringProperty',
type: { kind: 'literal_value', value: 'stringValue' },
});
const numberProperty = getMockProperty({
propertyName: 'numberProperty',
type: { kind: 'literal_value', value: 14 },
});
const booleanProperty = getMockProperty({
propertyName: 'booleanProperty',
type: { kind: 'literal_value', value: true },
});
const urlParams = convertUrlProperties(
[stringProperty, numberProperty, booleanProperty],
{},
mockSchema
);
expect(urlParams).toEqual({
stringProperty: ['stringValue'],
numberProperty: ['14'],
booleanProperty: ['true'],
});
});
describe('converts a union_of', () => {
it('flattens the array if one of the items is converted to an array', () => {
const enumType: SpecificationTypes.Enum = {
kind: 'enum',
members: [
{
name: 'enum1',
},
{ name: 'enum2' },
],
name: { name: 'EnumType', namespace: 'test.namespace' },
specLocation: '',
};
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'EnumType',
namespace: 'test.namespace',
},
},
],
},
});
const schema: SpecificationTypes.Model = { ...mockSchema, types: [enumType] };
const urlParams = convertUrlProperties([unionProperty], {}, schema);
expect(urlParams).toEqual({
unionProperty: ['enum1', 'enum2'],
});
});
it('removes empty string from the array', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
],
},
});
const urlParams = convertUrlProperties([unionProperty], {}, mockSchema);
expect(urlParams).toEqual({
unionProperty: [],
});
});
it('if one item is a boolean and others are empty, converts to a flag', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'instance_of',
type: {
name: 'string',
namespace: '_builtins',
},
},
{
kind: 'instance_of',
type: {
name: 'number',
namespace: '_builtins',
},
},
{
kind: 'instance_of',
type: {
name: 'boolean',
namespace: '_builtins',
},
},
],
},
});
const urlParams = convertUrlProperties([unionProperty], {}, mockSchema);
expect(urlParams).toEqual({
unionProperty: '__flag__',
});
});
it('if one item is an unknown type, converts it to an empty string', () => {
const unionProperty = getMockProperty({
propertyName: 'unionProperty',
type: {
kind: 'union_of',
items: [
{
kind: 'literal_value',
value: 'test',
},
{
kind: 'instance_of',
type: {
name: 'UnknownType',
namespace: 'test.namespace',
},
},
],
},
});
const urlParams = convertUrlProperties([unionProperty], {}, mockSchema);
// check that no `undefined` values are added
const value = urlParams.unionProperty as [];
expect(value.length).toEqual(1);
});
});
it('converts an unknown type to an empty string', () => {
const unknownTypeProperty = getMockProperty({
propertyName: 'unknownTypeProperty',
type: {
kind: 'instance_of',
type: {
name: 'UnknownType',
namespace: 'test.namespace',
},
},
});
const urlParams = convertUrlProperties([unknownTypeProperty], {}, mockSchema);
expect(urlParams).toEqual({
unknownTypeProperty: '',
});
});
});

View file

@ -0,0 +1,158 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { DefinitionUrlParams } from '@kbn/console-plugin/common/types';
import type { SpecificationTypes } from '../types';
import { findTypeDefinition } from './find_type_definition';
const booleanFlagString = '__flag__';
const trueValueString = String(true);
const falseValueString = String(false);
// use unknown for now since DefinitionUrlParams is Record<string, unknown>
// TODO update with more concrete types
type UrlParamValue = unknown;
export const convertUrlProperties = (
properties: SpecificationTypes.Property[],
urlParams: DefinitionUrlParams,
schema: SpecificationTypes.Model
): DefinitionUrlParams => {
for (const property of properties) {
const { name, serverDefault, type } = property;
// property has `type` which is `ValueOf`
const convertedValue = convertValueOf(type, serverDefault, schema);
urlParams[name] = convertedValue ?? '';
}
return urlParams;
};
const convertValueOf = (
valueOf: SpecificationTypes.ValueOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { kind } = valueOf;
if (kind === 'instance_of') {
return convertInstanceOf(valueOf, serverDefault, schema);
} else if (kind === 'array_of') {
return convertArrayOf(valueOf, serverDefault, schema);
} else if (kind === 'union_of') {
return convertUnionOf(valueOf, serverDefault, schema);
} else if (kind === 'literal_value') {
return convertLiteralValue(valueOf);
}
// for query params we can ignore 'dictionary_of' and 'user_defined_value'
return '';
};
const convertInstanceOf = (
type: SpecificationTypes.InstanceOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { type: typeName } = type;
const { name: propertyName, namespace } = typeName;
if (namespace === '_builtins') {
/**
* - `string`
* - `boolean`
* - `number`
* - `null` // ignore for query params
* - `void` // ignore for query params
* - `binary` // ignore for query params
*/
if (propertyName === 'boolean') {
// boolean is converted to a flag param
return booleanFlagString;
} else {
// if default value, convert to string and put in an array
return serverDefault ? [serverDefault.toString()] : '';
}
} else {
// if it's a defined type, try to convert it
const definedType = findTypeDefinition(schema, typeName);
if (definedType) {
// TypeDefinition can only be Enum or TypeAlias
if (definedType.kind === 'enum') {
return convertEnum(definedType as SpecificationTypes.Enum);
} else if (definedType.kind === 'type_alias') {
const aliasValueOf = definedType.type;
return convertValueOf(aliasValueOf, serverDefault, schema);
}
}
}
return '';
};
const convertArrayOf = (
type: SpecificationTypes.ArrayOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { value } = type;
// simply convert the value of an array item
return convertValueOf(value, serverDefault, schema);
};
const convertUnionOf = (
type: SpecificationTypes.UnionOf,
serverDefault: SpecificationTypes.Property['serverDefault'],
schema: SpecificationTypes.Model
): UrlParamValue | undefined => {
const { items } = type;
const itemValues = new Set();
for (const item of items) {
// each item is ValueOf
const convertedValue = convertValueOf(item, serverDefault, schema);
// flatten array if needed
if (convertedValue instanceof Array) {
convertedValue.forEach((v) => itemValues.add(v));
} else itemValues.add(convertedValue);
}
// if an empty string is in values, delete it
if (itemValues.has('')) {
itemValues.delete('');
}
// if there is a flag in the values, convert it to "true" + "false"
if (itemValues.size > 1 && itemValues.has(booleanFlagString)) {
itemValues.delete(booleanFlagString);
itemValues.add(trueValueString);
itemValues.add(falseValueString);
}
// if only 2 values ("true","false"), convert back to a flag
// that can happen if the values before were ("true", "__flag__") or ("false", "__flag__")
if (
itemValues.size === 2 &&
itemValues.has(trueValueString) &&
itemValues.has(falseValueString)
) {
itemValues.clear();
itemValues.add(booleanFlagString);
}
// if only 1 element that is a flag, don't put it in an array
if (itemValues.size === 1 && itemValues.has(booleanFlagString)) {
return itemValues.values().next().value;
}
return [...itemValues] as UrlParamValue;
};
const convertLiteralValue = (type: SpecificationTypes.LiteralValue): UrlParamValue | undefined => {
// convert the value to a string
return [type.value.toString()];
};
const convertEnum = (enumDefinition: SpecificationTypes.Enum): UrlParamValue => {
const { members } = enumDefinition;
// only need the `name` property
return members.map((member) => member.name);
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { SpecificationTypes } from './types';
import type { SpecificationTypes } from '../types';
export const findTypeDefinition = (
schema: SpecificationTypes.Model,
typeName: SpecificationTypes.TypeName

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { findTypeDefinition } from './find_type_definition';
export { convertUrlProperties } from './convert_url_properties';

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SpecificationTypes } from '../types';
export const mockRequestType: SpecificationTypes.Request = {
body: { kind: 'no_body' },
kind: 'request',
name: {
name: 'TestRequest',
namespace: 'test.namespace',
},
path: [],
query: [],
specLocation: '',
};
export const getMockProperty = ({
propertyName,
typeName,
serverDefault,
type,
}: {
propertyName: string;
typeName?: SpecificationTypes.TypeName;
serverDefault?: SpecificationTypes.Property['serverDefault'];
type?: SpecificationTypes.ValueOf;
}): SpecificationTypes.Property => {
return {
description: 'Description',
name: propertyName,
required: false,
serverDefault: serverDefault ?? undefined,
type: type ?? {
kind: 'instance_of',
type: typeName ?? {
name: 'string',
namespace: '_builtins',
},
},
};
};
export const mockSchema: SpecificationTypes.Model = {
endpoints: [],
types: [],
};

View file

@ -1,29 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type UrlParamValue = number | string | number[] | string[] | boolean;
export interface AutocompleteUrlParams {
[key: string]: UrlParamValue;
}
export interface AutocompleteBodyParams {
[key: string]: number | string;
}
export interface AutocompleteAvailability {
stack: boolean;
serverless: boolean;
}
export interface AutocompleteDefinition {
documentation?: string;
methods?: string[];
patterns?: string[];
url_params?: AutocompleteUrlParams;
data_autocomplete_rules?: AutocompleteBodyParams;
availability?: AutocompleteAvailability;
}

View file

@ -6,11 +6,4 @@
* Side Public License, v 1.
*/
export type {
AutocompleteDefinition,
AutocompleteUrlParams,
AutocompleteBodyParams,
AutocompleteAvailability,
} from './autocomplete_definition_types';
export * as SpecificationTypes from './specification_types';

View file

@ -9,15 +9,51 @@
export type EndpointsAvailability = 'stack' | 'serverless';
export interface EndpointDescription {
/**
* HTTP request methods this endpoint accepts: GET, POST, DELETE etc
*/
methods?: string[];
/**
* URLs paths this endpoint accepts, can contain parameters in curly braces.
* For example, /_cat/indices/{index}
*/
patterns?: string | string[];
url_params?: Record<string, unknown>;
/**
* List of possible values for parameters used in patterns.
*/
url_components?: DefinitionUrlParams;
/**
* Query parameters for this endpoint.
*/
url_params?: DefinitionUrlParams;
/**
* Request body parameters for this endpoint.
*/
data_autocomplete_rules?: Record<string, unknown>;
url_components?: Record<string, unknown>;
/**
* A priority number when the same endpoint name is used.
*/
priority?: number;
/**
* An url of the documentation page of this endpoint.
* Can contain a parameter {branch}.
*/
documentation?: string;
/**
* If the endpoint is available different environments (stack, serverless).
*/
availability?: Record<EndpointsAvailability, boolean>;
}
export type DefinitionUrlParams = Record<string, unknown>;
export interface EndpointDefinition {
[endpointName: string]: EndpointDescription;
}

View file

@ -0,0 +1,16 @@
{
"cluster.state": {
"url_components": {
"metrics": [
"_all",
"blocks",
"master_node",
"metadata",
"nodes",
"routing_nodes",
"routing_table",
"version"
]
}
}
}

View file

@ -0,0 +1,24 @@
{
"indices.stats": {
"url_components": {
"metrics": [
"_all",
"bulk",
"completion",
"docs",
"fielddata",
"flush",
"get",
"indexing",
"merge",
"query_cache",
"refresh",
"request_cache",
"search",
"segments",
"store",
"warmer"
]
}
}
}

View file

@ -0,0 +1,39 @@
{
"nodes.stats": {
"url_components": {
"metrics": [
"_all",
"breaker",
"discovery",
"fs",
"http",
"indexing_pressure",
"indices",
"jvm",
"os",
"process",
"thread_pool",
"transport"
],
"index_metric": [
"_all",
"bulk",
"completion",
"docs",
"fielddata",
"flush",
"get",
"indexing",
"merge",
"query_cache",
"refresh",
"request_cache",
"search",
"segments",
"shard_stats",
"store",
"warmer"
]
}
}
}

View file

@ -0,0 +1,10 @@
{
"nodes.usage": {
"url_components": {
"metrics": [
"_all",
"rest_actions"
]
}
}
}

View file

@ -9,9 +9,9 @@
import globby from 'globby';
import fs from 'fs';
import { SpecDefinitionsService } from '.';
import { EndpointDefinition, EndpointsAvailability } from '../../common/types';
import type { EndpointDefinition, EndpointsAvailability } from '../../common/types';
const mockReadFilySync = jest.spyOn(fs, 'readFileSync');
const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
const mockGlobbySync = jest.spyOn(globby, 'sync');
const mockJsLoadersGetter = jest.fn();
@ -57,7 +57,7 @@ describe('SpecDefinitionsService', () => {
// mock the function that lists files in the definitions folders
mockGlobbySync.mockImplementation(() => []);
// mock the function that reads files
mockReadFilySync.mockImplementation(() => '');
mockReadFileSync.mockImplementation(() => '');
// mock the function that returns the list of js definitions loaders
mockJsLoadersGetter.mockImplementation(() => []);
});
@ -117,7 +117,7 @@ describe('SpecDefinitionsService', () => {
return [];
});
mockReadFilySync.mockImplementation((path) => {
mockReadFileSync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'endpoint1' }));
}
@ -162,7 +162,7 @@ describe('SpecDefinitionsService', () => {
return [];
});
mockReadFilySync.mockImplementation((path) => {
mockReadFileSync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'endpoint1' }));
}
@ -219,7 +219,7 @@ describe('SpecDefinitionsService', () => {
return [];
});
mockReadFilySync.mockImplementation((path) => {
mockReadFileSync.mockImplementation((path) => {
if (path.toString() === 'manual_endpoint.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'manual_endpoint' }));
}
@ -250,7 +250,7 @@ describe('SpecDefinitionsService', () => {
return [];
});
mockReadFilySync.mockImplementation((path) => {
mockReadFileSync.mockImplementation((path) => {
if (path.toString() === 'generated_endpoint.json') {
return JSON.stringify(getMockEndpoint({ endpointName: 'test', methods: ['GET'] }));
}
@ -288,7 +288,7 @@ describe('SpecDefinitionsService', () => {
return [];
});
mockReadFilySync.mockImplementation((path) => {
mockReadFileSync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(
getMockEndpoint({
@ -330,7 +330,7 @@ describe('SpecDefinitionsService', () => {
return [];
});
mockReadFilySync.mockImplementation((path) => {
mockReadFileSync.mockImplementation((path) => {
if (path.toString() === '/generated/endpoint1.json') {
return JSON.stringify(
getMockEndpoint({

View file

@ -12,6 +12,7 @@ import { basename, join } from 'path';
import normalizePath from 'normalize-path';
import { readFileSync } from 'fs';
import { EndpointDefinition, EndpointDescription, EndpointsAvailability } from '../../common/types';
import {
AUTOCOMPLETE_DEFINITIONS_FOLDER,
GENERATED_SUBFOLDER,
@ -19,11 +20,6 @@ import {
OVERRIDES_SUBFOLDER,
} from '../../common/constants';
import { jsSpecLoaders } from '../lib';
import type {
EndpointsAvailability,
EndpointDescription,
EndpointDefinition,
} from '../../common/types';
export interface SpecDefinitionsDependencies {
endpointsAvailability: EndpointsAvailability;