@common-grants/cli
Version:
The CommonGrants protocol CLI tool
226 lines (225 loc) • 9.64 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.checkMatchingRoutes = checkMatchingRoutes;
const check_schema_compatibility_1 = require("./check-schema-compatibility");
const flatten_schemas_1 = require("./flatten-schemas");
const error_utils_1 = require("./error-utils");
// ############################################################
// Top-level checks
// ############################################################
/**
* For each path+method that exists in both specs, run deeper checks.
*/
function checkMatchingRoutes(baseDoc, implDoc) {
const errors = new error_utils_1.ErrorCollection();
const basePaths = baseDoc.paths || {};
const implPaths = implDoc.paths || {};
// For each path in baseDoc, see if it exists in implDoc
for (const [basePathKey, basePathItem] of Object.entries(basePaths)) {
if (!basePathItem)
continue;
const implPathItem = implPaths[basePathKey];
if (!implPathItem) {
// If missing, already flagged in checkMissingRequiredRoutes
continue;
}
// Declare the types for base and impl paths for readability
const basePathObj = basePathItem;
const implPathObj = implPathItem;
// For each method in the base path, check if it also exists in the impl path
for (const method of Object.keys(basePathObj)) {
// Get base and impl operations (e.g. get, post, put, delete, etc.)
const baseOp = basePathObj[method];
const implOp = implPathObj[method];
// If missing, already flagged in either checkMissingRequiredRoutes or checkExtraRoutes
if (!baseOp)
continue;
if (!implOp)
continue;
// If experimental, skip deeper checks
if (baseOp.tags?.includes("experimental"))
continue;
// Both base and impl define this route => deeper checks:
errors.addErrors(checkMatchingRoute(basePathKey, method, baseOp, implOp).getAllErrors());
}
}
return errors;
}
// ############################################################
// Route-specific checks
// ############################################################
/**
* Compare a single route (defined in both base and impl) for compliance.
*/
function checkMatchingRoute(path, method, baseOp, implOp) {
const errors = new error_utils_1.ErrorCollection();
const endpoint = `${method.toUpperCase()} ${path}`;
// 1) Check for missing status codes
errors.addErrors(checkStatusCodes(endpoint, baseOp, implOp).getAllErrors());
// 2) For each matching status code, check that response schemas match
if (baseOp.responses && implOp.responses) {
for (const statusCode of Object.keys(baseOp.responses)) {
const baseResp = baseOp.responses[statusCode];
const implResp = implOp.responses[statusCode];
if (!implResp)
continue; // Possibly flagged in checkStatusCodes
errors.addErrors(checkResponseSchemas(endpoint, baseResp, implResp).getAllErrors());
}
}
// 3) Check that query parameters and request body match
errors.addErrors(checkQueryParameters(endpoint, baseOp, implOp).getAllErrors());
errors.addErrors(checkRequestBody(endpoint, baseOp, implOp).getAllErrors());
return errors;
}
// ############################################################
// Status code checks
// ############################################################
/**
* Check status codes
*
* Extra status codes are okay, but missing status codes are not.
*/
function checkStatusCodes(endpoint, baseOp, implOp) {
const errors = new error_utils_1.ErrorCollection();
const baseRespCodes = Object.keys(baseOp.responses || {});
const implRespCodes = Object.keys(implOp.responses || {});
// Find missing status codes
for (const code of baseRespCodes) {
if (!implRespCodes.includes(code)) {
errors.addError({
type: "ROUTE_CONFLICT",
subType: "MISSING_STATUS_CODE",
endpoint,
message: `Missing response status code [${code}]`,
});
}
}
return errors;
}
// ############################################################
// Query parameter checks
// ############################################################
/**
* Compare query parameters between base and implementation operations
*/
function checkQueryParameters(endpoint, baseOp, implOp) {
const errors = new error_utils_1.ErrorCollection();
// Get query parameters from both specs
const baseParams = (baseOp.parameters || []).filter((p) => "in" in p && p.in === "query");
const implParams = (implOp.parameters || []).filter((p) => "in" in p && p.in === "query");
// Check for required parameters missing from implementation
for (const baseParam of baseParams) {
const implParam = implParams.find(p => p.name === baseParam.name);
if (!implParam) {
errors.addError({
type: "ROUTE_CONFLICT",
subType: "QUERY_PARAM_CONFLICT",
endpoint,
message: `Missing required query parameter [${baseParam.name}]`,
});
continue;
}
// If parameter exists, check if required status matches
if (baseParam.required && !implParam.required) {
errors.addError({
type: "ROUTE_CONFLICT",
subType: "QUERY_PARAM_CONFLICT",
endpoint,
message: `Query parameter [${baseParam.name}] must be required`,
});
}
// Check parameter schema compatibility if schemas exist
if (baseParam.schema && implParam.schema) {
// Flatten schemas before compatibility check
const flattenedBaseSchema = (0, flatten_schemas_1.deepFlattenAllOf)(baseParam.schema);
const flattenedImplSchema = (0, flatten_schemas_1.deepFlattenAllOf)(implParam.schema);
errors.addErrors((0, check_schema_compatibility_1.checkSchemaCompatibility)(`${baseParam.name}`, flattenedBaseSchema, flattenedImplSchema, {
errorSubType: "QUERY_PARAM_CONFLICT",
endpoint,
}).getAllErrors());
}
}
return errors;
}
// ############################################################
// Content schema checks (shared between request and response)
// ############################################################
/**
* Compare content schemas between base and implementation content objects
*/
function checkContentSchemas(endpoint, baseContent, implContent, errorSubType) {
const errors = new error_utils_1.ErrorCollection();
if (!baseContent)
return errors;
if (!implContent) {
errors.addError({
type: "ROUTE_CONFLICT",
subType: errorSubType,
endpoint,
message: "Implementation missing content for expected mime type(s)",
});
return errors;
}
for (const [mimeType, baseMedia] of Object.entries(baseContent)) {
const implMedia = implContent[mimeType];
if (!implMedia?.schema) {
errors.addError({
type: "ROUTE_CONFLICT",
subType: errorSubType,
endpoint,
message: `Implementation missing schema for expected mime type [${mimeType}]`,
});
continue;
}
// Flatten schemas once and cache the results
const baseSchema = (0, flatten_schemas_1.deepFlattenAllOf)(baseMedia.schema);
const implSchema = (0, flatten_schemas_1.deepFlattenAllOf)(implMedia.schema);
// Deeper check: see if implSchema is a valid "subset" of baseSchema
errors.addErrors((0, check_schema_compatibility_1.checkSchemaCompatibility)("", baseSchema, implSchema, {
errorSubType,
endpoint,
mimeType,
}).getAllErrors());
}
return errors;
}
// ############################################################
// Response schema checks (for each status code)
// ############################################################
/**
* Compare the response content schemas for a single status code.
*/
function checkResponseSchemas(endpoint, baseResponse, implResponse) {
const errors = new error_utils_1.ErrorCollection();
if (!baseResponse || !implResponse)
return errors;
if (baseResponse.content) {
errors.addErrors(checkContentSchemas(endpoint, baseResponse.content, implResponse.content, "RESPONSE_BODY_CONFLICT").getAllErrors());
}
return errors;
}
// ############################################################
// Request body checks
// ############################################################
/**
* Compare the request body between base and implementation operations
*/
function checkRequestBody(endpoint, baseOp, implOp) {
const errors = new error_utils_1.ErrorCollection();
if (baseOp.requestBody) {
const baseReq = baseOp.requestBody;
const implReq = implOp.requestBody;
if (!implReq) {
errors.addError({
type: "ROUTE_CONFLICT",
subType: "REQUEST_BODY_CONFLICT",
endpoint,
message: "Missing required request body",
});
}
else if (baseReq.content) {
errors.addErrors(checkContentSchemas(endpoint, baseReq.content, implReq.content, "REQUEST_BODY_CONFLICT").getAllErrors());
}
}
return errors;
}