diff --git a/README.md b/README.md index 8e6e013..be34dbd 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,4 @@ composer require internal/dload -W [![PHP](https://img.shields.io/packagist/php-v/internal/dload.svg?style=flat-square&logo=php)](https://packagist.org/packages/internal/dload) [![Latest Version on Packagist](https://img.shields.io/packagist/v/internal/dload.svg?style=flat-square&logo=packagist)](https://packagist.org/packages/internal/dload) [![License](https://img.shields.io/packagist/l/internal/dload.svg?style=flat-square)](LICENSE.md) -[![Total DLoads](https://img.shields.io/packagist/dt/internal/dload.svg?style=flat-square)](https://packagist.org/packages/internal/dload) +[![Total DLoads](https://img.shields.io/packagist/dt/internal/dload.svg?style=flat-square)](https://packagist.org/packages/internal/dload/stats) diff --git a/src/Module/Repository/AssetInterface.php b/src/Module/Repository/AssetInterface.php new file mode 100644 index 0000000..b556ece --- /dev/null +++ b/src/Module/Repository/AssetInterface.php @@ -0,0 +1,27 @@ +release; + } + + public function getOperatingSystem(): ?OperatingSystem + { + return $this->os; + } + + public function getArchitecture(): ?Architecture + { + return $this->arch; + } + + public function getName(): string + { + return $this->name; + } + + public function getUri(): string + { + return $this->uri; + } +} diff --git a/src/Module/Repository/Internal/AssetsCollection.php b/src/Module/Repository/Internal/AssetsCollection.php new file mode 100644 index 0000000..6082448 --- /dev/null +++ b/src/Module/Repository/Internal/AssetsCollection.php @@ -0,0 +1,39 @@ + + */ +final class AssetsCollection extends Collection +{ + public function exceptDebPackages(): self + { + return $this->except( + static fn(AssetInterface $asset): bool => + \str_ends_with(\strtolower($asset->getName()), '.deb'), + ); + } + + public function whereArchitecture(Architecture $arch): self + { + return $this->filter( + static fn(AssetInterface $asset): bool => + \str_contains($asset->getName(), '-' . \strtolower($arch->name) . '.'), + ); + } + + public function whereOperatingSystem(OperatingSystem $os): self + { + return $this->filter( + static fn(AssetInterface $asset): bool => + \str_contains($asset->getName(), '-' . \strtolower($os->name) . '-'), + ); + } +} diff --git a/src/Module/Repository/Internal/Collection.php b/src/Module/Repository/Internal/Collection.php new file mode 100644 index 0000000..b0c63c6 --- /dev/null +++ b/src/Module/Repository/Internal/Collection.php @@ -0,0 +1,143 @@ + + * + * @internal + * @psalm-internal Internal\DLoad\Module\Repository + */ +abstract class Collection implements \IteratorAggregate, \Countable +{ + /** + * @var array + */ + protected array $items; + + /** + * @param array $items + */ + final public function __construct(array $items) + { + $this->items = $items; + } + + /** + * @param self|iterable|\Closure $items + * @return static + */ + public static function create(mixed $items): static + { + return match (true) { + $items instanceof static => $items, + $items instanceof \Traversable => new static(\iterator_to_array($items)), + \is_array($items) => new static($items), + $items instanceof \Closure => static::from($items), + default => throw new \InvalidArgumentException( + \sprintf('Unsupported iterable type %s.', \get_debug_type($items)), + ), + }; + } + + /** + * @param \Closure $generator + * @return static + */ + public static function from(\Closure $generator): static + { + return static::create($generator()); + } + + /** + * @param callable(T): bool $filter + * @return $this + */ + public function filter(callable $filter): static + { + return new static(\array_filter($this->items, $filter)); + } + + /** + * @param callable(T): mixed $map + * @return $this + */ + public function map(callable $map): static + { + return new static(\array_map($map, $this->items)); + } + + /** + * @param callable(T): bool $filter + * @return $this + * + * @psalm-suppress MissingClosureParamType + * @psalm-suppress MixedArgument + */ + public function except(callable $filter): static + { + $callback = static fn(...$args): bool => ! $filter(...$args); + + return new static(\array_filter($this->items, $callback)); + } + + /** + * @param null|callable(T): bool $filter + * @return T|null + */ + public function first(callable $filter = null): ?object + { + $self = $filter === null ? $this : $this->filter($filter); + + return $self->items === [] ? null : \reset($self->items); + } + + /** + * @param callable(): T $otherwise + * @param null|callable(T): bool $filter + * @return T + */ + public function firstOr(callable $otherwise, callable $filter = null): object + { + return $this->first($filter) ?? $otherwise(); + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->items); + } + + public function count(): int + { + return \count($this->items); + } + + /** + * @param callable $then + * @return $this + */ + public function whenEmpty(callable $then): static + { + if ($this->empty()) { + $then(); + } + + return $this; + } + + public function empty(): bool + { + return $this->items === []; + } + + /** + * @return array + */ + public function toArray(): array + { + return \array_values($this->items); + } +} diff --git a/src/Module/Repository/Internal/Release.php b/src/Module/Repository/Internal/Release.php new file mode 100644 index 0000000..0bb88f7 --- /dev/null +++ b/src/Module/Repository/Internal/Release.php @@ -0,0 +1,90 @@ +name = $this->simplifyReleaseName($name); + $this->assets = AssetsCollection::create($assets); + $this->stability = $this->parseStability($version); + } + + public function getRepository(): RepositoryInterface + { + return $this->repository; + } + + public function getName(): string + { + return $this->name; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getStability(): Stability + { + return $this->stability; + } + + public function getAssets(): AssetsCollection + { + return $this->assets; + } + + /** + * @param non-empty-string $version + */ + private function parseStability(string $version): Stability + { + return Stability::tryFrom(VersionParser::parseStability($version)); + } + + /** + * @param non-empty-string $name + * @return non-empty-string + */ + private function simplifyReleaseName(string $name): string + { + $version = (new VersionParser())->normalize($name); + + $parts = \explode('-', $version); + $number = \substr($parts[0], 0, -2); + + return isset($parts[1]) + ? $number . '-' . $parts[1] + : $number + ; + } +} diff --git a/src/Module/Repository/Internal/ReleasesCollection.php b/src/Module/Repository/Internal/ReleasesCollection.php new file mode 100644 index 0000000..e11948e --- /dev/null +++ b/src/Module/Repository/Internal/ReleasesCollection.php @@ -0,0 +1,126 @@ + + * @psalm-import-type StabilityType from Stability + */ +final class ReleasesCollection extends Collection +{ + /** + * @param string ...$constraints + * @return $this + */ + public function satisfies(string ...$constraints): self + { + $result = $this; + + foreach ($this->constraints($constraints) as $constraint) { + $result = $result->filter(static fn(ReleaseInterface $r): bool => $r->satisfies($constraint)); + } + + return $result; + } + + /** + * @param string ...$constraints + * @return $this + */ + public function notSatisfies(string ...$constraints): self + { + $result = $this; + + foreach ($this->constraints($constraints) as $constraint) { + $result = $result->except(static fn(ReleaseInterface $r): bool => $r->satisfies($constraint)); + } + + return $result; + } + + /** + * @return $this + */ + public function withAssets(): self + { + return $this->filter( + static fn(ReleaseInterface $r): bool => ! $r->getAssets() + ->empty(), + ); + } + + /** + * @return $this + */ + public function sortByVersion(): self + { + $result = $this->items; + + $sort = function (ReleaseInterface $a, ReleaseInterface $b): int { + return \version_compare($this->comparisonVersionString($b), $this->comparisonVersionString($a)); + }; + + \uasort($result, $sort); + + return new self($result); + } + + /** + * @return $this + */ + public function stable(): self + { + return $this->stability(Stability::Stable); + } + + /** + * @return $this + */ + public function stability(Stability $stability): self + { + return $this->filter(static fn(ReleaseInterface $rel): bool => $rel->getStability() === $stability); + } + + /** + * @return $this + */ + public function minimumStability(Stability $stability): self + { + $weight = $stability->getWeight(); + return $this->filter( + static fn(ReleaseInterface $release): bool => $release->getStability()->getWeight() >= $weight, + ); + } + + /** + * @param array $constraints + * @return array + */ + private function constraints(array $constraints): array + { + $result = []; + + foreach ($constraints as $constraint) { + foreach (\explode('|', $constraint) as $expression) { + $result[] = $expression; + } + } + + return \array_unique(\array_filter(\array_map('\\trim', $result))); + } + + /** + * @return non-empty-string + */ + private function comparisonVersionString(ReleaseInterface $release): string + { + $stability = $release->getStability(); + + return \str_replace('-' . $stability->value, '.' . $stability->getWeight() . '.', $release->getVersion()); + } +} diff --git a/src/Module/Repository/Internal/RepositoriesCollection.php b/src/Module/Repository/Internal/RepositoriesCollection.php new file mode 100644 index 0000000..ebdded2 --- /dev/null +++ b/src/Module/Repository/Internal/RepositoriesCollection.php @@ -0,0 +1,40 @@ + + */ + private array $repositories; + + /** + * @param array $repositories + */ + public function __construct(array $repositories) + { + $this->repositories = $repositories; + } + + /** + * @return non-empty-string + */ + public function getName(): string + { + return 'unknown/unknown'; + } + + public function getReleases(): ReleasesCollection + { + return ReleasesCollection::from(function () { + foreach ($this->repositories as $repository) { + yield from $repository->getReleases(); + } + }); + } +} diff --git a/src/Module/Repository/ReleaseInterface.php b/src/Module/Repository/ReleaseInterface.php new file mode 100644 index 0000000..486ffcb --- /dev/null +++ b/src/Module/Repository/ReleaseInterface.php @@ -0,0 +1,33 @@ +