197 lines
6.4 KiB
PHP
197 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace Toolbox;
|
|
|
|
final class Manifest
|
|
{
|
|
/**
|
|
* @param array<string, Component> $components
|
|
* @param list<string> $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<string, Component> */
|
|
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<string>
|
|
*/
|
|
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;
|
|
}
|
|
}
|