$components * @param list $default */ private function __construct( public readonly string $name, public readonly string $version, public readonly string $description, private readonly array $components, private readonly ?array $default, public readonly ?string $coreRequires, public readonly ?string $coreTested, public readonly ?string $php, public readonly array $postInstall, public readonly ?bool $coreIndependent = null, ) { } public static function fromFile(string $path): self { if (!is_file($path)) { throw new ToolboxException("No se encontró el manifiesto: {$path}"); } $data = json_decode((string) file_get_contents($path), true); if (!is_array($data)) { throw new ToolboxException("Manifiesto JSON inválido: {$path}"); } return self::fromArray($data, $path); } public static function fromArray(array $data, string $origin = 'duckbrain.json'): self { $name = $data['name'] ?? null; $version = $data['version'] ?? null; if (!is_string($name) || $name === '') { throw new ToolboxException("El manifiesto '{$origin}' no declara 'name'"); } if (!is_string($version) || $version === '') { throw new ToolboxException("El manifiesto '{$origin}' no declara 'version'"); } Semver::parse($version); if (!isset($data['components']) || !is_array($data['components']) || $data['components'] === []) { throw new ToolboxException("El manifiesto '{$origin}' no declara 'components'"); } $components = []; foreach ($data['components'] as $componentName => $componentData) { if (!is_string($componentName) || !is_array($componentData)) { throw new ToolboxException("Componente inválido en '{$origin}'"); } $components[$componentName] = Component::fromArray($componentName, $componentData); } $default = null; if (array_key_exists('default', $data)) { if (!is_array($data['default'])) { throw new ToolboxException("El manifiesto '{$origin}' tiene 'default' inválido"); } $default = array_values($data['default']); foreach ($default as $componentName) { if (!is_string($componentName) || !isset($components[$componentName])) { throw new ToolboxException("'default' referencia un componente inexistente en '{$origin}'"); } } } $postInstall = []; foreach (($data['post-install'] ?? []) as $command) { if (!is_string($command)) { throw new ToolboxException("'post-install' inválido en '{$origin}'"); } $postInstall[] = $command; } $coreRequires = $coreTested = null; $coreIndependent = null; if (array_key_exists('core', $data)) { [$coreRequires, $coreTested, $coreIndependent] = Component::parseCore($data['core'], "el manifiesto '{$origin}'"); } return new self( name: $name, version: $version, description: is_string($data['description'] ?? null) ? $data['description'] : '', components: $components, default: $default, coreRequires: $coreRequires, coreTested: $coreTested, php: is_string($data['php'] ?? null) ? $data['php'] : null, postInstall: $postInstall, coreIndependent: $coreIndependent, ); } /** @return array */ public function components(): array { return $this->components; } public function hasComponent(string $name): bool { return isset($this->components[$name]); } public function component(string $name): Component { if (!isset($this->components[$name])) { throw new ToolboxException("El paquete '{$this->name}' no tiene el componente '{$name}'"); } return $this->components[$name]; } /** * Expande una petición (componente o conjunto por defecto) a nombres. * * @return list */ public function expand(?string $component): array { if ($component !== null) { if (!$this->hasComponent($component)) { throw new ToolboxException("El paquete '{$this->name}' no tiene el componente '{$component}'"); } return [$component]; } if ($this->default === null) { throw new ToolboxException( "El paquete '{$this->name}' no declara 'default'; indica un componente explícito" ); } return $this->default; } /** * Bloque de core efectivo: si el componente declara `core` (rango o * "none") ese bloque gana por completo; si no, hereda el del paquete. * * @return array{requires: ?string, tested: ?string, independent: ?bool} */ private function coreBlock(?string $component): array { if ($component !== null) { $c = $this->component($component); if ($c->coreDeclared) { return ['requires' => $c->coreRequires, 'tested' => $c->coreTested, 'independent' => $c->coreIndependent]; } } return ['requires' => $this->coreRequires, 'tested' => $this->coreTested, 'independent' => $this->coreIndependent]; } public function coreRequirement(?string $component = null): ?string { return $this->coreBlock($component)['requires']; } public function testedCore(?string $component = null): ?string { return $this->coreBlock($component)['tested']; } public function coreIndependent(?string $component = null): bool { return $this->coreBlock($component)['independent'] === true; } /** * Descripcion efectiva: la propia del componente o, en su ausencia, la del * paquete (mismo patrón de herencia que los rangos de core). */ public function descriptionOf(?string $component = null): ?string { if ($component !== null) { return $this->component($component)->description ?? $this->description; } return $this->description; } }