UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

1,317 lines (1,232 loc) 55.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.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", "test", "assertion", "environment", "MCPTool", "MCPServerConfig" ]; // 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 } } } }; } // Rule for minLength validation (for strings) if (propSchema.minLength !== undefined && propType === 'string') { rules[`${rulePrefix}-minLength`] = { "description": `The ${propName} field must have a minimum length of ${propSchema.minLength}`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "type": "string", "minLength": propSchema.minLength } } } }; } // Rule for maxLength validation (for strings) if (propSchema.maxLength !== undefined && propType === 'string') { rules[`${rulePrefix}-maxLength`] = { "description": `The ${propName} field must have a maximum length of ${propSchema.maxLength}`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "type": "string", "maxLength": propSchema.maxLength } } } }; } // Rule for pattern validation (for strings) if (propSchema.pattern && propType === 'string') { rules[`${rulePrefix}-pattern`] = { "description": `The ${propName} field must match the pattern: ${propSchema.pattern}`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "type": "string", "pattern": propSchema.pattern } } } }; } // Rule for minimum validation (for numbers) if (propSchema.minimum !== undefined && (propType === 'number' || propType === 'integer')) { rules[`${rulePrefix}-minimum`] = { "description": `The ${propName} field must have a minimum value of ${propSchema.minimum}`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "type": propType, "minimum": propSchema.minimum } } } }; } // Rule for maximum validation (for numbers) if (propSchema.maximum !== undefined && (propType === 'number' || propType === 'integer')) { rules[`${rulePrefix}-maximum`] = { "description": `The ${propName} field must have a maximum value of ${propSchema.maximum}`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "type": propType, "maximum": propSchema.maximum } } } }; } // Rule for minItems validation (for arrays) if (propSchema.minItems !== undefined && propType === 'array') { rules[`${rulePrefix}-minItems`] = { "description": `The ${propName} array must have at least ${propSchema.minItems} items`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "type": "array", "minItems": propSchema.minItems } } } }; } // Rule for maxItems validation (for arrays) if (propSchema.maxItems !== undefined && propType === 'array') { rules[`${rulePrefix}-maxItems`] = { "description": `The ${propName} array must have at most ${propSchema.maxItems} items`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "type": "array", "maxItems": propSchema.maxItems } } } }; } // 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 if/then conditional schemas in array items if (propSchema.items.if && propSchema.items.then) { // Extract condition information for user-friendly description let conditionDesc = ''; let thenDesc = ''; if (propSchema.items.if.properties) { const ifCondition = propSchema.items.if.properties; const conditionField = Object.keys(ifCondition)[0]; const conditionValue = ifCondition[conditionField].const; conditionDesc = `when ${conditionField} is ${conditionValue}`; } if (propSchema.items.then.properties) { const thenProps = Object.entries(propSchema.items.then.properties); const thenDescriptions = thenProps.map(([thenPropName, thenPropSchema]) => { if (thenPropSchema.pattern) { return `${thenPropName} must match pattern ${thenPropSchema.pattern}`; } else if (thenPropSchema.type) { return `${thenPropName} must be of type ${thenPropSchema.type}`; } return `${thenPropName} must satisfy specific requirements`; }); thenDesc = thenDescriptions.join(' and '); } rules[`${rulePrefix}-items-conditional`] = { "description": `Items in the ${propName} array must satisfy conditional validation: ${conditionDesc}, ${thenDesc}`, "severity": "error", "given": `${jsonPath}[*]`, "then": { "function": "schema", "functionOptions": { "schema": { "if": propSchema.items.if, "then": propSchema.items.then } } } }; // If the 'then' clause has a pattern, create a more specific rule if (propSchema.items.then.properties) { Object.entries(propSchema.items.then.properties).forEach(([thenPropName, thenPropSchema]) => { if (thenPropSchema.pattern) { // Extract the condition from the 'if' clause const ifCondition = propSchema.items.if.properties; const conditionField = Object.keys(ifCondition)[0]; const conditionValue = ifCondition[conditionField].const; // Use the description from the schema if available, otherwise create a user-friendly one const userFriendlyDesc = thenPropSchema.description || `When ${conditionField} is ${conditionValue}, ${thenPropName} must match the pattern ${thenPropSchema.pattern}`; rules[`${rulePrefix}-items-${thenPropName}-conditional-pattern`] = { "description": userFriendlyDesc, "message": thenPropSchema.description || `The ${thenPropName} field must match the required pattern when ${conditionField} is ${conditionValue}`, "severity": "error", "given": `${jsonPath}[*]`, "then": { "function": "schema", "functionOptions": { "schema": { "if": propSchema.items.if, "then": { "properties": { [thenPropName]: { "type": thenPropSchema.type, "pattern": thenPropSchema.pattern } } } } } } }; } }); } } // 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 if/then conditional schemas if (propSchema.if && propSchema.then) { // Extract condition information for user-friendly description let conditionDesc = ''; let thenDesc = ''; if (propSchema.if.properties) { const ifCondition = propSchema.if.properties; const conditionField = Object.keys(ifCondition)[0]; const conditionValue = ifCondition[conditionField].const; conditionDesc = `when ${conditionField} is ${conditionValue}`; } if (propSchema.then.properties) { const thenProps = Object.entries(propSchema.then.properties); const thenDescriptions = thenProps.map(([thenPropName, thenPropSchema]) => { if (thenPropSchema.pattern) { return `${thenPropName} must match pattern ${thenPropSchema.pattern}`; } else if (thenPropSchema.type) { return `${thenPropName} must be of type ${thenPropSchema.type}`; } return `${thenPropName} must satisfy specific requirements`; }); thenDesc = thenDescriptions.join(' and '); } rules[`${rulePrefix}-conditional`] = { "description": `The ${propName} field must satisfy conditional validation: ${conditionDesc}, ${thenDesc}`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "if": propSchema.if, "then": propSchema.then } } } }; // If the 'then' clause has a pattern, create a more specific rule if (propSchema.then.properties) { Object.entries(propSchema.then.properties).forEach(([thenPropName, thenPropSchema]) => { if (thenPropSchema.pattern) { // Extract the condition from the 'if' clause const ifCondition = propSchema.if.properties; const conditionField = Object.keys(ifCondition)[0]; const conditionValue = ifCondition[conditionField].const; // Use the description from the schema if available, otherwise create a user-friendly one const userFriendlyDesc = thenPropSchema.description || `When ${conditionField} is ${conditionValue}, ${thenPropName} must match the pattern ${thenPropSchema.pattern}`; rules[`${rulePrefix}-${thenPropName}-conditional-pattern`] = { "description": userFriendlyDesc, "message": thenPropSchema.description || `The ${thenPropName} field must match the required pattern when ${conditionField} is ${conditionValue}`, "severity": "error", "given": jsonPath, "then": { "function": "schema", "functionOptions": { "schema": { "if": propSchema.if, "then": { "properties": { [thenPropName]: { "type": thenPropSchema.type, "pattern": thenPropSchema.pattern } } } } } } }; } }); } } // 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,