@apistudio/apim-cli
Version:
CLI for API Management Products
1,194 lines (1,102 loc) • 44.6 kB
JavaScript
// Recursive script to generate detailed Spectral rulesets for schema components
// This version creates detailed rules for ALL nested properties at any depth
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
// Get the directory name using ES modules approach
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Path to the OpenAPI specification file - corrected path
const specFilePath = '../../../../schema_specification/V12_openAPI_specification_smith_lwgw.yaml';
// Function to read and parse the OpenAPI specification
function readOpenAPISpec() {
try {
const specContent = fs.readFileSync(path.resolve(__dirname, specFilePath), 'utf8');
return yaml.load(specContent);
} catch (error) {
console.error('Error reading OpenAPI specification:', error);
process.exit(1);
}
}
const kindEnums = [
"API",
"Scope",
"Project",
"StagedPolicySequence",
"InvokeAWSLambda",
"ValidateAPISpecification",
"CORS",
"Quota",
"Plan",
"Product",
"URISchemes",
"properties",
"Telemetry",
"Properties",
"LoadBalancer",
"SetAuthorization",
"Invoke",
"GlobalPolicy",
"InboundBulkHead",
"SetMediaType",
"InboundMessaging",
"IAM",
"AuthorizeUser",
"SetContextVariable",
"WebMethodsISService",
"Log",
"MonitorTraffic",
"CacheServiceResult",
"OutboundAlias",
"OutboundAnonymous",
"HTTPInvoke",
"InvokeMessagingExtension",
"DataMasking",
"TransformRequest",
"TransformResponse",
"Route",
"MessageConfig",
"HTTPEndpoint",
"MockEndpoint",
"MockResponse",
"ErrorProcessing",
"Set",
"RateLimitDef",
"RateLimit",
"Redact",
"Remove",
"Transform",
"DataPowerAssembly",
"Switch",
"If",
"OperationSwitch",
"Try",
"IBMCloudLogin",
"WatsonXAIInvoke",
"OpenAIInvoke",
"FreeFlowPolicySequence",
"Block",
"TokenMediation",
"EnforceCircuitBreaker",
"JavaScript",
"LuaScript",
"Cache",
"Antivirus",
"SQLInjectionFilter",
"CountLimit",
"CountLimitDef",
"Return",
"Retry",
"Throw",
"HandlebarsTemplate",
"ExtractIdentity",
"Authorize",
"Authenticate",
"Parse"
];
// Function to generate a ruleset for a schema component with recursive property validation
function generateRuleset(componentName, schema, depth = 0) {
const rules = {
// Common rules for all components
"invalid-kind-value-combined": {
"description": `Kind must be one of ${kindEnums.map(k => `'${k}'`).join(' | ')}`,
"severity": "error",
"given": "$",
"then": {
"field": "kind",
"function": "schema",
"functionOptions": {
"schema": {
"type": "string",
"enum": kindEnums
}
}
}
},
"kind-not-exist": {
"description": `Kind does not exist.`,
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "kind",
"function": "truthy"
}
},
"invalid-kind-value": {
"description": `Kind must be '${componentName}'`,
"severity": "error",
"given": "$",
"then": {
"field": "kind",
"function": "schema",
"functionOptions": {
"schema": {
"type": "string",
"enum": schema.properties.kind.enum
}
}
}
},
"invalid-kind-spl-character": {
"description": "kind should not be having empty or special characters",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "kind",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"invalid-api-version": {
"description": "apiVersion must be one of the valid values 'api.ibm.com/v1'",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "apiVersion",
"function": "schema",
"functionOptions": {
"schema": {
"type": "string",
"enum": [
"api.ibm.com/v1"
]
}
}
}
},
"api-version-not-exist": {
"description": "apiVersion does not exist.",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "apiVersion",
"function": "truthy"
}
},
"metadata-not-exist": {
"description": "Metadata does not exist.",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "metadata",
"function": "truthy"
}
},
"metadata-whitelist-check": {
"description": "Metadata should not be having empty or special characters",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "metadata",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"metadata-name-not-exist": {
"description": "Metadata name does not exist",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "name",
"function": "truthy"
}
},
"metadata-name-whitelist-check": {
"description": "Metadata name should not be having empty or special characters",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "name",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"metadata-version-not-exist": {
"description": "Metadata version does not exist",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "version",
"function": "truthy"
}
},
"metadata-version-whitelist-check": {
"description": "Metadata version should not be having empty or special characters",
"severity": "error",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "version",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"metadata-namespace-not-exist": {
"description": "Metadata namespace does not exist",
"severity": "warn",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "namespace",
"function": "truthy"
}
},
"metadata-namespace-whitelist-check": {
"description": "Metadata namespace should not be having empty or special characters",
"severity": "warn",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "namespace",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"spec-details-not-exist": {
"description": "Spec details does not exist",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "spec",
"function": "truthy"
}
},
"spec-details-whitelist-check": {
"description": "Spec should not be having empty or special characters",
"severity": "error",
"given": "$",
"resolved": false,
"then": {
"field": "spec",
"function": "pattern",
"functionOptions": {
"match": "^(?![\\s\\W_]+$).+$"
}
}
},
"tags-not-exist": {
"description": "Tag does not exist",
"severity": "warn",
"given": "$.metadata",
"resolved": false,
"then": {
"field": "tags",
"function": "truthy"
}
},
"invalid-tag-type": {
"description": "Invalid Tag Type",
"severity": "error",
"given": "$.metadata.tags",
"resolved": false,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": "array"
}
}
}
}
};
// Create a set to track rules we've already added to avoid duplicates
const addedRules = new Set();
// Add metadata field validation rules
if (schema.properties && schema.properties.metadata && schema.properties.metadata.properties) {
const metadataProps = schema.properties.metadata.properties;
const metadataRequired = schema.properties.metadata.required || [];
// Process metadata properties recursively
processObjectProperties(metadataProps, metadataRequired, "$.metadata", rules, 0, false, addedRules);
}
// Add spec field validation rules
if (schema.properties && schema.properties.spec) {
const specSchema = schema.properties.spec;
const requiredProps = specSchema.required || [];
// Process spec properties recursively if they exist
if (specSchema.properties) {
const specProperties = specSchema.properties;
processObjectProperties(specProperties, requiredProps, "$.spec", rules, 0, false, addedRules);
}
// Handle oneOf/anyOf/allOf in spec
['oneOf', 'anyOf', 'allOf'].forEach(schemaType => {
if (specSchema[schemaType]) {
rules[`spec-${schemaType}`] = {
"description": `The spec field must match the ${schemaType} schema definition`,
"severity": "error",
"given": "$.spec",
"then": {
"function": "schema",
"functionOptions": {
"schema": {
[schemaType]: specSchema[schemaType]
}
}
}
};
// For oneOf schemas, we don't generate individual required property rules
// as the oneOf validation will handle that - this prevents conflicting validation rules
if (schemaType !== 'oneOf') {
// Process each schema in the anyOf/allOf array
specSchema[schemaType].forEach((subSchema, index) => {
// Handle required properties in this variant
if (subSchema.required && Array.isArray(subSchema.required)) {
subSchema.required.forEach(requiredProp => {
rules[`spec-${schemaType}-${index}-${requiredProp}-required`] = {
"description": `The ${requiredProp} field is required in spec when using schema variant ${index + 1}`,
"severity": "error",
"given": "$.spec",
"then": {
"field": requiredProp,
"function": "defined"
}
};
});
}
});
}
// Process each schema in the oneOf/anyOf/allOf array
specSchema[schemaType].forEach((subSchema, index) => {
// Process properties in this variant
if (subSchema.properties) {
Object.entries(subSchema.properties).forEach(([propName, propSchema]) => {
const jsonPath = `$.spec.${propName}`;
const rulePrefix = `spec-${propName}`;
// Add validation for this property
if (propSchema.type) {
rules[`${rulePrefix}-type`] = {
"description": `The spec.${propName} field must be of type ${propSchema.type}`,
"severity": "error",
"given": jsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": propSchema.type
}
}
}
};
// Special handling for objects with additionalProperties: false
if (propSchema.type === 'object' && propSchema.additionalProperties === false) {
const allowedProps = propSchema.properties ? Object.keys(propSchema.properties) : [];
rules[`${rulePrefix}-no-additional-properties`] = {
"description": `The spec.${propName} field must be an object with no additional properties`,
"severity": "error",
"given": jsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": Object.fromEntries(allowedProps.map(prop => [prop, {}]))
}
}
}
};
}
}
// Process nested oneOf/anyOf/allOf in this property
['oneOf', 'anyOf', 'allOf'].forEach(nestedSchemaType => {
if (propSchema[nestedSchemaType]) {
rules[`${rulePrefix}-${nestedSchemaType}`] = {
"description": `The spec.${propName} field must match the ${nestedSchemaType} schema definition`,
"severity": "error",
"given": jsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
[nestedSchemaType]: propSchema[nestedSchemaType]
}
}
}
};
// For nested objects with useIncoming property, add a type validation rule
propSchema[nestedSchemaType].forEach((variant) => {
if (variant.properties && variant.properties.useIncoming) {
rules[`${rulePrefix}-useIncoming-type`] = {
"description": `The ${jsonPath.replace(/^\$\./, '')}.useIncoming field must be an object`,
"severity": "error",
"given": `${jsonPath}.useIncoming`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": "object"
}
}
}
};
}
});
// Process each nested schema
propSchema[nestedSchemaType].forEach((nestedSubSchema, nestedIndex) => {
// Process required fields in each variant
if (nestedSubSchema.required && Array.isArray(nestedSubSchema.required)) {
nestedSubSchema.required.forEach(requiredField => {
// Check if the field has properties defined
if (nestedSubSchema.properties && nestedSubSchema.properties[requiredField]) {
const fieldSchema = nestedSubSchema.properties[requiredField];
const fieldPath = `${jsonPath}.${requiredField}`;
const fieldRulePrefix = `${rulePrefix}-${requiredField}`;
// Add type validation for the field
if (fieldSchema.type) {
rules[`${fieldRulePrefix}-type`] = {
"description": `The ${jsonPath.replace(/^\$\./, '')}.${requiredField} field must be of type ${fieldSchema.type}`,
"severity": "error",
"given": fieldPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": fieldSchema.type
}
}
}
};
}
// Special handling for objects with additionalProperties: false
if (fieldSchema.type === 'object') {
if (fieldSchema.additionalProperties === false) {
const allowedProps = fieldSchema.properties ? Object.keys(fieldSchema.properties) : [];
rules[`${fieldRulePrefix}-no-additional-properties`] = {
"description": `The ${jsonPath.replace(/^\$\./, '')}.${requiredField} field must be an object with no additional properties`,
"severity": "error",
"given": fieldPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": "object",
"additionalProperties": false,
"properties": Object.fromEntries(allowedProps.map(prop => [prop, {}]))
}
}
}
};
}
// Process nested properties of this object
if (fieldSchema.properties) {
processObjectProperties(
fieldSchema.properties,
fieldSchema.required || [],
fieldPath,
rules,
depth + 2,
false,
addedRules
);
}
}
// Handle nested oneOf/anyOf/allOf in the field
['oneOf', 'anyOf', 'allOf'].forEach(deeperSchemaType => {
if (fieldSchema[deeperSchemaType]) {
fieldSchema[deeperSchemaType].forEach((deeperSubSchema, deeperIndex) => {
if (deeperSubSchema.properties) {
processObjectProperties(
deeperSubSchema.properties,
deeperSubSchema.required || [],
fieldPath,
rules,
depth + 2,
false,
addedRules
);
}
});
}
});
}
});
}
// Process all properties in this variant
if (nestedSubSchema.properties) {
processObjectProperties(
nestedSubSchema.properties,
nestedSubSchema.required || [],
jsonPath,
rules,
depth + 1,
false,
addedRules
);
}
});
}
});
// Recursively process object properties
if (propSchema.type === 'object' && propSchema.properties) {
processObjectProperties(
propSchema.properties,
propSchema.required || [],
jsonPath,
rules,
depth + 1,
false,
addedRules
);
}
});
}
});
}
});
// Add rule for required spec properties
if (requiredProps.length > 0) {
rules["spec-required-properties"] = {
"description": `The spec object must contain the required properties: ${requiredProps.join(', ')}`,
"severity": "error",
"given": "$.spec",
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"required": requiredProps
}
}
}
};
}
// Add rule to prevent additional properties if specified
if (specSchema.additionalProperties === false) {
const allowedProps = specSchema.properties ? Object.keys(specSchema.properties) : [];
rules["spec-no-additional-properties"] = {
"description": `The spec object should only contain the defined properties: ${allowedProps.join(', ')}`,
"severity": "error",
"given": "$.spec",
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"additionalProperties": false,
"properties": Object.fromEntries(allowedProps.map(prop => [prop, {}]))
}
}
}
};
}
}
return { rules };
}
/**
* Recursively process object properties to generate validation rules
* @param {Object} properties - Object properties to process
* @param {Array} requiredProps - List of required property names
* @param {string} path - JSON path to the current object
* @param {Object} rules - Rules object to add rules to
* @param {number} depth - Current recursion depth (for debugging)
* @param {boolean} isInOneOf - Whether this property is inside a oneOf schema
* @param {Set} addedRules - Set to track already added rules to avoid duplicates
*/
function processObjectProperties(properties, requiredProps, path, rules, depth = 0, isInOneOf = false, addedRules = new Set()) {
// Limit recursion depth to prevent infinite loops
if (depth > 10) {
console.warn(`Warning: Maximum recursion depth reached at path ${path}`);
return;
}
// Process each property
Object.entries(properties).forEach(([propName, propSchema]) => {
const jsonPath = path === "$" ? `$.${propName}` : `$.${path.replace(/^\$\./, '')}.${propName}`;
const rulePrefix = path === "$" ? propName : `${path.replace(/\./g, "-")}-${propName}`;
const isRequired = requiredProps.includes(propName);
const propType = propSchema.type;
const propEnum = propSchema.enum;
const propDescription = propSchema.description || `${propName} field`;
// Rule for required properties - skip if inside a oneOf schema
if (isRequired && !isInOneOf) {
rules[`${rulePrefix}-required`] = {
"description": `The ${propName} field is required in the ${path}`,
"severity": "error",
"given": path === "$" ? "$" : `$.${path.replace(/^\$\./, '')}`,
"then": {
"field": propName,
"function": "defined"
}
};
}
// Rule for property type validation
if (propType) {
rules[`${rulePrefix}-type`] = {
"description": `The ${propName} field must be of type ${propType}`,
"severity": "error",
"given": jsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": propType
}
}
}
};
}
// Rule for enum validation
if (propEnum) {
rules[`${rulePrefix}-enum`] = {
"description": `The ${propName} field must be one of: ${propEnum.join(', ')}`,
"severity": "error",
"given": jsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"enum": propEnum
}
}
}
};
}
// Handle nested objects recursively
if (propType === 'object' && propSchema.properties) {
const nestedProps = propSchema.properties;
const nestedRequired = propSchema.required || [];
// Process nested object properties
processObjectProperties(nestedProps, nestedRequired, jsonPath, rules, depth + 1, false, addedRules);
// Add rule to prevent additional properties if specified
if (propSchema.additionalProperties === false) {
const allowedNestedProps = Object.keys(nestedProps);
rules[`${rulePrefix}-no-additional-properties`] = {
"description": `The ${propName} object should only contain the defined properties: ${allowedNestedProps.join(', ')}`,
"severity": "error",
"given": jsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"additionalProperties": false,
"properties": Object.fromEntries(allowedNestedProps.map(prop => [prop, {}]))
}
}
}
};
}
}
// Handle arrays
if (propType === 'array' && propSchema.items) {
const itemType = propSchema.items.type;
const itemEnum = propSchema.items.enum;
// Rule for array item type validation
if (itemType) {
rules[`${rulePrefix}-items-type`] = {
"description": `Items in the ${propName} array must be of type ${itemType}`,
"severity": "error",
"given": `${jsonPath}[*]`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": itemType
}
}
}
};
if(itemType==='object' && propSchema.items.properties ){
const nestedProps = propSchema.items.properties;
const nestedRequired = propSchema.items.properties.required || [];
// Process nested object properties
processObjectProperties(nestedProps, nestedRequired, `${jsonPath}[*]`, rules, depth + 1, false, addedRules);
// Add rule to prevent additional properties if specified
if (propSchema.items.additionalProperties === false) {
const allowedNestedProps = Object.keys(nestedProps);
rules[`${rulePrefix}-no-additional-properties`] = {
"description": `The ${propName} object should only contain the defined properties: ${allowedNestedProps.join(', ')}`,
"severity": "error",
"given": `${jsonPath}[*]`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"additionalProperties": false,
"properties": Object.fromEntries(allowedNestedProps.map(prop => [prop, {}]))
}
}
}
};
}
}
}
// Rule for array item enum validation
if (itemEnum) {
rules[`${rulePrefix}-items-enum`] = {
"description": `Items in the ${propName} array must be one of: ${itemEnum.join(', ')}`,
"severity": "error",
"given": `${jsonPath}[*]`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"enum": itemEnum
}
}
}
};
}
// Handle objects within arrays recursively
if (itemType === 'object' && propSchema.items.properties) {
const arrayItemProps = propSchema.items.properties;
const arrayItemRequired = propSchema.items.required || [];
// Process array item properties
Object.entries(arrayItemProps).forEach(([arrayItemPropName, arrayItemPropSchema]) => {
const arrayItemJsonPath = jsonPath.startsWith("$") ? `${jsonPath}[*].${arrayItemPropName}` : `$.${jsonPath}[*].${arrayItemPropName}`;
const arrayItemRulePrefix = `${rulePrefix}-items-${arrayItemPropName}`;
const isArrayItemRequired = arrayItemRequired.includes(arrayItemPropName);
const arrayItemPropType = arrayItemPropSchema.type;
const arrayItemPropEnum = arrayItemPropSchema.enum;
// Rule for required properties in array items
// Create a unique key based on the given path and validation logic
const requiredRuleKey = `required:${jsonPath}[*]:${arrayItemPropName}`;
if (isArrayItemRequired && !addedRules.has(requiredRuleKey)) {
rules[requiredRuleKey] = {
"description": `The ${arrayItemPropName} field is required in ${propName} array items`,
"severity": "error",
"given": `${jsonPath}[*]`,
"then": {
"field": arrayItemPropName,
"function": "defined"
}
};
addedRules.add(requiredRuleKey);
}
// Rule for property type validation in array items
// Create a unique key based on the given path and validation logic
const typeRuleKey = `type:${arrayItemJsonPath}:${arrayItemPropType}`;
if (arrayItemPropType && !addedRules.has(typeRuleKey)) {
rules[typeRuleKey] = {
"description": `The ${arrayItemPropName} field in ${propName} array items must be of type ${arrayItemPropType}`,
"severity": "error",
"given": arrayItemJsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": arrayItemPropType
}
}
}
};
addedRules.add(typeRuleKey);
}
// Rule for enum validation in array items
// Create a unique key based on the given path and validation logic
const enumRuleKey = `enum:${arrayItemJsonPath}:${arrayItemPropEnum ? arrayItemPropEnum.join(',') : ''}`;
if (arrayItemPropEnum && !addedRules.has(enumRuleKey)) {
rules[enumRuleKey] = {
"description": `The ${arrayItemPropName} field in ${propName} array items must be one of: ${arrayItemPropEnum.join(', ')}`,
"severity": "error",
"given": arrayItemJsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"enum": arrayItemPropEnum
}
}
}
};
addedRules.add(enumRuleKey);
}
// Recursively process nested objects in array items
if (arrayItemPropType === 'object' && arrayItemPropSchema.properties) {
const nestedArrayItemProps = arrayItemPropSchema.properties;
const nestedArrayItemRequired = arrayItemPropSchema.required || [];
processObjectProperties(
nestedArrayItemProps,
nestedArrayItemRequired,
`${jsonPath}[*].${arrayItemPropName}`,
rules,
depth + 1,
false,
addedRules
);
}
});
// Add rule to prevent additional properties in array items if specified
// Create a unique key based on the given path and validation logic
const noAdditionalPropsKey = `no-additional-properties:${jsonPath}[*]`;
if (propSchema.items.additionalProperties === false && !addedRules.has(noAdditionalPropsKey)) {
const allowedArrayItemProps = Object.keys(arrayItemProps);
rules[noAdditionalPropsKey] = {
"description": `Items in the ${propName} array should only contain the defined properties: ${allowedArrayItemProps.join(', ')}`,
"severity": "error",
"given": `${jsonPath}[*]`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"additionalProperties": false,
"properties": Object.fromEntries(allowedArrayItemProps.map(prop => [prop, {}]))
}
}
}
};
addedRules.add(noAdditionalPropsKey);
}
}
// Handle arrays of arrays (nested arrays)
if (itemType === 'array' && propSchema.items.items) {
const nestedItemType = propSchema.items.items.type;
const nestedItemEnum = propSchema.items.items.enum;
// Rule for nested array item type validation
if (nestedItemType) {
rules[`${rulePrefix}-items-items-type`] = {
"description": `Items in the nested arrays of ${propName} must be of type ${nestedItemType}`,
"severity": "error",
"given": jsonPath.startsWith("$") ? `${jsonPath}[*][*]` : `$.${jsonPath}[*][*]`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": nestedItemType
}
}
}
};
}
// Rule for nested array item enum validation
if (nestedItemEnum) {
rules[`${rulePrefix}-items-items-enum`] = {
"description": `Items in the nested arrays of ${propName} must be one of: ${nestedItemEnum.join(', ')}`,
"severity": "error",
"given": jsonPath.startsWith("$") ? `${jsonPath}[*][*]` : `$.${jsonPath}[*][*]`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"enum": nestedItemEnum
}
}
}
};
}
// Handle objects within nested arrays
if (nestedItemType === 'object' && propSchema.items.items.properties) {
// This would be a very deep nesting, but we could handle it similarly to the above
// For brevity, we'll skip implementing this level of nesting
console.log(`Note: Detected deeply nested array of objects at ${jsonPath}[*][*]`);
}
}
}
// Handle oneOf, anyOf, allOf schemas
['oneOf', 'anyOf', 'allOf'].forEach(schemaType => {
if (propSchema[schemaType]) {
rules[`${rulePrefix}-${schemaType}`] = {
"description": `The ${propName} field must match the ${schemaType} schema definition`,
"severity": "error",
"given": jsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
[schemaType]: propSchema[schemaType]
}
}
}
};
// For oneOf schemas, we don't generate individual required property rules
// as the oneOf validation will handle that
const isOneOf = schemaType === 'oneOf';
// Process each schema in the oneOf/anyOf/allOf array
propSchema[schemaType].forEach((subSchema, index) => {
if (subSchema.type === 'object' && subSchema.properties) {
const subSchemaProps = subSchema.properties;
const subSchemaRequired = subSchema.required || [];
// Process properties in this sub-schema
processObjectProperties(
subSchemaProps,
subSchemaRequired,
jsonPath, // Use the original path to ensure proper rule generation
rules,
depth + 1,
isOneOf, // Pass whether this is in a oneOf schema
addedRules
);
} else if (subSchema.required && Array.isArray(subSchema.required)) {
// Handle case where anyOf/allOf has required fields but no explicit type
subSchema.required.forEach(requiredProp => {
if (subSchema.properties && subSchema.properties[requiredProp]) {
const requiredPropSchema = subSchema.properties[requiredProp];
const requiredJsonPath = `${jsonPath}.${requiredProp}`;
const requiredRulePrefix = `${rulePrefix}-${requiredProp}`;
// Skip generating individual required property rules for oneOf schemas
// This prevents conflicting validation rules for properties that should be mutually exclusive
if (!isOneOf) {
rules[`${requiredRulePrefix}-required`] = {
"description": `The ${requiredProp} field is required in ${propName} when using this schema variant`,
"severity": "error",
"given": jsonPath,
"then": {
"field": requiredProp,
"function": "defined"
}
};
}
// Process the required property's schema
if (requiredPropSchema.type === 'object' && requiredPropSchema.properties) {
processObjectProperties(
requiredPropSchema.properties,
requiredPropSchema.required || [],
requiredJsonPath,
rules,
depth + 1,
isOneOf,
addedRules
);
}
// Handle nested oneOf/anyOf/allOf in the required property
['anyOf', 'allOf'].forEach(nestedSchemaType => {
if (requiredPropSchema[nestedSchemaType]) {
requiredPropSchema[nestedSchemaType].forEach((nestedSubSchema, nestedIndex) => {
if (nestedSubSchema.type === 'object' && nestedSubSchema.properties) {
processObjectProperties(
nestedSubSchema.properties,
nestedSubSchema.required || [],
requiredJsonPath,
rules,
depth + 2,
false,
addedRules
);
}
});
}
});
// For oneOf, just add a schema validation rule
if (requiredPropSchema.oneOf) {
rules[`${requiredRulePrefix}-oneOf`] = {
"description": `The ${requiredProp} field must match the oneOf schema definition`,
"severity": "error",
"given": requiredJsonPath,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"oneOf": requiredPropSchema.oneOf
}
}
}
};
}
}
});
}
});
// For oneOf, add special handling for useIncoming fields
if (isOneOf) {
propSchema[schemaType].forEach((variant) => {
if (variant.properties && variant.properties.useIncoming) {
rules[`${rulePrefix}-useIncoming-type`] = {
"description": `The ${jsonPath.replace(/^\$\./, '')}.useIncoming field must be an object`,
"severity": "error",
"given": `${jsonPath}.useIncoming`,
"then": {
"function": "schema",
"functionOptions": {
"schema": {
"type": "object"
}
}
}
};
}
});
}
}
});
});
}
// Function to clean up ruleset by removing conflicting rules for oneOf schemas
function cleanupRulesetForOneOf(ruleset) {
const rules = ruleset.rules;
const oneOfPaths = [];
// Find all oneOf schema paths
Object.entries(rules).forEach(([ruleName, rule]) => {
if (ruleName.includes('-oneOf') && rule.given && rule.then?.functionOptions?.schema?.oneOf) {
const path = rule.given.replace(/^\$\./, '');
oneOfPaths.push(path);
}
});
// Remove individual required rules for properties in oneOf paths
if (oneOfPaths.length > 0) {
const rulesToRemove = [];
Object.keys(rules).forEach(ruleName => {
if (ruleName.startsWith('$-') && ruleName.endsWith('-required')) {
// Check if this required rule is for a property in a oneOf path
for (const oneOfPath of oneOfPaths) {
const pathParts = oneOfPath.split('.');
const parentPath = pathParts.join('-');
if (ruleName.includes(parentPath)) {
rulesToRemove.push(ruleName);
break;
}
}
}
});
// Remove the identified rules
rulesToRemove.forEach(ruleName => {
delete rules[ruleName];
});
}
return ruleset;
}
// Function to write the ruleset to a file
function writeRulesetToFile(componentName, ruleset) {
// Clean up the ruleset to remove conflicting rules for oneOf schemas
ruleset = cleanupRulesetForOneOf(ruleset);
const fileName = `${componentName.toLowerCase()}.ruleset.js`;
const filePath = path.resolve(__dirname, fileName);
const fileContent = `export default ${JSON.stringify(ruleset, null, 2)};`;
fs.writeFileSync(filePath, fileContent);
console.log(`Generated recursive ruleset for ${componentName} at ${filePath}`);
}
// Function to update the smith-ruleset.ts file
async function updateSmithRuleset() {
try {
// Import the update function from update-smith-ruleset.js
const { default: updateFunction } = await import('./update-nano-smith-ruleset.js');
// Run the update function
updateFunction();
console.log('Updated smith-ruleset.ts with the latest rulesets');
} catch (error) {
console.error('Error updating smith-ruleset.ts:', error);
}
}
// Main function to generate rulesets for all components
async function generateAllRulesets() {
const openApiSpec = readOpenAPISpec();
const schemas = openApiSpec.components.schemas;
// Generate rulesets for each schema component
Object.entries(schemas).forEach(([componentName, schema]) => {
// Skip the KindEnums schema as it's not a component
if (componentName === 'KindEnums') return;
const ruleset = generateRuleset(schema.properties.kind.enum[0], schema);
writeRulesetToFile(schema.properties.kind.enum[0], ruleset);
});
console.log('All recursive ruleset files have been generated.');
// Update the smith-ruleset.ts file
await updateSmithRuleset();
}
// Run the generator
generateAllRulesets();
// Made with Bob