From 4317bf3d32fdbc23be46799c7f49c730cb755058 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 Aug 2025 21:13:22 +0200 Subject: [PATCH 1/2] use Attempt to expose errors --- CHANGELOG.md | 7 +++++++ src/Control/Permissions.php | 20 +++++++++++--------- src/Control/Users.php | 20 +++++++++++--------- src/Control/VHosts.php | 20 +++++++++++--------- 4 files changed, 40 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5c4f21..548348e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ - Drop support for PHP `8.1` - Requires `innmind/foundation:^1.7.1` +- The following methods now return an `Innmind\Immutable\Attempt`: + - `Innmind\RabbitMQ\Management\Control\Permissions::declare()` + - `Innmind\RabbitMQ\Management\Control\Permissions::delete()` + - `Innmind\RabbitMQ\Management\Control\Users::declare()` + - `Innmind\RabbitMQ\Management\Control\Users::delete()` + - `Innmind\RabbitMQ\Management\Control\VHosts::declare()` + - `Innmind\RabbitMQ\Management\Control\VHosts::delete()` ### Fixed diff --git a/src/Control/Permissions.php b/src/Control/Permissions.php index a49ebb8..1557db4 100644 --- a/src/Control/Permissions.php +++ b/src/Control/Permissions.php @@ -8,7 +8,7 @@ Server\Command, }; use Innmind\Immutable\{ - Maybe, + Attempt, SideEffect, }; @@ -29,7 +29,7 @@ public static function of(Server $server): self } /** - * @return Maybe + * @return Attempt */ public function declare( string $vhost, @@ -37,7 +37,7 @@ public function declare( string $configure, string $write, string $read, - ): Maybe { + ): Attempt { return $this ->server ->processes() @@ -52,15 +52,16 @@ public function declare( ->withArgument('write='.$write) ->withArgument('read='.$read), ) - ->maybe() - ->flatMap(static fn($process) => $process->wait()->maybe()) + ->flatMap(static fn($process) => $process->wait()->attempt( + static fn($error) => new \RuntimeException($error::class), + )) ->map(static fn() => new SideEffect); } /** - * @return Maybe + * @return Attempt */ - public function delete(string $vhost, string $user): Maybe + public function delete(string $vhost, string $user): Attempt { return $this ->server @@ -73,8 +74,9 @@ public function delete(string $vhost, string $user): Maybe ->withArgument('vhost='.$vhost) ->withArgument('user='.$user), ) - ->maybe() - ->flatMap(static fn($process) => $process->wait()->maybe()) + ->flatMap(static fn($process) => $process->wait()->attempt( + static fn($error) => new \RuntimeException($error::class), + )) ->map(static fn() => new SideEffect); } } diff --git a/src/Control/Users.php b/src/Control/Users.php index 5647398..dc7da88 100644 --- a/src/Control/Users.php +++ b/src/Control/Users.php @@ -8,7 +8,7 @@ Server\Command, }; use Innmind\Immutable\{ - Maybe, + Attempt, SideEffect, }; @@ -29,9 +29,9 @@ public static function of(Server $server): self } /** - * @return Maybe + * @return Attempt */ - public function declare(string $name, string $password, string ...$tags): Maybe + public function declare(string $name, string $password, string ...$tags): Attempt { return $this ->server @@ -45,15 +45,16 @@ public function declare(string $name, string $password, string ...$tags): Maybe ->withArgument('password='.$password) ->withArgument('tags='.\implode(',', $tags)), ) - ->maybe() - ->flatMap(static fn($process) => $process->wait()->maybe()) + ->flatMap(static fn($process) => $process->wait()->attempt( + static fn($error) => new \RuntimeException($error::class), + )) ->map(static fn() => new SideEffect); } /** - * @return Maybe + * @return Attempt */ - public function delete(string $name): Maybe + public function delete(string $name): Attempt { return $this ->server @@ -65,8 +66,9 @@ public function delete(string $name): Maybe ->withArgument('user') ->withArgument('name='.$name), ) - ->maybe() - ->flatMap(static fn($process) => $process->wait()->maybe()) + ->flatMap(static fn($process) => $process->wait()->attempt( + static fn($error) => new \RuntimeException($error::class), + )) ->map(static fn() => new SideEffect); } } diff --git a/src/Control/VHosts.php b/src/Control/VHosts.php index e2303b8..d7e8612 100644 --- a/src/Control/VHosts.php +++ b/src/Control/VHosts.php @@ -8,7 +8,7 @@ Server\Command, }; use Innmind\Immutable\{ - Maybe, + Attempt, SideEffect, }; @@ -29,9 +29,9 @@ public static function of(Server $server): self } /** - * @return Maybe + * @return Attempt */ - public function declare(string $name): Maybe + public function declare(string $name): Attempt { return $this ->server @@ -43,15 +43,16 @@ public function declare(string $name): Maybe ->withArgument('vhost') ->withArgument('name='.$name), ) - ->maybe() - ->flatMap(static fn($process) => $process->wait()->maybe()) + ->flatMap(static fn($process) => $process->wait()->attempt( + static fn($error) => new \RuntimeException($error::class), + )) ->map(static fn() => new SideEffect); } /** - * @return Maybe + * @return Attempt */ - public function delete(string $name): Maybe + public function delete(string $name): Attempt { return $this ->server @@ -63,8 +64,9 @@ public function delete(string $name): Maybe ->withArgument('vhost') ->withArgument('name='.$name), ) - ->maybe() - ->flatMap(static fn($process) => $process->wait()->maybe()) + ->flatMap(static fn($process) => $process->wait()->attempt( + static fn($error) => new \RuntimeException($error::class), + )) ->map(static fn() => new SideEffect); } } From 943ac89896e7df3920e034c7eb0b9962e771bad5 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 Aug 2025 22:35:25 +0200 Subject: [PATCH 2/2] validate command outputs --- src/Status.php | 708 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 529 insertions(+), 179 deletions(-) diff --git a/src/Status.php b/src/Status.php index 675c09e..0f68295 100644 --- a/src/Status.php +++ b/src/Status.php @@ -29,7 +29,6 @@ }; use Innmind\TimeContinuum\{ Clock, - PointInTime, Format, }; use Innmind\Url\Authority\{ @@ -76,17 +75,36 @@ public static function of( */ public function users(): Sequence { - /** @var Sequence */ - $users = $this->list('users'); - - return $users->map(static fn($user) => User::of( - User\Name::of($user['name']), - User\Password::of( - $user['password_hash'], - $user['hashing_algorithm'], - ), - ...\explode(',', $user['tags']), - )); + /** @psalm-suppress MixedArgument */ + return $this + ->list('users') + ->map( + Is::shape( + 'name', + Is::string()->map(User\Name::of(...)), + ) + ->with( + 'password_hash', + Is::string(), + ) + ->with( + 'hashing_algorithm', + Is::string(), + ) + ->with( + 'tags', + Is::string(), + ) + ->map(static fn($shape) => User::of( + $shape['name'], + User\Password::of( + $shape['password_hash'], + $shape['hashing_algorithm'], + ), + ...\explode(',', $shape['tags']), + )), + ) + ->flatMap(static fn($user) => $user->maybe()->toSequence()); } /** @@ -94,18 +112,50 @@ public function users(): Sequence */ public function vhosts(): Sequence { - /** @var Sequence */ - $vhosts = $this->list('vhosts'); - - return $vhosts->map(static fn($vhost) => VHost::of( - VHost\Name::of($vhost['name']), - VHost\Messages::of( - Count::of($vhost['messages']), - Count::of($vhost['messages_ready']), - Count::of($vhost['messages_unacknowledged']), - ), - $vhost['tracing'], - )); + /** @psalm-suppress MixedArgument */ + return $this + ->list('vhosts') + ->map( + Is::shape( + 'name', + Is::string()->map(VHost\Name::of(...)), + ) + ->with( + 'messages', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'messages_ready', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'messages_unacknowledged', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'tracing', + Is::bool(), + ) + ->map(static fn($shape) => VHost::of( + $shape['name'], + VHost\Messages::of( + $shape['messages'], + $shape['messages_ready'], + $shape['messages_unacknowledged'], + ), + $shape['tracing'], + )), + ) + ->flatMap(static fn($vhost) => $vhost->maybe()->toSequence()); } /** @@ -113,51 +163,112 @@ public function vhosts(): Sequence */ public function connections(): Sequence { - /** @var Sequence */ - $connections = $this->list('connections'); - - /** @psalm-suppress ArgumentTypeCoercion */ - return $connections + /** @psalm-suppress MixedArgument */ + return $this + ->list('connections') ->map( - fn($connection) => Maybe::all( - $this - ->clock - ->at( - \date( - \DateTime::ATOM, - (int) \round($connection['connected_at'] / 1000), - ), - Format::iso8601(), - ), - Node\Name::maybe($connection['node']), + Is::shape( + 'name', + Is::string()->map(Connection\Name::of(...)), ) - ->map(static fn(PointInTime $connectedAt, Node\Name $node) => Connection::of( - Connection\Name::of($connection['name']), - $connectedAt, - Timeout::of($connection['timeout']), - VHost\Name::of($connection['vhost']), - User\Name::of($connection['user']), - Protocol::of($connection['protocol']), - AuthenticationMechanism::of($connection['auth_mechanism']), - $connection['ssl'], + ->with( + 'connected_at', + Is::int() + ->map(static fn($value) => (string) (int) ($value / 1000)) + ->map( + $this + ->clock + ->ofFormat(Format::of('U')) + ->at(...), + ) + ->and(Is::just()), + ) + ->with( + 'timeout', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Timeout::of(...)), + ) + ->with( + 'vhost', + Is::string()->map(VHost\Name::of(...)), + ) + ->with( + 'user', + Is::string()->map(User\Name::of(...)), + ) + ->with( + 'protocol', + Is::string()->map(Protocol::of(...)), + ) + ->with( + 'auth_mechanism', + Is::string()->map(AuthenticationMechanism::of(...)), + ) + ->with( + 'ssl', + Is::bool(), + ) + ->with( + 'peer_host', + Is::string()->map(Host::of(...)), + ) + ->with( + 'peer_port', + Is::int()->map(Port::of(...)), + ) + ->with( + 'host', + Is::string()->map(Host::of(...)), + ) + ->with( + 'port', + Is::int()->map(Port::of(...)), + ) + ->with( + 'node', + Is::string() + ->map(Node\Name::maybe(...)) + ->and(Is::just()), + ) + ->with( + 'type', + Is::value('network') + ->map(static fn() => Connection\Type::network) + ->or(Is::value('direct')->map( + static fn() => Connection\Type::direct, + )), + ) + ->with( + 'state', + Is::value('running') + ->map(static fn() => State::running) + ->or(Is::value('idle')->map( + static fn() => State::idle, + )), + ) + ->map(static fn($shape) => Connection::of( + $shape['name'], + $shape['connected_at'], + $shape['timeout'], + $shape['vhost'], + $shape['user'], + $shape['protocol'], + $shape['auth_mechanism'], + $shape['ssl'], Peer::of( - Host::of($connection['peer_host']), - Port::of($connection['peer_port']), + $shape['peer_host'], + $shape['peer_port'], ), - Host::of($connection['host']), - Port::of($connection['port']), - $node, - match ($connection['type']) { - 'network' => Connection\Type::network, - 'direct' => Connection\Type::direct, - }, - match ($connection['state']) { - 'running' => State::running, - 'idle' => State::idle, - }, + $shape['host'], + $shape['port'], + $shape['node'], + $shape['type'], + $shape['state'], )), ) - ->flatMap(static fn($maybe) => $maybe->toSequence()); + ->flatMap(static fn($connection) => $connection->maybe()->toSequence()); } /** @@ -165,22 +276,54 @@ public function connections(): Sequence */ public function exchanges(): Sequence { - /** @var Sequence */ - $exchanges = $this->list('exchanges'); - - return $exchanges->map(static fn($exchange) => Exchange::of( - Exchange\Name::of($exchange['name']), - VHost\Name::of($exchange['vhost']), - match ($exchange['type']) { - 'topic' => Exchange\Type::topic, - 'headers' => Exchange\Type::headers, - 'direct' => Exchange\Type::direct, - 'fanout' => Exchange\Type::fanout, - }, - $exchange['durable'], - $exchange['auto_delete'], - $exchange['internal'], - )); + /** @psalm-suppress MixedArgument */ + return $this + ->list('exchanges') + ->map( + Is::shape( + 'name', + Is::string()->map(Exchange\Name::of(...)), + ) + ->with( + 'vhost', + Is::string()->map(VHost\Name::of(...)), + ) + ->with( + 'type', + Is::value('topic') + ->map(static fn() => Exchange\Type::topic) + ->or(Is::value('headers')->map( + static fn() => Exchange\Type::headers, + )) + ->or(Is::value('direct')->map( + static fn() => Exchange\Type::direct, + )) + ->or(Is::value('fanout')->map( + static fn() => Exchange\Type::fanout, + )), + ) + ->with( + 'durable', + Is::bool(), + ) + ->with( + 'auto_delete', + Is::bool(), + ) + ->with( + 'internal', + Is::bool(), + ) + ->map(static fn($shape) => Exchange::of( + $shape['name'], + $shape['vhost'], + $shape['type'], + $shape['durable'], + $shape['auto_delete'], + $shape['internal'], + )), + ) + ->flatMap(static fn($exchange) => $exchange->maybe()->toSequence()); } /** @@ -188,16 +331,39 @@ public function exchanges(): Sequence */ public function permissions(): Sequence { - /** @var Sequence */ - $permissions = $this->list('permissions'); - - return $permissions->map(static fn($permission) => Permission::of( - User\Name::of($permission['user']), - VHost\Name::of($permission['vhost']), - $permission['configure'], - $permission['write'], - $permission['read'], - )); + /** @psalm-suppress MixedArgument */ + return $this + ->list('permissions') + ->map( + Is::shape( + 'user', + Is::string()->map(User\Name::of(...)), + ) + ->with( + 'vhost', + Is::string()->map(VHost\Name::of(...)), + ) + ->with( + 'configure', + Is::string(), + ) + ->with( + 'write', + Is::string(), + ) + ->with( + 'read', + Is::string(), + ) + ->map(static fn($shape) => Permission::of( + $shape['user'], + $shape['vhost'], + $shape['configure'], + $shape['write'], + $shape['read'], + )), + ) + ->flatMap(static fn($permission) => $permission->maybe()->toSequence()); } /** @@ -205,38 +371,107 @@ public function permissions(): Sequence */ public function channels(): Sequence { - /** @var Sequence */ - $channels = $this->list('channels'); - - return $channels - ->map(fn($channel) => Node\Name::maybe($channel['node'])->map( - fn($node) => Channel::of( - Channel\Name::of($channel['name']), - VHost\Name::of($channel['vhost']), - User\Name::of($channel['user']), - $channel['number'], - $node, - match ($channel['state']) { - 'running' => State::running, - 'idle' => State::idle, - }, - Channel\Messages::of( - Count::of($channel['messages_uncommitted']), - Count::of($channel['messages_unconfirmed']), - Count::of($channel['messages_unacknowledged']), - ), - Count::of($channel['consumer_count']), - $channel['confirm'], - $channel['transactional'], - Maybe::of($channel['idle_since'] ?? null)->flatMap( - $this - ->clock - ->ofFormat(Format::of('Y-m-d G:i:s')) - ->at(...), - ), - ), - )) - ->flatMap(static fn($maybe) => $maybe->toSequence()); + /** @psalm-suppress MixedArgument */ + return $this + ->list('channels') + ->map( + Is::shape( + 'name', + Is::string()->map(Channel\Name::of(...)), + ) + ->with( + 'vhost', + Is::string()->map(VHost\Name::of(...)), + ) + ->with( + 'user', + Is::string()->map(User\Name::of(...)), + ) + ->with( + 'number', + Is::int(), + ) + ->with( + 'node', + Is::string() + ->map(Node\Name::maybe(...)) + ->and(Is::just()), + ) + ->with( + 'state', + Is::value('running') + ->map(static fn() => State::running) + ->or(Is::value('idle')->map( + static fn() => State::idle, + )), + ) + ->with( + 'messages_uncommitted', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'messages_unconfirmed', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'messages_unacknowledged', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'consumer_count', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'confirm', + Is::bool(), + ) + ->with( + 'transactional', + Is::bool(), + ) + ->optional( + 'idle_since', + Is::string() + ->nonEmpty() + ->map( + $this + ->clock + ->ofFormat(Format::of('Y-m-d G:i:s')) + ->at(...), + ), + ) + ->default('idle_since', Maybe::nothing()) + ->map(static fn($shape) => Channel::of( + $shape['name'], + $shape['vhost'], + $shape['user'], + $shape['number'], + $shape['node'], + $shape['state'], + Channel\Messages::of( + $shape['messages_uncommitted'], + $shape['messages_unconfirmed'], + $shape['messages_unacknowledged'], + ), + $shape['consumer_count'], + $shape['confirm'], + $shape['transactional'], + $shape['idle_since'], + )), + ) + ->flatMap(static fn($channel) => $channel->maybe()->toSequence()); } /** @@ -244,20 +479,57 @@ public function channels(): Sequence */ public function consumers(): Sequence { - /** @var Sequence */ - $consumers = $this->list('consumers'); - - return $consumers->map(static fn($consumer) => Consumer::of( - Tag::of($consumer['consumer_tag']), - Channel\Name::of($consumer['channel_details']['name']), - Identity::of( - $consumer['queue']['name'], - VHost\Name::of($consumer['queue']['vhost']), - ), - Connection\Name::of($consumer['channel_details']['connection_name']), - $consumer['ack_required'], - $consumer['exclusive'], - )); + /** @psalm-suppress MixedArgument,MixedArrayAccess */ + return $this + ->list('consumers') + ->map( + Is::shape( + 'consumer_tag', + Is::string()->map(Tag::of(...)), + ) + ->with( + 'channel_details', + Is::shape( + 'name', + Is::string()->map(Channel\Name::of(...)), + ) + ->with( + 'connection_name', + Is::string()->map(Connection\Name::of(...)), + ), + ) + ->with( + 'queue', + Is::shape( + 'name', + Is::string(), + ) + ->with( + 'vhost', + Is::string()->map(VHost\Name::of(...)), + ), + ) + ->with( + 'ack_required', + Is::bool(), + ) + ->with( + 'exclusive', + Is::bool(), + ) + ->map(static fn($shape) => Consumer::of( + $shape['consumer_tag'], + $shape['channel_details']['name'], + Identity::of( + $shape['queue']['name'], + $shape['queue']['vhost'], + ), + $shape['channel_details']['connection_name'], + $shape['ack_required'], + $shape['exclusive'], + )), + ) + ->flatMap(static fn($consumer) => $consumer->maybe()->toSequence()); } /** @@ -265,35 +537,102 @@ public function consumers(): Sequence */ public function queues(): Sequence { - /** @var Sequence */ - $queues = $this->list('queues'); - - return $queues->map(fn($queue) => Queue::of( - Identity::of( - $queue['name'], - VHost\Name::of($queue['vhost']), - ), - Queue\Messages::of( - Count::of($queue['messages']), - Count::of($queue['messages_ready']), - Count::of($queue['messages_unacknowledged']), - ), - Maybe::of($queue['idle_since'] ?? null)->flatMap( - $this - ->clock - ->ofFormat(Format::of('Y-m-d G:i:s')) - ->at(...), - ), - Count::of($queue['consumers']), - match ($queue['state']) { - 'running' => State::running, - 'idle' => State::idle, - }, - Node\Name::of($queue['node']), - $queue['exclusive'], - $queue['auto_delete'], - $queue['durable'], - )); + /** @psalm-suppress MixedArgument */ + return $this + ->list('queues') + ->map( + Is::shape( + 'name', + Is::string(), + ) + ->with( + 'vhost', + Is::string()->map(VHost\Name::of(...)), + ) + ->with( + 'messages', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'messages_ready', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'messages_unacknowledged', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->optional( + 'idle_since', + Is::string() + ->nonEmpty() + ->map( + $this + ->clock + ->ofFormat(Format::of('Y-m-d G:i:s')) + ->at(...), + ), + ) + ->default('idle_since', Maybe::nothing()) + ->with( + 'consumers', + Is::int() + ->positive() + ->or(Is::value(0)) + ->map(Count::of(...)), + ) + ->with( + 'state', + Is::value('running') + ->map(static fn() => State::running) + ->or(Is::value('idle')->map( + static fn() => State::idle, + )), + ) + ->with( + 'node', + Is::string()->map(Node\Name::of(...)), + ) + ->with( + 'exclusive', + Is::bool(), + ) + ->with( + 'auto_delete', + Is::bool(), + ) + ->with( + 'durable', + Is::bool(), + ) + ->map(static fn($shape) => Queue::of( + Identity::of( + $shape['name'], + $shape['vhost'], + ), + Queue\Messages::of( + $shape['messages'], + $shape['messages_ready'], + $shape['messages_unacknowledged'], + ), + $shape['idle_since'], + $shape['consumers'], + $shape['state'], + $shape['node'], + $shape['exclusive'], + $shape['auto_delete'], + $shape['durable'], + )), + ) + ->flatMap(static fn($queue) => $queue->maybe()->toSequence()); } /** @@ -301,21 +640,32 @@ public function queues(): Sequence */ public function nodes(): Sequence { - /** @var Sequence */ - $nodes = $this->list('nodes'); - - return $nodes - ->map(static fn($node) => Node\Name::maybe($node['name'])->map( - static fn($name) => Node::of( - $name, - match ($node['type']) { - 'disc' => Node\Type::disc, - 'ram' => Node\Type::ram, - }, - $node['running'], - ), - )) - ->flatMap(static fn($maybe) => $maybe->toSequence()); + /** @psalm-suppress MixedArgument */ + return $this + ->list('nodes') + ->map( + Is::shape( + 'name', + Is::string() + ->map(Node\Name::maybe(...)) + ->and(Is::just()), + ) + ->with( + 'type', + Is::value('disc') + ->map(static fn() => Node\Type::disc) + ->or(Is::value('ram')->map( + static fn() => Node\Type::ram, + )), + ) + ->with('running', Is::bool()) + ->map(static fn($shape) => Node::of( + $shape['name'], + $shape['type'], + $shape['running'], + )), + ) + ->flatMap(static fn($node) => $node->maybe()->toSequence()); } /**