blob: 62e3598cf555b0cc0009f474317f3641b13f8409 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.
*/
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const specPath = path.resolve(__dirname, '..', 'static', 'rest-catalog-open-api.yaml');
const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
function check(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function decodePointerSegment(segment) {
return segment.replace(/~1/g, '/').replace(/~0/g, '~');
}
function resolveLocalRef(ref) {
check(ref.startsWith('#/'), `Only local OpenAPI references are supported, found: ${ref}`);
return ref
.slice(2)
.split('/')
.map(decodePointerSegment)
.reduce((current, segment) => {
check(
current && Object.prototype.hasOwnProperty.call(current, segment),
`Unresolved OpenAPI reference: ${ref}`,
);
return current[segment];
}, spec);
}
function visit(value) {
if (Array.isArray(value)) {
value.forEach(visit);
return;
}
if (!value || typeof value !== 'object') {
return;
}
check(!Object.prototype.hasOwnProperty.call(value, 'nullable'), 'OpenAPI 3.1 schemas must not use nullable');
if (typeof value.$ref === 'string') {
resolveLocalRef(value.$ref);
}
Object.values(value).forEach(visit);
}
function schema(name) {
const value = spec.components && spec.components.schemas && spec.components.schemas[name];
check(value, `Missing OpenAPI schema: ${name}`);
return value;
}
function requireProperties(schemaName, names) {
const properties = schema(schemaName).properties || {};
names.forEach((name) => {
check(properties[name], `Schema ${schemaName} is missing property: ${name}`);
});
return properties;
}
function requireTypedIntegerProperties(schemaName, names) {
const properties = requireProperties(schemaName, names);
names.forEach((name) => {
check(
properties[name].type === 'integer' && properties[name].format === 'int64',
`Schema ${schemaName}.${name} must be an int64 integer`,
);
});
}
function requireSchemaReference(schemaName, composition, referencedSchemaName) {
const references = schema(schemaName)[composition] || [];
const expected = `#/components/schemas/${referencedSchemaName}`;
check(
references.some((reference) => reference.$ref === expected),
`Schema ${schemaName}.${composition} is missing reference: ${expected}`,
);
}
function requireArrayOfIdentifiers(schemaName, propertyName) {
const properties = requireProperties(schemaName, [propertyName]);
check(properties[propertyName].type === 'array', `Schema ${schemaName}.${propertyName} must be an array`);
check(
properties[propertyName].items &&
properties[propertyName].items.$ref === '#/components/schemas/Identifier',
`Schema ${schemaName}.${propertyName} items must reference Identifier`,
);
}
function requireNullableStringProperty(schemaName, propertyName) {
const property = requireProperties(schemaName, [propertyName])[propertyName];
check(
Array.isArray(property.type) &&
property.type.includes('string') &&
property.type.includes('null'),
`Schema ${schemaName}.${propertyName} must accept string and null`,
);
}
check(spec.openapi === '3.1.1', `Expected OpenAPI 3.1.1, found: ${spec.openapi}`);
check(spec.paths && spec.components && spec.components.schemas, 'Incomplete OpenAPI document');
visit(spec);
const operationIds = new Set();
for (const pathItem of Object.values(spec.paths)) {
for (const operation of Object.values(pathItem)) {
if (!operation || typeof operation !== 'object' || !operation.operationId) {
continue;
}
check(!operationIds.has(operation.operationId), `Duplicate operationId: ${operation.operationId}`);
operationIds.add(operation.operationId);
}
}
[
'getConfig',
'createDatabase',
'getDatabase',
'alterDatabase',
'dropDatabase',
'createTable',
'getTable',
'alterTable',
'dropTable',
].forEach((operationId) => {
check(operationIds.has(operationId), `Missing provider-facing operationId: ${operationId}`);
});
requireProperties('ConfigResponse', ['defaults', 'overrides']);
requireProperties('CreateDatabaseRequest', ['name', 'options']);
requireProperties('AlterDatabaseRequest', ['removals', 'updates']);
requireProperties('CreateTableRequest', ['identifier', 'schema']);
requireProperties('AlterTableRequest', ['changes']);
requireProperties('Identifier', ['database', 'object']);
requireProperties('Schema', ['fields', 'partitionKeys', 'primaryKeys', 'options', 'comment']);
requireProperties('DataField', ['id', 'name', 'type', 'description', 'defaultValue']);
requireSchemaReference('DataType', 'oneOf', 'VectorType');
requireProperties('VectorType', ['type', 'element', 'length']);
requireSchemaReference('SchemaChange', 'anyOf', 'DropPrimaryKey');
check(
schema('BaseSchemaChange').discriminator.mapping.dropPrimaryKey ===
'#/components/schemas/DropPrimaryKey',
'BaseSchemaChange discriminator is missing dropPrimaryKey',
);
const dropPrimaryKey = requireProperties('DropPrimaryKey', ['action']);
check(
dropPrimaryKey.action.const === 'dropPrimaryKey',
'Schema DropPrimaryKey.action must be dropPrimaryKey',
);
check(
schema('BaseInstant').discriminator.propertyName === 'type',
'BaseInstant discriminator must use the JSON field type',
);
const updateViewComment = requireProperties('UpdateViewComment', ['action', 'comment']);
check(!updateViewComment.key, 'Schema UpdateViewComment must use comment instead of key');
['UpdateComment', 'UpdateViewComment', 'UpdateFunctionComment'].forEach((schemaName) =>
requireNullableStringProperty(schemaName, 'comment'),
);
const errorResourceTypes = requireProperties('ErrorResponse', ['resourceType']).resourceType.enum || [];
['FUNCTION', 'DEFINITION'].forEach((resourceType) => {
check(
errorResourceTypes.includes(resourceType),
`Schema ErrorResponse.resourceType is missing value: ${resourceType}`,
);
});
requireArrayOfIdentifiers('ListTablesGloballyResponse', 'tables');
requireArrayOfIdentifiers('ListViewsGloballyResponse', 'views');
requireArrayOfIdentifiers('ListFunctionsGloballyResponse', 'functions');
requireProperties('ListFunctionsGloballyResponse', ['nextPageToken']);
const getFunctionProperties = requireProperties('GetFunctionResponse', ['uuid']);
check(getFunctionProperties.uuid.type === 'string', 'Schema GetFunctionResponse.uuid must be a string');
['GetDatabaseResponse', 'GetTableResponse', 'GetViewResponse', 'GetFunctionResponse'].forEach(
(schemaName) => requireTypedIntegerProperties(schemaName, ['createdAt', 'updatedAt']),
);
console.log(`Validated REST OpenAPI contract with ${operationIds.size} operations.`);