first commit.

This commit is contained in:
kj
2026-10-05 15:08:39 -03:00
commit b7799f9fff
93 changed files with 8966 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
<?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 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;
}
return new self(
name: $name,
version: $version,
description: is_string($data['description'] ?? null) ? $data['description'] : '',
components: $components,
default: $default,
coreRequires: $data['core']['requires'] ?? null,
coreTested: $data['core']['tested'] ?? null,
php: is_string($data['php'] ?? null) ? $data['php'] : null,
postInstall: $postInstall,
);
}
/** @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;
}
public function coreRequirement(?string $component = null): ?string
{
if ($component !== null) {
return $this->component($component)->coreRequires ?? $this->coreRequires;
}
return $this->coreRequires;
}
public function testedCore(?string $component = null): ?string
{
if ($component !== null) {
return $this->component($component)->coreTested ?? $this->coreTested;
}
return $this->coreTested;
}
}