UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

1,194 lines (1,102 loc) 44.6 kB
// 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