A project with a board now carries its key, its address and the address of the board's public showcase in the public API, so the website can show its roadmap, releases and tickets for newcomers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
3.1 KiB
PHP
106 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\TaskStream;
|
|
|
|
use Illuminate\Http\Client\ConnectionException;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* The few calls this application makes to TaskStream's REST API.
|
|
*
|
|
* Every call is made as somebody: TaskStream accepts the same Pireon tokens
|
|
* this application does, and whoever's token it is becomes the owner of
|
|
* what is created.
|
|
*/
|
|
class TaskStreamClient
|
|
{
|
|
public function __construct(
|
|
protected string $apiUrl,
|
|
protected string $webUrl,
|
|
protected ?string $serviceToken = null,
|
|
protected int $timeout = 10,
|
|
) {}
|
|
|
|
/**
|
|
* @param array<string, mixed> $config
|
|
*/
|
|
public static function fromConfig(array $config): self
|
|
{
|
|
return new self(
|
|
apiUrl: rtrim((string) ($config['api_url'] ?? ''), '/'),
|
|
webUrl: rtrim((string) ($config['web_url'] ?? ''), '/'),
|
|
serviceToken: filled($config['token'] ?? null) ? $config['token'] : null,
|
|
timeout: (int) ($config['timeout'] ?? 10),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The token for requests that do not carry a Pireon token of their own.
|
|
*/
|
|
public function serviceToken(): ?string
|
|
{
|
|
return $this->serviceToken;
|
|
}
|
|
|
|
/**
|
|
* @return array{id: string, key: string}
|
|
*
|
|
* @throws TaskStreamException
|
|
*/
|
|
public function createProject(string $token, string $key, string $name, ?string $description, bool $public): array
|
|
{
|
|
try {
|
|
$response = Http::withToken($token)
|
|
->acceptJson()
|
|
->timeout($this->timeout)
|
|
->post("{$this->apiUrl}/api/projects", array_filter([
|
|
'key' => $key,
|
|
'name' => $name,
|
|
'description' => $description,
|
|
'isPublic' => $public,
|
|
], fn ($value) => $value !== null));
|
|
} catch (ConnectionException $e) {
|
|
throw TaskStreamException::unreachable($this->apiUrl, $e);
|
|
}
|
|
|
|
if ($response->failed()) {
|
|
// Nest answers `message` as a string, or as a list from validation.
|
|
$message = $response->json('message');
|
|
|
|
throw new TaskStreamException(
|
|
is_array($message) ? implode(' ', $message) : (string) ($message ?: $response->reason()),
|
|
status: $response->status(),
|
|
);
|
|
}
|
|
|
|
return ['id' => (string) $response->json('id'), 'key' => (string) $response->json('key')];
|
|
}
|
|
|
|
/**
|
|
* Where people open a project's board.
|
|
*/
|
|
public function projectUrl(string $key): string
|
|
{
|
|
return "{$this->webUrl}/p/{$key}";
|
|
}
|
|
|
|
/**
|
|
* The public summary of a board (roadmap, releases, sprint, tickets for
|
|
* newcomers). Anyone may read it, so the public site can ask directly.
|
|
*/
|
|
public function showcaseUrl(string $key): string
|
|
{
|
|
return "{$this->apiUrl}/api/explore/projects/{$key}/showcase";
|
|
}
|
|
|
|
/**
|
|
* Where the paths of a showcase (`/p/KEY/issues/KEY-3`) are opened.
|
|
*/
|
|
public function webUrl(): string
|
|
{
|
|
return $this->webUrl;
|
|
}
|
|
}
|