diff --git a/diff-schema/package-lock.json b/diff-schema/package-lock.json new file mode 100644 index 0000000000..86a9e799c7 --- /dev/null +++ b/diff-schema/package-lock.json @@ -0,0 +1,45 @@ +{ + "name": "diff-schema", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "diff-schema", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/node": "^24.6.0", + "typescript": "^5.9.2" + } + }, + "node_modules/@types/node": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz", + "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.13.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", + "license": "MIT" + } + } +} diff --git a/diff-schema/package.json b/diff-schema/package.json new file mode 100644 index 0000000000..45f1280079 --- /dev/null +++ b/diff-schema/package.json @@ -0,0 +1,18 @@ +{ + "name": "diff-schema", + "version": "1.0.0", + "main": "diff-schema.js", + "type": "commonjs", + "scripts": { + "diff-schema": "npx tsc && node lib/diff-schema.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@types/node": "^24.6.0", + "typescript": "^5.9.2" + } +} diff --git a/diff-schema/src/diff-schema.ts b/diff-schema/src/diff-schema.ts new file mode 100644 index 0000000000..75d1acda67 --- /dev/null +++ b/diff-schema/src/diff-schema.ts @@ -0,0 +1,419 @@ +/* + * 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. + */ + +/* global $ argv, path, cd, nothrow */ + +import { Model, TypeName, Property, ValueOf } from './metamodel'; + +type TypeChange = { + codegenName: string, + oldType: string, + newType: string, +}; + +type PropertyChanges = { + addedProperties: string[], + removedProperties: string[], + changedProperties: Record, + isEnum: boolean, +}; + +type EndpointChanges = { + requestPath?: PropertyChanges, + requestQuery?: PropertyChanges, + requestBody?: PropertyChanges, + responseBody?: PropertyChanges, +}; + +type Changes = { + addedEndpoints: string[], + removedEndpoints: string[], + changedEndpoints: Record, + changedTypes: Record, +}; + +async function downloadSchema(branch: string): Promise { + const response = await fetch(`https://raw.githubusercontent.com/elastic/elasticsearch-specification/${branch}/output/schema/schema.json`); + if (response.ok) { + return await response.json(); + } + console.log(`Cannot read schema for branch ${branch}`); + process.exit(1); +} + +function findEndpoint(name: string, schema: Model) { + for (const endpoint of schema.endpoints) { + if (endpoint.name === name) { + return endpoint; + } + } +} + +function findType(typeName: TypeName, schema: Model) { + for (const type of schema.types) { + if (type.name.namespace === typeName.namespace && type.name.name === typeName.name) { + return type; + } + } +} + +function findProp(name: string, props: Property[]) { + for (const prop of props) { + if (prop.name === name) { + return prop; + } + } +} + +function typeToString(type: ValueOf) { + if (type.kind === "instance_of") { + return `\`${type.type.name}\``; + } + else if (type.kind === "array_of") { + return "array of " + typeToString(type.value); + } + else if (type.kind === "dictionary_of") { + return "associative array with " + typeToString(type.key) + " keys and " + typeToString(type.value) + " values"; + } + else if (type.kind === "union_of") { + const items = type.items.map(t => typeToString(t)) + return items.slice(0, -1).join(', ') + ' or ' + items[items.length - 1]; + } + else if (type.kind === "literal_value") { + return JSON.stringify(type.value); + } + else if (type.kind === "user_defined_value") { + return ""; + } +} + +async function diffSchema(oldBranch: string, newBranch: string) { + const changes: Changes = { + addedEndpoints: [], + removedEndpoints: [], + changedEndpoints: {}, + changedTypes: {}, + }; + + function diffProps(oldProps: Property[], newProps: Property[], propKind: string, endpoint?: string) { + const chg: PropertyChanges = { + addedProperties: [], + removedProperties: [], + changedProperties: {}, + isEnum: false, + }; + for (const newProp of newProps) { + const oldProp = findProp(newProp.name, oldProps); + if (!oldProp) { + chg.addedProperties.push(newProp.name); + } + else { + const oldType = typeToString(oldProp.type); + const newType = typeToString(newProp.type); + if (oldType != newType) { + chg.changedProperties[newProp.name] = { + codegenName: newProp.codegenName ?? '', + oldType: oldType, + newType: newType, + }; + } + } + } + for (const oldProp of oldProps) { + const newProp = findProp(oldProp.name, newProps); + if (!newProp) { + chg.removedProperties.push(oldProp.name); + continue; + } + } + if (chg.addedProperties.length || chg.removedProperties.length || Object.keys(chg.changedProperties).length) { + if (endpoint) { + if (!changes.changedEndpoints[endpoint]) { + changes.changedEndpoints[endpoint] = {}; + } + if (!changes.changedEndpoints[endpoint][propKind]) { + changes.changedEndpoints[endpoint][propKind] = {}; + } + changes.changedEndpoints[endpoint][propKind] = chg; + } + else { + changes.changedTypes[propKind] = chg; + } + } + } + + const oldSchema = await downloadSchema(oldBranch); + const newSchema = await downloadSchema(newBranch); + + for (const newEndpoint of newSchema.endpoints) { + const oldEndpoint = findEndpoint(newEndpoint.name, oldSchema); + if (!oldEndpoint) { + changes.addedEndpoints.push(newEndpoint.name); + continue; + } + + if (newEndpoint.request && oldEndpoint.request) { + const newRequest = findType(newEndpoint.request, newSchema); + const oldRequest = findType(oldEndpoint.request, oldSchema); + if (newRequest && newRequest.kind != 'request') { + console.log(`Error: Request for endpoint ${newEndpoint.name} has unexpected type`); + continue; + } + else if (!newRequest) { + continue; + } + if (oldRequest && oldRequest.kind != 'request') { + console.log(`Error: Request for endpoint ${newEndpoint.name} has unexpected type`); + continue; + } + else if (!oldRequest) { + continue; + } + diffProps(oldRequest.path, newRequest.path, 'requestPath', newEndpoint.name); + diffProps(oldRequest.query, newRequest.query, 'requestQuery', newEndpoint.name); + if (oldRequest.body.kind === 'properties' && newRequest.body.kind === 'properties') { + diffProps(oldRequest.body.properties, newRequest.body.properties, 'requestBody', newEndpoint.name); + } + else if (oldRequest.body.kind !== newRequest.body.kind) { + console.log(`Error: Unsupported request body type change for endpoint ${newEndpoint.name}`); + } + } + if (newEndpoint.response && oldEndpoint.response) { + const newResponse = findType(newEndpoint.response, newSchema); + const oldResponse = findType(oldEndpoint.response, oldSchema); + if (newResponse && newResponse.kind != 'response') { + console.log(`Error: Response for endpoint ${newEndpoint.name} has unexpected type`); + continue; + } + else if (!newResponse) { + continue; + } + if (oldResponse && oldResponse.kind != 'response') { + console.log(`Error: Response for endpoint ${newEndpoint.name} has unexpected type`); + continue; + } + else if (!oldResponse) { + continue; + } + if (oldResponse.body.kind === 'properties' && newResponse.body.kind === 'properties') { + diffProps(oldResponse.body.properties, newResponse.body.properties, 'responseBody', newEndpoint.name); + } + else if (oldResponse.body.kind !== newResponse.body.kind) { + console.log(`Error: Unsupported response body type change for endpoint ${newEndpoint.name}`); + } + } + } + for (const oldEndpoint of oldSchema.endpoints) { + const newEndpoint = findEndpoint(oldEndpoint.name, newSchema); + if (!newEndpoint) { + changes.removedEndpoints.push(oldEndpoint.name); + } + } + + for (const newType of newSchema.types) { + const name = `${newType.name.namespace}::${newType.name.name}`; + if (newType.kind === "interface") { + const oldType = findType(newType.name, oldSchema); + if (oldType && oldType.kind === 'interface') { + diffProps(oldType.properties, newType.properties, name); + } + } + else if (newType.kind === "enum") { + const oldType = findType(newType.name, oldSchema); + if (oldType && oldType.kind === 'enum') { + for (const newMember of newType.members) { + const oldMember = oldType.members.find(m => m.name === newMember.name); + if (!oldMember) { + if (!changes.changedTypes[name]) { + changes.changedTypes[name] = { + addedProperties: [], + removedProperties: [], + changedProperties: {}, + isEnum: true, + } + } + changes.changedTypes[name].addedProperties.push(newMember.name); + } + } + for (const oldMember of oldType.members) { + const newMember = newType.members.find(m => m.name === oldMember.name); + if (!newMember) { + if (!changes.changedTypes[name]) { + changes.changedTypes[name] = { + addedProperties: [], + removedProperties: [], + changedProperties: {}, + isEnum: true, + } + } + changes.changedTypes[name].removedProperties.push(oldMember.name); + } + } + } + } + } + + return changes; +} + +function diffToMarkdown(changes: any) { + const formatEndpoint = (e: string) => { + const p = e.split('.'); + if (p.length == 1) { + return `\`${e}\``; + } + return `\`${p[1]}\` (\`${p[0]}\` namespace)`; + }; + + const formatType = (t: string) => { + const p = t.split('::'); + if (p.length == 1) { + return `\`${t}\``; + } + const q = p[0].split('.'); + if (q.length == 1) { + if (q[0].startsWith('_')) { + return `\`${p[1]}\``; + } + return `\`${p[1]}\` (\`${p[0]}\` namespace)`; + } + return `\`${p[1]}\` (\`${q[0]}\` namespace)`; + }; + + if (changes.addedEndpoints.length) { + console.log('# Added APIs'); + for (const e of changes.addedEndpoints) { + console.log(`- ${formatEndpoint(e)}`); + } + console.log(''); + } + if (changes.removedEndpoints.length) { + console.log('# Removed APIs'); + for (const e of changes.removedEndpoints) { + console.log(`- ${formatEndpoint(e)}`); + } + console.log(''); + } + if (changes.changedEndpoints) { + console.log('# Modified APIs'); + for (const e in changes.changedEndpoints) { + console.log(`\n## ${formatEndpoint(e)}`); + for (const p of changes.changedEndpoints[e].requestPath?.addedProperties ?? []) { + console.log(`- Added \`${p}\` path property`); + } + for (const p of changes.changedEndpoints[e].requestQuery?.addedProperties ?? []) { + console.log(`- Added \`${p}\` query property`); + } + for (const p of changes.changedEndpoints[e].requestBody?.addedProperties ?? []) { + console.log(`- Added \`${p}\` request body property`); + } + for (const p of changes.changedEndpoints[e].responseBody?.addedProperties ?? []) { + console.log(`- Added \`${p}\` response body property`); + } + + for (const p in changes.changedEndpoints[e].requestPath?.changedProperties ?? {}) { + const ch = changes.changedEndpoints[e].requestPath?.changedProperties?.[p]; + if (ch) { + console.log(`- Changed type of \`${p}\` path property to ${ch.newType}`); + } + } + for (const p in changes.changedEndpoints[e].requestQuery?.changedProperties ?? {}) { + const ch = changes.changedEndpoints[e].requestQuery?.changedProperties?.[p]; + if (ch) { + console.log(`- Changed type of \`${p}\` query property to ${ch.newType}`); + } + } + for (const p in changes.changedEndpoints[e].requestBody?.changedProperties ?? {}) { + const ch = changes.changedEndpoints[e].requestBody?.changedProperties?.[p]; + if (ch) { + console.log(`- Changed type of \`${p}\` request body property to ${ch.newType}`); + } + } + for (const p in changes.changedEndpoints[e].responseBody?.changedProperties ?? {}) { + const ch = changes.changedEndpoints[e].responseBody?.changedProperties?.[p]; + if (ch) { + console.log(`- Changed type of \`${p}\` response body property to ${ch.newType}`); + } + } + + for (const p of changes.changedEndpoints[e].requestPath?.removedProperties ?? []) { + console.log(`- Removed \`${p}\` path property`); + } + for (const p of changes.changedEndpoints[e].requestQuery?.removedProperties ?? []) { + console.log(`- Removed \`${p}\` query property`); + } + for (const p of changes.changedEndpoints[e].requestBody?.removedProperties ?? []) { + console.log(`- Removed \`${p}\` request body property`); + } + for (const p of changes.changedEndpoints[e].responseBody?.removedProperties ?? []) { + console.log(`- Removed \`${p}\` response body property`); + } + if (changes.changedEndpoints[e].requestBody?.changed) { + console.log(`- Response body type changed to ${changes.changedEndpoints[e].requestBody.changed.newType}`); + } + if (changes.changedEndpoints[e].responseBody?.changed) { + console.log(`- Response body type changed to ${changes.changedEndpoints[e].responseBody.changed.newType}`); + } + } + console.log(''); + } + if (changes.changedTypes) { + console.log('# Modified Types'); + for (const t in changes.changedTypes) { + const ch = changes.changedTypes[t]; + if (!ch.isEnum) { + console.log(`\n## ${formatType(t)} type`); + for (const p of changes.changedTypes[t].addedProperties ?? []) { + console.log(`- Added \`${p}\` property`); + } + for (const p in changes.changedTypes[t].changedProperties ?? {}) { + const ch = changes.changedTypes[t].changedProperties?.[p]; + if (ch) { + console.log(`- Changed type of \`${p}\` property to ${ch.newType}`); + } + } + for (const p of changes.changedTypes[t].removedProperties ?? []) { + console.log(`- Removed \`${p}\` property`); + } + } + else { + console.log(`\n## ${formatType(t)} enumeration`); + if (ch.addedProperties.length) { + const members = ch.addedProperties.map(m => `\`${m}\``).join(', '); + const plural = (ch.addedProperties.length > 1) ? 's' : ''; + console.log(`- Added ${members} member${plural}`); + } + if (ch.removedProperties.length) { + const members = ch.removedProperties.map(m => `\`${m}\``).join(', '); + const plural = (ch.removedProperties.length > 1) ? 's' : ''; + console.log(`- Removed ${members} member${plural}`); + } + } + } + } +} + +async function main() { + const oldBranch = process.argv[process.argv.length - 2] ?? ''; + const newBranch = process.argv[process.argv.length - 1] ?? ''; + const diff = await diffSchema(oldBranch, newBranch); + diffToMarkdown(diff); +} + +main() diff --git a/diff-schema/src/metamodel.ts b/diff-schema/src/metamodel.ts new file mode 100644 index 0000000000..26f47a9895 --- /dev/null +++ b/diff-schema/src/metamodel.ts @@ -0,0 +1,491 @@ +/* + * 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. + */ + +/** + * The name of a type, composed of a simple name and a namespace. Hierarchical namespace elements are separated by + * a dot, e.g 'cat.cat_aliases'. + * + * Builtin namespaces: + * - "generic" for type names that are generic parameter values from the enclosing type. + * - "internal" for primitive and builtin types (e.g. Id, IndexName, etc) + * Builtin types: + * - boolean, + * - string, + * - number: a 64bits floating point number. Additional types will be added for integers. + * - null: the null value. Since JS distinguishes undefined and null, some APIs make use of this value. + * - object: used to represent "any". We may forbid it at some point. UserDefinedValue should be used for user data. + */ +export class TypeName { + namespace: string + name: string +} + +// ------------------------------------------------------------------------------------------------ +// Value types + +// Note: "required" is part of Property. This means we can have optional properties but we can't have null entries in +// containers (array and dictionary), which doesn't seem to be needed. +// +// The 'kind' property is used to tag and disambiguate union type members, and allow type-safe pattern matching in TS: +// see https://blog.logrocket.com/pattern-matching-and-type-safety-in-typescript-1da1231a2e34/ +// and https://medium.com/@fillopeter/pattern-matching-with-typescript-done-right-94049ddd671c + +/** + * Type of a value. Used both for property types and nested type definitions. + */ +export type ValueOf = InstanceOf | ArrayOf | UnionOf | DictionaryOf | UserDefinedValue | LiteralValue + +/** + * A single value + */ +export class InstanceOf { + kind: 'instance_of' + type: TypeName + /** generic parameters: either concrete types or open parameters from the enclosing type */ + generics?: ValueOf[] +} + +/** + * An array + */ +export class ArrayOf { + kind: 'array_of' + value: ValueOf +} + +/** + * One of several possible types which don't necessarily have a common superclass + */ +export class UnionOf { + kind: 'union_of' + items: ValueOf[] +} + +/** + * A dictionary (or map). The key is a string or a number (or a union thereof), possibly through an alias. + * + * If `singleKey` is true, then this dictionary can only have a single key. This is a common pattern in ES APIs, + * used to associate a value to a field name or some other identifier. + */ +export class DictionaryOf { + kind: 'dictionary_of' + key: ValueOf + value: ValueOf + singleKey: boolean +} + +/** + * A user defined value. To be used when bubbling a generic parameter up to the top-level class is + * inconvenient or impossible (e.g. for lists of user-defined values of possibly different types). + * + * Clients will allow providing a serializer/deserializer when reading/writing properties of this type, + * and should also accept raw json. + * + * Think twice before using this as it defeats the purpose of a strongly typed API, and deserialization + * will also require to buffer raw JSON data which may have performance implications. + */ +export class UserDefinedValue { + kind: 'user_defined_value' +} + +/** + * A literal value. This is used for tagged unions, where each type member of a union has a 'type' + * attribute that defines its kind. This metamodel heavily uses this approach with its 'kind' attributes. + * + * It may later be used to set a property to a constant value, which is why it accepts not only strings but also + * other primitive types. + */ +export class LiteralValue { + kind: 'literal_value' + value: string | number | boolean +} + +/** + * An interface or request interface property. + */ +export class Property { + name: string + type: ValueOf + required: boolean + description?: string + docUrl?: string + docId?: string + extDocId?: string + extDocUrl?: string + serverDefault?: boolean | string | number | string[] | number[] + deprecation?: Deprecation + availability?: Availabilities + /** + * If specified takes precedence over `name` when generating code. `name` is always the value + * to be sent over the wire + */ + codegenName?: string + /** An optional set of aliases for `name` */ + aliases?: string[] + /** If the enclosing class is a variants container, is this a property of the container and not a variant? */ + containerProperty?: boolean + /** If this property has a quirk that needs special attention, give a short explanation about it */ + esQuirk?: string +} + +// ------------------------------------------------------------------------------------------------ +// Type definitions + +export type TypeDefinition = Interface | Request | Response | Enum | TypeAlias + +// ------------------------------------------------------------------------------------------------ + +/** + * Common attributes for all type definitions + */ +export abstract class BaseType { + name: TypeName + description?: string + /** Link to public documentation */ + docUrl?: string + docId?: string + extDocId?: string + extDocUrl?: string + deprecation?: Deprecation + /** If this endpoint has a quirk that needs special attention, give a short explanation about it */ + esQuirk?: string + kind: string + /** Variant name for externally tagged variants */ + variantName?: string + /** + * Additional identifiers for use by code generators. Usage depends on the actual type: + * - on unions (modeled as alias(union_of)), these are identifiers for the union members + * - for additional properties, this is the name of the dict that holds these properties + * - for additional property, this is the name of the key and value fields that hold the + * additional property + */ + codegenNames?: string[] + /** + * Location of an item. The path is relative to the "specification" directory, e.g "_types/common.ts#L1-L2" + */ + specLocation: string +} + +export type Variants = ExternalTag | InternalTag | Container | Untagged + +export class VariantBase { + /** + * Is this variant type open to extensions? Default to false. Used for variants that can + * be extended with plugins. If true, target clients should allow for additional variants + * with a variant tag outside the ones defined in the spec and arbitrary data as the value. + */ + nonExhaustive?: boolean +} + +export class ExternalTag extends VariantBase { + kind: 'external_tag' +} + +export class InternalTag extends VariantBase { + kind: 'internal_tag' + /* Name of the property that holds the variant tag */ + tag: string + /* Default value for the variant tag if it's missing */ + defaultTag?: string +} + +export class Container extends VariantBase { + kind: 'container' +} + +export class Untagged extends VariantBase { + kind: 'untagged' + untypedVariant: TypeName +} + +/** + * Inherits clause (aka extends or implements) for an interface or request + */ +export class Inherits { + type: TypeName + generics?: ValueOf[] +} + +export class Behavior { + type: TypeName + generics?: ValueOf[] + meta?: Record +} + +/** + * An interface type + */ +export class Interface extends BaseType { + kind: 'interface' + /** + * Open generic parameters. The name is that of the parameter, the namespace is an arbitrary value that allows + * this fully qualified type name to be used when this open generic parameter is used in property's type. + */ + generics?: TypeName[] + inherits?: Inherits + + /** + * Behaviors directly implemented by this interface + */ + behaviors?: Behavior[] + + /** + * Behaviors attached to this interface, coming from the interface itself (see `behaviors`) + * or from inherits and implements ancestors + */ + attachedBehaviors?: string[] + properties: Property[] + /** + * The property that can be used as a shortcut for the entire data structure in the JSON. + */ + shortcutProperty?: string + + /** Identify containers */ + variants?: Container +} + +/** + * An alternative of an example, coded in a given language. + */ +export class ExampleAlternative { + language: string + code: string +} + +/** + * The Example type is used for both requests and responses + * This type definition is taken from the OpenAPI spec + * https://spec.openapis.org/oas/v3.1.0#example-object + * With the exception of using String as the 'value' type + */ +export class Example { + /** Short description. */ + summary?: string + /** Long description. */ + description?: string + /** request method and URL */ + method_request?: string + /** Embedded literal example. Mutually exclusive with `external_value` */ + value?: string + /** A URI that points to the literal example */ + external_value?: string + /** An array of alternatives for this example in other languages */ + alternatives?: ExampleAlternative[] +} + +/** + * A request type + */ +export class Request extends BaseType { + // Note: does not extend Interface as properties are split across path, query and body + kind: 'request' + generics?: TypeName[] + /** The parent defines additional body properties that are added to the body, that has to be a PropertyBody */ + inherits?: Inherits + /** URL path properties */ + path: Property[] + /** Query string properties */ + query: Property[] + // FIXME: we need an annotation that lists query params replaced by a body property so that we can skip them. + // Examples on _search: sort -> sort, _source -> (_source, _source_include, _source_exclude) + // Or can we say that implicitly a body property replaces all path params starting with its name? + // Is there a priority rule between path and body parameters? + // + // We can also pull path parameter descriptions on body properties they replace + + /** + * Body type. Most often a list of properties (that can extend those of the inherited class, see above), except for a + * few specific cases that use other types such as bulk (array) or create (generic parameter). Or NoBody for requests + * that don't have a body. + */ + body: Body + behaviors?: Behavior[] + attachedBehaviors?: string[] + examples?: Record +} + +/** + * A response type + */ +export class Response extends BaseType { + kind: 'response' + generics?: TypeName[] + body: Body + behaviors?: Behavior[] + attachedBehaviors?: string[] + exceptions?: ResponseException[] + examples?: Record +} + +export class ResponseException { + description?: string + body: Body + statusCodes: number[] +} + +export type Body = ValueBody | PropertiesBody | NoBody + +export class ValueBody { + kind: 'value' + value: ValueOf + codegenName?: string +} + +export class PropertiesBody { + kind: 'properties' + properties: Property[] +} + +export class NoBody { + kind: 'no_body' +} + +/** + * An enumeration member. + * + * When enumeration members can become ambiguous when translated to an identifier, the `name` property will be a good + * identifier name, and `stringValue` will be the string value to use on the wire. + * See DateMathTimeUnit for an example of this, which have members for "m" (minute) and "M" (month). + */ +export class EnumMember { + /** The identifier to use for this enum */ + name: string + /** An optional set of aliases for `name` */ + aliases?: string[] + /** + * If specified takes precedence over `name` when generating code. `name` is always the value + * to be sent over the wire + */ + codegenName?: string + description?: string + deprecation?: Deprecation + availability?: Availabilities +} + +/** + * An enumeration + */ +export class Enum extends BaseType { + kind: 'enum' + /** + * If the enum is open, it means that other than the specified values it can accept an arbitrary value. + * If this property is not present, it means that the enum is not open (in other words, is closed). + */ + isOpen?: boolean + members: EnumMember[] +} + +/** + * An alias for an existing type. + */ +export class TypeAlias extends BaseType { + kind: 'type_alias' + type: ValueOf + /** generic parameters: either concrete types or open parameters from the enclosing type */ + generics?: TypeName[] + /** + * Only applicable to `union_of` aliases: identify typed_key unions (external), variant inventories (internal) + * and untagged variants + */ + variants?: InternalTag | ExternalTag | Untagged +} + +// ------------------------------------------------------------------------------------------------ + +export enum Stability { + stable = 'stable', + beta = 'beta', + experimental = 'experimental' +} +export enum Visibility { + public = 'public', + feature_flag = 'feature_flag', + private = 'private' +} + +export class Deprecation { + version: string + description: string +} + +export class Availabilities { + stack?: Availability + serverless?: Availability +} + +export class Availability { + since?: string + featureFlag?: string + stability?: Stability + visibility?: Visibility +} + +export class Endpoint { + name: string + description: string + docUrl: string + docId?: string + extDocId?: string + extDocUrl?: string + extDocDescription?: string + extPreviousVersionDocUrl?: string + deprecation?: Deprecation + availability: Availabilities + docTag?: string + /** + * If the request value is `null` it means that there is not yet a + * request type definition for this endpoint. + */ + request: TypeName | null + requestBodyRequired: boolean // Not sure this is useful + + /** + * If the response value is `null` it means that there is not yet a + * response type definition for this endpoint. + */ + response: TypeName | null + + urls: UrlTemplate[] + + requestMediaType?: string[] + responseMediaType?: string[] + privileges?: { + index?: string[] + cluster?: string[] + } +} + +export class UrlTemplate { + path: string + methods: string[] + deprecation?: Deprecation +} + +export class Model { + _info?: { + title: string + license: { + name: string + url: string + } + } + + types: TypeDefinition[] + endpoints: Endpoint[] +} diff --git a/diff-schema/tsconfig.json b/diff-schema/tsconfig.json new file mode 100644 index 0000000000..0f8082d6b0 --- /dev/null +++ b/diff-schema/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "isolatedModules": false, + "jsx": "react", + "outDir": "lib", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "strictNullChecks": true, + "declaration": true, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "preserveConstEnums": true, + "sourceMap": false, + "strictPropertyInitialization": false + }, + "compileOnSave": true, + "buildOnSave": true, + "exclude": [ + "specification", + "lib" + ], + "include": [ + "./src" + ] +}