From 951f0cd0eaae95c11e69c366ce94d169f9333171 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Thu, 23 Jan 2025 00:49:24 +0100 Subject: [PATCH] Allow to publish artifact package via API or composer push plugin --- .../Repository/ArtifactRepository.php | 15 +++- src/Controller/PackageController.php | 8 +- src/Controller/PushPackagesController.php | 10 ++- src/Controller/ZipballController.php | 12 ++- src/Entity/Package.php | 1 + src/Entity/PackageSerializedTrait.php | 44 +++++++++- src/Exception/ValidationException.php | 44 ++++++++++ src/Exception/ZipballException.php | 9 ++ src/Form/Handler/PushPackageHandler.php | 24 ++++-- src/Form/Model/NexusPushRequestDto.php | 13 +++ src/Form/Model/PushRequestDtoInterface.php | 2 + src/Form/RequestHandler/PutRequestHandler.php | 62 ++++++++++---- src/Form/Type/Push/NexusPushType.php | 5 +- src/Model/UploadZipballStorage.php | 15 ++-- src/Repository/PackageRepository.php | 6 +- src/Service/Artifact/ArtifactPushHandler.php | 84 ++++++++++++++++++- src/Service/SwaggerDumper.php | 18 +++- src/Util/PacketonUtils.php | 10 ++- swagger/packages-api.yaml | 18 +++- 19 files changed, 338 insertions(+), 62 deletions(-) create mode 100644 src/Exception/ValidationException.php create mode 100644 src/Exception/ZipballException.php diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index c405d9e5..7345c055 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -31,7 +31,11 @@ class ArtifactRepository extends ArrayRepository implements PacketonRepositoryIn protected $lookup; /** @var array */ - protected $archives; + protected array $archives = []; + + protected array $archivesOverwriteInfo = []; + + protected array $archivesOverwriteInfoByRef = []; public function __construct( protected array $repoConfig, @@ -51,6 +55,7 @@ public function __construct( $this->lookup = $repoConfig['url'] ?? null; $this->lookup = $this->lookup === '_unset' ? null : $this->lookup; $this->archives = $this->repoConfig['archives'] ?? []; + $this->archivesOverwriteInfo = $this->repoConfig['archive_overwrite_mapping'] ?? []; $this->process ??= new ProcessExecutor($this->io); } @@ -136,7 +141,7 @@ public function allArtifacts(): iterable private function doInitialize(): void { foreach ($this->allArtifacts() as $ref => $file) { - $package = $this->getComposerInformation($file, $ref); + $package = $this->getComposerInformation($file, $ref, $this->archivesOverwriteInfoByRef[$ref] ?? null); if (!$package) { $this->io->writeError("File {$file->getBasename()} doesn't seem to hold a package", true, IOInterface::VERBOSE); continue; @@ -164,6 +169,8 @@ private function scanArchives(array $archives): iterable continue; } + $this->archivesOverwriteInfoByRef[$zip->getReference()] = $this->archivesOverwriteInfo[$zip->getId()] ?? null; + yield $zip->getReference() => new \SplFileInfo($path); } } @@ -190,7 +197,7 @@ private function scanDirectory(string $path): iterable /** * {@inheritdoc} */ - public function getComposerInformation(\SplFileInfo $file, $ref = null): ?BasePackage + public function getComposerInformation(\SplFileInfo $file, mixed $ref = null, ?array $overwrite = null): ?BasePackage { $json = null; $fileExtension = pathinfo($file->getPathname(), PATHINFO_EXTENSION); @@ -217,6 +224,8 @@ public function getComposerInformation(\SplFileInfo $file, $ref = null): ?BasePa } $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json'); + $package = array_merge($package, $overwrite ?? []); + $package['dist'] = [ 'type' => $fileType, 'url' => strtr($file->getPathname(), '\\', '/'), diff --git a/src/Controller/PackageController.php b/src/Controller/PackageController.php index 5bb6074b..7c97f240 100644 --- a/src/Controller/PackageController.php +++ b/src/Controller/PackageController.php @@ -598,12 +598,8 @@ public function updatePackageAction(Request $req, $name): Response $doctrine = $this->registry; $this->checkSubrepositoryAccess($name); - try { - /** @var Package $package */ - $package = $doctrine - ->getRepository(Package::class) - ->getPackageByName($name); - } catch (NoResultException) { + $package = $doctrine->getRepository(Package::class)->getPackageByName($name); + if (null === $package) { return new JsonResponse(['status' => 'error', 'message' => 'Package not found'], 404); } diff --git a/src/Controller/PushPackagesController.php b/src/Controller/PushPackagesController.php index 424a0dea..15c6fc66 100644 --- a/src/Controller/PushPackagesController.php +++ b/src/Controller/PushPackagesController.php @@ -4,6 +4,7 @@ namespace Packeton\Controller; +use Packeton\Exception\ValidationException; use Packeton\Form\Handler\PushPackageHandler; use Packeton\Form\Type\Push\NexusPushType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -17,13 +18,18 @@ #[Route(defaults: ['_format' => 'json'])] class PushPackagesController extends AbstractController { - #[Route('/packages/upload/{name}/{version}', name: 'package_push_nexus', requirements: ['name' => '%package_name_regex%'], methods: ['PUT', 'POST'])] #[IsGranted('ROLE_MAINTAINER')] + #[Route('/packages/upload/{name}/{version}', name: 'package_push_nexus', requirements: ['name' => '%package_name_regex%'], methods: ['PUT', 'POST'])] + #[Route('/api/packages/upload/{name}/{version}', name: 'package_push_api', requirements: ['name' => '%package_name_regex%'], methods: ['PUT'])] public function pushNexusAction(PushPackageHandler $handler, Request $request, string $name, string $version): Response { $form = $this->createApiForm(NexusPushType::class, options: ['method' => $request->getMethod()]); - $handler($form, $request, $name, $version); + try { + $handler($form, $request, $name, $version, $this->getUser()); + } catch (ValidationException $e) { + return new JsonResponse(['title' => $e->getMessage(), 'errors' => $e->getFormErrors()], 400); + } return new JsonResponse([], 201); } diff --git a/src/Controller/ZipballController.php b/src/Controller/ZipballController.php index 8e597115..fc8a5e91 100644 --- a/src/Controller/ZipballController.php +++ b/src/Controller/ZipballController.php @@ -9,6 +9,7 @@ use Packeton\Entity\Package; use Packeton\Entity\Zipball; use Packeton\Event\ZipballEvent; +use Packeton\Exception\ZipballException; use Packeton\Model\UploadZipballStorage; use Packeton\Package\RepTypes; use Packeton\Service\DistManager; @@ -40,7 +41,7 @@ public function __construct( ) { } - #[Route('/archive/upload', name: 'archive_upload', methods: ["POST"])] + #[Route('/archive/upload', name: 'archive_upload', methods: ["POST"], format: 'json')] #[IsGranted('ROLE_MAINTAINER')] public function upload(Request $request): Response { @@ -52,8 +53,13 @@ public function upload(Request $request): Response return new JsonResponse(['error' => 'File is empty'], 400); } - $result = $this->storage->save($file); - return new JsonResponse($result, $result['code'] ?? 201); + try { + $result = $this->storage->save($file); + } catch (ZipballException $e) { + return new JsonResponse(['error' => $e->getMessage()], max($e->getCode(), 300)); + } + + return new JsonResponse(['id' => $result->getId(), 'filename' => $result->getFilename(), 'size' => $result->getFileSize()], 201); } #[Route('/archive/remove/{id}', name: 'archive_remove', methods: ["DELETE"])] diff --git a/src/Entity/Package.php b/src/Entity/Package.php index 482a6758..c9607777 100644 --- a/src/Entity/Package.php +++ b/src/Entity/Package.php @@ -455,6 +455,7 @@ public function getRepoConfig(): array 'repoType' => $this->repoType ?: 'vcs', 'subDirectory' => $this->getSubDirectory(), 'archives' => $this->getArchives(), + 'archive_overwrite_mapping' => $this->getArchiveOverwrite(), 'oauth2' => $this->integration, 'externalRef' => $this->externalRef, 'customVersions' => $this->getCustomVersions(), diff --git a/src/Entity/PackageSerializedTrait.php b/src/Entity/PackageSerializedTrait.php index a22145cd..bbe0df37 100644 --- a/src/Entity/PackageSerializedTrait.php +++ b/src/Entity/PackageSerializedTrait.php @@ -6,6 +6,7 @@ use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\Constraint; +use Packeton\Util\PacketonUtils; trait PackageSerializedTrait { @@ -76,7 +77,7 @@ public function getArchives(): ?array return $this->serializedFields['archives'] ?? null; } - public function getAllArchives(): ?array + public function getAllArchives(): array { $archives = $this->serializedFields['archives'] ?? []; foreach ($this->getCustomVersions() as $version) { @@ -102,6 +103,38 @@ public function setArchives(?array $archives): void $this->setSerialized('archives', $archives); } + public function setArchiveOverwrite(int $archiveId, array $versionData): void + { + $mapping = $this->getSerialized('archive_version_mapping', 'array') ?? []; + $archives = $this->getArchives(); + $version = $versionData['version'] ?? null; + + $unset = []; + foreach ($mapping as $id => $data) { + if (($data['version'] ?? '_na') === $version) { + $unset[] = $id; + } + } + + $archives[] = $archiveId; + $archives = array_diff($archives, $unset); + $this->setArchives(array_values(array_unique($archives))); + + $mapping[$archiveId] = $versionData; + foreach ($mapping as $id => $data) { + if (!in_array($id, $archives)) { + unset($mapping[$id]); + } + } + + $this->setSerialized('archive_version_mapping', $mapping); + } + + public function getArchiveOverwrite(): array + { + return $this->getSerialized('archive_version_mapping', 'array') ?? []; + } + public function setUpdateFlags(int $flags): void { $this->setSerialized('update_flags', $flags); @@ -199,6 +232,15 @@ public function setCustomVersions($versions): void $this->setSerialized('custom_versions', $versions); } + public function addCustomVersions(array $versionData): void + { + $versions = $this->getCustomVersions(); + $versions = PacketonUtils::buildChoices($versions, 'version'); + $versions[$versionData['version']] = $versionData; + + $this->setCustomVersions(array_values($versions)); + } + public function getCustomComposerJson(): array { return $this->getSerialized('custom_composer_json', 'array', []); diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php new file mode 100644 index 00000000..8bafefa0 --- /dev/null +++ b/src/Exception/ValidationException.php @@ -0,0 +1,44 @@ +errors = $errors instanceof FormInterface ? $exception->getErrors($errors) : $errors; + + return $exception; + } + + private function getErrors(FormInterface $form): array + { + $errors = $base = []; + + foreach ($form->getErrors() as $error) { + $base[] = $error->getMessage(); + } + foreach ($form as $child) { + foreach ($child->getErrors(true) as $error) { + $errors[$child->getName()] = $error->getMessage(); + } + } + if (count($base) > 0) { + $errors['root'] = implode("\n", $base); + } + + return $errors; + } + + public function getFormErrors(): array + { + return $this->errors; + } +} diff --git a/src/Exception/ZipballException.php b/src/Exception/ZipballException.php new file mode 100644 index 00000000..94101bb0 --- /dev/null +++ b/src/Exception/ZipballException.php @@ -0,0 +1,9 @@ +request->set('version', $version ?? 'dev-master'); + $form->handleRequest($request); if (!$form->isSubmitted() || !$form->isValid()) { - throw new \RuntimeException('todo'); + throw ValidationException::create("Validation errors", $form); } - /** @var PushRequestDtoInterface $artifact */ - $artifact = $form->getData(); + /** @var PushRequestDtoInterface $dtoRequest */ + $dtoRequest = $form->getData(); $package = $this->getRepo()->getPackageByName($name); if (null === $package) { $package = $this->createArtifactPackage($name, $user); } - + try { + $this->handler->addVersion($dtoRequest, $package); + } catch (ZipballException $e) { + throw ValidationException::create($e->getMessage(), previous: $e); + } } private function getRepo(): PackageRepository @@ -53,7 +60,12 @@ private function createArtifactPackage(string $name, ?UserInterface $user = null $em = $this->registry->getManager(); $package = new Package(); $package->setName($name); - $package->setRepoType(RepTypes::ARTIFACT); + $package->setRepoType(RepTypes::CUSTOM); + $package->setRepositoryPath(null); + + $user = null !== $user ? + $this->registry->getRepository(User::class)->findOneBy(['username' => $user->getUserIdentifier()]) + : $user; if ($user instanceof User) { $package->addMaintainer($user); diff --git a/src/Form/Model/NexusPushRequestDto.php b/src/Form/Model/NexusPushRequestDto.php index c3c3df0a..73e2c953 100644 --- a/src/Form/Model/NexusPushRequestDto.php +++ b/src/Form/Model/NexusPushRequestDto.php @@ -35,4 +35,17 @@ public function getPackageVersion(): string { return $this->version; } + + public function getSource(): ?array + { + if ($this->srcRef !== null && $this->srcUrl !== null) { + return [ + 'type' => $this->srcType ?? 'git', + 'url' => $this->srcUrl, + 'reference' => $this->srcRef, + ]; + } + + return null; + } } diff --git a/src/Form/Model/PushRequestDtoInterface.php b/src/Form/Model/PushRequestDtoInterface.php index 662b66e5..39fdc05f 100644 --- a/src/Form/Model/PushRequestDtoInterface.php +++ b/src/Form/Model/PushRequestDtoInterface.php @@ -13,4 +13,6 @@ public function getArtifact(): File; public function getPackageName(): string; public function getPackageVersion(): string; + + public function getSource(): ?array; } diff --git a/src/Form/RequestHandler/PutRequestHandler.php b/src/Form/RequestHandler/PutRequestHandler.php index 64b82254..0abcdca6 100644 --- a/src/Form/RequestHandler/PutRequestHandler.php +++ b/src/Form/RequestHandler/PutRequestHandler.php @@ -8,13 +8,15 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\RequestHandlerInterface; use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; #[Exclude] class PutRequestHandler implements RequestHandlerInterface { public function __construct( - private readonly RequestHandlerInterface $requestHandle + private readonly RequestHandlerInterface $requestHandle, + private readonly string $singleFieldName = 'file', ) { } @@ -31,13 +33,25 @@ public function isFileUpload(mixed $data): bool return $data instanceof File; } - protected function decodeMultiPartFormData(Request $request): void + private function decodeMultiPartFormData(Request $request): void { $rawData = $request->getContent(); if (!$rawData) { return; } + $contentType = $request->headers->get('Content-Type'); + if (null === $contentType || !str_contains($contentType, 'multipart/form-data')) { + $this->createUploadedFile( + request: $request, + content: $rawData, + fileName: (string)time(), + fieldName: $this->singleFieldName, + contentType: $contentType + ); + return; + } + $boundary = substr($rawData, 0, strpos($rawData, "\r\n")); $parts = array_slice(explode($boundary, $rawData), 1); foreach ($parts as $part) { @@ -66,23 +80,35 @@ protected function decodeMultiPartFormData(Request $request): void if ($fileName === null) { $request->request->set($fieldName, $content); } else { - $localFileName = tempnam(sys_get_temp_dir(), 'sfy'); - file_put_contents($localFileName, $content); - - $file = [ - 'name' => $fileName, - 'type' => $headers['content-type'], - 'tmp_name' => $localFileName, - 'error' => 0, - 'size' => filesize($localFileName) - ]; - - register_shutdown_function(static function () use ($localFileName) { - @unlink($localFileName); - }); - - $request->files->set($fieldName, $file); + $this->createUploadedFile( + request: $request, + content: $content, + fileName: $fileName, + fieldName: $fieldName, + contentType: $headers['content-type'] ?? null, + ); } } } + + private function createUploadedFile(Request $request, string $content, string $fileName, string $fieldName, ?string $contentType): void + { + $localFileName = tempnam(sys_get_temp_dir(), 'sfy'); + file_put_contents($localFileName, $content); + $contentType ??= 'binary/octet-stream'; + + register_shutdown_function(static function () use ($localFileName) { + @unlink($localFileName); + }); + + $file = new UploadedFile( + path: $localFileName, + originalName: $fileName, + mimeType: $contentType, + error: 0, + test: true + ); + + $request->files->set($fieldName, $file); + } } diff --git a/src/Form/Type/Push/NexusPushType.php b/src/Form/Type/Push/NexusPushType.php index 796ce37a..c6e3bee0 100644 --- a/src/Form/Type/Push/NexusPushType.php +++ b/src/Form/Type/Push/NexusPushType.php @@ -20,10 +20,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('src-type', TextType::class, ['property_path' => 'srcType']) ->add('src-url', TextType::class, ['property_path' => 'srcUrl']) ->add('src-ref', TextType::class, ['property_path' => 'srcRef']) - ->add('package', FileType::class); + ->add('package', FileType::class) + ->add('version', TextType::class); $requestHandler = $builder->getRequestHandler(); - $builder->setRequestHandler(new PutRequestHandler($requestHandler)); + $builder->setRequestHandler(new PutRequestHandler($requestHandler, 'package')); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Model/UploadZipballStorage.php b/src/Model/UploadZipballStorage.php index 199fc598..e0080270 100644 --- a/src/Model/UploadZipballStorage.php +++ b/src/Model/UploadZipballStorage.php @@ -7,6 +7,7 @@ use Doctrine\Persistence\ManagerRegistry; use League\Flysystem\FilesystemOperator; use Packeton\Entity\Zipball; +use Packeton\Exception\ZipballException; use Packeton\Util\PacketonUtils; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\File\File; @@ -67,7 +68,7 @@ public function moveToLocal(Zipball|string $zipOrReference): ?string return $localName; } - public function save(File $file): array + public function save(File $file): Zipball { $mime = $file->getMimeType(); $extension = $this->guessExtension($file, $mime); @@ -76,7 +77,7 @@ public function save(File $file): array // Limited by ArtifactRepository, mimetype will check later, not necessary a strict validation. if (!in_array($extension, $this->supportTypes, true)) { $supportTypes = json_encode($this->supportTypes); - return ['code' => 400, 'error' => "Allowed only $supportTypes archives, but given *.$extension"]; + throw new ZipballException("Allowed only $supportTypes archives, but given *.$extension", 400); } $hash = sha1(random_bytes(30)); @@ -85,13 +86,13 @@ public function save(File $file): array try { $file->move($this->tmpDir, $filename); $fullname = PacketonUtils::buildPath($this->tmpDir, $filename); + if (!$this->artifactStorage->fileExists($filename)) { $stream = fopen($fullname, 'r'); $this->artifactStorage->writeStream($filename, $stream); } - } catch (\Exception $e) { - return ['code' => 400, 'error' => $e->getMessage()]; + throw new ZipballException($e->getMessage(), 400, $e); } $zipball = new Zipball(); @@ -106,11 +107,7 @@ public function save(File $file): array $manager->persist($zipball); $manager->flush(); - return [ - 'id' => $zipball->getId(), - 'filename' => $zipball->getOriginalFilename(), - 'size' => $zipball->getFileSize(), - ]; + return $zipball; } protected function guessExtension(File $file, ?string $mimeType): ?string diff --git a/src/Repository/PackageRepository.php b/src/Repository/PackageRepository.php index 45b3a4cf..4b7f13d5 100644 --- a/src/Repository/PackageRepository.php +++ b/src/Repository/PackageRepository.php @@ -300,16 +300,16 @@ public function getPartialPackageByNameWithVersions($name) return $pkg; } - public function getPackageByName($name) + public function getPackageByName(string $name): ?Package { $qb = $this->getEntityManager()->createQueryBuilder(); $qb->select('p', 'm') - ->from('Packeton\Entity\Package', 'p') + ->from(Package::class, 'p') ->leftJoin('p.maintainers', 'm') ->where('p.name = ?0') ->setParameters(array($name)); - return $qb->getQuery()->getSingleResult(); + return $qb->getQuery()->getOneOrNullResult(); } public function getPackagesWithVersions(?array $ids = null, $filters = []) diff --git a/src/Service/Artifact/ArtifactPushHandler.php b/src/Service/Artifact/ArtifactPushHandler.php index 9e587ee0..16c7bbb7 100644 --- a/src/Service/Artifact/ArtifactPushHandler.php +++ b/src/Service/Artifact/ArtifactPushHandler.php @@ -4,21 +4,97 @@ namespace Packeton\Service\Artifact; +use Composer\IO\IOInterface; +use Composer\Util\Tar; +use Composer\Util\Zip; +use Doctrine\Persistence\ManagerRegistry; use Packeton\Entity\Package; +use Packeton\Entity\Zipball; +use Packeton\Exception\ValidationException; use Packeton\Form\Model\PushRequestDtoInterface; use Packeton\Model\UploadZipballStorage; +use Packeton\Package\RepTypes; +use Packeton\Service\Scheduler; class ArtifactPushHandler { public function __construct( - private readonly UploadZipballStorage $storage + private readonly UploadZipballStorage $storage, + private readonly ManagerRegistry $registry, + private readonly Scheduler $scheduler, ) { } - public function addVersion(PushRequestDtoInterface $requestDto, Package $package, ?string $version = null): void + public function addVersion(PushRequestDtoInterface $requestDto, Package $package): void { - $version ??= 'dev-master'; + $zip = $this->storage->save($requestDto->getArtifact()); + $zip->setUsed(true); - $this->storage->save($requestDto->getArtifact()); + $path = $this->storage->moveToLocal($zip); + if (!file_exists($path)) { + throw new ValidationException("Unable to create archive file", 400); + } + + switch ($package->getRepoType()) { + case RepTypes::ARTIFACT: + $package->setArchiveOverwrite( + $zip->getId(), + $this->generateArchiveData($requestDto) + ); + break; + case RepTypes::CUSTOM: + $package->addCustomVersions( + $this->generateCustomData($requestDto, $zip, $path) + ); + break; + default: + throw new ValidationException("Support only CUSTOM and ARTIFACT repository types", 400); + } + + $manager = $this->registry->getManager(); + $manager->flush(); + + $this->scheduler->scheduleUpdate($package); + } + + private function generateArchiveData(PushRequestDtoInterface $requestDto): array + { + return [ + 'version' => $requestDto->getPackageVersion(), + 'source' => $requestDto->getSource(), + ]; + } + + private function generateCustomData(PushRequestDtoInterface $requestDto, Zipball $zipball, string $path): array + { + $file = new \SplFileInfo($path); + $fileExtension = pathinfo($file->getPathname(), PATHINFO_EXTENSION); + + if (in_array($fileExtension, ['gz', 'tar', 'tgz'], true)) { + $fileType = 'tar'; + } elseif ($fileExtension === 'zip') { + $fileType = 'zip'; + } else { + throw new ValidationException('Files with "'.$fileExtension.'" extensions aren\'t supported. Only ZIP and TAR/TAR.GZ/TGZ archives are supported.'); + } + + try { + $json = $fileType === 'tar' ? Tar::getComposerJson($file->getPathname()) : Zip::getComposerJson($file->getPathname()); + } catch (\Throwable $e) { + throw new ValidationException('Failed loading package '.$file->getPathname().': '.$e->getMessage(), previous: $e); + } + + $json = json_decode($json, true) ?? []; + + $json['version'] = $requestDto->getPackageVersion(); + $json['source'] ??= $requestDto->getSource(); + + unset($json['extra']['push']); + + return [ + 'version' => $requestDto->getPackageVersion(), + 'dist' => $zipball->getId(), + 'definition' => $json, + ]; } } diff --git a/src/Service/SwaggerDumper.php b/src/Service/SwaggerDumper.php index 45c04c83..1fb56e3a 100644 --- a/src/Service/SwaggerDumper.php +++ b/src/Service/SwaggerDumper.php @@ -83,7 +83,14 @@ protected function wrapExamples(array $spec): array foreach ($resources as $name => $resource) { if (isset($resource['example'])) { - $example = ($isRef = str_starts_with($resource['example'], '$')) ? $resource['example'] : json_decode($resource['example']); + $isRef = false; + if (is_string($resource['example'])) { + $example = ($isRef = str_starts_with($resource['example'], '$')) ? $resource['example'] : + (null !== json_decode($resource['example']) ? json_decode($resource['example']) : $resource['example']); + } else { + $example = $resource['example']; + } + $hash = sha1(serialize($example)); unset($resource['example']); if ($example === null) { @@ -115,8 +122,15 @@ protected function wrapExamples(array $spec): array return $spec; } - protected function dumpExample($example) + protected function dumpExample(mixed $example) { + if (is_string($example)) { + return [ + 'type' => 'string', + 'format' => 'binary' + ]; + } + $obj = [ 'type' => 'object', 'properties' => [] diff --git a/src/Util/PacketonUtils.php b/src/Util/PacketonUtils.php index 10a43159..58c99623 100644 --- a/src/Util/PacketonUtils.php +++ b/src/Util/PacketonUtils.php @@ -216,7 +216,15 @@ public static function buildPath(string $baseDir, ...$paths): string public static function buildChoices(array $listOf, string $key, ?string $value = null): array { - return array_combine(array_column($listOf, $key), $value ? array_column($listOf, $value) : $listOf); + $result = []; + foreach ($listOf as $item) { + if (!isset($item[$key])) { + continue; + } + $result[$item[$key]] = null === $value ? $item : ($item[$value] ?? null); + } + + return $result; } public static function matchGlobAll(array $listOf, null|string|array $globs, string|array|null $excluded = null): array diff --git a/swagger/packages-api.yaml b/swagger/packages-api.yaml index ec42fcf6..c259dc16 100644 --- a/swagger/packages-api.yaml +++ b/swagger/packages-api.yaml @@ -51,7 +51,7 @@ paths: tags: [ Packages ] summary: 'Create a package' example: $PackageCreate - + '/api/packages': get: tags: [ Packages ] @@ -84,12 +84,26 @@ paths: example: $PackageUpdate <<: *pkg-param + '/api/packages/upload/{name}/{version}': + put: + tags: [ Packages ] + summary: 'Put a new version to package and upload zip artifact. Support custom JSON and artifact package type. Request body is ' + parameters: + - name: name + in: path + required: true + type: string + - name: version + description: version, example v2.8.0 + in: query + example: "" + '/api/packages/{name}/dependents': get: tags: [ Packages ] summary: 'View the dependents of a package' <<: *pkg-param - + '/api/packages/{name}/changelog': get: tags: [ Packages ]