addInfo("Validando Schema JSON: schemas/{$componentName}.json"); // 1. Verificar que el archivo existe if (!file_exists($schemaPath)) { $result->addError("Schema JSON no encontrado: {$schemaPath}"); return $result; } // 2. Leer y parsear JSON $jsonContent = file_get_contents($schemaPath); $schema = json_decode($jsonContent, true); if (json_last_error() !== JSON_ERROR_NONE) { $result->addError("JSON inválido: " . json_last_error_msg()); return $result; } // 3. Validar estructura top-level $this->validateTopLevelStructure($schema, $componentName, $result); // 4. Validar grupos if (isset($schema['groups'])) { $this->validateGroups($schema['groups'], $result); } // 5. Validar campos obligatorios de visibilidad (excepto componentes de inyeccion) $this->validateVisibilityFields($schema, $componentName, $result); // Estadísticas $totalFields = $this->countTotalFields($schema); $totalGroups = isset($schema['groups']) ? count($schema['groups']) : 0; $result->setStat('Archivo', "schemas/{$componentName}.json"); $result->setStat('Grupos totales', $totalGroups); $result->setStat('Campos totales', $totalFields); $result->setStat('Tamaño JSON', strlen($jsonContent) . ' bytes'); return $result; } private function validateTopLevelStructure(array $schema, string $componentName, ValidationResult $result): void { // Campos obligatorios $requiredFields = ['component_name', 'version', 'description', 'groups']; foreach ($requiredFields as $field) { if (!isset($schema[$field])) { $result->addError("Campo obligatorio faltante: '{$field}'"); } } // Validar component_name coincide con archivo if (isset($schema['component_name']) && $schema['component_name'] !== $componentName) { $result->addError( "component_name '{$schema['component_name']}' no coincide con nombre de archivo '{$componentName}'" ); } // Validar versión semver if (isset($schema['version'])) { if (!preg_match('/^\d+\.\d+\.\d+$/', $schema['version'])) { $result->addError("Versión '{$schema['version']}' no es semver válido (debe ser X.Y.Z)"); } } } private function validateGroups(array $groups, ValidationResult $result): void { if (empty($groups)) { $result->addError("Schema debe tener al menos un grupo"); return; } foreach ($groups as $groupName => $group) { // Validar nombre de grupo es snake_case if (!preg_match('/^[a-z_]+$/', $groupName)) { $result->addError("Nombre de grupo '{$groupName}' debe estar en snake_case (solo minúsculas y _)"); } // Advertencia si grupo no es estándar if (!in_array($groupName, self::STANDARD_GROUPS, true)) { $result->addWarning("Grupo '{$groupName}' no es estándar (considerar usar: " . implode(', ', self::STANDARD_GROUPS) . ")"); } // Validar estructura del grupo if (!isset($group['label'])) { $result->addError("Grupo '{$groupName}' no tiene 'label'"); } if (!isset($group['priority'])) { $result->addError("Grupo '{$groupName}' no tiene 'priority'"); } elseif (!in_array($group['priority'], self::ALLOWED_PRIORITIES, true)) { $result->addError( "Grupo '{$groupName}' tiene priority inválido ({$group['priority']}). " . "Debe ser uno de: " . implode(', ', self::ALLOWED_PRIORITIES) ); } if (!isset($group['fields'])) { $result->addError("Grupo '{$groupName}' no tiene 'fields'"); } elseif (!is_array($group['fields']) || empty($group['fields'])) { $result->addError("Grupo '{$groupName}' debe tener al menos un campo"); } else { $this->validateFields($groupName, $group['fields'], $result); } } } private function validateFields(string $groupName, array $fields, ValidationResult $result): void { foreach ($fields as $fieldName => $field) { $fullFieldName = "{$groupName}.{$fieldName}"; // Validar nombre de campo es snake_case if (!preg_match('/^[a-z_]+$/', $fieldName)) { $result->addError("Campo '{$fullFieldName}' debe estar en snake_case (solo minúsculas y _)"); } // Campos obligatorios if (!isset($field['type'])) { $result->addError("Campo '{$fullFieldName}' no tiene 'type'"); } elseif (!in_array($field['type'], self::ALLOWED_TYPES, true)) { $result->addError( "Campo '{$fullFieldName}' tiene type inválido '{$field['type']}'. " . "Debe ser uno de: " . implode(', ', self::ALLOWED_TYPES) ); } if (!isset($field['label'])) { $result->addError("Campo '{$fullFieldName}' no tiene 'label'"); } if (!array_key_exists('default', $field)) { $result->addError("Campo '{$fullFieldName}' no tiene 'default'"); } if (!isset($field['editable'])) { $result->addError("Campo '{$fullFieldName}' no tiene 'editable'"); } elseif (!is_bool($field['editable'])) { $result->addError("Campo '{$fullFieldName}' tiene 'editable' que no es boolean"); } // Si type es select, debe tener options if (isset($field['type']) && $field['type'] === 'select') { if (!isset($field['options']) || !is_array($field['options']) || empty($field['options'])) { $result->addError("Campo '{$fullFieldName}' es type 'select' pero no tiene array 'options' válido"); } } // Si tiene required, debe ser boolean if (isset($field['required']) && !is_bool($field['required'])) { $result->addError("Campo '{$fullFieldName}' tiene 'required' que no es boolean"); } } } private function validateVisibilityFields(array $schema, string $componentName, ValidationResult $result): void { // Componentes de inyeccion no requieren grupo visibility if (in_array($componentName, self::INJECTION_COMPONENTS, true)) { $result->addInfo("✓ Componente de inyección '{$componentName}' - grupo visibility no requerido"); return; } if (!isset($schema['groups']['visibility'])) { $result->addError("Grupo 'visibility' es obligatorio y no está presente"); return; } $visibilityFields = $schema['groups']['visibility']['fields'] ?? []; // Campos obligatorios de visibilidad $requiredVisibilityFields = [ 'is_enabled' => 'boolean', 'show_on_desktop' => 'boolean', 'show_on_mobile' => 'boolean', ]; foreach ($requiredVisibilityFields as $fieldName => $expectedType) { if (!isset($visibilityFields[$fieldName])) { $result->addError("Campo obligatorio de visibilidad faltante: 'visibility.{$fieldName}'"); } elseif (isset($visibilityFields[$fieldName]['type']) && $visibilityFields[$fieldName]['type'] !== $expectedType) { $result->addError( "Campo 'visibility.{$fieldName}' debe ser type '{$expectedType}' " . "(encontrado: '{$visibilityFields[$fieldName]['type']}')" ); } } } private function countTotalFields(array $schema): int { $count = 0; if (isset($schema['groups'])) { foreach ($schema['groups'] as $group) { if (isset($group['fields'])) { $count += count($group['fields']); } } } return $count; } public function getPhaseNumber(): int|string { return 1; } public function getPhaseDescription(): string { return 'Schema JSON'; } }