You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by pi...@apache.org on 2026/04/02 07:41:16 UTC
(iggy) branch master updated: refactor(server,sdk): replace ServerCommand/BytesSerializable with wire types (#3041)
This is an automated email from the ASF dual-hosted git repository.
piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new cea344cd8 refactor(server,sdk): replace ServerCommand/BytesSerializable with wire types (#3041)
cea344cd8 is described below
commit cea344cd8837f75e32f24f2ad3aaa315b60b12f5
Author: Hubert Gruszecki <h....@gmail.com>
AuthorDate: Thu Apr 2 09:41:12 2026 +0200
refactor(server,sdk): replace ServerCommand/BytesSerializable with wire types (#3041)
The binary protocol layer had three redundant serialization
paths: ServerCommand enum + binary_mapper (hand-rolled
command dispatch), BytesSerializable trait (domain type
serialization), and WireEncode/WireDecode (wire protocol
types). All three did the same job with different APIs.
This PR collapses them into one: wire types flow from
transport through shard dispatch to handlers. The
ServerCommand enum, binary_mapper, 45+ command structs,
and BytesSerializable trait are deleted entirely.
Changes:
- Route wire types through ShardRequestPayload and SDK
binary impls, eliminating ServerCommand dispatch
- Delete 45+ command structs from iggy_common/commands/,
replace with thin HTTP DTOs in iggy_common/http/
- Switch WAL (EntryCommand/StateEntry) to WireEncode/
WireDecode, delete BytesSerializable from WAL path
- Remove all 21 BytesSerializable impls and delete the
trait itself
- SDK poll/send now use PollMessagesRequest and
SendMessagesEncoder (zero-copy) instead of
intermediate BytesSerializable allocations
- Add wire conversion functions and response-to-domain
conversions in wire_conversions.rs
---
Cargo.lock | 217 ++--
DEPENDENCIES.md | 86 +-
core/binary_protocol/src/lib.rs | 7 +
core/binary_protocol/src/primitives/mod.rs | 1 +
.../binary_protocol/src/primitives/user_headers.rs | 544 +++++++++++
core/cli/Cargo.toml | 2 +-
core/cli/src/args/permissions/mod.rs | 8 +-
core/cli/src/args/permissions/stream.rs | 10 +-
core/cli/src/commands/binary_client/get_client.rs | 20 +-
core/cli/src/commands/binary_client/get_clients.rs | 8 +-
.../binary_consumer_groups/get_consumer_group.rs | 23 +-
.../binary_consumer_groups/get_consumer_groups.rs | 19 +-
.../binary_consumer_offsets/get_consumer_offset.rs | 53 +-
.../binary_consumer_offsets/set_consumer_offset.rs | 43 +-
.../src/commands/binary_message/poll_messages.rs | 13 +-
.../src/commands/binary_message/send_messages.rs | 8 +-
.../get_personal_access_tokens.rs | 7 +-
core/cli/src/commands/binary_streams/get_stream.rs | 20 +-
.../cli/src/commands/binary_streams/get_streams.rs | 8 +-
core/cli/src/commands/binary_system/me.rs | 14 +-
core/cli/src/commands/binary_system/ping.rs | 7 +-
core/cli/src/commands/binary_system/snapshot.rs | 36 +-
core/cli/src/commands/binary_system/stats.rs | 8 +-
core/cli/src/commands/binary_topics/get_topic.rs | 18 +-
core/cli/src/commands/binary_topics/get_topics.rs | 19 +-
core/cli/src/commands/binary_users/get_user.rs | 15 +-
core/cli/src/commands/binary_users/get_users.rs | 8 +-
core/common/Cargo.toml | 1 -
.../consumer_groups/create_consumer_group.rs | 189 ----
.../consumer_groups/delete_consumer_group.rs | 147 ---
.../commands/consumer_groups/get_consumer_group.rs | 147 ---
.../consumer_groups/get_consumer_groups.rs | 126 ---
.../consumer_groups/join_consumer_group.rs | 147 ---
.../consumer_groups/leave_consumer_group.rs | 147 ---
.../consumer_offsets/delete_consumer_offset.rs | 220 -----
.../consumer_offsets/get_consumer_offset.rs | 225 -----
.../consumer_offsets/store_consumer_offset.rs | 240 -----
.../src/commands/messages/flush_unsaved_buffer.rs | 132 ---
core/common/src/commands/messages/poll_messages.rs | 341 -------
.../src/commands/partitions/create_partitions.rs | 167 ----
.../src/commands/partitions/delete_partitions.rs | 167 ----
.../create_personal_access_token.rs | 157 ---
.../delete_personal_access_token.rs | 156 ---
.../get_personal_access_tokens.rs | 86 --
.../login_with_personal_access_token.rs | 173 ----
.../src/commands/segments/delete_segments.rs | 202 ----
core/common/src/commands/streams/create_stream.rs | 153 ---
core/common/src/commands/streams/delete_stream.rs | 99 --
core/common/src/commands/streams/get_stream.rs | 99 --
core/common/src/commands/streams/get_streams.rs | 86 --
core/common/src/commands/streams/purge_stream.rs | 102 --
core/common/src/commands/streams/update_stream.rs | 151 ---
core/common/src/commands/system/get_client.rs | 113 ---
core/common/src/commands/system/get_clients.rs | 86 --
.../src/commands/system/get_cluster_metadata.rs | 59 --
core/common/src/commands/system/get_me.rs | 86 --
core/common/src/commands/system/get_snapshot.rs | 168 ----
core/common/src/commands/system/get_stats.rs | 86 --
core/common/src/commands/system/ping.rs | 87 --
core/common/src/commands/topics/create_topic.rs | 301 ------
core/common/src/commands/topics/delete_topic.rs | 126 ---
core/common/src/commands/topics/get_topic.rs | 126 ---
core/common/src/commands/topics/get_topics.rs | 99 --
core/common/src/commands/topics/purge_topic.rs | 124 ---
core/common/src/commands/topics/update_topic.rs | 302 ------
core/common/src/commands/users/change_password.rs | 246 -----
core/common/src/commands/users/create_user.rs | 347 -------
core/common/src/commands/users/delete_user.rs | 99 --
core/common/src/commands/users/get_user.rs | 99 --
core/common/src/commands/users/get_users.rs | 88 --
core/common/src/commands/users/login_user.rs | 399 --------
core/common/src/commands/users/logout_user.rs | 87 --
.../src/commands/users/update_permissions.rs | 189 ----
core/common/src/commands/users/update_user.rs | 231 -----
core/common/src/error/iggy_error.rs | 2 +
.../http/consumer_groups/create_consumer_group.rs | 60 ++
.../http/consumer_groups/delete_consumer_group.rs | 46 +
.../src/{commands => http}/consumer_groups/mod.rs | 6 +-
.../consumer_offsets/delete_consumer_offset.rs} | 13 +-
.../consumer_offsets/get_consumer_offset.rs} | 16 +-
.../src/{commands => http}/consumer_offsets/mod.rs | 0
.../consumer_offsets/store_consumer_offset.rs} | 16 +-
.../messages/flush_unsaved_buffer.rs} | 9 +-
core/common/src/{commands => http}/messages/mod.rs | 0
core/common/src/http/messages/poll_messages.rs | 89 ++
.../{commands => http}/messages/send_messages.rs | 131 +--
core/common/src/{commands => http}/mod.rs | 0
.../src/http/partitions/create_partitions.rs | 60 ++
.../src/http/partitions/delete_partitions.rs | 60 ++
.../src/{commands => http}/partitions/mod.rs | 2 +-
.../create_personal_access_token.rs | 57 ++
.../delete_personal_access_token.rs | 52 +
.../login_with_personal_access_token.rs} | 14 +-
.../personal_access_tokens/mod.rs | 1 -
core/common/src/http/segments/delete_segments.rs | 59 ++
core/common/src/{commands => http}/segments/mod.rs | 0
core/common/src/http/streams/create_stream.rs | 49 +
.../cluster.rs => http/streams/delete_stream.rs} | 27 +-
core/common/src/{commands => http}/streams/mod.rs | 4 +-
.../cluster.rs => http/streams/purge_stream.rs} | 27 +-
core/common/src/http/streams/update_stream.rs | 55 ++
.../mod.rs => http/system/get_snapshot.rs} | 15 +-
core/common/src/{commands => http}/system/mod.rs | 6 -
core/common/src/http/topics/create_topic.rs | 89 ++
.../topics/delete_topic.rs} | 39 +-
core/common/src/{commands => http}/topics/mod.rs | 5 +-
.../segments.rs => http/topics/purge_topic.rs} | 41 +-
core/common/src/http/topics/update_topic.rs | 86 ++
core/common/src/http/users/change_password.rs | 74 ++
core/common/src/http/users/create_user.rs | 76 ++
.../src/{commands => http}/users/defaults.rs | 0
.../cluster.rs => http/users/delete_user.rs} | 27 +-
core/common/src/http/users/login_user.rs | 55 ++
core/common/src/{commands => http}/users/mod.rs | 3 -
.../users/update_permissions.rs} | 41 +-
core/common/src/http/users/update_user.rs | 55 ++
core/common/src/lib.rs | 29 +-
core/common/src/traits/binary_impls/cluster.rs | 19 +-
.../src/traits/binary_impls/consumer_groups.rs | 138 ++-
.../src/traits/binary_impls/consumer_offsets.rs | 82 +-
core/common/src/traits/binary_impls/messages.rs | 82 +-
core/common/src/traits/binary_impls/mod.rs | 11 +
core/common/src/traits/binary_impls/partitions.rs | 43 +-
.../traits/binary_impls/personal_access_tokens.rs | 71 +-
core/common/src/traits/binary_impls/segments.rs | 26 +-
core/common/src/traits/binary_impls/streams.rs | 86 +-
core/common/src/traits/binary_impls/system.rs | 75 +-
core/common/src/traits/binary_impls/topics.rs | 140 ++-
core/common/src/traits/binary_impls/users.rs | 151 ++-
core/common/src/traits/binary_mapper.rs | 1033 --------------------
core/common/src/traits/binary_transport.rs | 4 +-
core/common/src/traits/mod.rs | 2 -
core/common/src/types/cluster/metadata.rs | 95 +-
core/common/src/types/cluster/node.rs | 98 +-
core/common/src/types/cluster/role.rs | 27 +-
core/common/src/types/cluster/status.rs | 27 +-
.../src/types/cluster/transport_endpoints.rs | 69 --
core/common/src/types/command/mod.rs | 173 ----
core/common/src/types/configuration/transport.rs | 23 +-
core/common/src/types/consumer/consumer_kind.rs | 27 -
core/common/src/types/identifier/mod.rs | 76 +-
core/common/src/types/message/iggy_message.rs | 201 ++--
core/common/src/types/message/message_header.rs | 8 +-
.../src/types/message/message_header_view.rs | 25 +-
core/common/src/types/message/message_view.rs | 44 +-
core/common/src/types/message/messages_batch.rs | 24 +-
.../common/src/types/message/messages_batch_mut.rs | 6 +-
core/common/src/types/message/mod.rs | 5 +-
core/common/src/types/message/partitioning.rs | 71 +-
core/common/src/types/message/polled_messages.rs | 12 +-
core/common/src/types/message/polling_strategy.rs | 27 -
core/common/src/types/message/user_headers.rs | 176 +---
core/common/src/types/mod.rs | 1 -
.../src/types/permissions/permissions_global.rs | 264 +----
core/common/src/utils/crypto.rs | 4 +-
core/common/src/wire_conversions.rs | 785 +++++++++++++++
core/connectors/runtime/src/sink.rs | 2 +-
core/connectors/runtime/src/source.rs | 4 +-
core/connectors/sdk/src/lib.rs | 10 +-
core/connectors/sinks/http_sink/src/lib.rs | 3 +-
core/integration/Cargo.toml | 1 -
.../tests/cli/message/test_message_poll_command.rs | 4 +-
.../message/test_message_poll_to_file_command.rs | 8 +-
.../cli/message/test_message_reply_via_file.rs | 4 +-
.../tests/cli/message/test_message_send_command.rs | 8 +-
.../message/test_message_send_from_file_command.rs | 8 +-
.../tests/cli/user/test_user_create_command.rs | 12 +-
.../tests/cli/user/test_user_get_command.rs | 6 +-
.../cli/user/test_user_permissions_command.rs | 12 +-
.../server/scenarios/create_message_payload.rs | 6 +-
.../tests/server/scenarios/encryption_scenario.rs | 8 +-
.../server/scenarios/message_headers_scenario.rs | 6 +-
.../server/scenarios/message_size_scenario.rs | 6 +-
.../tests/server/scenarios/offset_scenario.rs | 8 +-
.../tests/server/scenarios/permissions_scenario.rs | 36 +-
.../tests/server/scenarios/timestamp_scenario.rs | 8 +-
core/integration/tests/state/file.rs | 35 +-
core/integration/tests/state/system.rs | 154 +--
core/metadata/Cargo.toml | 1 -
core/metadata/src/stm/consumer_group.rs | 115 +--
core/metadata/src/stm/mod.rs | 10 +-
core/metadata/src/stm/stream.rs | 107 +-
core/metadata/src/stm/user.rs | 84 +-
core/sdk/src/http/consumer_offsets.rs | 4 -
core/sdk/src/http/messages.rs | 7 +-
core/sdk/src/prelude.rs | 18 +-
core/sdk/src/quic/quic_client.rs | 8 +-
core/sdk/src/tcp/tcp_client.rs | 12 +-
core/sdk/src/websocket/websocket_client.rs | 8 +-
core/server/src/binary/dispatch.rs | 125 +--
.../create_consumer_group_handler.rs | 20 +-
.../delete_consumer_group_handler.rs | 17 +-
.../consumer_groups/join_consumer_group_handler.rs | 17 +-
.../leave_consumer_group_handler.rs | 17 +-
.../partitions/create_partitions_handler.rs | 22 +-
.../partitions/delete_partitions_handler.rs | 21 +-
.../create_personal_access_token_handler.rs | 19 +-
.../delete_personal_access_token_handler.rs | 10 +-
.../login_with_personal_access_token_handler.rs | 9 +-
.../handlers/streams/create_stream_handler.rs | 10 +-
.../handlers/streams/delete_stream_handler.rs | 8 +-
.../handlers/streams/purge_stream_handler.rs | 8 +-
.../handlers/streams/update_stream_handler.rs | 14 +-
.../binary/handlers/topics/create_topic_handler.rs | 31 +-
.../binary/handlers/topics/delete_topic_handler.rs | 15 +-
.../binary/handlers/topics/purge_topic_handler.rs | 15 +-
.../binary/handlers/topics/update_topic_handler.rs | 29 +-
.../handlers/users/change_password_handler.rs | 28 +-
.../binary/handlers/users/create_user_handler.rs | 34 +-
.../binary/handlers/users/delete_user_handler.rs | 9 +-
.../src/binary/handlers/users/get_user_handler.rs | 5 +-
.../binary/handlers/users/login_user_handler.rs | 25 +-
.../handlers/users/update_permissions_handler.rs | 22 +-
.../binary/handlers/users/update_user_handler.rs | 23 +-
core/server/src/http/consumer_groups.rs | 27 +-
core/server/src/http/consumer_offsets.rs | 38 +-
core/server/src/http/partitions.rs | 23 +-
core/server/src/http/personal_access_tokens.rs | 20 +-
core/server/src/http/segments.rs | 10 +-
core/server/src/http/streams.rs | 29 +-
core/server/src/http/system.rs | 10 +-
core/server/src/http/topics.rs | 42 +-
core/server/src/http/users.rs | 50 +-
core/server/src/quic/listener.rs | 4 +-
core/server/src/shard/execution.rs | 273 +++---
core/server/src/shard/system/messages.rs | 3 +-
core/server/src/shard/transmission/message.rs | 55 +-
core/server/src/state/command.rs | 342 ++++---
core/server/src/state/entry.rs | 70 +-
core/server/src/state/file.rs | 13 +-
core/server/src/state/models.rs | 511 +++++-----
core/server/src/state/system.rs | 193 ++--
core/server/src/tcp/connection_handler.rs | 4 +-
core/simulator/src/client.rs | 26 +-
core/tools/src/data-seeder/seeder.rs | 4 +-
examples/rust/Cargo.toml | 2 +-
.../message-compression/consumer/main.rs | 12 +-
.../message-compression/producer/main.rs | 4 +-
.../message-headers/message-type/producer/main.rs | 4 +-
.../message-headers/typed-headers/producer/main.rs | 4 +-
examples/rust/src/multi-tenant/consumer/main.rs | 6 +-
examples/rust/src/multi-tenant/producer/main.rs | 4 +-
242 files changed, 5063 insertions(+), 12353 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 2433404ee..b5c76c369 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -990,9 +990,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.39.0"
+version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
+checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
dependencies = [
"cc",
"cmake",
@@ -1349,16 +1349,16 @@ dependencies = [
[[package]]
name = "blake3"
-version = "1.8.3"
+version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d"
+checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
- "cpufeatures 0.2.17",
+ "cpufeatures 0.3.0",
]
[[package]]
@@ -1730,9 +1730,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.57"
+version = "1.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
+checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1904,9 +1904,9 @@ dependencies = [
[[package]]
name = "cmake"
-version = "0.1.57"
+version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
dependencies = [
"cc",
]
@@ -1994,9 +1994,9 @@ dependencies = [
[[package]]
name = "compio-driver"
-version = "0.11.3"
+version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec8557d3804525f0e97594681f675929931782c9f8783a51c9f86da86b819150"
+checksum = "74d42d98dc890ee4db00c1e68a723391711aab6d67085880d716b72830f7c715"
dependencies = [
"cfg-if",
"cfg_aliases",
@@ -2093,9 +2093,9 @@ dependencies = [
[[package]]
name = "compio-quic"
-version = "0.7.1"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3864d7362ba5ec270178690e72f854e9360fa3163036fe8b88a3c4475321f8be"
+checksum = "ad9efdad81b920108b9de57148e1b9d73dc408b6d06a59ee64836dde651cf026"
dependencies = [
"cfg_aliases",
"compio-buf",
@@ -3165,9 +3165,9 @@ dependencies = [
[[package]]
name = "dircpy"
-version = "0.3.19"
+version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a88521b0517f5f9d51d11925d8ab4523497dcf947073fa3231a311b63941131c"
+checksum = "ebcbec2b9a580ddee352ac38523d2ecd4dcaad53532957034394556909e27f4b"
dependencies = [
"jwalk",
"log",
@@ -4860,9 +4860,9 @@ dependencies = [
[[package]]
name = "hyper"
-version = "1.8.1"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [
"atomic-waker",
"bytes",
@@ -4875,7 +4875,6 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
- "pin-utils",
"smallvec",
"tokio",
"want",
@@ -5296,7 +5295,6 @@ dependencies = [
name = "iggy-cli"
version = "0.12.1-edge.1"
dependencies = [
- "ahash 0.8.12",
"anyhow",
"async-trait",
"bytes",
@@ -5307,6 +5305,7 @@ dependencies = [
"dirs",
"figlet-rs",
"iggy",
+ "iggy_binary_protocol",
"iggy_common",
"keyring",
"passterm",
@@ -5316,7 +5315,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"tokio",
- "toml 1.1.0+spec-1.1.0",
+ "toml 1.1.1+spec-1.1.0",
"tracing",
"tracing-appender",
"tracing-subscriber",
@@ -5367,7 +5366,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"tokio",
- "toml 1.1.0+spec-1.1.0",
+ "toml 1.1.1+spec-1.1.0",
"tower-http",
"tracing",
"tracing-opentelemetry",
@@ -5423,7 +5422,6 @@ name = "iggy_common"
version = "0.9.5-edge.1"
dependencies = [
"aes-gcm",
- "ahash 0.8.12",
"aligned-vec",
"async-broadcast",
"async-trait",
@@ -5523,7 +5521,7 @@ dependencies = [
"simd-json",
"strum_macros 0.28.0",
"tokio",
- "toml 1.1.0+spec-1.1.0",
+ "toml 1.1.1+spec-1.1.0",
"tracing",
]
@@ -5736,13 +5734,13 @@ dependencies = [
name = "iggy_examples"
version = "0.0.6"
dependencies = [
- "ahash 0.8.12",
"anyhow",
"bytes",
"chrono",
"clap",
"futures-util",
"iggy",
+ "iggy_common",
"lz4_flex 0.13.0",
"rand 0.10.0",
"serde",
@@ -5790,7 +5788,7 @@ dependencies = [
"rgb",
"tiff",
"zune-core 0.5.1",
- "zune-jpeg 0.5.14",
+ "zune-jpeg 0.5.15",
]
[[package]]
@@ -5915,7 +5913,6 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02"
name = "integration"
version = "0.0.1"
dependencies = [
- "ahash 0.8.12",
"assert_cmd",
"async-trait",
"base64 0.22.1",
@@ -5958,7 +5955,7 @@ dependencies = [
"test-case",
"testcontainers-modules",
"tokio",
- "toml 1.1.0+spec-1.1.0",
+ "toml 1.1.1+spec-1.1.0",
"tracing",
"tracing-subscriber",
"twox-hash",
@@ -5979,9 +5976,9 @@ dependencies = [
[[package]]
name = "inventory"
-version = "0.3.22"
+version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "009ae045c87e7082cb72dab0ccd01ae075dd00141ddc108f43a0ea150a9e7227"
+checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
dependencies = [
"rustversion",
]
@@ -6037,9 +6034,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "iri-string"
-version = "0.7.11"
+version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb"
+checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
dependencies = [
"memchr",
"serde",
@@ -6183,10 +6180,12 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.91"
+version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
+checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [
+ "cfg-if",
+ "futures-util",
"once_cell",
"wasm-bindgen",
]
@@ -6404,9 +6403,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
[[package]]
name = "libc"
-version = "0.2.183"
+version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "libdbus-sys"
@@ -6488,9 +6487,9 @@ dependencies = [
[[package]]
name = "libredox"
-version = "0.1.14"
+version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
+checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
dependencies = [
"bitflags 2.11.0",
"libc",
@@ -6803,7 +6802,6 @@ dependencies = [
"message_bus",
"paste",
"rmp-serde",
- "secrecy",
"serde",
"slab",
"tempfile",
@@ -6875,9 +6873,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.1.1"
+version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"log",
@@ -7220,9 +7218,9 @@ dependencies = [
[[package]]
name = "num-conv"
-version = "0.2.0"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "num-derive"
@@ -7346,9 +7344,9 @@ dependencies = [
[[package]]
name = "octocrab"
-version = "0.49.6"
+version = "0.49.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "481d01ffe3fa4347e55474798e16d8d678aab19b8d7ca631ebb3c607cc87f9db"
+checksum = "63f6687a23731011d0117f9f4c3cdabaa7b5e42ca671f42b5cc0657c492540e3"
dependencies = [
"arc-swap",
"async-trait",
@@ -7671,9 +7669,9 @@ dependencies = [
[[package]]
name = "papaya"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f"
+checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7"
dependencies = [
"equivalent",
"seize",
@@ -8211,7 +8209,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
- "toml_edit 0.25.8+spec-1.1.0",
+ "toml_edit 0.25.9+spec-1.1.0",
]
[[package]]
@@ -8304,9 +8302,9 @@ dependencies = [
[[package]]
name = "prometheus-client"
-version = "0.24.0"
+version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4500adecd7af8e0e9f4dbce15cfee07ce913fbf6ad605cc468b83f2d531ee94"
+checksum = "cca3d75b4566b9a29fe1ed623587fb058e826eb329a0be4b7c4da1ebb2d7a6ca"
dependencies = [
"dtoa",
"itoa",
@@ -9100,9 +9098,9 @@ dependencies = [
[[package]]
name = "rmcp"
-version = "1.2.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba6b9d2f0efe2258b23767f1f9e0054cfbcac9c2d6f81a031214143096d7864f"
+checksum = "2231b2c085b371c01bc90c0e6c1cab8834711b6394533375bdbf870b0166d419"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -9132,9 +9130,9 @@ dependencies = [
[[package]]
name = "rmcp-macros"
-version = "1.2.0"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab9d95d7ed26ad8306352b0d5f05b593222b272790564589790d210aa15caa9e"
+checksum = "36ea0e100fadf81be85d7ff70f86cd805c7572601d4ab2946207f36540854b43"
dependencies = [
"darling 0.23.0",
"proc-macro2",
@@ -9259,9 +9257,9 @@ dependencies = [
[[package]]
name = "rust_decimal"
-version = "1.40.0"
+version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0"
+checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
dependencies = [
"arrayvec",
"borsh",
@@ -9271,13 +9269,14 @@ dependencies = [
"rkyv",
"serde",
"serde_json",
+ "wasm-bindgen",
]
[[package]]
name = "rustc-hash"
-version = "2.1.1"
+version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustc_version"
@@ -9749,9 +9748,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
-version = "1.1.0"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
@@ -9913,7 +9912,7 @@ dependencies = [
"sysinfo 0.38.4",
"tempfile",
"thiserror 2.0.18",
- "toml 1.1.0+spec-1.1.0",
+ "toml 1.1.1+spec-1.1.0",
"tower-http",
"tracing",
"tracing-appender",
@@ -10012,9 +10011,9 @@ dependencies = [
[[package]]
name = "simd-adler32"
-version = "0.3.8"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "simd-json"
@@ -10611,9 +10610,9 @@ dependencies = [
[[package]]
name = "synchrony"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6d5f5d3091c2d998f6ab4c8b495c0d232ad0aecbc1fa9ac52c247a79d497e16"
+checksum = "c174d82fd56da8214ec095cfe4568e59e5ccb49d060e70c2f98e3ba352b23e45"
dependencies = [
"futures-util",
"loom",
@@ -10665,9 +10664,9 @@ dependencies = [
[[package]]
name = "sys_traits"
-version = "0.1.26"
+version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe719c6bb50991671073b1469be68ccbcb5a2a2730873eab49b88c35a908037e"
+checksum = "5a79feaa49de4a6c8191bdbd5fb3eada50671e9367d874d1c12e3d36db131414"
dependencies = [
"sys_traits_macros",
]
@@ -10930,7 +10929,7 @@ dependencies = [
"half",
"quick-error",
"weezl",
- "zune-jpeg 0.5.14",
+ "zune-jpeg 0.5.15",
]
[[package]]
@@ -11136,17 +11135,17 @@ dependencies = [
[[package]]
name = "toml"
-version = "1.1.0+spec-1.1.0"
+version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
+checksum = "994b95d9e7bae62b34bab0e2a4510b801fa466066a6a8b2b57361fa1eba068ee"
dependencies = [
"indexmap 2.13.0",
"serde_core",
- "serde_spanned 1.1.0",
- "toml_datetime 1.1.0+spec-1.1.0",
+ "serde_spanned 1.1.1",
+ "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"toml_writer",
- "winnow 1.0.0",
+ "winnow 1.0.1",
]
[[package]]
@@ -11160,9 +11159,9 @@ dependencies = [
[[package]]
name = "toml_datetime"
-version = "1.1.0+spec-1.1.0"
+version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
@@ -11194,23 +11193,23 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.25.8+spec-1.1.0"
+version = "0.25.9+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
+checksum = "da053d28fe57e2c9d21b48261e14e7b4c8b670b54d2c684847b91feaf4c7dac5"
dependencies = [
"indexmap 2.13.0",
- "toml_datetime 1.1.0+spec-1.1.0",
+ "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
- "winnow 1.0.0",
+ "winnow 1.0.1",
]
[[package]]
name = "toml_parser"
-version = "1.1.0+spec-1.1.0"
+version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
+checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
dependencies = [
- "winnow 1.0.0",
+ "winnow 1.0.1",
]
[[package]]
@@ -11221,9 +11220,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
-version = "1.1.0+spec-1.1.0"
+version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
name = "tonic"
@@ -11687,9 +11686,9 @@ checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee"
[[package]]
name = "unicode-segmentation"
-version = "1.13.0"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a559e63b5d8004e12f9bce88af5c6d939c58de839b7532cfe9653846cedd2a9e"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-vo"
@@ -11844,9 +11843,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.22.0"
+version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
+checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -12034,9 +12033,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
+checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [
"cfg-if",
"once_cell",
@@ -12047,23 +12046,19 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.64"
+version = "0.4.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8"
+checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
dependencies = [
- "cfg-if",
- "futures-util",
"js-sys",
- "once_cell",
"wasm-bindgen",
- "web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
+checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -12071,9 +12066,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
+checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -12084,9 +12079,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.114"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
+checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [
"unicode-ident",
]
@@ -12177,9 +12172,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.91"
+version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
+checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -12815,9 +12810,9 @@ dependencies = [
[[package]]
name = "winnow"
-version = "1.0.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
+checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
dependencies = [
"memchr",
]
@@ -13080,18 +13075,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.47"
+version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.47"
+version = "0.8.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
dependencies = [
"proc-macro2",
"quote",
@@ -13282,9 +13277,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
-version = "0.5.14"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core 0.5.1",
]
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 9b019c26b..0135e7ed2 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -80,7 +80,7 @@ av-scenechange: 0.14.1, "MIT",
av1-grain: 0.2.5, "BSD-2-Clause",
avif-serialize: 0.8.8, "BSD-3-Clause",
aws-lc-rs: 1.16.2, "(Apache-2.0 OR ISC) AND ISC",
-aws-lc-sys: 0.39.0, "(Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0) AND (Apache-2.0 OR ISC) AND Apache-2.0 AND BSD-3-Clause AND ISC AND MIT",
+aws-lc-sys: 0.39.1, "(Apache-2.0 OR ISC OR MIT) AND (Apache-2.0 OR ISC OR MIT-0) AND (Apache-2.0 OR ISC) AND Apache-2.0 AND BSD-3-Clause AND ISC AND MIT",
axum: 0.8.8, "MIT",
axum-core: 0.5.6, "MIT",
axum-macros: 0.5.0, "MIT",
@@ -112,7 +112,7 @@ bitflags: 2.11.0, "Apache-2.0 OR MIT",
bitstream-io: 4.9.0, "Apache-2.0 OR MIT",
bitvec: 1.0.1, "MIT",
blake2: 0.10.6, "Apache-2.0 OR MIT",
-blake3: 1.8.3, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR CC0-1.0",
+blake3: 1.8.4, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR CC0-1.0",
block-buffer: 0.10.4, "Apache-2.0 OR MIT",
block2: 0.6.2, "MIT",
bnum: 0.12.1, "Apache-2.0 OR MIT",
@@ -145,7 +145,7 @@ capacity_builder: 0.5.0, "MIT",
capacity_builder_macros: 0.3.0, "MIT",
cargo-platform: 0.3.2, "Apache-2.0 OR MIT",
cargo_metadata: 0.23.1, "MIT",
-cc: 1.2.57, "Apache-2.0 OR MIT",
+cc: 1.2.58, "Apache-2.0 OR MIT",
cesu8: 1.1.0, "Apache-2.0 OR MIT",
cexpr: 0.6.0, "Apache-2.0 OR MIT",
cfg-if: 1.0.4, "Apache-2.0 OR MIT",
@@ -162,7 +162,7 @@ clap_complete: 4.6.0, "Apache-2.0 OR MIT",
clap_derive: 4.6.0, "Apache-2.0 OR MIT",
clap_lex: 1.1.0, "Apache-2.0 OR MIT",
clock: 0.1.0, "N/A",
-cmake: 0.1.57, "Apache-2.0 OR MIT",
+cmake: 0.1.58, "Apache-2.0 OR MIT",
cobs: 0.3.0, "Apache-2.0 OR MIT",
color_quant: 1.1.0, "MIT",
colorchoice: 1.0.5, "Apache-2.0 OR MIT",
@@ -171,13 +171,13 @@ combine: 4.6.7, "MIT",
comfy-table: 7.2.2, "MIT",
compio: 0.18.0, "MIT",
compio-buf: 0.8.1, "MIT",
-compio-driver: 0.11.3, "MIT",
+compio-driver: 0.11.4, "MIT",
compio-fs: 0.11.0, "MIT",
compio-io: 0.9.1, "MIT",
compio-log: 0.1.0, "MIT",
compio-macros: 0.1.2, "MIT",
compio-net: 0.11.1, "MIT",
-compio-quic: 0.7.1, "MIT",
+compio-quic: 0.7.2, "MIT",
compio-runtime: 0.11.0, "MIT",
compio-tls: 0.9.0, "MIT",
compio-ws: 0.3.0, "MIT",
@@ -270,7 +270,7 @@ derive_more: 2.1.1, "MIT",
derive_more-impl: 2.1.1, "MIT",
difflib: 0.4.0, "MIT",
digest: 0.10.7, "Apache-2.0 OR MIT",
-dircpy: 0.3.19, "MIT",
+dircpy: 0.3.20, "MIT",
dirs: 6.0.0, "Apache-2.0 OR MIT",
dirs-sys: 0.5.0, "Apache-2.0 OR MIT",
dispatch2: 0.3.1, "Apache-2.0 OR MIT OR Zlib",
@@ -430,7 +430,7 @@ human-repr: 1.1.0, "MIT",
humantime: 2.3.0, "Apache-2.0 OR MIT",
hwlocality: 1.0.0-alpha.11, "MIT",
hwlocality-sys: 0.6.4, "MIT",
-hyper: 1.8.1, "MIT",
+hyper: 1.9.0, "MIT",
hyper-named-pipe: 0.1.0, "Apache-2.0",
hyper-rustls: 0.27.7, "Apache-2.0 OR ISC OR MIT",
hyper-timeout: 0.5.2, "Apache-2.0 OR MIT",
@@ -493,13 +493,13 @@ inout: 0.1.4, "Apache-2.0 OR MIT",
integer-encoding: 3.0.4, "MIT",
integration: 0.0.1, "Apache-2.0",
interpolate_name: 0.2.4, "MIT",
-inventory: 0.3.22, "Apache-2.0 OR MIT",
+inventory: 0.3.24, "Apache-2.0 OR MIT",
io-uring: 0.7.11, "Apache-2.0 OR MIT",
io_uring_buf_ring: 0.2.3, "MIT",
iobuf: 0.1.0, "Apache-2.0",
ipconfig: 0.3.4, "Apache-2.0 OR MIT",
ipnet: 2.12.0, "Apache-2.0 OR MIT",
-iri-string: 0.7.11, "Apache-2.0 OR MIT",
+iri-string: 0.7.12, "Apache-2.0 OR MIT",
is_terminal_polyfill: 1.70.2, "Apache-2.0 OR MIT",
itertools: 0.13.0, "Apache-2.0 OR MIT",
itertools: 0.14.0, "Apache-2.0 OR MIT",
@@ -514,7 +514,7 @@ jni-sys: 0.4.1, "Apache-2.0 OR MIT",
jni-sys-macros: 0.4.1, "Apache-2.0 OR MIT",
jobserver: 0.1.34, "Apache-2.0 OR MIT",
journal: 0.1.0, "Apache-2.0",
-js-sys: 0.3.91, "Apache-2.0 OR MIT",
+js-sys: 0.3.94, "Apache-2.0 OR MIT",
jsonwebtoken: 10.3.0, "MIT",
jwalk: 0.8.1, "MIT",
keccak: 0.1.6, "Apache-2.0 OR MIT",
@@ -536,7 +536,7 @@ lexical-util: 1.0.7, "Apache-2.0 OR MIT",
lexical-write-float: 1.0.6, "Apache-2.0 OR MIT",
lexical-write-integer: 1.0.6, "Apache-2.0 OR MIT",
libbz2-rs-sys: 0.2.2, "bzip2-1.0.6",
-libc: 0.2.183, "Apache-2.0 OR MIT",
+libc: 0.2.184, "Apache-2.0 OR MIT",
libdbus-sys: 0.2.7, "Apache-2.0 OR MIT",
libfuzzer-sys: 0.4.12, "(Apache-2.0 OR MIT) AND NCSA",
libgit2-sys: 0.18.3+1.9.2, "Apache-2.0 OR MIT",
@@ -545,7 +545,7 @@ liblzma: 0.4.6, "Apache-2.0 OR MIT",
liblzma-sys: 0.4.5, "Apache-2.0 OR MIT",
libm: 0.2.16, "MIT",
libmimalloc-sys: 0.1.44, "MIT",
-libredox: 0.1.14, "MIT",
+libredox: 0.1.15, "MIT",
libsqlite3-sys: 0.30.1, "MIT",
libz-sys: 1.1.25, "Apache-2.0 OR MIT",
linked-hash-map: 0.5.6, "Apache-2.0 OR MIT",
@@ -586,7 +586,7 @@ mime: 0.3.17, "Apache-2.0 OR MIT",
mime_guess: 2.0.5, "MIT",
minimal-lexical: 0.2.1, "Apache-2.0 OR MIT",
miniz_oxide: 0.8.9, "Apache-2.0 OR MIT OR Zlib",
-mio: 1.1.1, "MIT",
+mio: 1.2.0, "MIT",
mockall: 0.14.0, "Apache-2.0 OR MIT",
mockall_derive: 0.14.0, "Apache-2.0 OR MIT",
moka: 0.12.15, "(Apache-2.0 OR MIT) AND Apache-2.0",
@@ -616,7 +616,7 @@ num: 0.4.3, "Apache-2.0 OR MIT",
num-bigint: 0.4.6, "Apache-2.0 OR MIT",
num-bigint-dig: 0.8.6, "Apache-2.0 OR MIT",
num-complex: 0.4.6, "Apache-2.0 OR MIT",
-num-conv: 0.2.0, "Apache-2.0 OR MIT",
+num-conv: 0.2.1, "Apache-2.0 OR MIT",
num-derive: 0.4.2, "Apache-2.0 OR MIT",
num-integer: 0.1.46, "Apache-2.0 OR MIT",
num-iter: 0.1.45, "Apache-2.0 OR MIT",
@@ -630,7 +630,7 @@ objc2: 0.6.4, "MIT",
objc2-core-foundation: 0.3.2, "Apache-2.0 OR MIT OR Zlib",
objc2-encode: 4.1.0, "MIT",
objc2-io-kit: 0.3.2, "Apache-2.0 OR MIT OR Zlib",
-octocrab: 0.49.6, "Apache-2.0 OR MIT",
+octocrab: 0.49.7, "Apache-2.0 OR MIT",
oid-registry: 0.8.1, "Apache-2.0 OR MIT",
once_cell: 1.21.4, "Apache-2.0 OR MIT",
once_cell_polyfill: 1.70.2, "Apache-2.0 OR MIT",
@@ -656,7 +656,7 @@ os_pipe: 1.2.3, "MIT",
outref: 0.5.2, "MIT",
p256: 0.13.2, "Apache-2.0 OR MIT",
p384: 0.13.1, "Apache-2.0 OR MIT",
-papaya: 0.2.3, "MIT",
+papaya: 0.2.4, "MIT",
parking: 2.2.1, "Apache-2.0 OR MIT",
parking_lot: 0.12.5, "Apache-2.0 OR MIT",
parking_lot_core: 0.9.12, "Apache-2.0 OR MIT",
@@ -718,7 +718,7 @@ proc-macro2: 1.0.106, "Apache-2.0 OR MIT",
proc-macro2-diagnostics: 0.10.1, "Apache-2.0 OR MIT",
profiling: 1.0.17, "Apache-2.0 OR MIT",
profiling-procmacros: 1.0.17, "Apache-2.0 OR MIT",
-prometheus-client: 0.24.0, "Apache-2.0 OR MIT",
+prometheus-client: 0.24.1, "Apache-2.0 OR MIT",
prometheus-client-derive-encode: 0.5.0, "Apache-2.0 OR MIT",
prost: 0.14.3, "Apache-2.0",
prost-derive: 0.14.3, "Apache-2.0",
@@ -782,8 +782,8 @@ ring: 0.17.14, "Apache-2.0 AND ISC",
ringbuffer: 0.16.0, "MIT",
rkyv: 0.7.46, "MIT",
rkyv_derive: 0.7.46, "MIT",
-rmcp: 1.2.0, "Apache-2.0",
-rmcp-macros: 1.2.0, "Apache-2.0",
+rmcp: 1.3.0, "Apache-2.0",
+rmcp-macros: 1.3.0, "Apache-2.0",
rmp: 0.8.15, "MIT",
rmp-serde: 1.3.1, "MIT",
roaring: 0.11.3, "Apache-2.0 OR MIT",
@@ -795,8 +795,8 @@ rust-embed: 8.11.0, "MIT",
rust-embed-impl: 8.11.0, "MIT",
rust-embed-utils: 8.11.0, "MIT",
rust-ini: 0.21.3, "MIT",
-rust_decimal: 1.40.0, "MIT",
-rustc-hash: 2.1.1, "Apache-2.0 OR MIT",
+rust_decimal: 1.41.0, "MIT",
+rustc-hash: 2.1.2, "Apache-2.0 OR MIT",
rustc_version: 0.4.1, "Apache-2.0 OR MIT",
rustc_version_runtime: 0.3.0, "MIT",
rusticata-macros: 4.1.0, "Apache-2.0 OR MIT",
@@ -842,7 +842,7 @@ serde_json: 1.0.149, "Apache-2.0 OR MIT",
serde_path_to_error: 0.1.20, "Apache-2.0 OR MIT",
serde_repr: 0.1.20, "Apache-2.0 OR MIT",
serde_spanned: 0.6.9, "Apache-2.0 OR MIT",
-serde_spanned: 1.1.0, "Apache-2.0 OR MIT",
+serde_spanned: 1.1.1, "Apache-2.0 OR MIT",
serde_urlencoded: 0.7.1, "Apache-2.0 OR MIT",
serde_v8: 0.260.0, "MIT",
serde_with: 3.18.0, "Apache-2.0 OR MIT",
@@ -859,7 +859,7 @@ sharded-slab: 0.1.7, "MIT",
shlex: 1.3.0, "Apache-2.0 OR MIT",
signal-hook-registry: 1.4.8, "Apache-2.0 OR MIT",
signature: 2.2.0, "Apache-2.0 OR MIT",
-simd-adler32: 0.3.8, "MIT",
+simd-adler32: 0.3.9, "MIT",
simd-json: 0.17.0, "Apache-2.0 OR MIT",
simd_helpers: 0.1.0, "MIT",
simdutf8: 0.1.5, "Apache-2.0 OR MIT",
@@ -907,12 +907,12 @@ svgtypes: 0.15.3, "Apache-2.0 OR MIT",
syn: 1.0.109, "Apache-2.0 OR MIT",
syn: 2.0.117, "Apache-2.0 OR MIT",
sync_wrapper: 1.0.2, "Apache-2.0",
-synchrony: 0.1.6, "MIT",
+synchrony: 0.1.7, "MIT",
synstructure: 0.13.2, "MIT",
synthez: 0.4.0, "BlueOak-1.0.0",
synthez-codegen: 0.4.0, "BlueOak-1.0.0",
synthez-core: 0.4.0, "BlueOak-1.0.0",
-sys_traits: 0.1.26, "MIT",
+sys_traits: 0.1.27, "MIT",
sys_traits_macros: 0.1.0, "MIT",
sysinfo: 0.37.2, "MIT",
sysinfo: 0.38.4, "MIT",
@@ -954,15 +954,15 @@ tokio-tungstenite: 0.29.0, "MIT",
tokio-util: 0.7.18, "MIT",
tokise: 0.2.1, "Apache-2.0 OR MIT",
toml: 0.8.23, "Apache-2.0 OR MIT",
-toml: 1.1.0+spec-1.1.0, "Apache-2.0 OR MIT",
+toml: 1.1.1+spec-1.1.0, "Apache-2.0 OR MIT",
toml_datetime: 0.6.11, "Apache-2.0 OR MIT",
-toml_datetime: 1.1.0+spec-1.1.0, "Apache-2.0 OR MIT",
+toml_datetime: 1.1.1+spec-1.1.0, "Apache-2.0 OR MIT",
toml_edit: 0.19.15, "Apache-2.0 OR MIT",
toml_edit: 0.22.27, "Apache-2.0 OR MIT",
-toml_edit: 0.25.8+spec-1.1.0, "Apache-2.0 OR MIT",
-toml_parser: 1.1.0+spec-1.1.0, "Apache-2.0 OR MIT",
+toml_edit: 0.25.9+spec-1.1.0, "Apache-2.0 OR MIT",
+toml_parser: 1.1.1+spec-1.1.0, "Apache-2.0 OR MIT",
toml_write: 0.1.2, "Apache-2.0 OR MIT",
-toml_writer: 1.1.0+spec-1.1.0, "Apache-2.0 OR MIT",
+toml_writer: 1.1.1+spec-1.1.0, "Apache-2.0 OR MIT",
tonic: 0.14.5, "MIT",
tonic-prost: 0.14.5, "MIT",
tools: 0.1.0, "Apache-2.0",
@@ -1007,7 +1007,7 @@ unicode-linebreak: 0.1.5, "Apache-2.0",
unicode-normalization: 0.1.25, "Apache-2.0 OR MIT",
unicode-properties: 0.1.4, "Apache-2.0 OR MIT",
unicode-script: 0.5.8, "Apache-2.0 OR MIT",
-unicode-segmentation: 1.13.0, "Apache-2.0 OR MIT",
+unicode-segmentation: 1.13.2, "Apache-2.0 OR MIT",
unicode-vo: 0.1.0, "Apache-2.0 OR MIT",
unicode-width: 0.1.14, "Apache-2.0 OR MIT",
unicode-width: 0.2.2, "Apache-2.0 OR MIT",
@@ -1025,7 +1025,7 @@ utf8-width: 0.1.8, "MIT",
utf8-zero: 0.8.1, "Apache-2.0 OR MIT",
utf8_iter: 1.0.4, "Apache-2.0 OR MIT",
utf8parse: 0.2.2, "Apache-2.0 OR MIT",
-uuid: 1.22.0, "Apache-2.0 OR MIT",
+uuid: 1.23.0, "Apache-2.0 OR MIT",
v8: 137.3.0, "MIT",
v_frame: 0.3.9, "BSD-2-Clause",
v_htmlescape: 0.15.8, "Apache-2.0 OR MIT",
@@ -1045,11 +1045,11 @@ wasi: 0.11.1+wasi-snapshot-preview1, "Apache-2.0 OR Apache-2.0 WITH LLVM-excepti
wasip2: 1.0.2+wasi-0.2.9, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
wasip3: 0.4.0+wasi-0.3.0-rc-2026-01-06, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
wasite: 0.1.0, "Apache-2.0 OR BSL-1.0 OR MIT",
-wasm-bindgen: 0.2.114, "Apache-2.0 OR MIT",
-wasm-bindgen-futures: 0.4.64, "Apache-2.0 OR MIT",
-wasm-bindgen-macro: 0.2.114, "Apache-2.0 OR MIT",
-wasm-bindgen-macro-support: 0.2.114, "Apache-2.0 OR MIT",
-wasm-bindgen-shared: 0.2.114, "Apache-2.0 OR MIT",
+wasm-bindgen: 0.2.117, "Apache-2.0 OR MIT",
+wasm-bindgen-futures: 0.4.67, "Apache-2.0 OR MIT",
+wasm-bindgen-macro: 0.2.117, "Apache-2.0 OR MIT",
+wasm-bindgen-macro-support: 0.2.117, "Apache-2.0 OR MIT",
+wasm-bindgen-shared: 0.2.117, "Apache-2.0 OR MIT",
wasm-encoder: 0.244.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
wasm-metadata: 0.244.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
wasm-streams: 0.4.2, "Apache-2.0 OR MIT",
@@ -1057,7 +1057,7 @@ wasm-streams: 0.5.0, "Apache-2.0 OR MIT",
wasm_dep_analyzer: 0.3.0, "MIT",
wasmparser: 0.244.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
wasmtimer: 0.4.3, "MIT",
-web-sys: 0.3.91, "Apache-2.0 OR MIT",
+web-sys: 0.3.94, "Apache-2.0 OR MIT",
web-time: 1.1.0, "Apache-2.0 OR MIT",
webpki-root-certs: 1.0.6, "CDLA-Permissive-2.0",
webpki-roots: 0.26.11, "CDLA-Permissive-2.0",
@@ -1133,7 +1133,7 @@ windows_x86_64_msvc: 0.52.6, "Apache-2.0 OR MIT",
windows_x86_64_msvc: 0.53.1, "Apache-2.0 OR MIT",
winnow: 0.5.40, "MIT",
winnow: 0.7.15, "MIT",
-winnow: 1.0.0, "MIT",
+winnow: 1.0.1, "MIT",
winsafe: 0.0.19, "MIT",
wit-bindgen: 0.51.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
wit-bindgen-core: 0.51.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
@@ -1155,8 +1155,8 @@ yew-router: 0.20.0, "Apache-2.0 OR MIT",
yew-router-macro: 0.20.0, "Apache-2.0 OR MIT",
yoke: 0.8.1, "Unicode-3.0",
yoke-derive: 0.8.1, "Unicode-3.0",
-zerocopy: 0.8.47, "Apache-2.0 OR BSD-2-Clause OR MIT",
-zerocopy-derive: 0.8.47, "Apache-2.0 OR BSD-2-Clause OR MIT",
+zerocopy: 0.8.48, "Apache-2.0 OR BSD-2-Clause OR MIT",
+zerocopy-derive: 0.8.48, "Apache-2.0 OR BSD-2-Clause OR MIT",
zerofrom: 0.1.6, "Unicode-3.0",
zerofrom-derive: 0.1.6, "Unicode-3.0",
zeroize: 1.8.2, "Apache-2.0 OR MIT",
@@ -1176,4 +1176,4 @@ zune-core: 0.4.12, "Apache-2.0 OR MIT OR Zlib",
zune-core: 0.5.1, "Apache-2.0 OR MIT OR Zlib",
zune-inflate: 0.2.54, "Apache-2.0 OR MIT OR Zlib",
zune-jpeg: 0.4.21, "Apache-2.0 OR MIT OR Zlib",
-zune-jpeg: 0.5.14, "Apache-2.0 OR MIT OR Zlib",
+zune-jpeg: 0.5.15, "Apache-2.0 OR MIT OR Zlib",
diff --git a/core/binary_protocol/src/lib.rs b/core/binary_protocol/src/lib.rs
index 8e5541516..a63aad99a 100644
--- a/core/binary_protocol/src/lib.rs
+++ b/core/binary_protocol/src/lib.rs
@@ -87,3 +87,10 @@ pub use primitives::permissions::{
WireGlobalPermissions, WirePermissions, WireStreamPermissions, WireTopicPermissions,
};
pub use primitives::polling_strategy::WirePollingStrategy;
+pub use primitives::user_headers::{
+ WireHeaderKind, WireUserHeaderEntry, WireUserHeaderIterator, WireUserHeaders,
+ encode_user_headers, user_headers_encoded_size, validate_user_headers,
+};
+
+/// Maximum number of partitions allowed in a single create/delete request.
+pub const MAX_PARTITIONS_PER_REQUEST: u32 = 1000;
diff --git a/core/binary_protocol/src/primitives/mod.rs b/core/binary_protocol/src/primitives/mod.rs
index dcbef623e..d1d7ee56a 100644
--- a/core/binary_protocol/src/primitives/mod.rs
+++ b/core/binary_protocol/src/primitives/mod.rs
@@ -22,3 +22,4 @@ pub mod identifier;
pub mod partitioning;
pub mod permissions;
pub mod polling_strategy;
+pub mod user_headers;
diff --git a/core/binary_protocol/src/primitives/user_headers.rs b/core/binary_protocol/src/primitives/user_headers.rs
new file mode 100644
index 000000000..2f49b39d6
--- /dev/null
+++ b/core/binary_protocol/src/primitives/user_headers.rs
@@ -0,0 +1,544 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Wire-level primitives for user header TLV validation, iteration, and encoding.
+//!
+//! User headers are encoded as a sequence of key-value TLV pairs:
+//! ```text
+//! [key_kind:u8][key_len:u32_le][key_data:N][val_kind:u8][val_len:u32_le][val_data:M] ...
+//! ```
+//!
+//! This module provides structural validation and zero-copy iteration
+//! over these bytes without interpreting semantic meaning of kind codes.
+//! Domain-level interpretation (mapping kind codes to typed values)
+//! happens in `iggy_common`.
+
+use std::borrow::Cow;
+
+use bytes::{BufMut, Bytes, BytesMut};
+
+use crate::WireError;
+use crate::codec::{WireEncode, read_u8, read_u32_le};
+
+/// Maximum byte length for a single header key or value on the wire.
+const MAX_FIELD_LENGTH: u32 = 255;
+
+/// Opaque header kind tag as transmitted on the wire.
+///
+/// This is a `u8` newtype - NOT an exhaustive enum. New kind codes can be
+/// added without breaking structural validation, which is critical for
+/// VSR rolling upgrades where older nodes must forward messages containing
+/// kind codes they do not yet understand.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct WireHeaderKind(pub u8);
+
+impl WireHeaderKind {
+ /// Highest currently defined kind code (Float64 = 15).
+ pub const KNOWN_MAX: u8 = 15;
+
+ /// Returns `true` if the kind code is within the currently defined range.
+ #[must_use]
+ pub fn is_known(self) -> bool {
+ (1..=Self::KNOWN_MAX).contains(&self.0)
+ }
+}
+
+/// A single key-value header entry borrowing the underlying buffer.
+#[derive(Debug)]
+pub struct WireUserHeaderEntry<'a> {
+ pub key_kind: WireHeaderKind,
+ pub key: &'a [u8],
+ pub value_kind: WireHeaderKind,
+ pub value: &'a [u8],
+}
+
+/// Validate the structural integrity of a user headers byte buffer.
+///
+/// Walks TLV pairs and checks:
+/// - Every kind byte is non-zero
+/// - Every length is in `1..=255`
+/// - No buffer overrun
+/// - Total consumed bytes equals `buf.len()` (no trailing garbage)
+/// - Even number of TLV entries (each pair = key + value)
+///
+/// Returns the number of complete key-value pairs on success.
+///
+/// # Errors
+///
+/// Returns `WireError::UnexpectedEof` if the buffer is truncated mid-entry,
+/// or `WireError::Validation` if structural constraints are violated.
+pub fn validate_user_headers(buf: &[u8]) -> Result<u32, WireError> {
+ if buf.is_empty() {
+ return Ok(0);
+ }
+
+ let mut pos = 0;
+ let mut tlv_count: u32 = 0;
+
+ while pos < buf.len() {
+ let kind = read_u8(buf, pos)?;
+ if kind == 0 {
+ return Err(WireError::Validation(Cow::Owned(format!(
+ "header kind is 0 (reserved) at offset {pos}"
+ ))));
+ }
+ pos += 1;
+
+ let length = read_u32_le(buf, pos)?;
+ pos += 4;
+
+ if length == 0 || length > MAX_FIELD_LENGTH {
+ return Err(WireError::Validation(Cow::Owned(format!(
+ "header field length {length} out of range 1..={MAX_FIELD_LENGTH} at offset {}",
+ pos - 4
+ ))));
+ }
+
+ let data_end = pos
+ .checked_add(length as usize)
+ .ok_or(WireError::Validation(Cow::Borrowed(
+ "header field length overflow",
+ )))?;
+ if data_end > buf.len() {
+ return Err(WireError::UnexpectedEof {
+ offset: pos,
+ need: length as usize,
+ have: buf.len() - pos,
+ });
+ }
+ pos = data_end;
+
+ tlv_count = tlv_count
+ .checked_add(1)
+ .ok_or(WireError::Validation(Cow::Borrowed(
+ "header entry count overflow",
+ )))?;
+ }
+
+ if !tlv_count.is_multiple_of(2) {
+ return Err(WireError::Validation(Cow::Owned(format!(
+ "odd number of TLV entries ({tlv_count}), expected key-value pairs"
+ ))));
+ }
+
+ Ok(tlv_count / 2)
+}
+
+/// Zero-copy iterator over pre-validated user header bytes.
+///
+/// Yields one [`WireUserHeaderEntry`] per key-value pair. The buffer
+/// **must** have been validated by [`validate_user_headers`] first;
+/// iterating an invalid buffer may yield garbage or panic.
+pub struct WireUserHeaderIterator<'a> {
+ buf: &'a [u8],
+ pos: usize,
+}
+
+impl<'a> WireUserHeaderIterator<'a> {
+ /// Create an iterator over a pre-validated user headers buffer.
+ #[must_use]
+ pub const fn new(buf: &'a [u8]) -> Self {
+ Self { buf, pos: 0 }
+ }
+}
+
+impl<'a> Iterator for WireUserHeaderIterator<'a> {
+ type Item = WireUserHeaderEntry<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.pos >= self.buf.len() {
+ return None;
+ }
+
+ // Key TLV
+ let key_kind = self.buf[self.pos];
+ self.pos += 1;
+
+ let key_len = u32::from_le_bytes(
+ self.buf[self.pos..self.pos + 4]
+ .try_into()
+ .expect("pre-validated buffer"),
+ ) as usize;
+ self.pos += 4;
+
+ let key = &self.buf[self.pos..self.pos + key_len];
+ self.pos += key_len;
+
+ // Value TLV
+ let value_kind = self.buf[self.pos];
+ self.pos += 1;
+
+ let value_len = u32::from_le_bytes(
+ self.buf[self.pos..self.pos + 4]
+ .try_into()
+ .expect("pre-validated buffer"),
+ ) as usize;
+ self.pos += 4;
+
+ let value = &self.buf[self.pos..self.pos + value_len];
+ self.pos += value_len;
+
+ Some(WireUserHeaderEntry {
+ key_kind: WireHeaderKind(key_kind),
+ key,
+ value_kind: WireHeaderKind(value_kind),
+ value,
+ })
+ }
+}
+
+/// Calculate the exact encoded size for a set of header entries.
+///
+/// Each entry tuple is `(key_kind, key_data, value_kind, value_data)`.
+#[must_use]
+pub fn user_headers_encoded_size(entries: &[(u8, &[u8], u8, &[u8])]) -> usize {
+ entries
+ .iter()
+ .map(|(_, k, _, v)| 1 + 4 + k.len() + 1 + 4 + v.len())
+ .sum()
+}
+
+/// Encode header entries into a caller-owned buffer.
+///
+/// Each entry tuple is `(key_kind, key_data, value_kind, value_data)`.
+/// The caller should pre-allocate `buf` with [`user_headers_encoded_size`].
+pub fn encode_user_headers(entries: &[(u8, &[u8], u8, &[u8])], buf: &mut BytesMut) {
+ for &(key_kind, key_data, value_kind, value_data) in entries {
+ buf.put_u8(key_kind);
+ #[allow(clippy::cast_possible_truncation)]
+ buf.put_u32_le(key_data.len() as u32);
+ buf.put_slice(key_data);
+ buf.put_u8(value_kind);
+ #[allow(clippy::cast_possible_truncation)]
+ buf.put_u32_le(value_data.len() as u32);
+ buf.put_slice(value_data);
+ }
+}
+
+/// Pre-validated user headers as a contiguous TLV byte buffer.
+///
+/// Construction validates structural integrity (no partial TLVs, no zero kinds,
+/// no oversized fields, even entry count). The inner bytes are immutable.
+///
+/// This type is intentionally opaque at the wire layer. Domain-level
+/// interpretation of kind codes happens in `iggy_common` via the conversion
+/// bridge in `wire_conversions.rs`.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct WireUserHeaders(Bytes);
+
+impl WireUserHeaders {
+ /// Validate and wrap a borrowed byte slice (copies into new `Bytes`).
+ ///
+ /// # Errors
+ /// Returns `WireError` if the buffer is not structurally valid TLV.
+ pub fn from_slice(buf: &[u8]) -> Result<Self, WireError> {
+ validate_user_headers(buf)?;
+ Ok(Self(Bytes::copy_from_slice(buf)))
+ }
+
+ /// Validate and wrap an owned `Bytes` buffer (zero-copy).
+ ///
+ /// # Errors
+ /// Returns `WireError` if the buffer is not structurally valid TLV.
+ pub fn from_bytes(buf: Bytes) -> Result<Self, WireError> {
+ validate_user_headers(&buf)?;
+ Ok(Self(buf))
+ }
+
+ /// Wrap pre-validated bytes without re-checking.
+ ///
+ /// # Safety contract (not `unsafe`, but caller must ensure):
+ /// The bytes must be structurally valid TLV as defined by
+ /// [`validate_user_headers`]. Iterating invalid bytes will panic.
+ pub const fn from_validated(buf: Bytes) -> Self {
+ Self(buf)
+ }
+
+ /// Empty headers (zero-length buffer).
+ #[must_use]
+ pub const fn empty() -> Self {
+ Self(Bytes::new())
+ }
+
+ #[must_use]
+ pub const fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ /// The raw validated bytes, suitable for wire transmission.
+ #[must_use]
+ pub fn as_bytes(&self) -> &[u8] {
+ &self.0
+ }
+
+ /// Consume into the inner `Bytes` handle (zero-copy).
+ #[must_use]
+ pub fn into_bytes(self) -> Bytes {
+ self.0
+ }
+
+ /// Zero-copy iteration over the pre-validated entries.
+ #[must_use]
+ pub fn iter(&self) -> WireUserHeaderIterator<'_> {
+ WireUserHeaderIterator::new(&self.0)
+ }
+}
+
+impl<'a> IntoIterator for &'a WireUserHeaders {
+ type Item = WireUserHeaderEntry<'a>;
+ type IntoIter = WireUserHeaderIterator<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.iter()
+ }
+}
+
+impl WireEncode for WireUserHeaders {
+ fn encoded_size(&self) -> usize {
+ self.0.len()
+ }
+
+ fn encode(&self, buf: &mut BytesMut) {
+ buf.put_slice(&self.0);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn encode_test_entries(entries: &[(u8, &[u8], u8, &[u8])]) -> Vec<u8> {
+ let size = user_headers_encoded_size(entries);
+ let mut buf = BytesMut::with_capacity(size);
+ encode_user_headers(entries, &mut buf);
+ buf.to_vec()
+ }
+
+ #[test]
+ fn empty_buffer_is_valid() {
+ assert_eq!(validate_user_headers(&[]).unwrap(), 0);
+ }
+
+ #[test]
+ fn single_pair_round_trip() {
+ let entries = [(6u8, b"test" as &[u8], 2u8, b"val" as &[u8])];
+ let encoded = encode_test_entries(&entries);
+
+ assert_eq!(validate_user_headers(&encoded).unwrap(), 1);
+
+ let mut iter = WireUserHeaderIterator::new(&encoded);
+ let entry = iter.next().unwrap();
+ assert_eq!(entry.key_kind, WireHeaderKind(6));
+ assert_eq!(entry.key, b"test");
+ assert_eq!(entry.value_kind, WireHeaderKind(2));
+ assert_eq!(entry.value, b"val");
+ assert!(iter.next().is_none());
+ }
+
+ #[test]
+ fn multiple_pairs() {
+ let entries = [
+ (1u8, b"k1" as &[u8], 2u8, b"v1" as &[u8]),
+ (
+ 6u8,
+ &42i32.to_le_bytes() as &[u8],
+ 12u8,
+ &99u64.to_le_bytes() as &[u8],
+ ),
+ (3u8, &[1u8] as &[u8], 9u8, &[255u8] as &[u8]),
+ ];
+ let encoded = encode_test_entries(&entries);
+
+ assert_eq!(validate_user_headers(&encoded).unwrap(), 3);
+
+ let items: Vec<_> = WireUserHeaderIterator::new(&encoded).collect();
+ assert_eq!(items.len(), 3);
+ assert_eq!(items[0].key, b"k1");
+ assert_eq!(items[1].key, &42i32.to_le_bytes());
+ assert_eq!(items[2].value, &[255u8]);
+ }
+
+ #[test]
+ fn unknown_kind_codes_pass_validation() {
+ let entries = [(16u8, b"a" as &[u8], 255u8, b"b" as &[u8])];
+ let encoded = encode_test_entries(&entries);
+ assert_eq!(validate_user_headers(&encoded).unwrap(), 1);
+
+ let entry = WireUserHeaderIterator::new(&encoded).next().unwrap();
+ assert_eq!(entry.key_kind, WireHeaderKind(16));
+ assert!(!entry.key_kind.is_known());
+ assert_eq!(entry.value_kind, WireHeaderKind(255));
+ assert!(!entry.value_kind.is_known());
+ }
+
+ #[test]
+ fn known_kind_codes() {
+ for code in 1..=15u8 {
+ assert!(WireHeaderKind(code).is_known());
+ }
+ assert!(!WireHeaderKind(0).is_known());
+ assert!(!WireHeaderKind(16).is_known());
+ }
+
+ #[test]
+ fn truncated_at_kind_byte() {
+ let entries = [(1u8, b"k" as &[u8], 2u8, b"v" as &[u8])];
+ let encoded = encode_test_entries(&entries);
+ // Truncate to just the first kind byte and length, missing data
+ let result = validate_user_headers(&encoded[..3]);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn truncated_at_length_field() {
+ let entries = [(1u8, b"k" as &[u8], 2u8, b"v" as &[u8])];
+ let encoded = encode_test_entries(&entries);
+ // Only kind byte, partial length
+ let result = validate_user_headers(&encoded[..2]);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn truncated_at_data() {
+ let entries = [(1u8, b"longkey" as &[u8], 2u8, b"v" as &[u8])];
+ let encoded = encode_test_entries(&entries);
+ // Kind + length present, but data truncated
+ let result = validate_user_headers(&encoded[..7]);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn zero_length_rejected() {
+ // kind=1, length=0
+ let buf = [1u8, 0, 0, 0, 0];
+ let result = validate_user_headers(&buf);
+ assert!(matches!(result, Err(WireError::Validation(_))));
+ }
+
+ #[test]
+ fn length_exceeds_max_rejected() {
+ // kind=1, length=256
+ let mut buf = vec![1u8];
+ buf.extend_from_slice(&256u32.to_le_bytes());
+ buf.extend_from_slice(&[0u8; 256]);
+ let result = validate_user_headers(&buf);
+ assert!(matches!(result, Err(WireError::Validation(_))));
+ }
+
+ #[test]
+ fn kind_zero_rejected() {
+ // kind=0 is reserved
+ let mut buf = vec![0u8];
+ buf.extend_from_slice(&1u32.to_le_bytes());
+ buf.push(42);
+ let result = validate_user_headers(&buf);
+ assert!(matches!(result, Err(WireError::Validation(_))));
+ }
+
+ #[test]
+ fn trailing_bytes_rejected() {
+ let entries = [(1u8, b"k" as &[u8], 2u8, b"v" as &[u8])];
+ let mut encoded = encode_test_entries(&entries);
+ encoded.push(0xFF); // trailing garbage
+ let result = validate_user_headers(&encoded);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn odd_tlv_count_rejected() {
+ // One TLV entry (key without value) - odd count
+ let mut buf = vec![1u8];
+ buf.extend_from_slice(&2u32.to_le_bytes());
+ buf.extend_from_slice(b"ab");
+ let result = validate_user_headers(&buf);
+ assert!(matches!(result, Err(WireError::Validation(_))));
+ }
+
+ #[test]
+ fn encoded_size_matches_actual() {
+ let entries = [
+ (1u8, b"key1" as &[u8], 2u8, b"value1" as &[u8]),
+ (
+ 6u8,
+ &100i32.to_le_bytes() as &[u8],
+ 14u8,
+ &std::f32::consts::PI.to_le_bytes() as &[u8],
+ ),
+ ];
+ let expected_size = user_headers_encoded_size(&entries);
+ let encoded = encode_test_entries(&entries);
+ assert_eq!(expected_size, encoded.len());
+ }
+
+ #[test]
+ fn max_length_field_accepted() {
+ let data = [0xAA; 255];
+ let entries = [(1u8, &data[..], 1u8, &data[..])];
+ let encoded = encode_test_entries(&entries);
+ assert_eq!(validate_user_headers(&encoded).unwrap(), 1);
+ }
+
+ #[test]
+ fn wire_user_headers_empty() {
+ let wire = WireUserHeaders::empty();
+ assert!(wire.is_empty());
+ assert_eq!(wire.as_bytes(), &[]);
+ assert_eq!(wire.encoded_size(), 0);
+ }
+
+ #[test]
+ fn wire_user_headers_from_slice_round_trip() {
+ let entries = [(6u8, b"test" as &[u8], 2u8, b"val" as &[u8])];
+ let encoded = encode_test_entries(&entries);
+
+ let wire = WireUserHeaders::from_slice(&encoded).unwrap();
+ assert!(!wire.is_empty());
+ assert_eq!(wire.as_bytes(), &encoded);
+ assert_eq!(wire.encoded_size(), encoded.len());
+
+ let entry = wire.iter().next().unwrap();
+ assert_eq!(entry.key_kind, WireHeaderKind(6));
+ assert_eq!(entry.key, b"test");
+ }
+
+ #[test]
+ fn wire_user_headers_from_bytes_zero_copy() {
+ let entries = [(1u8, b"k" as &[u8], 2u8, b"v" as &[u8])];
+ let encoded = Bytes::from(encode_test_entries(&entries));
+
+ let wire = WireUserHeaders::from_bytes(encoded.clone()).unwrap();
+ assert_eq!(wire.as_bytes(), &encoded[..]);
+ assert_eq!(wire.into_bytes(), encoded);
+ }
+
+ #[test]
+ fn wire_user_headers_rejects_invalid() {
+ let buf = [0u8, 1, 0, 0, 0, 42]; // kind=0 is invalid
+ assert!(WireUserHeaders::from_slice(&buf).is_err());
+ }
+
+ #[test]
+ fn wire_user_headers_encode() {
+ let entries = [(1u8, b"k" as &[u8], 2u8, b"v" as &[u8])];
+ let encoded = encode_test_entries(&entries);
+ let wire = WireUserHeaders::from_slice(&encoded).unwrap();
+
+ let mut buf = BytesMut::with_capacity(wire.encoded_size());
+ wire.encode(&mut buf);
+ assert_eq!(&buf[..], &encoded);
+ }
+}
diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml
index 85694e31d..5fabc500d 100644
--- a/core/cli/Cargo.toml
+++ b/core/cli/Cargo.toml
@@ -44,7 +44,6 @@ default = ["login-session"]
login-session = ["dep:keyring"]
[dependencies]
-ahash = { workspace = true }
anyhow = { workspace = true }
async-trait = { workspace = true }
bytes = { workspace = true }
@@ -55,6 +54,7 @@ comfy-table = { workspace = true }
dirs = { workspace = true }
figlet-rs = { workspace = true }
iggy = { workspace = true }
+iggy_binary_protocol = { workspace = true }
iggy_common = { workspace = true }
keyring = { workspace = true, optional = true }
passterm = { workspace = true }
diff --git a/core/cli/src/args/permissions/mod.rs b/core/cli/src/args/permissions/mod.rs
index 51fa073c5..8f692fdfa 100644
--- a/core/cli/src/args/permissions/mod.rs
+++ b/core/cli/src/args/permissions/mod.rs
@@ -17,9 +17,9 @@
*/
use self::{global::GlobalPermissionsArg, stream::StreamPermissionsArg};
-use ahash::AHashMap;
use clap::ValueEnum;
use iggy::prelude::{Permissions, StreamPermissions, UserStatus};
+use std::collections::BTreeMap;
pub(crate) mod constants;
pub(crate) mod global;
@@ -49,7 +49,7 @@ impl From<PermissionsArgs> for Option<Permissions> {
.stream
.into_iter()
.map(|s| (s.stream_id, s.into()))
- .collect::<AHashMap<usize, StreamPermissions>>();
+ .collect::<BTreeMap<usize, StreamPermissions>>();
match (value.global, stream_permissions.is_empty()) {
(Some(global), true) => Some(Permissions {
@@ -114,7 +114,7 @@ mod tests {
Option::from(PermissionsArgs::new(None, Some(vec![stream])));
let permissions = Permissions {
- streams: Some(AHashMap::from([(1, StreamPermissions::default())])),
+ streams: Some(BTreeMap::from([(1, StreamPermissions::default())])),
..Default::default()
};
assert_eq!(permissions_args, Some(permissions));
@@ -128,7 +128,7 @@ mod tests {
Option::from(PermissionsArgs::new(Some(global), Some(vec![stream])));
let mut permissions = Permissions {
- streams: Some(AHashMap::from([(1, StreamPermissions::default())])),
+ streams: Some(BTreeMap::from([(1, StreamPermissions::default())])),
..Default::default()
};
permissions.global.manage_topics = true;
diff --git a/core/cli/src/args/permissions/stream.rs b/core/cli/src/args/permissions/stream.rs
index b371d8720..6769499f8 100644
--- a/core/cli/src/args/permissions/stream.rs
+++ b/core/cli/src/args/permissions/stream.rs
@@ -22,8 +22,8 @@ use super::constants::{
READ_TOPICS_SHORT, SEND_MESSAGES_LONG, SEND_MESSAGES_SHORT,
};
use crate::args::permissions::topic::TopicPermissionsArg;
-use ahash::AHashMap;
use iggy::prelude::StreamPermissions;
+use std::collections::BTreeMap;
use std::str::FromStr;
#[derive(Debug, PartialEq)]
@@ -84,7 +84,7 @@ impl StreamPermissionsArg {
}
if !topic_permissions.is_empty() {
- let mut permissions = AHashMap::new();
+ let mut permissions = BTreeMap::new();
for permission in topic_permissions {
permissions.insert(permission.topic_id, permission.permissions);
}
@@ -382,7 +382,7 @@ mod tests {
read_topics: false,
poll_messages: false,
send_messages: false,
- topics: Some(AHashMap::from([
+ topics: Some(BTreeMap::from([
(
2,
TopicPermissions {
@@ -417,7 +417,7 @@ mod tests {
read_topics: false,
poll_messages: false,
send_messages: false,
- topics: Some(AHashMap::from([
+ topics: Some(BTreeMap::from([
(
2,
TopicPermissions {
@@ -515,7 +515,7 @@ mod tests {
read_topics: false,
poll_messages: false,
send_messages: false,
- topics: Some(AHashMap::from([
+ topics: Some(BTreeMap::from([
(
2,
TopicPermissions {
diff --git a/core/cli/src/commands/binary_client/get_client.rs b/core/cli/src/commands/binary_client/get_client.rs
index 33667098e..0150638f6 100644
--- a/core/cli/src/commands/binary_client/get_client.rs
+++ b/core/cli/src/commands/binary_client/get_client.rs
@@ -21,40 +21,32 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::{Table, presets::ASCII_NO_BORDERS};
use iggy_common::Client;
-use iggy_common::get_client::GetClient;
use tracing::{Level, event};
pub struct GetClientCmd {
- get_client: GetClient,
+ client_id: u32,
}
impl GetClientCmd {
pub fn new(client_id: u32) -> Self {
- Self {
- get_client: GetClient { client_id },
- }
+ Self { client_id }
}
}
#[async_trait]
impl CliCommand for GetClientCmd {
fn explain(&self) -> String {
- format!("get client with ID: {}", self.get_client.client_id)
+ format!("get client with ID: {}", self.client_id)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let client_details = client
- .get_client(self.get_client.client_id)
+ .get_client(self.client_id)
.await
- .with_context(|| {
- format!(
- "Problem getting client with ID: {}",
- self.get_client.client_id
- )
- })?;
+ .with_context(|| format!("Problem getting client with ID: {}", self.client_id))?;
if client_details.is_none() {
- event!(target: PRINT_TARGET, Level::INFO, "Client with ID: {} was not found", self.get_client.client_id);
+ event!(target: PRINT_TARGET, Level::INFO, "Client with ID: {} was not found", self.client_id);
return Ok(());
}
diff --git a/core/cli/src/commands/binary_client/get_clients.rs b/core/cli/src/commands/binary_client/get_clients.rs
index 02708e404..907c5c73f 100644
--- a/core/cli/src/commands/binary_client/get_clients.rs
+++ b/core/cli/src/commands/binary_client/get_clients.rs
@@ -21,7 +21,6 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_clients::GetClients;
use tracing::{Level, event};
pub enum GetClientsOutput {
@@ -30,23 +29,18 @@ pub enum GetClientsOutput {
}
pub struct GetClientsCmd {
- _get_clients: GetClients,
output: GetClientsOutput,
}
impl GetClientsCmd {
pub fn new(output: GetClientsOutput) -> Self {
- GetClientsCmd {
- _get_clients: GetClients {},
- output,
- }
+ GetClientsCmd { output }
}
}
impl Default for GetClientsCmd {
fn default() -> Self {
GetClientsCmd {
- _get_clients: GetClients {},
output: GetClientsOutput::Table,
}
}
diff --git a/core/cli/src/commands/binary_consumer_groups/get_consumer_group.rs b/core/cli/src/commands/binary_consumer_groups/get_consumer_group.rs
index 59f053e01..705dd1ee8 100644
--- a/core/cli/src/commands/binary_consumer_groups/get_consumer_group.rs
+++ b/core/cli/src/commands/binary_consumer_groups/get_consumer_group.rs
@@ -22,21 +22,20 @@ use async_trait::async_trait;
use comfy_table::{Table, presets::ASCII_NO_BORDERS};
use iggy_common::Client;
use iggy_common::Identifier;
-use iggy_common::get_consumer_group::GetConsumerGroup;
use tracing::{Level, event};
pub struct GetConsumerGroupCmd {
- get_consumer_group: GetConsumerGroup,
+ stream_id: Identifier,
+ topic_id: Identifier,
+ group_id: Identifier,
}
impl GetConsumerGroupCmd {
pub fn new(stream_id: Identifier, topic_id: Identifier, consumer_group_id: Identifier) -> Self {
Self {
- get_consumer_group: GetConsumerGroup {
- stream_id,
- topic_id,
- group_id: consumer_group_id,
- },
+ stream_id,
+ topic_id,
+ group_id: consumer_group_id,
}
}
}
@@ -46,25 +45,23 @@ impl CliCommand for GetConsumerGroupCmd {
fn explain(&self) -> String {
format!(
"get consumer group with ID: {} for topic with ID: {} and stream with ID: {}",
- self.get_consumer_group.group_id,
- self.get_consumer_group.topic_id,
- self.get_consumer_group.stream_id,
+ self.group_id, self.topic_id, self.stream_id,
)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let consumer_group = client
- .get_consumer_group(&self.get_consumer_group.stream_id, &self.get_consumer_group.topic_id, &self.get_consumer_group.group_id)
+ .get_consumer_group(&self.stream_id, &self.topic_id, &self.group_id)
.await
.with_context(|| {
format!(
"Problem getting consumer group with ID: {} for topic with ID: {} and stream with ID: {}",
- self.get_consumer_group.group_id, self.get_consumer_group.topic_id, self.get_consumer_group.stream_id
+ self.group_id, self.topic_id, self.stream_id
)
})?;
if consumer_group.is_none() {
- event!(target: PRINT_TARGET, Level::INFO, "Consumer group with ID: {} was not found", self.get_consumer_group.group_id);
+ event!(target: PRINT_TARGET, Level::INFO, "Consumer group with ID: {} was not found", self.group_id);
return Ok(());
}
diff --git a/core/cli/src/commands/binary_consumer_groups/get_consumer_groups.rs b/core/cli/src/commands/binary_consumer_groups/get_consumer_groups.rs
index 79362b88c..146a38cdb 100644
--- a/core/cli/src/commands/binary_consumer_groups/get_consumer_groups.rs
+++ b/core/cli/src/commands/binary_consumer_groups/get_consumer_groups.rs
@@ -22,7 +22,6 @@ use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
use iggy_common::Identifier;
-use iggy_common::get_consumer_groups::GetConsumerGroups;
use std::fmt::{self, Display, Formatter};
use tracing::{Level, event};
@@ -43,7 +42,8 @@ impl Display for GetConsumerGroupsOutput {
}
pub struct GetConsumerGroupsCmd {
- get_consumer_groups: GetConsumerGroups,
+ stream_id: Identifier,
+ topic_id: Identifier,
output: GetConsumerGroupsOutput,
}
@@ -54,10 +54,8 @@ impl GetConsumerGroupsCmd {
output: GetConsumerGroupsOutput,
) -> Self {
Self {
- get_consumer_groups: GetConsumerGroups {
- stream_id,
- topic_id,
- },
+ stream_id,
+ topic_id,
output,
}
}
@@ -68,21 +66,18 @@ impl CliCommand for GetConsumerGroupsCmd {
fn explain(&self) -> String {
format!(
"list consumer groups for stream with ID: {} and topic with ID: {} in {} mode",
- self.get_consumer_groups.stream_id, self.get_consumer_groups.topic_id, self.output
+ self.stream_id, self.topic_id, self.output
)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let consumer_groups = client
- .get_consumer_groups(
- &self.get_consumer_groups.stream_id,
- &self.get_consumer_groups.topic_id,
- )
+ .get_consumer_groups(&self.stream_id, &self.topic_id)
.await
.with_context(|| {
format!(
"Problem getting consumer groups for stream with ID: {} and topic with ID: {}",
- self.get_consumer_groups.stream_id, self.get_consumer_groups.topic_id
+ self.stream_id, self.topic_id
)
})?;
diff --git a/core/cli/src/commands/binary_consumer_offsets/get_consumer_offset.rs b/core/cli/src/commands/binary_consumer_offsets/get_consumer_offset.rs
index 7fb7066af..fb69ffb26 100644
--- a/core/cli/src/commands/binary_consumer_offsets/get_consumer_offset.rs
+++ b/core/cli/src/commands/binary_consumer_offsets/get_consumer_offset.rs
@@ -21,12 +21,14 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_consumer_offset::GetConsumerOffset;
use iggy_common::{Consumer, ConsumerKind, Identifier};
use tracing::{Level, event};
pub struct GetConsumerOffsetCmd {
- get_consumer_offset: GetConsumerOffset,
+ consumer: Consumer,
+ stream_id: Identifier,
+ topic_id: Identifier,
+ partition_id: u32,
}
impl GetConsumerOffsetCmd {
@@ -38,27 +40,22 @@ impl GetConsumerOffsetCmd {
kind: ConsumerKind,
) -> Self {
Self {
- get_consumer_offset: GetConsumerOffset {
- consumer: Consumer {
- kind,
- id: consumer_id,
- },
- stream_id,
- topic_id,
- partition_id: Some(partition_id),
+ consumer: Consumer {
+ kind,
+ id: consumer_id,
},
+ stream_id,
+ topic_id,
+ partition_id,
}
}
pub fn get_consumer_info(&self) -> String {
- match self.get_consumer_offset.consumer.kind {
+ match self.consumer.kind {
ConsumerKind::Consumer => {
- format!("consumer with ID: {}", self.get_consumer_offset.consumer.id)
+ format!("consumer with ID: {}", self.consumer.id)
}
- ConsumerKind::ConsumerGroup => format!(
- "consumer group with ID: {}",
- self.get_consumer_offset.consumer.id
- ),
+ ConsumerKind::ConsumerGroup => format!("consumer group with ID: {}", self.consumer.id),
}
}
}
@@ -69,22 +66,22 @@ impl CliCommand for GetConsumerOffsetCmd {
format!(
"get consumer offset for {} for stream with ID: {} and topic with ID: {} and partition with ID: {}",
self.get_consumer_info(),
- self.get_consumer_offset.stream_id,
- self.get_consumer_offset.topic_id,
- self.get_consumer_offset.partition_id.unwrap(),
+ self.stream_id,
+ self.topic_id,
+ self.partition_id,
)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
- let consumer_offset = client.get_consumer_offset(&self.get_consumer_offset.consumer, &self.get_consumer_offset.stream_id, &self.get_consumer_offset.topic_id, self.get_consumer_offset.partition_id).await.with_context(|| {
+ let consumer_offset = client.get_consumer_offset(&self.consumer, &self.stream_id, &self.topic_id, Some(self.partition_id)).await.with_context(|| {
format!(
"Problem getting consumer offset for {} for stream with ID: {} and topic with ID: {} and partition with ID: {}",
- self.get_consumer_info(), self.get_consumer_offset.stream_id, self.get_consumer_offset.topic_id, self.get_consumer_offset.partition_id.unwrap()
+ self.get_consumer_info(), self.stream_id, self.topic_id, self.partition_id
)
})?;
if consumer_offset.is_none() {
- event!(target: PRINT_TARGET, Level::INFO, "Consumer offset for {} for stream with ID: {} and topic with ID: {} and partition with ID: {} was not found", self.get_consumer_info(), self.get_consumer_offset.stream_id, self.get_consumer_offset.topic_id, self.get_consumer_offset.partition_id.unwrap());
+ event!(target: PRINT_TARGET, Level::INFO, "Consumer offset for {} for stream with ID: {} and topic with ID: {} and partition with ID: {} was not found", self.get_consumer_info(), self.stream_id, self.topic_id, self.partition_id);
return Ok(());
}
@@ -94,16 +91,10 @@ impl CliCommand for GetConsumerOffsetCmd {
table.set_header(vec!["Property", "Value"]);
table.add_row(vec![
"Consumer ID",
- format!("{}", self.get_consumer_offset.consumer.id).as_str(),
- ]);
- table.add_row(vec![
- "Stream ID",
- format!("{}", self.get_consumer_offset.stream_id).as_str(),
- ]);
- table.add_row(vec![
- "Topic ID",
- format!("{}", self.get_consumer_offset.topic_id).as_str(),
+ format!("{}", self.consumer.id).as_str(),
]);
+ table.add_row(vec!["Stream ID", format!("{}", self.stream_id).as_str()]);
+ table.add_row(vec!["Topic ID", format!("{}", self.topic_id).as_str()]);
table.add_row(vec![
"Partition ID",
format!("{}", consumer_offset.partition_id).as_str(),
diff --git a/core/cli/src/commands/binary_consumer_offsets/set_consumer_offset.rs b/core/cli/src/commands/binary_consumer_offsets/set_consumer_offset.rs
index e61eaaf96..21c18c6f6 100644
--- a/core/cli/src/commands/binary_consumer_offsets/set_consumer_offset.rs
+++ b/core/cli/src/commands/binary_consumer_offsets/set_consumer_offset.rs
@@ -21,12 +21,15 @@ use anyhow::Context;
use async_trait::async_trait;
use iggy_common::Client;
use iggy_common::Identifier;
-use iggy_common::store_consumer_offset::StoreConsumerOffset;
use iggy_common::{Consumer, ConsumerKind};
use tracing::{Level, event};
pub struct SetConsumerOffsetCmd {
- set_consumer_offset: StoreConsumerOffset,
+ consumer: Consumer,
+ stream_id: Identifier,
+ topic_id: Identifier,
+ partition_id: u32,
+ offset: u64,
}
impl SetConsumerOffsetCmd {
@@ -39,16 +42,14 @@ impl SetConsumerOffsetCmd {
kind: ConsumerKind,
) -> Self {
Self {
- set_consumer_offset: StoreConsumerOffset {
- consumer: Consumer {
- kind,
- id: consumer_id,
- },
- stream_id,
- topic_id,
- partition_id: Some(partition_id),
- offset,
+ consumer: Consumer {
+ kind,
+ id: consumer_id,
},
+ stream_id,
+ topic_id,
+ partition_id,
+ offset,
}
}
}
@@ -58,32 +59,28 @@ impl CliCommand for SetConsumerOffsetCmd {
fn explain(&self) -> String {
format!(
"set consumer offset for consumer with ID: {} for stream with ID: {} and topic with ID: {} and partition with ID: {} to {}",
- self.set_consumer_offset.consumer.id,
- self.set_consumer_offset.stream_id,
- self.set_consumer_offset.topic_id,
- self.set_consumer_offset.partition_id.unwrap(),
- self.set_consumer_offset.offset,
+ self.consumer.id, self.stream_id, self.topic_id, self.partition_id, self.offset,
)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
client
- .store_consumer_offset(&self.set_consumer_offset.consumer, &self.set_consumer_offset.stream_id, &self.set_consumer_offset.topic_id, self.set_consumer_offset.partition_id, self.set_consumer_offset.offset)
+ .store_consumer_offset(&self.consumer, &self.stream_id, &self.topic_id, Some(self.partition_id), self.offset)
.await
.with_context(|| {
format!(
"Problem setting consumer offset for consumer with ID: {} for stream with ID: {} and topic with ID: {} and partition with ID: {}",
- self.set_consumer_offset.consumer.id, self.set_consumer_offset.stream_id, self.set_consumer_offset.topic_id, self.set_consumer_offset.partition_id.unwrap()
+ self.consumer.id, self.stream_id, self.topic_id, self.partition_id
)
})?;
event!(target: PRINT_TARGET, Level::INFO,
"Consumer offset for consumer with ID: {} for stream with ID: {} and topic with ID: {} and partition with ID: {} set to {}",
- self.set_consumer_offset.consumer.id,
- self.set_consumer_offset.stream_id,
- self.set_consumer_offset.topic_id,
- self.set_consumer_offset.partition_id.unwrap(),
- self.set_consumer_offset.offset,
+ self.consumer.id,
+ self.stream_id,
+ self.topic_id,
+ self.partition_id,
+ self.offset,
);
Ok(())
diff --git a/core/cli/src/commands/binary_message/poll_messages.rs b/core/cli/src/commands/binary_message/poll_messages.rs
index 3cb4c4abe..3274bb943 100644
--- a/core/cli/src/commands/binary_message/poll_messages.rs
+++ b/core/cli/src/commands/binary_message/poll_messages.rs
@@ -20,12 +20,14 @@ use crate::commands::cli_command::{CliCommand, PRINT_TARGET};
use anyhow::Context;
use async_trait::async_trait;
use comfy_table::{Cell, CellAlignment, Row, Table};
+use iggy_binary_protocol::WireUserHeaders;
use iggy_common::Client;
use iggy_common::{
- BytesSerializable, Consumer, HeaderKey, HeaderKind, HeaderValue, Identifier, IggyByteSize,
- IggyDuration, IggyMessage, IggyTimestamp, PollMessages, PollingStrategy, Sizeable,
+ Consumer, HeaderKey, HeaderKind, Identifier, IggyByteSize, IggyDuration, IggyMessage,
+ IggyTimestamp, PollMessages, PollingStrategy, Sizeable,
+ wire_conversions::user_headers_from_wire,
};
-use std::collections::{HashMap, HashSet};
+use std::collections::HashSet;
use tokio::io::AsyncWriteExt;
use tracing::{Level, event};
@@ -85,7 +87,10 @@ impl PollMessagesCmd {
.iter()
.flat_map(|m| {
if let Some(user_headers) = &m.user_headers {
- match HashMap::<HeaderKey, HeaderValue>::from_bytes(user_headers.clone()) {
+ match WireUserHeaders::from_bytes(user_headers.clone())
+ .map_err(|_| iggy_common::IggyError::InvalidHeaderKey)
+ .and_then(|w| user_headers_from_wire(&w))
+ {
Ok(headers) => headers
.iter()
.map(|(k, v)| (k.clone(), v.kind()))
diff --git a/core/cli/src/commands/binary_message/send_messages.rs b/core/cli/src/commands/binary_message/send_messages.rs
index 9dbdcbe22..99d40ebda 100644
--- a/core/cli/src/commands/binary_message/send_messages.rs
+++ b/core/cli/src/commands/binary_message/send_messages.rs
@@ -21,10 +21,8 @@ use anyhow::Context;
use async_trait::async_trait;
use bytes::Bytes;
use iggy_common::Client;
-use iggy_common::{
- BytesSerializable, HeaderKey, HeaderValue, Identifier, IggyMessage, Partitioning, Sizeable,
-};
-use std::collections::HashMap;
+use iggy_common::{HeaderKey, HeaderValue, Identifier, IggyMessage, Partitioning, Sizeable};
+use std::collections::BTreeMap;
use std::io::{self, Read};
use tokio::io::AsyncReadExt;
use tracing::{Level, event};
@@ -75,7 +73,7 @@ impl SendMessagesCmd {
Ok(buffer)
}
- fn get_headers(&self) -> Option<HashMap<HeaderKey, HeaderValue>> {
+ fn get_headers(&self) -> Option<BTreeMap<HeaderKey, HeaderValue>> {
match self.headers.len() {
0 => None,
_ => Some(self.headers.iter().cloned().collect()),
diff --git a/core/cli/src/commands/binary_personal_access_tokens/get_personal_access_tokens.rs b/core/cli/src/commands/binary_personal_access_tokens/get_personal_access_tokens.rs
index 53a5dc9fc..da058a6f6 100644
--- a/core/cli/src/commands/binary_personal_access_tokens/get_personal_access_tokens.rs
+++ b/core/cli/src/commands/binary_personal_access_tokens/get_personal_access_tokens.rs
@@ -21,7 +21,6 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_personal_access_tokens::GetPersonalAccessTokens;
use tracing::{Level, event};
pub enum GetPersonalAccessTokensOutput {
@@ -30,16 +29,12 @@ pub enum GetPersonalAccessTokensOutput {
}
pub struct GetPersonalAccessTokensCmd {
- _get_tokens: GetPersonalAccessTokens,
output: GetPersonalAccessTokensOutput,
}
impl GetPersonalAccessTokensCmd {
pub fn new(output: GetPersonalAccessTokensOutput) -> Self {
- Self {
- _get_tokens: GetPersonalAccessTokens {},
- output,
- }
+ Self { output }
}
}
diff --git a/core/cli/src/commands/binary_streams/get_stream.rs b/core/cli/src/commands/binary_streams/get_stream.rs
index 6e22dd2da..18df52611 100644
--- a/core/cli/src/commands/binary_streams/get_stream.rs
+++ b/core/cli/src/commands/binary_streams/get_stream.rs
@@ -22,40 +22,32 @@ use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
use iggy_common::Identifier;
-use iggy_common::get_stream::GetStream;
use tracing::{Level, event};
pub struct GetStreamCmd {
- get_stream: GetStream,
+ stream_id: Identifier,
}
impl GetStreamCmd {
pub fn new(stream_id: Identifier) -> Self {
- Self {
- get_stream: GetStream { stream_id },
- }
+ Self { stream_id }
}
}
#[async_trait]
impl CliCommand for GetStreamCmd {
fn explain(&self) -> String {
- format!("get stream with ID: {}", self.get_stream.stream_id)
+ format!("get stream with ID: {}", self.stream_id)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let stream = client
- .get_stream(&self.get_stream.stream_id)
+ .get_stream(&self.stream_id)
.await
- .with_context(|| {
- format!(
- "Problem getting stream with ID: {}",
- self.get_stream.stream_id
- )
- })?;
+ .with_context(|| format!("Problem getting stream with ID: {}", self.stream_id))?;
if stream.is_none() {
- event!(target: PRINT_TARGET, Level::INFO, "Stream with ID: {} was not found", self.get_stream.stream_id);
+ event!(target: PRINT_TARGET, Level::INFO, "Stream with ID: {} was not found", self.stream_id);
return Ok(());
}
diff --git a/core/cli/src/commands/binary_streams/get_streams.rs b/core/cli/src/commands/binary_streams/get_streams.rs
index 55c785ed1..4ff5c7ad8 100644
--- a/core/cli/src/commands/binary_streams/get_streams.rs
+++ b/core/cli/src/commands/binary_streams/get_streams.rs
@@ -21,7 +21,6 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_streams::GetStreams;
use tracing::{Level, event};
pub enum GetStreamsOutput {
@@ -30,23 +29,18 @@ pub enum GetStreamsOutput {
}
pub struct GetStreamsCmd {
- _get_streams: GetStreams,
output: GetStreamsOutput,
}
impl GetStreamsCmd {
pub fn new(output: GetStreamsOutput) -> Self {
- GetStreamsCmd {
- _get_streams: GetStreams {},
- output,
- }
+ GetStreamsCmd { output }
}
}
impl Default for GetStreamsCmd {
fn default() -> Self {
GetStreamsCmd {
- _get_streams: GetStreams {},
output: GetStreamsOutput::Table,
}
}
diff --git a/core/cli/src/commands/binary_system/me.rs b/core/cli/src/commands/binary_system/me.rs
index ad3834277..522a6c468 100644
--- a/core/cli/src/commands/binary_system/me.rs
+++ b/core/cli/src/commands/binary_system/me.rs
@@ -21,22 +21,14 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_me::GetMe;
use tracing::{Level, event};
-pub struct GetMeCmd {
- _get_me: GetMe,
-}
+#[derive(Default)]
+pub struct GetMeCmd;
impl GetMeCmd {
pub fn new() -> Self {
- Self::default()
- }
-}
-
-impl Default for GetMeCmd {
- fn default() -> Self {
- Self { _get_me: GetMe {} }
+ Self
}
}
diff --git a/core/cli/src/commands/binary_system/ping.rs b/core/cli/src/commands/binary_system/ping.rs
index 990d72ad6..0552994f4 100644
--- a/core/cli/src/commands/binary_system/ping.rs
+++ b/core/cli/src/commands/binary_system/ping.rs
@@ -20,23 +20,18 @@ use crate::commands::cli_command::{CliCommand, PRINT_TARGET};
use anyhow::Context;
use async_trait::async_trait;
use iggy_common::Client;
-use iggy_common::ping::Ping;
use std::fmt::{Display, Formatter, Result};
use std::time::Duration;
use tokio::time::{Instant, sleep};
use tracing::{Level, event};
pub struct PingCmd {
- _ping: Ping,
count: u32,
}
impl PingCmd {
pub fn new(count: u32) -> Self {
- Self {
- _ping: Ping {},
- count,
- }
+ Self { count }
}
}
diff --git a/core/cli/src/commands/binary_system/snapshot.rs b/core/cli/src/commands/binary_system/snapshot.rs
index b0178f111..4c4ef999a 100644
--- a/core/cli/src/commands/binary_system/snapshot.rs
+++ b/core/cli/src/commands/binary_system/snapshot.rs
@@ -23,13 +23,13 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_snapshot::GetSnapshot;
use iggy_common::{SnapshotCompression, SystemSnapshotType};
use tokio::io::AsyncWriteExt;
use tracing::{Level, event};
pub struct GetSnapshotCmd {
- _get_snapshot: GetSnapshot,
+ compression: SnapshotCompression,
+ snapshot_types: Vec<SystemSnapshotType>,
out_dir: String,
}
@@ -39,28 +39,23 @@ impl GetSnapshotCmd {
snapshot_types: Option<Vec<SystemSnapshotType>>,
out_dir: Option<String>,
) -> Self {
- let mut cmd = GetSnapshotCmd::default();
-
- if let Some(compress) = compression {
- cmd._get_snapshot.compression = compress;
- }
- if let Some(types) = snapshot_types {
- cmd._get_snapshot.snapshot_types = types
- }
- if let Some(out) = out_dir {
- cmd.out_dir = out
+ let default_types = vec![
+ SystemSnapshotType::FilesystemOverview,
+ SystemSnapshotType::ProcessList,
+ SystemSnapshotType::ResourceUsage,
+ SystemSnapshotType::ServerLogs,
+ ];
+ Self {
+ compression: compression.unwrap_or(SnapshotCompression::Deflated),
+ snapshot_types: snapshot_types.unwrap_or(default_types),
+ out_dir: out_dir.unwrap_or_else(|| ".".to_string()),
}
-
- cmd
}
}
impl Default for GetSnapshotCmd {
fn default() -> Self {
- Self {
- _get_snapshot: GetSnapshot::default(),
- out_dir: ".".to_string(),
- }
+ Self::new(None, None, None)
}
}
@@ -72,10 +67,7 @@ impl CliCommand for GetSnapshotCmd {
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let snapshot_data = client
- .snapshot(
- self._get_snapshot.compression,
- self._get_snapshot.snapshot_types.to_owned(),
- )
+ .snapshot(self.compression, self.snapshot_types.to_owned())
.await
.with_context(|| "Problem sending snapshot command".to_owned())?;
let file_path = Path::new(&self.out_dir).join(format!(
diff --git a/core/cli/src/commands/binary_system/stats.rs b/core/cli/src/commands/binary_system/stats.rs
index c63e6ef48..e73959808 100644
--- a/core/cli/src/commands/binary_system/stats.rs
+++ b/core/cli/src/commands/binary_system/stats.rs
@@ -21,7 +21,6 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_stats::GetStats;
use std::fmt::Display;
use std::time::SystemTime;
use tracing::{Level, event};
@@ -48,16 +47,11 @@ impl Display for GetStatsOutput {
pub struct GetStatsCmd {
quiet_mode: bool,
output: GetStatsOutput,
- _get_stats: GetStats,
}
impl GetStatsCmd {
pub fn new(quiet_mode: bool, output: GetStatsOutput) -> Self {
- Self {
- quiet_mode,
- output,
- _get_stats: GetStats {},
- }
+ Self { quiet_mode, output }
}
}
diff --git a/core/cli/src/commands/binary_topics/get_topic.rs b/core/cli/src/commands/binary_topics/get_topic.rs
index 570fcc72b..6443b2d0d 100644
--- a/core/cli/src/commands/binary_topics/get_topic.rs
+++ b/core/cli/src/commands/binary_topics/get_topic.rs
@@ -23,20 +23,18 @@ use comfy_table::Table;
use iggy_common::Client;
use iggy_common::Identifier;
use iggy_common::IggyExpiry;
-use iggy_common::get_topic::GetTopic;
use tracing::{Level, event};
pub struct GetTopicCmd {
- get_topic: GetTopic,
+ stream_id: Identifier,
+ topic_id: Identifier,
}
impl GetTopicCmd {
pub fn new(stream_id: Identifier, topic_id: Identifier) -> Self {
Self {
- get_topic: GetTopic {
- stream_id,
- topic_id,
- },
+ stream_id,
+ topic_id,
}
}
}
@@ -46,23 +44,23 @@ impl CliCommand for GetTopicCmd {
fn explain(&self) -> String {
format!(
"get topic with ID: {} from stream with ID: {}",
- self.get_topic.topic_id, self.get_topic.stream_id
+ self.topic_id, self.stream_id
)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let topic = client
- .get_topic(&self.get_topic.stream_id, &self.get_topic.topic_id)
+ .get_topic(&self.stream_id, &self.topic_id)
.await
.with_context(|| {
format!(
"Problem getting topic with ID: {} in stream {}",
- self.get_topic.topic_id, self.get_topic.stream_id
+ self.topic_id, self.stream_id
)
})?;
if topic.is_none() {
- event!(target: PRINT_TARGET, Level::INFO, "Topic with ID: {} in stream {} was not found", self.get_topic.topic_id, self.get_topic.stream_id);
+ event!(target: PRINT_TARGET, Level::INFO, "Topic with ID: {} in stream {} was not found", self.topic_id, self.stream_id);
return Ok(());
}
diff --git a/core/cli/src/commands/binary_topics/get_topics.rs b/core/cli/src/commands/binary_topics/get_topics.rs
index a4812f376..ff8d4c4e3 100644
--- a/core/cli/src/commands/binary_topics/get_topics.rs
+++ b/core/cli/src/commands/binary_topics/get_topics.rs
@@ -21,7 +21,6 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_topics::GetTopics;
use iggy_common::{Identifier, IggyExpiry};
use std::fmt::{self, Display, Formatter};
use tracing::{Level, event};
@@ -43,16 +42,13 @@ impl Display for GetTopicsOutput {
}
pub struct GetTopicsCmd {
- get_topics: GetTopics,
+ stream_id: Identifier,
output: GetTopicsOutput,
}
impl GetTopicsCmd {
pub fn new(stream_id: Identifier, output: GetTopicsOutput) -> Self {
- Self {
- get_topics: GetTopics { stream_id },
- output,
- }
+ Self { stream_id, output }
}
}
@@ -61,20 +57,15 @@ impl CliCommand for GetTopicsCmd {
fn explain(&self) -> String {
format!(
"list topics from stream with ID: {} in {} mode",
- self.get_topics.stream_id, self.output
+ self.stream_id, self.output
)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let topics = client
- .get_topics(&self.get_topics.stream_id)
+ .get_topics(&self.stream_id)
.await
- .with_context(|| {
- format!(
- "Problem getting topics from stream {}",
- self.get_topics.stream_id
- )
- })?;
+ .with_context(|| format!("Problem getting topics from stream {}", self.stream_id))?;
match self.output {
GetTopicsOutput::Table => {
diff --git a/core/cli/src/commands/binary_users/get_user.rs b/core/cli/src/commands/binary_users/get_user.rs
index b68e2a9ec..91c37ca7d 100644
--- a/core/cli/src/commands/binary_users/get_user.rs
+++ b/core/cli/src/commands/binary_users/get_user.rs
@@ -22,39 +22,36 @@ use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
use iggy_common::Identifier;
-use iggy_common::get_user::GetUser;
use tracing::{Level, event};
pub struct GetUserCmd {
- get_user: GetUser,
+ user_id: Identifier,
}
impl GetUserCmd {
pub fn new(user_id: Identifier) -> Self {
- Self {
- get_user: GetUser { user_id },
- }
+ Self { user_id }
}
}
#[async_trait]
impl CliCommand for GetUserCmd {
fn explain(&self) -> String {
- format!("get user with ID: {}", self.get_user.user_id)
+ format!("get user with ID: {}", self.user_id)
}
async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
let user = client
- .get_user(&self.get_user.user_id)
+ .get_user(&self.user_id)
.await
- .with_context(|| format!("Problem getting user with ID: {}", self.get_user.user_id))?;
+ .with_context(|| format!("Problem getting user with ID: {}", self.user_id))?;
if user.is_none() {
event!(
target: PRINT_TARGET,
Level::INFO,
"User with ID: {} was not found",
- self.get_user.user_id
+ self.user_id
);
return Ok(());
}
diff --git a/core/cli/src/commands/binary_users/get_users.rs b/core/cli/src/commands/binary_users/get_users.rs
index cfb55407f..d60b23596 100644
--- a/core/cli/src/commands/binary_users/get_users.rs
+++ b/core/cli/src/commands/binary_users/get_users.rs
@@ -21,7 +21,6 @@ use anyhow::Context;
use async_trait::async_trait;
use comfy_table::Table;
use iggy_common::Client;
-use iggy_common::get_users::GetUsers;
use tracing::{Level, event};
pub enum GetUsersOutput {
@@ -30,23 +29,18 @@ pub enum GetUsersOutput {
}
pub struct GetUsersCmd {
- _get_users: GetUsers,
output: GetUsersOutput,
}
impl GetUsersCmd {
pub fn new(output: GetUsersOutput) -> Self {
- GetUsersCmd {
- _get_users: GetUsers {},
- output,
- }
+ GetUsersCmd { output }
}
}
impl Default for GetUsersCmd {
fn default() -> Self {
GetUsersCmd {
- _get_users: GetUsers {},
output: GetUsersOutput::Table,
}
}
diff --git a/core/common/Cargo.toml b/core/common/Cargo.toml
index 3a89f6cd3..e600fc4b0 100644
--- a/core/common/Cargo.toml
+++ b/core/common/Cargo.toml
@@ -29,7 +29,6 @@ readme = "../../README.md"
[dependencies]
aes-gcm = { workspace = true }
-ahash = { workspace = true }
aligned-vec = { workspace = true }
async-broadcast = { workspace = true }
async-trait = { workspace = true }
diff --git a/core/common/src/commands/consumer_groups/create_consumer_group.rs b/core/common/src/commands/consumer_groups/create_consumer_group.rs
deleted file mode 100644
index f697bc9c7..000000000
--- a/core/common/src/commands/consumer_groups/create_consumer_group.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::MAX_NAME_LENGTH;
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{CREATE_CONSUMER_GROUP_CODE, Command};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `CreateConsumerGroup` command creates a new consumer group for the topic.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `name` - unique consumer group name, max length is 255 characters.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct CreateConsumerGroup {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Unique consumer group name, max length is 255 characters.
- pub name: String,
-}
-
-impl Command for CreateConsumerGroup {
- fn code(&self) -> u32 {
- CREATE_CONSUMER_GROUP_CODE
- }
-}
-
-impl Default for CreateConsumerGroup {
- fn default() -> Self {
- CreateConsumerGroup {
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- name: "consumer_group_1".to_string(),
- }
- }
-}
-
-impl Validatable<IggyError> for CreateConsumerGroup {
- fn validate(&self) -> Result<(), IggyError> {
- if self.name.is_empty() || self.name.len() > MAX_NAME_LENGTH {
- return Err(IggyError::InvalidConsumerGroupName);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for CreateConsumerGroup {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 1 + stream_id_bytes.len() + topic_id_bytes.len() + self.name.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.name.len() as u8);
- bytes.put_slice(self.name.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<CreateConsumerGroup, IggyError> {
- if bytes.len() < 6 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let name_length = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- let name = from_utf8(
- bytes
- .get(position + 1..position + 1 + name_length as usize)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
- let command = CreateConsumerGroup {
- stream_id,
- topic_id,
- name,
- };
- Ok(command)
- }
-}
-
-impl Display for CreateConsumerGroup {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}|{}", self.stream_id, self.topic_id, self.name)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = CreateConsumerGroup {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- name: "test".to_string(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
-
- let name_length = bytes[position];
- let name = from_utf8(&bytes[position + 1..position + 1 + name_length as usize]).unwrap();
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(name, command.name);
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(CreateConsumerGroup::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = CreateConsumerGroup::default();
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- CreateConsumerGroup::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let name = "test".to_string();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes =
- BytesMut::with_capacity(1 + stream_id_bytes.len() + topic_id_bytes.len() + name.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(name.len() as u8);
- bytes.put_slice(name.as_bytes());
- let command = CreateConsumerGroup::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.name, name);
- }
-}
diff --git a/core/common/src/commands/consumer_groups/delete_consumer_group.rs b/core/common/src/commands/consumer_groups/delete_consumer_group.rs
deleted file mode 100644
index 859ded6f9..000000000
--- a/core/common/src/commands/consumer_groups/delete_consumer_group.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, DELETE_CONSUMER_GROUP_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `DeleteConsumerGroup` command deletes an existing consumer group from the topic.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `group_id` - unique consumer group ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
-pub struct DeleteConsumerGroup {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Unique consumer group ID (numeric or name).
- #[serde(skip)]
- pub group_id: Identifier,
-}
-
-impl Command for DeleteConsumerGroup {
- fn code(&self) -> u32 {
- DELETE_CONSUMER_GROUP_CODE
- }
-}
-
-impl Validatable<IggyError> for DeleteConsumerGroup {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for DeleteConsumerGroup {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let group_id_bytes = self.group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<DeleteConsumerGroup, IggyError> {
- if bytes.len() < 9 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = DeleteConsumerGroup {
- stream_id,
- topic_id,
- group_id,
- };
- Ok(command)
- }
-}
-
-impl Display for DeleteConsumerGroup {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}|{}", self.stream_id, self.topic_id, self.group_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeleteConsumerGroup {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- group_id: Identifier::numeric(3).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(group_id, command.group_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let group_id = Identifier::numeric(3).unwrap();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let group_id_bytes = group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- let command = DeleteConsumerGroup::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.group_id, group_id);
- }
-}
diff --git a/core/common/src/commands/consumer_groups/get_consumer_group.rs b/core/common/src/commands/consumer_groups/get_consumer_group.rs
deleted file mode 100644
index 6febd5393..000000000
--- a/core/common/src/commands/consumer_groups/get_consumer_group.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_CONSUMER_GROUP_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetConsumerGroup` command retrieves the consumer group from the topic.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `group_id` - unique consumer group ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct GetConsumerGroup {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Unique consumer group ID (numeric or name).
- #[serde(skip)]
- pub group_id: Identifier,
-}
-
-impl Command for GetConsumerGroup {
- fn code(&self) -> u32 {
- GET_CONSUMER_GROUP_CODE
- }
-}
-
-impl Validatable<IggyError> for GetConsumerGroup {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetConsumerGroup {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let group_id_bytes = self.group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetConsumerGroup, IggyError> {
- if bytes.len() < 9 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = GetConsumerGroup {
- stream_id,
- topic_id,
- group_id,
- };
- Ok(command)
- }
-}
-
-impl Display for GetConsumerGroup {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}|{}", self.stream_id, self.topic_id, self.group_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetConsumerGroup {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- group_id: Identifier::numeric(3).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(group_id, command.group_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let group_id = Identifier::numeric(3).unwrap();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let group_id_bytes = group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- let command = GetConsumerGroup::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.group_id, group_id);
- }
-}
diff --git a/core/common/src/commands/consumer_groups/get_consumer_groups.rs b/core/common/src/commands/consumer_groups/get_consumer_groups.rs
deleted file mode 100644
index d2f64293b..000000000
--- a/core/common/src/commands/consumer_groups/get_consumer_groups.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_CONSUMER_GROUPS_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetConsumerGroups` command retrieves the consumer groups from the topic.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct GetConsumerGroups {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
-}
-
-impl Command for GetConsumerGroups {
- fn code(&self) -> u32 {
- GET_CONSUMER_GROUPS_CODE
- }
-}
-
-impl Validatable<IggyError> for GetConsumerGroups {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetConsumerGroups {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetConsumerGroups, IggyError> {
- if bytes.len() < 6 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = GetConsumerGroups {
- stream_id,
- topic_id,
- };
- Ok(command)
- }
-}
-
-impl Display for GetConsumerGroups {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}", self.stream_id, self.topic_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use bytes::BufMut;
-
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetConsumerGroups {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let mut bytes = BytesMut::new();
- bytes.put(stream_id.to_bytes());
- bytes.put(topic_id.to_bytes());
- let command = GetConsumerGroups::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- }
-}
diff --git a/core/common/src/commands/consumer_groups/join_consumer_group.rs b/core/common/src/commands/consumer_groups/join_consumer_group.rs
deleted file mode 100644
index 5385373e6..000000000
--- a/core/common/src/commands/consumer_groups/join_consumer_group.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, JOIN_CONSUMER_GROUP_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `JoinConsumerGroup` command joins the consumer group by currently authenticated user.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `group_id` - unique consumer group ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct JoinConsumerGroup {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Unique consumer group ID (numeric or name).
- #[serde(skip)]
- pub group_id: Identifier,
-}
-
-impl Command for JoinConsumerGroup {
- fn code(&self) -> u32 {
- JOIN_CONSUMER_GROUP_CODE
- }
-}
-
-impl Validatable<IggyError> for JoinConsumerGroup {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for JoinConsumerGroup {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let group_id_bytes = self.group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<JoinConsumerGroup, IggyError> {
- if bytes.len() < 9 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = JoinConsumerGroup {
- stream_id,
- topic_id,
- group_id,
- };
- Ok(command)
- }
-}
-
-impl Display for JoinConsumerGroup {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}|{}", self.stream_id, self.topic_id, self.group_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = JoinConsumerGroup {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- group_id: Identifier::numeric(3).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(group_id, command.group_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let group_id = Identifier::numeric(3).unwrap();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let group_id_bytes = group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- let command = JoinConsumerGroup::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.group_id, group_id);
- }
-}
diff --git a/core/common/src/commands/consumer_groups/leave_consumer_group.rs b/core/common/src/commands/consumer_groups/leave_consumer_group.rs
deleted file mode 100644
index ff22829dc..000000000
--- a/core/common/src/commands/consumer_groups/leave_consumer_group.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, LEAVE_CONSUMER_GROUP_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `LeaveConsumerGroup` command leaves the consumer group by currently authenticated user.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `group_id` - unique consumer group ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct LeaveConsumerGroup {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Unique consumer group ID (numeric or name).
- #[serde(skip)]
- pub group_id: Identifier,
-}
-
-impl Command for LeaveConsumerGroup {
- fn code(&self) -> u32 {
- LEAVE_CONSUMER_GROUP_CODE
- }
-}
-
-impl Validatable<IggyError> for LeaveConsumerGroup {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for LeaveConsumerGroup {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let group_id_bytes = self.group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<LeaveConsumerGroup, IggyError> {
- if bytes.len() < 9 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = LeaveConsumerGroup {
- stream_id,
- topic_id,
- group_id,
- };
- Ok(command)
- }
-}
-
-impl Display for LeaveConsumerGroup {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}|{}", self.stream_id, self.topic_id, self.group_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = LeaveConsumerGroup {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- group_id: Identifier::numeric(3).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let group_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(group_id, command.group_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let group_id = Identifier::numeric(3).unwrap();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let group_id_bytes = group_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- stream_id_bytes.len() + topic_id_bytes.len() + group_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_slice(&group_id_bytes);
- let command = LeaveConsumerGroup::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.group_id, group_id);
- }
-}
diff --git a/core/common/src/commands/consumer_offsets/delete_consumer_offset.rs b/core/common/src/commands/consumer_offsets/delete_consumer_offset.rs
deleted file mode 100644
index b0fc12aa2..000000000
--- a/core/common/src/commands/consumer_offsets/delete_consumer_offset.rs
+++ /dev/null
@@ -1,220 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, DELETE_CONSUMER_OFFSET_CODE};
-use crate::{Consumer, ConsumerKind};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `DeleteConsumerOffset` command deletes the offset of a consumer from a given partition on the server.
-/// It has additional payload:
-/// - `consumer` - the consumer that is deleting the offset, either the regular consumer or the consumer group.
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `partition_id` - partition ID on which the offset is stored. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
-#[derive(Debug, Serialize, Deserialize, PartialEq)]
-pub struct DeleteConsumerOffset {
- /// The consumer that is storing the offset, either the regular consumer or the consumer group.
- #[serde(skip)]
- pub consumer: Consumer,
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Partition ID on which the offset is stored. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
- pub partition_id: Option<u32>,
-}
-
-impl Default for DeleteConsumerOffset {
- fn default() -> Self {
- DeleteConsumerOffset {
- consumer: Consumer::default(),
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- partition_id: Some(0),
- }
- }
-}
-
-impl Command for DeleteConsumerOffset {
- fn code(&self) -> u32 {
- DELETE_CONSUMER_OFFSET_CODE
- }
-}
-
-impl Validatable<IggyError> for DeleteConsumerOffset {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for DeleteConsumerOffset {
- fn to_bytes(&self) -> Bytes {
- let consumer_bytes = self.consumer.to_bytes();
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 5 + consumer_bytes.len() + stream_id_bytes.len() + topic_id_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- // Encode partition_id with a flag byte: 1 = Some, 0 = None
- if let Some(partition_id) = self.partition_id {
- bytes.put_u8(1);
- bytes.put_u32_le(partition_id);
- } else {
- bytes.put_u8(0);
- bytes.put_u32_le(0); // Padding to keep structure consistent
- }
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<DeleteConsumerOffset, IggyError> {
- if bytes.len() < 16 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let consumer_kind =
- ConsumerKind::from_code(*bytes.first().ok_or(IggyError::InvalidCommand)?)?;
- let consumer_id = Identifier::from_bytes(bytes.slice(1..))?;
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- let partition_id_value = u32::from_le_bytes(
- bytes
- .get(position + 1..position + 5)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let partition_id = if has_partition_id == 1 {
- Some(partition_id_value)
- } else {
- None
- };
- let command = DeleteConsumerOffset {
- consumer,
- stream_id,
- topic_id,
- partition_id,
- };
- Ok(command)
- }
-}
-
-impl Display for DeleteConsumerOffset {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}",
- self.consumer,
- self.stream_id,
- self.topic_id,
- self.partition_id.unwrap_or(0),
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeleteConsumerOffset {
- consumer: Consumer::new(Identifier::numeric(1).unwrap()),
- stream_id: Identifier::numeric(2).unwrap(),
- topic_id: Identifier::numeric(3).unwrap(),
- partition_id: Some(4),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let consumer_kind = ConsumerKind::from_code(bytes[0]).unwrap();
- let consumer_id = Identifier::from_bytes(bytes.slice(1..)).unwrap();
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = bytes[position];
- let partition_id =
- u32::from_le_bytes(bytes[position + 1..position + 5].try_into().unwrap());
- let partition_id = if has_partition_id == 1 {
- Some(partition_id)
- } else {
- None
- };
-
- assert!(!bytes.is_empty());
- assert_eq!(consumer, command.consumer);
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partition_id, command.partition_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let consumer = Consumer::new(Identifier::numeric(1).unwrap());
- let stream_id = Identifier::numeric(2).unwrap();
- let topic_id = Identifier::numeric(3).unwrap();
- let partition_id = 4u32;
-
- let consumer_bytes = consumer.to_bytes();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 5 + consumer_bytes.len() + stream_id_bytes.len() + topic_id_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u8(1); // Flag: partition_id is Some
- bytes.put_u32_le(partition_id);
-
- let command = DeleteConsumerOffset::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.consumer, consumer);
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.partition_id, Some(partition_id));
- }
-}
diff --git a/core/common/src/commands/consumer_offsets/get_consumer_offset.rs b/core/common/src/commands/consumer_offsets/get_consumer_offset.rs
deleted file mode 100644
index f27624948..000000000
--- a/core/common/src/commands/consumer_offsets/get_consumer_offset.rs
+++ /dev/null
@@ -1,225 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_CONSUMER_OFFSET_CODE};
-use crate::{Consumer, ConsumerKind};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetConsumerOffset` command that retrieves the offset of a consumer for a given partition from the server.
-/// It has additional payload:
-/// - `consumer` - the consumer that is storing the offset, either the regular consumer or the consumer group.
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `partition_id` - partition ID on which the offset is stored. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
-#[derive(Debug, Serialize, Deserialize, PartialEq)]
-pub struct GetConsumerOffset {
- /// The consumer that is storing the offset, either the regular consumer or the consumer group.
- #[serde(flatten)]
- pub consumer: Consumer,
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Partition ID on which the offset is stored. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
- #[serde(default = "default_partition_id")]
- pub partition_id: Option<u32>,
-}
-
-impl Default for GetConsumerOffset {
- fn default() -> Self {
- GetConsumerOffset {
- consumer: Consumer::default(),
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- partition_id: default_partition_id(),
- }
- }
-}
-
-impl Command for GetConsumerOffset {
- fn code(&self) -> u32 {
- GET_CONSUMER_OFFSET_CODE
- }
-}
-
-fn default_partition_id() -> Option<u32> {
- Some(0)
-}
-
-impl Validatable<IggyError> for GetConsumerOffset {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetConsumerOffset {
- fn to_bytes(&self) -> Bytes {
- let consumer_bytes = self.consumer.to_bytes();
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 5 + consumer_bytes.len() + stream_id_bytes.len() + topic_id_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- // Encode partition_id with a flag byte: 1 = Some, 0 = None
- if let Some(partition_id) = self.partition_id {
- bytes.put_u8(1);
- bytes.put_u32_le(partition_id);
- } else {
- bytes.put_u8(0);
- bytes.put_u32_le(0); // Padding to keep structure consistent
- }
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetConsumerOffset, IggyError> {
- if bytes.len() < 16 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let consumer_kind =
- ConsumerKind::from_code(*bytes.first().ok_or(IggyError::InvalidCommand)?)?;
- let consumer_id = Identifier::from_bytes(bytes.slice(1..))?;
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- let partition_id_value = u32::from_le_bytes(
- bytes
- .get(position + 1..position + 5)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let partition_id = if has_partition_id == 1 {
- Some(partition_id_value)
- } else {
- None
- };
- let command = GetConsumerOffset {
- consumer,
- stream_id,
- topic_id,
- partition_id,
- };
- Ok(command)
- }
-}
-
-impl Display for GetConsumerOffset {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}",
- self.consumer,
- self.stream_id,
- self.topic_id,
- self.partition_id.unwrap_or(0)
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetConsumerOffset {
- consumer: Consumer::new(Identifier::numeric(1).unwrap()),
- stream_id: Identifier::numeric(2).unwrap(),
- topic_id: Identifier::numeric(3).unwrap(),
- partition_id: Some(4),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let consumer_kind = ConsumerKind::from_code(bytes[0]).unwrap();
- let consumer_id = Identifier::from_bytes(bytes.slice(1..)).unwrap();
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = bytes[position];
- let partition_id =
- u32::from_le_bytes(bytes[position + 1..position + 5].try_into().unwrap());
- let partition_id = if has_partition_id == 1 {
- Some(partition_id)
- } else {
- None
- };
-
- assert!(!bytes.is_empty());
- assert_eq!(consumer, command.consumer);
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partition_id, command.partition_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let consumer = Consumer::new(Identifier::numeric(1).unwrap());
- let stream_id = Identifier::numeric(2).unwrap();
- let topic_id = Identifier::numeric(3).unwrap();
- let partition_id = 4u32;
-
- let consumer_bytes = consumer.to_bytes();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 5 + consumer_bytes.len() + stream_id_bytes.len() + topic_id_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u8(1); // Flag: partition_id is Some
- bytes.put_u32_le(partition_id);
-
- let command = GetConsumerOffset::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(consumer, command.consumer);
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.partition_id, Some(partition_id));
- }
-}
diff --git a/core/common/src/commands/consumer_offsets/store_consumer_offset.rs b/core/common/src/commands/consumer_offsets/store_consumer_offset.rs
deleted file mode 100644
index d8512df75..000000000
--- a/core/common/src/commands/consumer_offsets/store_consumer_offset.rs
+++ /dev/null
@@ -1,240 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, STORE_CONSUMER_OFFSET_CODE};
-use crate::{Consumer, ConsumerKind};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `StoreConsumerOffset` command stores the offset of a consumer for a given partition on the server.
-/// It has additional payload:
-/// - `consumer` - the consumer that is storing the offset, either the regular consumer or the consumer group.
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `partition_id` - partition ID on which the offset is stored. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
-/// - `offset` - offset to store.
-#[derive(Debug, Serialize, Deserialize, PartialEq)]
-pub struct StoreConsumerOffset {
- /// The consumer that is storing the offset, either the regular consumer or the consumer group.
- #[serde(flatten)]
- pub consumer: Consumer,
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Partition ID on which the offset is stored. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
- pub partition_id: Option<u32>,
- /// Offset to store.
- pub offset: u64,
-}
-
-impl Default for StoreConsumerOffset {
- fn default() -> Self {
- StoreConsumerOffset {
- consumer: Consumer::default(),
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- partition_id: Some(0),
- offset: 0,
- }
- }
-}
-
-impl Command for StoreConsumerOffset {
- fn code(&self) -> u32 {
- STORE_CONSUMER_OFFSET_CODE
- }
-}
-
-impl Validatable<IggyError> for StoreConsumerOffset {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for StoreConsumerOffset {
- fn to_bytes(&self) -> Bytes {
- let consumer_bytes = self.consumer.to_bytes();
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 13 + consumer_bytes.len() + stream_id_bytes.len() + topic_id_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- // Encode partition_id with a flag byte: 1 = Some, 0 = None
- if let Some(partition_id) = self.partition_id {
- bytes.put_u8(1);
- bytes.put_u32_le(partition_id);
- } else {
- bytes.put_u8(0);
- bytes.put_u32_le(0); // Padding to keep structure consistent
- }
- bytes.put_u64_le(self.offset);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<StoreConsumerOffset, IggyError> {
- if bytes.len() < 24 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let consumer_kind =
- ConsumerKind::from_code(*bytes.first().ok_or(IggyError::InvalidCommand)?)?;
- let consumer_id = Identifier::from_bytes(bytes.slice(1..))?;
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- let partition_id_value = u32::from_le_bytes(
- bytes
- .get(position + 1..position + 5)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let partition_id = if has_partition_id == 1 {
- Some(partition_id_value)
- } else {
- None
- };
- let offset = u64::from_le_bytes(
- bytes
- .get(position + 5..position + 13)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let command = StoreConsumerOffset {
- consumer,
- stream_id,
- topic_id,
- partition_id,
- offset,
- };
- Ok(command)
- }
-}
-
-impl Display for StoreConsumerOffset {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}|{}",
- self.consumer,
- self.stream_id,
- self.topic_id,
- self.partition_id.unwrap_or(0),
- self.offset
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = StoreConsumerOffset {
- consumer: Consumer::new(Identifier::numeric(1).unwrap()),
- stream_id: Identifier::numeric(2).unwrap(),
- topic_id: Identifier::numeric(3).unwrap(),
- partition_id: Some(4),
- offset: 5,
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let consumer_kind = ConsumerKind::from_code(bytes[0]).unwrap();
- let consumer_id = Identifier::from_bytes(bytes.slice(1..)).unwrap();
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = bytes[position];
- let partition_id =
- u32::from_le_bytes(bytes[position + 1..position + 5].try_into().unwrap());
- let partition_id = if has_partition_id == 1 {
- Some(partition_id)
- } else {
- None
- };
- let offset = u64::from_le_bytes(bytes[position + 5..position + 13].try_into().unwrap());
-
- assert!(!bytes.is_empty());
- assert_eq!(consumer, command.consumer);
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partition_id, command.partition_id);
- assert_eq!(offset, command.offset);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let consumer = Consumer::new(Identifier::numeric(1).unwrap());
- let stream_id = Identifier::numeric(2).unwrap();
- let topic_id = Identifier::numeric(3).unwrap();
- let partition_id = 4u32;
- let offset = 5u64;
-
- let consumer_bytes = consumer.to_bytes();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 13 + consumer_bytes.len() + stream_id_bytes.len() + topic_id_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u8(1); // Flag: partition_id is Some
- bytes.put_u32_le(partition_id);
- bytes.put_u64_le(offset);
-
- let command = StoreConsumerOffset::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.consumer, consumer);
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.partition_id, Some(partition_id));
- assert_eq!(command.offset, offset);
- }
-}
diff --git a/core/common/src/commands/messages/flush_unsaved_buffer.rs b/core/common/src/commands/messages/flush_unsaved_buffer.rs
deleted file mode 100644
index 2235a911b..000000000
--- a/core/common/src/commands/messages/flush_unsaved_buffer.rs
+++ /dev/null
@@ -1,132 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::{
- BytesSerializable, Identifier, Validatable,
- error::IggyError,
- {Command, FLUSH_UNSAVED_BUFFER_CODE},
-};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `FlushUnsavedBuffer` command is used to force a flush of `unsaved_buffer` to disk for specific stream -> topic -> partition.
-/// - `stream_id` - stream identifier
-/// - `topic_id` - topic identifier
-/// - `partition_id` - partition identifier
-/// - `fsync` - if `true` then the data is flushed to disk and fsynced, if `false` then the data is only flushed to disk.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct FlushUnsavedBuffer {
- pub stream_id: Identifier,
- pub topic_id: Identifier,
- pub partition_id: u32,
- pub fsync: bool,
-}
-
-impl FlushUnsavedBuffer {
- fn fsync_stringified(&self) -> &'static str {
- if self.fsync { "f" } else { "n" }
- }
-}
-
-impl Display for FlushUnsavedBuffer {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}",
- self.stream_id,
- self.topic_id,
- self.partition_id,
- self.fsync_stringified()
- )
- }
-}
-
-impl Command for FlushUnsavedBuffer {
- fn code(&self) -> u32 {
- FLUSH_UNSAVED_BUFFER_CODE
- }
-}
-
-impl BytesSerializable for FlushUnsavedBuffer {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes =
- BytesMut::with_capacity(stream_id_bytes.len() + topic_id_bytes.len() + 4 + 1);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u32_le(self.partition_id);
- bytes.put_u8(if self.fsync { 1 } else { 0 });
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<Self, IggyError>
- where
- Self: Sized,
- {
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.to_bytes().len();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.to_bytes().len();
- let partition_id = u32::from_le_bytes(
- bytes
- .get(position..position + 4)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- position += 4;
- let fsync = *bytes.get(position).ok_or(IggyError::InvalidCommand)? == 1;
- Ok(FlushUnsavedBuffer {
- stream_id,
- topic_id,
- partition_id,
- fsync,
- })
- }
-}
-
-impl Validatable<IggyError> for FlushUnsavedBuffer {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::FlushUnsavedBuffer;
- use crate::BytesSerializable;
- use crate::Identifier;
-
- #[test]
- fn test_flush_unsaved_buffer_serialization() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(1).unwrap();
- let flush_unsaved_buffer = super::FlushUnsavedBuffer {
- stream_id,
- topic_id,
- partition_id: 1,
- fsync: false,
- };
- let bytes = flush_unsaved_buffer.to_bytes();
- let deserialized_flush_unsaved_buffer = FlushUnsavedBuffer::from_bytes(bytes).unwrap();
- assert_eq!(flush_unsaved_buffer, deserialized_flush_unsaved_buffer);
- }
-}
diff --git a/core/common/src/commands/messages/poll_messages.rs b/core/common/src/commands/messages/poll_messages.rs
deleted file mode 100644
index ac266ca4c..000000000
--- a/core/common/src/commands/messages/poll_messages.rs
+++ /dev/null
@@ -1,341 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::error::IggyError;
-use crate::{BytesSerializable, Identifier, PollingKind, PollingStrategy, Sizeable, Validatable};
-use crate::{Command, POLL_MESSAGES_CODE};
-use crate::{Consumer, ConsumerKind};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-pub const DEFAULT_PARTITION_ID: u32 = 0;
-pub const DEFAULT_NUMBER_OF_MESSAGES_TO_POLL: u32 = 10;
-
-/// `PollMessages` command is used to poll messages from a topic in a stream.
-/// It has additional payload:
-/// - `consumer` - consumer which will poll messages. Either regular consumer or consumer group.
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `partition_id` - partition ID from which messages will be polled. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
-/// - `strategy` - polling strategy which specifies from where to start polling messages.
-/// - `count` - number of messages to poll.
-/// - `auto_commit` - whether to commit offset on the server automatically after polling the messages.
-#[derive(Debug, Serialize, Deserialize, PartialEq)]
-pub struct PollMessages {
- /// Consumer which will poll messages. Either regular consumer or consumer group.
- #[serde(flatten)]
- pub consumer: Consumer,
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Partition ID from which messages will be polled. Has to be specified for the regular consumer. For consumer group it is ignored (use `None`).
- #[serde(default = "PollMessages::default_partition_id")]
- pub partition_id: Option<u32>,
- /// Polling strategy which specifies from where to start polling messages.
- #[serde(default = "PollingStrategy::default", flatten)]
- pub strategy: PollingStrategy,
- /// Number of messages to poll.
- #[serde(default = "PollMessages::default_number_of_messages_to_poll")]
- pub count: u32,
- /// Whether to commit offset on the server automatically after polling the messages.
- #[serde(default)]
- pub auto_commit: bool,
-}
-
-impl PollMessages {
- pub fn bytes(
- stream_id: &Identifier,
- topic_id: &Identifier,
- partition_id: Option<u32>,
- consumer: &Consumer,
- strategy: &PollingStrategy,
- count: u32,
- auto_commit: bool,
- ) -> Bytes {
- let consumer_bytes = consumer.to_bytes();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let strategy_bytes = strategy.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 10 + consumer_bytes.len()
- + stream_id_bytes.len()
- + topic_id_bytes.len()
- + strategy_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- // Encode partition_id with a flag byte: 1 = Some, 0 = None
- if let Some(partition_id) = partition_id {
- bytes.put_u8(1);
- bytes.put_u32_le(partition_id);
- } else {
- bytes.put_u8(0);
- bytes.put_u32_le(0); // Padding to keep structure consistent
- }
- bytes.put_slice(&strategy_bytes);
- bytes.put_u32_le(count);
- if auto_commit {
- bytes.put_u8(1);
- } else {
- bytes.put_u8(0);
- }
-
- bytes.freeze()
- }
-
- pub fn default_number_of_messages_to_poll() -> u32 {
- DEFAULT_NUMBER_OF_MESSAGES_TO_POLL
- }
-
- pub fn default_partition_id() -> Option<u32> {
- Some(DEFAULT_PARTITION_ID)
- }
-}
-
-impl Default for PollMessages {
- fn default() -> Self {
- Self {
- consumer: Consumer::default(),
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(1).unwrap(),
- partition_id: PollMessages::default_partition_id(),
- strategy: PollingStrategy::default(),
- count: PollMessages::default_number_of_messages_to_poll(),
- auto_commit: false,
- }
- }
-}
-
-impl Command for PollMessages {
- fn code(&self) -> u32 {
- POLL_MESSAGES_CODE
- }
-}
-
-impl Validatable<IggyError> for PollMessages {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for PollMessages {
- fn to_bytes(&self) -> Bytes {
- PollMessages::bytes(
- &self.stream_id,
- &self.topic_id,
- self.partition_id,
- &self.consumer,
- &self.strategy,
- self.count,
- self.auto_commit,
- )
- }
-
- fn from_bytes(bytes: Bytes) -> Result<Self, IggyError> {
- if bytes.len() < 30 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let consumer_kind =
- ConsumerKind::from_code(*bytes.first().ok_or(IggyError::InvalidCommand)?)?;
- let consumer_id = Identifier::from_bytes(bytes.slice(1..))?;
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- let partition_id_value = u32::from_le_bytes(
- bytes
- .get(position + 1..position + 5)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let partition_id = if has_partition_id == 1 {
- Some(partition_id_value)
- } else {
- None
- };
- let polling_kind =
- PollingKind::from_code(*bytes.get(position + 5).ok_or(IggyError::InvalidCommand)?)?;
- position += 6;
- let value = u64::from_le_bytes(
- bytes
- .get(position..position + 8)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let strategy = PollingStrategy {
- kind: polling_kind,
- value,
- };
- let count = u32::from_le_bytes(
- bytes
- .get(position + 8..position + 12)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let auto_commit = *bytes.get(position + 12).ok_or(IggyError::InvalidCommand)? == 1;
- let command = PollMessages {
- consumer,
- stream_id,
- topic_id,
- partition_id,
- strategy,
- count,
- auto_commit,
- };
- Ok(command)
- }
-}
-
-impl Display for PollMessages {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}|{}|{}|{}",
- self.consumer,
- self.stream_id,
- self.topic_id,
- self.partition_id.unwrap_or(0),
- self.strategy,
- self.count,
- auto_commit_to_string(self.auto_commit)
- )
- }
-}
-
-fn auto_commit_to_string(auto_commit: bool) -> &'static str {
- if auto_commit { "a" } else { "n" }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = PollMessages {
- consumer: Consumer::new(Identifier::numeric(1).unwrap()),
- stream_id: Identifier::numeric(2).unwrap(),
- topic_id: Identifier::numeric(3).unwrap(),
- partition_id: Some(4),
- strategy: PollingStrategy::offset(2),
- count: 3,
- auto_commit: true,
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let consumer_kind = ConsumerKind::from_code(bytes[0]).unwrap();
- let consumer_id = Identifier::from_bytes(bytes.slice(1..)).unwrap();
- position += 1 + consumer_id.get_size_bytes().as_bytes_usize();
- let consumer = Consumer {
- kind: consumer_kind,
- id: consumer_id,
- };
- let stream_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let has_partition_id = bytes[position];
- let partition_id =
- u32::from_le_bytes(bytes[position + 1..position + 5].try_into().unwrap());
- let partition_id = if has_partition_id == 1 {
- Some(partition_id)
- } else {
- None
- };
- let polling_kind = PollingKind::from_code(bytes[position + 5]).unwrap();
- position += 6;
- let value = u64::from_le_bytes(bytes[position..position + 8].try_into().unwrap());
- let strategy = PollingStrategy {
- kind: polling_kind,
- value,
- };
- let count = u32::from_le_bytes(bytes[position + 8..position + 12].try_into().unwrap());
- let auto_commit = bytes[position + 12];
- let auto_commit = matches!(auto_commit, 1);
-
- assert!(!bytes.is_empty());
- assert_eq!(consumer, command.consumer);
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partition_id, command.partition_id);
- assert_eq!(strategy, command.strategy);
- assert_eq!(count, command.count);
- assert_eq!(auto_commit, command.auto_commit);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let consumer = Consumer::new(Identifier::numeric(1).unwrap());
- let stream_id = Identifier::numeric(2).unwrap();
- let topic_id = Identifier::numeric(3).unwrap();
- let partition_id = 4u32;
- let strategy = PollingStrategy::offset(2);
- let count = 3u32;
- let auto_commit = 1u8;
-
- let consumer_bytes = consumer.to_bytes();
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let strategy_bytes = strategy.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 10 + consumer_bytes.len()
- + stream_id_bytes.len()
- + topic_id_bytes.len()
- + strategy_bytes.len(),
- );
- bytes.put_slice(&consumer_bytes);
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u8(1); // Flag: partition_id is Some
- bytes.put_u32_le(partition_id);
- bytes.put_slice(&strategy_bytes);
- bytes.put_u32_le(count);
- bytes.put_u8(auto_commit);
-
- let command = PollMessages::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let auto_commit = matches!(auto_commit, 1);
-
- let command = command.unwrap();
- assert_eq!(command.consumer, consumer);
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.partition_id, Some(partition_id));
- assert_eq!(command.strategy, strategy);
- assert_eq!(command.count, count);
- assert_eq!(command.auto_commit, auto_commit);
- }
-}
diff --git a/core/common/src/commands/partitions/create_partitions.rs b/core/common/src/commands/partitions/create_partitions.rs
deleted file mode 100644
index 2bd9ee454..000000000
--- a/core/common/src/commands/partitions/create_partitions.rs
+++ /dev/null
@@ -1,167 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::MAX_PARTITIONS_COUNT;
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{CREATE_PARTITIONS_CODE, Command};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `CreatePartitions` command is used to create new partitions for a topic.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `partitions_count` - number of partitions in the topic to create, max value is 1000.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct CreatePartitions {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Number of partitions in the topic to create, max value is 1000.
- pub partitions_count: u32,
-}
-
-impl Command for CreatePartitions {
- fn code(&self) -> u32 {
- CREATE_PARTITIONS_CODE
- }
-}
-
-impl Default for CreatePartitions {
- fn default() -> Self {
- CreatePartitions {
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- partitions_count: 1,
- }
- }
-}
-
-impl Validatable<IggyError> for CreatePartitions {
- fn validate(&self) -> Result<(), IggyError> {
- if !(1..=MAX_PARTITIONS_COUNT).contains(&self.partitions_count) {
- return Err(IggyError::TooManyPartitions);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for CreatePartitions {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(4 + stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u32_le(self.partitions_count);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<CreatePartitions, IggyError> {
- if bytes.len() < 10 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let partitions_count = u32::from_le_bytes(
- bytes
- .get(position..position + 4)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let command = CreatePartitions {
- stream_id,
- topic_id,
- partitions_count,
- };
- Ok(command)
- }
-}
-
-impl Display for CreatePartitions {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}",
- self.stream_id, self.topic_id, self.partitions_count
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use bytes::BufMut;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = CreatePartitions {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- partitions_count: 3,
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let partitions_count =
- u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partitions_count, command.partitions_count);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let partitions_count = 3u32;
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(4 + stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u32_le(partitions_count);
- let command = CreatePartitions::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.partitions_count, partitions_count);
- }
-}
diff --git a/core/common/src/commands/partitions/delete_partitions.rs b/core/common/src/commands/partitions/delete_partitions.rs
deleted file mode 100644
index 9e0cf1ddc..000000000
--- a/core/common/src/commands/partitions/delete_partitions.rs
+++ /dev/null
@@ -1,167 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::MAX_PARTITIONS_COUNT;
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, DELETE_PARTITIONS_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `DeletePartitions` command is used to delete partitions from a topic.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `partitions_count` - number of partitions in the topic to delete, max value is 1000.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct DeletePartitions {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Number of partitions in the topic to delete, max value is 1000.
- pub partitions_count: u32,
-}
-
-impl Command for DeletePartitions {
- fn code(&self) -> u32 {
- DELETE_PARTITIONS_CODE
- }
-}
-
-impl Default for DeletePartitions {
- fn default() -> Self {
- DeletePartitions {
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- partitions_count: 1,
- }
- }
-}
-
-impl Validatable<IggyError> for DeletePartitions {
- fn validate(&self) -> Result<(), IggyError> {
- if !(1..=MAX_PARTITIONS_COUNT).contains(&self.partitions_count) {
- return Err(IggyError::TooManyPartitions);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for DeletePartitions {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(4 + stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u32_le(self.partitions_count);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<DeletePartitions, IggyError> {
- if bytes.len() < 10 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let partitions_count = u32::from_le_bytes(
- bytes
- .get(position..position + 4)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let command = DeletePartitions {
- stream_id,
- topic_id,
- partitions_count,
- };
- Ok(command)
- }
-}
-
-impl Display for DeletePartitions {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}",
- self.stream_id, self.topic_id, self.partitions_count
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use bytes::BufMut;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeletePartitions {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- partitions_count: 3,
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let partitions_count =
- u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partitions_count, command.partitions_count);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let partitions_count = 3u32;
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(4 + stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u32_le(partitions_count);
- let command = DeletePartitions::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.partitions_count, partitions_count);
- }
-}
diff --git a/core/common/src/commands/personal_access_tokens/create_personal_access_token.rs b/core/common/src/commands/personal_access_tokens/create_personal_access_token.rs
deleted file mode 100644
index 395822854..000000000
--- a/core/common/src/commands/personal_access_tokens/create_personal_access_token.rs
+++ /dev/null
@@ -1,157 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::defaults::*;
-use crate::error::IggyError;
-use crate::utils::expiry::IggyExpiry;
-use crate::{CREATE_PERSONAL_ACCESS_TOKEN_CODE, Command};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::{Display, Formatter};
-use std::str::from_utf8;
-
-/// `CreatePersonalAccessToken` command is used to create a new personal access token for the authenticated user.
-/// It has additional payload:
-/// - `name` - unique name of the token, must be between 3 and 30 characters long.
-/// - `expiry` - expiry of the token.
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct CreatePersonalAccessToken {
- /// Unique name of the token, must be between 3 and 30 characters long.
- pub name: String,
- /// Expiry of the token.
- pub expiry: IggyExpiry,
-}
-
-impl Command for CreatePersonalAccessToken {
- fn code(&self) -> u32 {
- CREATE_PERSONAL_ACCESS_TOKEN_CODE
- }
-}
-
-impl Default for CreatePersonalAccessToken {
- fn default() -> Self {
- CreatePersonalAccessToken {
- name: "token".to_string(),
- expiry: IggyExpiry::NeverExpire,
- }
- }
-}
-
-impl Validatable<IggyError> for CreatePersonalAccessToken {
- fn validate(&self) -> Result<(), IggyError> {
- if self.name.is_empty()
- || self.name.len() > MAX_PERSONAL_ACCESS_TOKEN_NAME_LENGTH
- || self.name.len() < MIN_PERSONAL_ACCESS_TOKEN_NAME_LENGTH
- {
- return Err(IggyError::InvalidPersonalAccessTokenName);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for CreatePersonalAccessToken {
- fn to_bytes(&self) -> Bytes {
- let mut bytes = BytesMut::with_capacity(5 + self.name.len());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.name.len() as u8);
- bytes.put_slice(self.name.as_bytes());
- bytes.put_u64_le(self.expiry.into());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<CreatePersonalAccessToken, IggyError> {
- if bytes.len() < 12 {
- return Err(IggyError::InvalidCommand);
- }
-
- let name_length = *bytes.first().ok_or(IggyError::InvalidCommand)? as usize;
- let name = from_utf8(
- bytes
- .get(1..1 + name_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let position = 1 + name_length;
- let expiry = u64::from_le_bytes(
- bytes
- .get(position..position + 8)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let expiry: IggyExpiry = expiry.into();
-
- let command = CreatePersonalAccessToken { name, expiry };
- Ok(command)
- }
-}
-
-impl Display for CreatePersonalAccessToken {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}", self.name, self.expiry)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = CreatePersonalAccessToken {
- name: "test".to_string(),
- expiry: IggyExpiry::NeverExpire,
- };
-
- let bytes = command.to_bytes();
- let name_length = bytes[0];
- let name = from_utf8(&bytes[1..1 + name_length as usize]).unwrap();
- let expiry = u64::from_le_bytes(
- bytes[1 + name_length as usize..9 + name_length as usize]
- .try_into()
- .unwrap(),
- );
- let expiry: IggyExpiry = expiry.into();
- assert!(!bytes.is_empty());
- assert_eq!(name, command.name);
- assert_eq!(expiry, command.expiry);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let name = "test";
- let expiry = IggyExpiry::NeverExpire;
- let mut bytes = BytesMut::new();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(name.len() as u8);
- bytes.put_slice(name.as_bytes());
- bytes.put_u64_le(expiry.into());
-
- let command = CreatePersonalAccessToken::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.name, name);
- assert_eq!(command.expiry, expiry);
- }
-}
diff --git a/core/common/src/commands/personal_access_tokens/delete_personal_access_token.rs b/core/common/src/commands/personal_access_tokens/delete_personal_access_token.rs
deleted file mode 100644
index 0d19b68c9..000000000
--- a/core/common/src/commands/personal_access_tokens/delete_personal_access_token.rs
+++ /dev/null
@@ -1,156 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::Command;
-use crate::Validatable;
-use crate::defaults::*;
-use crate::error::IggyError;
-use crate::{BytesSerializable, DELETE_PERSONAL_ACCESS_TOKEN_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::{Display, Formatter};
-use std::str::from_utf8;
-
-/// `DeletePersonalAccessToken` command is used to delete a personal access token for the authenticated user.
-/// It has additional payload:
-/// - `name` - unique name of the token, must be between 3 and 30 characters long.
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct DeletePersonalAccessToken {
- /// Unique name of the token, must be between 3 and 30 characters long.
- pub name: String,
-}
-
-impl Command for DeletePersonalAccessToken {
- fn code(&self) -> u32 {
- DELETE_PERSONAL_ACCESS_TOKEN_CODE
- }
-}
-
-impl Default for DeletePersonalAccessToken {
- fn default() -> Self {
- DeletePersonalAccessToken {
- name: "token".to_string(),
- }
- }
-}
-
-impl Validatable<IggyError> for DeletePersonalAccessToken {
- fn validate(&self) -> Result<(), IggyError> {
- if self.name.is_empty()
- || self.name.len() > MAX_PERSONAL_ACCESS_TOKEN_NAME_LENGTH
- || self.name.len() < MIN_PERSONAL_ACCESS_TOKEN_NAME_LENGTH
- {
- return Err(IggyError::InvalidPersonalAccessTokenName);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for DeletePersonalAccessToken {
- fn to_bytes(&self) -> Bytes {
- let mut bytes = BytesMut::with_capacity(5 + self.name.len());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.name.len() as u8);
- bytes.put_slice(self.name.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<DeletePersonalAccessToken, IggyError> {
- if bytes.len() < 4 {
- return Err(IggyError::InvalidCommand);
- }
-
- let name_length = *bytes.first().ok_or(IggyError::InvalidCommand)? as usize;
- let name = from_utf8(
- bytes
- .get(1..1 + name_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let command = DeletePersonalAccessToken { name };
- Ok(command)
- }
-}
-
-impl Display for DeletePersonalAccessToken {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.name)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeletePersonalAccessToken {
- name: "test".to_string(),
- };
-
- let bytes = command.to_bytes();
- let name_length = bytes[0];
- let name = from_utf8(&bytes[1..1 + name_length as usize]).unwrap();
- assert!(!bytes.is_empty());
- assert_eq!(name, command.name);
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(DeletePersonalAccessToken::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = DeletePersonalAccessToken::default();
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- DeletePersonalAccessToken::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn from_bytes_should_fail_on_corrupted_name_length() {
- let mut buf = BytesMut::new();
- buf.put_u8(255);
- buf.put_slice(b"short");
- assert!(DeletePersonalAccessToken::from_bytes(buf.freeze()).is_err());
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let name = "test";
- let mut bytes = BytesMut::new();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(name.len() as u8);
- bytes.put_slice(name.as_bytes());
-
- let command = DeletePersonalAccessToken::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.name, name);
- }
-}
diff --git a/core/common/src/commands/personal_access_tokens/get_personal_access_tokens.rs b/core/common/src/commands/personal_access_tokens/get_personal_access_tokens.rs
deleted file mode 100644
index c07551a61..000000000
--- a/core/common/src/commands/personal_access_tokens/get_personal_access_tokens.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_PERSONAL_ACCESS_TOKENS_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetPersonalAccessTokens` command is used to get all personal access tokens for the authenticated user.
-/// It has no additional payload.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct GetPersonalAccessTokens {}
-
-impl Command for GetPersonalAccessTokens {
- fn code(&self) -> u32 {
- GET_PERSONAL_ACCESS_TOKENS_CODE
- }
-}
-
-impl Validatable<IggyError> for GetPersonalAccessTokens {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetPersonalAccessTokens {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetPersonalAccessTokens, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(GetPersonalAccessTokens {})
- }
-}
-
-impl Display for GetPersonalAccessTokens {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_empty_bytes() {
- let command = GetPersonalAccessTokens {};
- let bytes = command.to_bytes();
- assert!(bytes.is_empty());
- }
-
- #[test]
- fn should_be_deserialized_from_empty_bytes() {
- let command = GetPersonalAccessTokens::from_bytes(Bytes::new());
- assert!(command.is_ok());
- }
-
- #[test]
- fn should_not_be_deserialized_from_empty_bytes() {
- let command = GetPersonalAccessTokens::from_bytes(Bytes::from_static(&[0]));
- assert!(command.is_err());
- }
-}
diff --git a/core/common/src/commands/personal_access_tokens/login_with_personal_access_token.rs b/core/common/src/commands/personal_access_tokens/login_with_personal_access_token.rs
deleted file mode 100644
index 73dedb34b..000000000
--- a/core/common/src/commands/personal_access_tokens/login_with_personal_access_token.rs
+++ /dev/null
@@ -1,173 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::defaults::*;
-use crate::error::IggyError;
-use crate::{Command, LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use secrecy::{ExposeSecret, SecretString};
-use serde::{Deserialize, Serialize};
-use std::fmt::{Display, Formatter};
-use std::str::from_utf8;
-
-/// `LoginWithPersonalAccessToken` command is used to login the user with a personal access token, instead of the username and password.
-/// It has additional payload:
-/// - `token` - personal access token
-#[derive(Debug, Serialize, Deserialize)]
-pub struct LoginWithPersonalAccessToken {
- /// Personal access token
- #[serde(serialize_with = "crate::utils::serde_secret::serialize_secret")]
- pub token: SecretString,
-}
-
-impl Command for LoginWithPersonalAccessToken {
- fn code(&self) -> u32 {
- LOGIN_WITH_PERSONAL_ACCESS_TOKEN_CODE
- }
-}
-
-impl Default for LoginWithPersonalAccessToken {
- fn default() -> Self {
- LoginWithPersonalAccessToken {
- token: SecretString::from("token"),
- }
- }
-}
-
-impl Validatable<IggyError> for LoginWithPersonalAccessToken {
- fn validate(&self) -> Result<(), IggyError> {
- let token = self.token.expose_secret();
- if token.is_empty() || token.len() > MAX_PAT_LENGTH {
- return Err(IggyError::InvalidPersonalAccessToken);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for LoginWithPersonalAccessToken {
- fn to_bytes(&self) -> Bytes {
- let token = self.token.expose_secret();
- let mut bytes = BytesMut::with_capacity(5 + token.len());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(token.len() as u8);
- bytes.put_slice(token.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<LoginWithPersonalAccessToken, IggyError> {
- if bytes.len() < 4 {
- return Err(IggyError::InvalidCommand);
- }
-
- let token_length = *bytes.first().ok_or(IggyError::InvalidCommand)? as usize;
- let token = from_utf8(
- bytes
- .get(1..1 + token_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let command = LoginWithPersonalAccessToken {
- token: SecretString::from(token),
- };
- Ok(command)
- }
-}
-
-impl Display for LoginWithPersonalAccessToken {
- fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
- write!(f, "******")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = LoginWithPersonalAccessToken {
- token: SecretString::from("test"),
- };
-
- let bytes = command.to_bytes();
- let token_length = bytes[0];
- let token = from_utf8(&bytes[1..1 + token_length as usize]).unwrap();
- assert!(!bytes.is_empty());
- assert_eq!(token, command.token.expose_secret());
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(LoginWithPersonalAccessToken::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = LoginWithPersonalAccessToken::default();
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- LoginWithPersonalAccessToken::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn from_bytes_should_fail_on_corrupted_token_length() {
- let mut buf = BytesMut::new();
- buf.put_u8(255);
- buf.put_slice(b"short");
- assert!(LoginWithPersonalAccessToken::from_bytes(buf.freeze()).is_err());
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let token = "test";
- let mut bytes = BytesMut::new();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(token.len() as u8);
- bytes.put_slice(token.as_bytes());
-
- let command = LoginWithPersonalAccessToken::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.token.expose_secret(), token);
- }
-
- #[test]
- fn should_fail_validation_for_empty_token() {
- let command = LoginWithPersonalAccessToken {
- token: SecretString::from(""),
- };
- assert!(command.validate().is_err());
- }
-
- #[test]
- fn should_pass_validation_for_valid_token() {
- let command = LoginWithPersonalAccessToken::default();
- assert!(command.validate().is_ok());
- }
-}
diff --git a/core/common/src/commands/segments/delete_segments.rs b/core/common/src/commands/segments/delete_segments.rs
deleted file mode 100644
index c57833f4c..000000000
--- a/core/common/src/commands/segments/delete_segments.rs
+++ /dev/null
@@ -1,202 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, DELETE_SEGMENTS_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `DeleteSegments` command is used to delete segments from a partition.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `partition_id` - unique partition ID (numeric or name).
-/// - `segments_count` - number of segments in the partition to delete.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct DeleteSegments {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Unique partition ID (numeric or name).
- #[serde(skip)]
- pub partition_id: u32,
- /// Number of segments in the partition to delete, max value is 1000.
- pub segments_count: u32,
-}
-
-impl Command for DeleteSegments {
- fn code(&self) -> u32 {
- DELETE_SEGMENTS_CODE
- }
-}
-
-impl Default for DeleteSegments {
- fn default() -> Self {
- DeleteSegments {
- segments_count: 1,
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- partition_id: u32::default(),
- }
- }
-}
-
-impl Validatable<IggyError> for DeleteSegments {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for DeleteSegments {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- std::mem::size_of::<u32>()
- + std::mem::size_of::<u32>()
- + stream_id_bytes.len()
- + topic_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u32_le(self.partition_id);
- bytes.put_u32_le(self.segments_count);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<DeleteSegments, IggyError> {
- if bytes.len() < 10 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let partition_id = u32::from_le_bytes(
- bytes
- .get(position..position + std::mem::size_of::<u32>())
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- position += std::mem::size_of::<u32>();
- let segments_count = u32::from_le_bytes(
- bytes
- .get(position..position + std::mem::size_of::<u32>())
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let command = DeleteSegments {
- stream_id,
- topic_id,
- partition_id,
- segments_count,
- };
- Ok(command)
- }
-}
-
-impl Display for DeleteSegments {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}",
- self.stream_id, self.topic_id, self.partition_id, self.segments_count
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use bytes::BufMut;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeleteSegments {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- partition_id: 3,
- segments_count: 3,
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let partition_id = u32::from_le_bytes(
- bytes[position..position + std::mem::size_of::<u32>()]
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)
- .unwrap(),
- );
- position += std::mem::size_of::<u32>();
- let segments_count = u32::from_le_bytes(
- bytes[position..position + std::mem::size_of::<u32>()]
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)
- .unwrap(),
- );
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partition_id, command.partition_id);
- assert_eq!(segments_count, command.segments_count);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let partition_id = 3;
- let segments_count = 3u32;
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- std::mem::size_of::<u32>()
- + std::mem::size_of::<u32>()
- + stream_id_bytes.len()
- + topic_id_bytes.len(),
- );
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u32_le(partition_id);
- bytes.put_u32_le(segments_count);
- let command = DeleteSegments::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(partition_id, command.partition_id);
- assert_eq!(segments_count, command.segments_count);
- }
-}
diff --git a/core/common/src/commands/streams/create_stream.rs b/core/common/src/commands/streams/create_stream.rs
deleted file mode 100644
index d5964eb5e..000000000
--- a/core/common/src/commands/streams/create_stream.rs
+++ /dev/null
@@ -1,153 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::MAX_NAME_LENGTH;
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{CREATE_STREAM_CODE, Command};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `CreateStream` command is used to create a new stream.
-/// It has additional payload:
-/// - `name` - unique stream name (string), max length is 255 characters.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct CreateStream {
- /// Unique stream name (string), max length is 255 characters.
- pub name: String,
-}
-
-impl Command for CreateStream {
- fn code(&self) -> u32 {
- CREATE_STREAM_CODE
- }
-}
-
-impl Default for CreateStream {
- fn default() -> Self {
- CreateStream {
- name: "stream".to_string(),
- }
- }
-}
-
-impl Validatable<IggyError> for CreateStream {
- fn validate(&self) -> Result<(), IggyError> {
- if self.name.is_empty() || self.name.len() > MAX_NAME_LENGTH {
- return Err(IggyError::InvalidStreamName);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for CreateStream {
- fn to_bytes(&self) -> Bytes {
- let mut bytes = BytesMut::with_capacity(5 + self.name.len());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.name.len() as u8);
- bytes.put_slice(self.name.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<CreateStream, IggyError> {
- if bytes.len() < 2 {
- return Err(IggyError::InvalidCommand);
- }
-
- let name_length = *bytes.first().ok_or(IggyError::InvalidCommand)? as usize;
- let name = from_utf8(
- bytes
- .get(1..1 + name_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let command = CreateStream { name };
- Ok(command)
- }
-}
-
-impl Display for CreateStream {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.name)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = CreateStream {
- name: "test".to_string(),
- };
-
- let bytes = command.to_bytes();
- let name_length = bytes[0];
- let name = from_utf8(&bytes[1..1 + name_length as usize]).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(name, command.name);
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(CreateStream::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = CreateStream::default();
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- CreateStream::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn from_bytes_should_fail_on_corrupted_name_length() {
- let mut buf = BytesMut::new();
- buf.put_u8(255);
- buf.put_slice(b"short");
- assert!(CreateStream::from_bytes(buf.freeze()).is_err());
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let name = "test".to_string();
- let mut bytes = BytesMut::new();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(name.len() as u8);
- bytes.put_slice(name.as_bytes());
- let command = CreateStream::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.name, name);
- }
-}
diff --git a/core/common/src/commands/streams/delete_stream.rs b/core/common/src/commands/streams/delete_stream.rs
deleted file mode 100644
index f44110340..000000000
--- a/core/common/src/commands/streams/delete_stream.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, DELETE_STREAM_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `DeleteStream` command is used to delete an existing stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
-pub struct DeleteStream {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
-}
-
-impl Command for DeleteStream {
- fn code(&self) -> u32 {
- DELETE_STREAM_CODE
- }
-}
-
-impl Validatable<IggyError> for DeleteStream {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for DeleteStream {
- fn to_bytes(&self) -> Bytes {
- self.stream_id.to_bytes()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<DeleteStream, IggyError> {
- if bytes.len() < 3 {
- return Err(IggyError::InvalidCommand);
- }
-
- let stream_id = Identifier::from_bytes(bytes)?;
- let command = DeleteStream { stream_id };
- Ok(command)
- }
-}
-
-impl Display for DeleteStream {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.stream_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeleteStream {
- stream_id: Identifier::numeric(1).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let bytes = stream_id.to_bytes();
- let command = DeleteStream::from_bytes(bytes);
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- }
-}
diff --git a/core/common/src/commands/streams/get_stream.rs b/core/common/src/commands/streams/get_stream.rs
deleted file mode 100644
index 0e3cae734..000000000
--- a/core/common/src/commands/streams/get_stream.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_STREAM_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetStream` command is used to retrieve the information about a stream by unique ID.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct GetStream {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
-}
-
-impl Command for GetStream {
- fn code(&self) -> u32 {
- GET_STREAM_CODE
- }
-}
-
-impl Validatable<IggyError> for GetStream {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetStream {
- fn to_bytes(&self) -> Bytes {
- self.stream_id.to_bytes()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<GetStream, IggyError> {
- if bytes.len() < 3 {
- return Err(IggyError::InvalidCommand);
- }
-
- let stream_id = Identifier::from_bytes(bytes)?;
- let command = GetStream { stream_id };
- Ok(command)
- }
-}
-
-impl Display for GetStream {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.stream_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetStream {
- stream_id: Identifier::numeric(1).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let bytes = stream_id.to_bytes();
- let command = GetStream::from_bytes(bytes);
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- }
-}
diff --git a/core/common/src/commands/streams/get_streams.rs b/core/common/src/commands/streams/get_streams.rs
deleted file mode 100644
index 082894375..000000000
--- a/core/common/src/commands/streams/get_streams.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_STREAMS_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetStreams` command is used to retrieve the information about all streams.
-/// It has no additional payload.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct GetStreams {}
-
-impl Command for GetStreams {
- fn code(&self) -> u32 {
- GET_STREAMS_CODE
- }
-}
-
-impl Validatable<IggyError> for GetStreams {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetStreams {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<GetStreams, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(GetStreams {})
- }
-}
-
-impl Display for GetStreams {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_empty_bytes() {
- let command = GetStreams {};
- let bytes = command.to_bytes();
- assert!(bytes.is_empty());
- }
-
- #[test]
- fn should_be_deserialized_from_empty_bytes() {
- let command = GetStreams::from_bytes(Bytes::new());
- assert!(command.is_ok());
- }
-
- #[test]
- fn should_not_be_deserialized_from_empty_bytes() {
- let command = GetStreams::from_bytes(Bytes::from_static(&[0]));
- assert!(command.is_err());
- }
-}
diff --git a/core/common/src/commands/streams/purge_stream.rs b/core/common/src/commands/streams/purge_stream.rs
deleted file mode 100644
index 036930ff8..000000000
--- a/core/common/src/commands/streams/purge_stream.rs
+++ /dev/null
@@ -1,102 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, PURGE_STREAM_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `PurgeStream` command is used to purge stream data (all the messages from its topics).
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
-pub struct PurgeStream {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
-}
-
-impl Command for PurgeStream {
- fn code(&self) -> u32 {
- PURGE_STREAM_CODE
- }
-}
-
-impl Validatable<IggyError> for PurgeStream {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for PurgeStream {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(stream_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<PurgeStream, IggyError> {
- if bytes.len() < 5 {
- return Err(IggyError::InvalidCommand);
- }
-
- let stream_id = Identifier::from_bytes(bytes)?;
- let command = PurgeStream { stream_id };
- Ok(command)
- }
-}
-
-impl Display for PurgeStream {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.stream_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = PurgeStream {
- stream_id: Identifier::numeric(1).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let bytes = stream_id.to_bytes();
- let command = PurgeStream::from_bytes(bytes);
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- }
-}
diff --git a/core/common/src/commands/streams/update_stream.rs b/core/common/src/commands/streams/update_stream.rs
deleted file mode 100644
index a202105bf..000000000
--- a/core/common/src/commands/streams/update_stream.rs
+++ /dev/null
@@ -1,151 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::MAX_NAME_LENGTH;
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, UPDATE_STREAM_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `UpdateStream` command is used to update an existing stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `name` - unique stream name (string), max length is 255 characters.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct UpdateStream {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique stream name (string), max length is 255 characters.
- pub name: String,
-}
-
-impl Command for UpdateStream {
- fn code(&self) -> u32 {
- UPDATE_STREAM_CODE
- }
-}
-
-impl Default for UpdateStream {
- fn default() -> Self {
- UpdateStream {
- stream_id: Identifier::default(),
- name: "stream".to_string(),
- }
- }
-}
-
-impl Validatable<IggyError> for UpdateStream {
- fn validate(&self) -> Result<(), IggyError> {
- if self.name.is_empty() || self.name.len() > MAX_NAME_LENGTH {
- return Err(IggyError::InvalidStreamName);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for UpdateStream {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(1 + stream_id_bytes.len() + self.name.len());
- bytes.put_slice(&stream_id_bytes);
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.name.len() as u8);
- bytes.put_slice(self.name.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<UpdateStream, IggyError> {
- if bytes.len() < 5 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let name_length = *bytes.get(position).ok_or(IggyError::InvalidCommand)? as usize;
- let name = from_utf8(
- bytes
- .get(position + 1..position + 1 + name_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let command = UpdateStream { stream_id, name };
- Ok(command)
- }
-}
-
-impl Display for UpdateStream {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}", self.stream_id, self.name)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = UpdateStream {
- stream_id: Identifier::numeric(1).unwrap(),
- name: "test".to_string(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let name_length = bytes[position];
- let name = from_utf8(&bytes[position + 1..position + 1 + name_length as usize])
- .unwrap()
- .to_string();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(name, command.name);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let name = "test".to_string();
-
- let stream_id_bytes = stream_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(1 + stream_id_bytes.len() + name.len());
- bytes.put_slice(&stream_id_bytes);
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(name.len() as u8);
- bytes.put_slice(name.as_bytes());
- let command = UpdateStream::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.name, name);
- }
-}
diff --git a/core/common/src/commands/system/get_client.rs b/core/common/src/commands/system/get_client.rs
deleted file mode 100644
index 6307355b5..000000000
--- a/core/common/src/commands/system/get_client.rs
+++ /dev/null
@@ -1,113 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_CLIENT_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetClient` command is used to get the information about a specific client by unique ID.
-/// It has additional payload:
-/// - `client_id` - unique ID (numeric) of the client.
-#[derive(Debug, Serialize, Deserialize, PartialEq)]
-pub struct GetClient {
- /// Unique ID (numeric) of the client.
- pub client_id: u32,
-}
-
-impl Command for GetClient {
- fn code(&self) -> u32 {
- GET_CLIENT_CODE
- }
-}
-
-impl Default for GetClient {
- fn default() -> Self {
- GetClient { client_id: 1 }
- }
-}
-
-impl Validatable<IggyError> for GetClient {
- fn validate(&self) -> Result<(), IggyError> {
- if self.client_id == 0 {
- return Err(IggyError::InvalidClientId);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for GetClient {
- fn to_bytes(&self) -> Bytes {
- let mut bytes = BytesMut::with_capacity(4);
- bytes.put_u32_le(self.client_id);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetClient, IggyError> {
- if bytes.len() != 4 {
- return Err(IggyError::InvalidCommand);
- }
-
- let client_id = u32::from_le_bytes(
- bytes
- .as_ref()
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let command = GetClient { client_id };
- Ok(command)
- }
-}
-
-impl Display for GetClient {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.client_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetClient { client_id: 1 };
-
- let bytes = command.to_bytes();
- let client_id = u32::from_le_bytes(bytes[..4].try_into().unwrap());
-
- assert!(!bytes.is_empty());
- assert_eq!(client_id, command.client_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let client_id = 1u32;
- let mut bytes = BytesMut::with_capacity(4);
- bytes.put_u32_le(client_id);
- let command = GetClient::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.client_id, client_id);
- }
-}
diff --git a/core/common/src/commands/system/get_clients.rs b/core/common/src/commands/system/get_clients.rs
deleted file mode 100644
index 4fdbad67b..000000000
--- a/core/common/src/commands/system/get_clients.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_CLIENTS_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetClients` command is used to get the information about all connected clients.
-/// It has no additional payload.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct GetClients {}
-
-impl Command for GetClients {
- fn code(&self) -> u32 {
- GET_CLIENTS_CODE
- }
-}
-
-impl Validatable<IggyError> for GetClients {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetClients {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetClients, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(GetClients {})
- }
-}
-
-impl Display for GetClients {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_empty_bytes() {
- let command = GetClients {};
- let bytes = command.to_bytes();
- assert!(bytes.is_empty());
- }
-
- #[test]
- fn should_be_deserialized_from_empty_bytes() {
- let command = GetClients::from_bytes(Bytes::new());
- assert!(command.is_ok());
- }
-
- #[test]
- fn should_not_be_deserialized_from_empty_bytes() {
- let command = GetClients::from_bytes(Bytes::from_static(&[0]));
- assert!(command.is_err());
- }
-}
diff --git a/core/common/src/commands/system/get_cluster_metadata.rs b/core/common/src/commands/system/get_cluster_metadata.rs
deleted file mode 100644
index ad9693ab3..000000000
--- a/core/common/src/commands/system/get_cluster_metadata.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::error::IggyError;
-use crate::{BytesSerializable, Command, GET_CLUSTER_METADATA_CODE, Validatable};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetClusterMetadata` command is used to retrieve cluster metadata including
-/// available nodes, their roles, and connection information.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct GetClusterMetadata {}
-
-impl Command for GetClusterMetadata {
- fn code(&self) -> u32 {
- GET_CLUSTER_METADATA_CODE
- }
-}
-
-impl Validatable<IggyError> for GetClusterMetadata {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetClusterMetadata {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetClusterMetadata, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
- Ok(GetClusterMetadata {})
- }
-}
-
-impl Display for GetClusterMetadata {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "GetClusterMetadata")
- }
-}
diff --git a/core/common/src/commands/system/get_me.rs b/core/common/src/commands/system/get_me.rs
deleted file mode 100644
index 5b8d772cf..000000000
--- a/core/common/src/commands/system/get_me.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_ME_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetMe` command is used to get the information connected client.
-/// It has no additional payload.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct GetMe {}
-
-impl Command for GetMe {
- fn code(&self) -> u32 {
- GET_ME_CODE
- }
-}
-
-impl Validatable<IggyError> for GetMe {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetMe {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetMe, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(GetMe {})
- }
-}
-
-impl Display for GetMe {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_empty_bytes() {
- let command = GetMe {};
- let bytes = command.to_bytes();
- assert!(bytes.is_empty());
- }
-
- #[test]
- fn should_be_deserialized_from_empty_bytes() {
- let command = GetMe::from_bytes(Bytes::new());
- assert!(command.is_ok());
- }
-
- #[test]
- fn should_not_be_deserialized_from_empty_bytes() {
- let command = GetMe::from_bytes(Bytes::from_static(&[0]));
- assert!(command.is_err());
- }
-}
diff --git a/core/common/src/commands/system/get_snapshot.rs b/core/common/src/commands/system/get_snapshot.rs
deleted file mode 100644
index 83a20809d..000000000
--- a/core/common/src/commands/system/get_snapshot.rs
+++ /dev/null
@@ -1,168 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_SNAPSHOT_FILE_CODE};
-use crate::{SnapshotCompression, SystemSnapshotType};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use tracing::error;
-
-/// `GetSnapshot` command is used to get snapshot information.
-#[derive(Debug, Serialize, Deserialize, PartialEq)]
-pub struct GetSnapshot {
- pub snapshot_types: Vec<SystemSnapshotType>,
- pub compression: SnapshotCompression,
-}
-
-impl Default for GetSnapshot {
- fn default() -> Self {
- let types = vec![
- SystemSnapshotType::FilesystemOverview,
- SystemSnapshotType::ProcessList,
- SystemSnapshotType::ResourceUsage,
- SystemSnapshotType::ServerLogs,
- ];
- Self {
- compression: SnapshotCompression::Deflated,
- snapshot_types: types,
- }
- }
-}
-
-impl Command for GetSnapshot {
- fn code(&self) -> u32 {
- GET_SNAPSHOT_FILE_CODE
- }
-}
-
-impl Validatable<IggyError> for GetSnapshot {
- fn validate(&self) -> Result<(), IggyError> {
- if self.snapshot_types.contains(&SystemSnapshotType::All) && self.snapshot_types.len() > 1 {
- error!("When using 'All' snapshot type, no other types can be specified");
- return Err(IggyError::InvalidCommand);
- }
- Ok(())
- }
-}
-
-impl BytesSerializable for GetSnapshot {
- fn to_bytes(&self) -> Bytes {
- let mut bytes = BytesMut::new();
- bytes.put_u8(self.compression.as_code());
-
- bytes.put_u8(self.snapshot_types.len() as u8);
- for snapshot_type in &self.snapshot_types {
- bytes.put_u8(snapshot_type.as_code());
- }
-
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetSnapshot, IggyError> {
- let mut index = 0;
-
- let compression =
- SnapshotCompression::from_code(*bytes.get(index).ok_or(IggyError::InvalidCommand)?)?;
- index += 1;
-
- let types_count = *bytes.get(index).ok_or(IggyError::InvalidCommand)? as usize;
- index += 1;
-
- let mut snapshot_types = Vec::with_capacity(types_count);
-
- for _ in 0..types_count {
- let tool =
- SystemSnapshotType::from_code(*bytes.get(index).ok_or(IggyError::InvalidCommand)?)?;
- index += 1;
-
- snapshot_types.push(tool);
- }
-
- Ok(GetSnapshot {
- compression,
- snapshot_types,
- })
- }
-}
-
-impl Display for GetSnapshot {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "GetSnapshotFile {{\n snapshot_types: [\n")?;
- for snapshot_type in &self.snapshot_types {
- writeln!(f, " {snapshot_type}")?;
- }
- write!(f, " ],\n Compression: {}\n}}", self.compression)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let get_snapshot_file_command = GetSnapshot {
- compression: SnapshotCompression::Deflated,
- snapshot_types: vec![SystemSnapshotType::FilesystemOverview],
- };
-
- let bytes = get_snapshot_file_command.to_bytes();
-
- let deserialized = GetSnapshot::from_bytes(bytes.clone()).unwrap();
-
- assert!(!bytes.is_empty());
-
- assert_eq!(
- deserialized.compression,
- get_snapshot_file_command.compression
- );
- assert_eq!(
- deserialized.snapshot_types,
- get_snapshot_file_command.snapshot_types
- );
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let types = vec![SystemSnapshotType::FilesystemOverview];
-
- let mut bytes = BytesMut::new();
- bytes.put_u8(SnapshotCompression::Deflated.as_code());
- bytes.put_u8(1);
-
- bytes.put_u8(types.len() as u8);
- for t in &types {
- bytes.put_u8(t.as_code());
- }
-
- let get_snapshot = GetSnapshot::from_bytes(bytes.freeze());
- assert!(get_snapshot.is_ok());
-
- let get_snapshot = get_snapshot.unwrap();
- assert_eq!(get_snapshot.snapshot_types.len(), 1);
- assert_eq!(get_snapshot.compression, SnapshotCompression::Deflated);
- assert_eq!(
- get_snapshot.snapshot_types[0],
- SystemSnapshotType::FilesystemOverview
- );
- }
-}
diff --git a/core/common/src/commands/system/get_stats.rs b/core/common/src/commands/system/get_stats.rs
deleted file mode 100644
index 8d0fea06f..000000000
--- a/core/common/src/commands/system/get_stats.rs
+++ /dev/null
@@ -1,86 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_STATS_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetStats` command is used to get the statistics about the system.
-/// It has no additional payload.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct GetStats {}
-
-impl Command for GetStats {
- fn code(&self) -> u32 {
- GET_STATS_CODE
- }
-}
-
-impl Validatable<IggyError> for GetStats {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetStats {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetStats, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(GetStats {})
- }
-}
-
-impl Display for GetStats {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_empty_bytes() {
- let command = GetStats {};
- let bytes = command.to_bytes();
- assert!(bytes.is_empty());
- }
-
- #[test]
- fn should_be_deserialized_from_empty_bytes() {
- let command = GetStats::from_bytes(Bytes::new());
- assert!(command.is_ok());
- }
-
- #[test]
- fn should_not_be_deserialized_from_empty_bytes() {
- let command = GetStats::from_bytes(Bytes::from_static(&[0]));
- assert!(command.is_err());
- }
-}
diff --git a/core/common/src/commands/system/ping.rs b/core/common/src/commands/system/ping.rs
deleted file mode 100644
index 2a7e1b130..000000000
--- a/core/common/src/commands/system/ping.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, PING_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `Ping` command is used to check if the server is alive.
-/// It has no additional payload.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct Ping {}
-
-impl Command for Ping {
- fn code(&self) -> u32 {
- PING_CODE
- }
-}
-
-impl Validatable<IggyError> for Ping {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for Ping {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<Ping, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
-
- let command = Ping {};
- Ok(command)
- }
-}
-
-impl Display for Ping {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_empty_bytes() {
- let command = Ping {};
- let bytes = command.to_bytes();
- assert!(bytes.is_empty());
- }
-
- #[test]
- fn should_be_deserialized_from_empty_bytes() {
- let command = Ping::from_bytes(Bytes::new());
- assert!(command.is_ok());
- }
-
- #[test]
- fn should_not_be_deserialized_from_empty_bytes() {
- let command = Ping::from_bytes(Bytes::from_static(&[0]));
- assert!(command.is_err());
- }
-}
diff --git a/core/common/src/commands/topics/create_topic.rs b/core/common/src/commands/topics/create_topic.rs
deleted file mode 100644
index bd4d87f48..000000000
--- a/core/common/src/commands/topics/create_topic.rs
+++ /dev/null
@@ -1,301 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::{MAX_NAME_LENGTH, MAX_PARTITIONS_COUNT};
-use crate::BytesSerializable;
-use crate::CompressionAlgorithm;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::utils::expiry::IggyExpiry;
-use crate::utils::topic_size::MaxTopicSize;
-use crate::{CREATE_TOPIC_CODE, Command};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `CreateTopic` command is used to create a new topic in a stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `partitions_count` - number of partitions in the topic, max value is 1000.
-/// - `message_expiry` - message expiry, if `NeverExpire` then messages will never expire.
-/// - `max_topic_size` - maximum size of the topic, if `Unlimited` then topic size is unlimited.
-/// Can't be lower than segment size in the config.
-/// - `replication_factor` - replication factor for the topic.
-/// - `name` - unique topic name, max length is 255 characters.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct CreateTopic {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Number of partitions in the topic, max value is 1000.
- pub partitions_count: u32,
- /// Compression algorithm for the topic.
- pub compression_algorithm: CompressionAlgorithm,
- /// Message expiry, if `NeverExpire` then messages will never expire.
- pub message_expiry: IggyExpiry,
- /// Max topic size, if `Unlimited` then topic size is unlimited.
- /// Can't be lower than segment size in the config.
- pub max_topic_size: MaxTopicSize,
- /// Replication factor for the topic.
- pub replication_factor: Option<u8>,
- /// Unique topic name, max length is 255 characters.
- pub name: String,
-}
-
-impl Command for CreateTopic {
- fn code(&self) -> u32 {
- CREATE_TOPIC_CODE
- }
-}
-
-impl Default for CreateTopic {
- fn default() -> Self {
- CreateTopic {
- stream_id: Identifier::default(),
- partitions_count: 1,
- compression_algorithm: CompressionAlgorithm::None,
- message_expiry: IggyExpiry::NeverExpire,
- max_topic_size: MaxTopicSize::ServerDefault,
- replication_factor: None,
- name: "topic".to_string(),
- }
- }
-}
-
-impl Validatable<IggyError> for CreateTopic {
- fn validate(&self) -> Result<(), IggyError> {
- if self.name.is_empty() || self.name.len() > MAX_NAME_LENGTH {
- return Err(IggyError::InvalidTopicName);
- }
-
- if !(0..=MAX_PARTITIONS_COUNT).contains(&self.partitions_count) {
- return Err(IggyError::TooManyPartitions);
- }
-
- if let Some(replication_factor) = self.replication_factor
- && replication_factor == 0
- {
- return Err(IggyError::InvalidReplicationFactor);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for CreateTopic {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(19 + stream_id_bytes.len() + self.name.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_u32_le(self.partitions_count);
- bytes.put_u8(self.compression_algorithm.as_code());
- bytes.put_u64_le(self.message_expiry.into());
- bytes.put_u64_le(self.max_topic_size.into());
- match self.replication_factor {
- Some(replication_factor) => bytes.put_u8(replication_factor),
- None => bytes.put_u8(0),
- }
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.name.len() as u8);
- bytes.put_slice(self.name.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<CreateTopic, IggyError> {
- if bytes.len() < 14 {
- return Err(IggyError::InvalidCommand);
- }
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let partitions_count = u32::from_le_bytes(
- bytes
- .get(position..position + 4)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let compression_algorithm = CompressionAlgorithm::from_code(
- *bytes.get(position + 4).ok_or(IggyError::InvalidCommand)?,
- )?;
- let message_expiry = u64::from_le_bytes(
- bytes
- .get(position + 5..position + 13)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let message_expiry: IggyExpiry = message_expiry.into();
- let max_topic_size = u64::from_le_bytes(
- bytes
- .get(position + 13..position + 21)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let max_topic_size: MaxTopicSize = max_topic_size.into();
- let replication_factor = match *bytes.get(position + 21).ok_or(IggyError::InvalidCommand)? {
- 0 => None,
- factor => Some(factor),
- };
- let name_length = *bytes.get(position + 22).ok_or(IggyError::InvalidCommand)? as usize;
- let name = from_utf8(
- bytes
- .get(position + 23..position + 23 + name_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
- let command = CreateTopic {
- stream_id,
- partitions_count,
- compression_algorithm,
- message_expiry,
- max_topic_size,
- replication_factor,
- name,
- };
- Ok(command)
- }
-}
-
-impl Display for CreateTopic {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}|{}|{}",
- self.stream_id,
- self.partitions_count,
- self.message_expiry,
- self.max_topic_size,
- self.replication_factor.unwrap_or(0),
- self.name
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use bytes::BufMut;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = CreateTopic {
- stream_id: Identifier::numeric(1).unwrap(),
- partitions_count: 3,
- message_expiry: IggyExpiry::NeverExpire,
- compression_algorithm: CompressionAlgorithm::None,
- max_topic_size: MaxTopicSize::ServerDefault,
- replication_factor: Some(1),
- name: "test".to_string(),
- };
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let partitions_count =
- u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
- let compression_algorithm = CompressionAlgorithm::from_code(bytes[position + 4]).unwrap();
- let message_expiry =
- u64::from_le_bytes(bytes[position + 5..position + 13].try_into().unwrap());
- let message_expiry: IggyExpiry = message_expiry.into();
- let max_topic_size =
- u64::from_le_bytes(bytes[position + 13..position + 21].try_into().unwrap());
- let max_topic_size: MaxTopicSize = max_topic_size.into();
- let replication_factor = bytes[position + 21];
- let name_length = bytes[position + 22];
- let name = from_utf8(&bytes[position + 23..(position + 23 + name_length as usize)])
- .unwrap()
- .to_string();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(partitions_count, command.partitions_count);
- assert_eq!(compression_algorithm, command.compression_algorithm);
- assert_eq!(message_expiry, command.message_expiry);
- assert_eq!(max_topic_size, command.max_topic_size);
- assert_eq!(replication_factor, command.replication_factor.unwrap());
- assert_eq!(name.len() as u8, command.name.len() as u8);
- assert_eq!(name, command.name);
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(CreateTopic::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = CreateTopic {
- stream_id: Identifier::numeric(1).unwrap(),
- partitions_count: 3,
- compression_algorithm: CompressionAlgorithm::None,
- message_expiry: IggyExpiry::NeverExpire,
- max_topic_size: MaxTopicSize::ServerDefault,
- replication_factor: Some(1),
- name: "test".to_string(),
- };
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- CreateTopic::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let partitions_count = 3u32;
- let compression_algorithm = CompressionAlgorithm::None;
- let name = "test".to_string();
- let message_expiry = IggyExpiry::NeverExpire;
- let max_topic_size = MaxTopicSize::ServerDefault;
- let replication_factor = 1;
- let stream_id_bytes = stream_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(10 + stream_id_bytes.len() + name.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_u32_le(partitions_count);
- bytes.put_u8(compression_algorithm.as_code());
- bytes.put_u64_le(message_expiry.into());
- bytes.put_u64_le(max_topic_size.as_bytes_u64());
- bytes.put_u8(replication_factor);
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(name.len() as u8);
- bytes.put_slice(name.as_bytes());
-
- let command = CreateTopic::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.name, name);
- assert_eq!(command.partitions_count, partitions_count);
- assert_eq!(command.compression_algorithm, compression_algorithm);
- assert_eq!(command.message_expiry, message_expiry);
- assert_eq!(command.max_topic_size, max_topic_size);
- assert_eq!(command.replication_factor.unwrap(), replication_factor);
- assert_eq!(command.partitions_count, partitions_count);
- }
-}
diff --git a/core/common/src/commands/topics/delete_topic.rs b/core/common/src/commands/topics/delete_topic.rs
deleted file mode 100644
index 45680311d..000000000
--- a/core/common/src/commands/topics/delete_topic.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, DELETE_TOPIC_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `DeleteTopic` command is used to delete a topic from a stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
-pub struct DeleteTopic {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
-}
-
-impl Command for DeleteTopic {
- fn code(&self) -> u32 {
- DELETE_TOPIC_CODE
- }
-}
-
-impl Validatable<IggyError> for DeleteTopic {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for DeleteTopic {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<DeleteTopic, IggyError> {
- if bytes.len() < 10 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = DeleteTopic {
- stream_id,
- topic_id,
- };
- Ok(command)
- }
-}
-
-impl Display for DeleteTopic {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}", self.stream_id, self.topic_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use bytes::BufMut;
-
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeleteTopic {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let mut bytes = BytesMut::new();
- bytes.put(stream_id.to_bytes());
- bytes.put(topic_id.to_bytes());
- let command = DeleteTopic::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- }
-}
diff --git a/core/common/src/commands/topics/get_topic.rs b/core/common/src/commands/topics/get_topic.rs
deleted file mode 100644
index d18d07b42..000000000
--- a/core/common/src/commands/topics/get_topic.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_TOPIC_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetTopic` command is used to retrieve a topic from a stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct GetTopic {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
-}
-
-impl Command for GetTopic {
- fn code(&self) -> u32 {
- GET_TOPIC_CODE
- }
-}
-
-impl Validatable<IggyError> for GetTopic {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetTopic {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<GetTopic, IggyError> {
- if bytes.len() < 6 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = GetTopic {
- stream_id,
- topic_id,
- };
- Ok(command)
- }
-}
-
-impl Display for GetTopic {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}", self.stream_id, self.topic_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use bytes::BufMut;
-
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetTopic {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let mut bytes = BytesMut::new();
- bytes.put(stream_id.to_bytes());
- bytes.put(topic_id.to_bytes());
- let command = GetTopic::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- }
-}
diff --git a/core/common/src/commands/topics/get_topics.rs b/core/common/src/commands/topics/get_topics.rs
deleted file mode 100644
index fa49c570c..000000000
--- a/core/common/src/commands/topics/get_topics.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_TOPICS_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetTopics` command is used to retrieve the collection of topics from a stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct GetTopics {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
-}
-
-impl Command for GetTopics {
- fn code(&self) -> u32 {
- GET_TOPICS_CODE
- }
-}
-
-impl Validatable<IggyError> for GetTopics {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetTopics {
- fn to_bytes(&self) -> Bytes {
- self.stream_id.to_bytes()
- }
-
- fn from_bytes(bytes: Bytes) -> std::result::Result<GetTopics, IggyError> {
- if bytes.len() < 3 {
- return Err(IggyError::InvalidCommand);
- }
-
- let stream_id = Identifier::from_bytes(bytes)?;
- let command = GetTopics { stream_id };
- Ok(command)
- }
-}
-
-impl Display for GetTopics {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.stream_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetTopics {
- stream_id: Identifier::numeric(1).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let bytes = stream_id.to_bytes();
- let command = GetTopics::from_bytes(bytes);
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- }
-}
diff --git a/core/common/src/commands/topics/purge_topic.rs b/core/common/src/commands/topics/purge_topic.rs
deleted file mode 100644
index 9473b38c7..000000000
--- a/core/common/src/commands/topics/purge_topic.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, PURGE_TOPIC_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `PurgeTopic` command is used to purge topic data (its messages in all the partitions) from a stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
-pub struct PurgeTopic {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
-}
-
-impl Command for PurgeTopic {
- fn code(&self) -> u32 {
- PURGE_TOPIC_CODE
- }
-}
-
-impl Validatable<IggyError> for PurgeTopic {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for PurgeTopic {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(stream_id_bytes.len() + topic_id_bytes.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<PurgeTopic, IggyError> {
- if bytes.len() < 10 {
- return Err(IggyError::InvalidCommand);
- }
-
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- let command = PurgeTopic {
- stream_id,
- topic_id,
- };
- Ok(command)
- }
-}
-
-impl Display for PurgeTopic {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|{}", self.stream_id, self.topic_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = PurgeTopic {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let mut bytes = BytesMut::new();
- bytes.put_slice(&stream_id.to_bytes());
- bytes.put_slice(&topic_id.to_bytes());
- let command = PurgeTopic::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- }
-}
diff --git a/core/common/src/commands/topics/update_topic.rs b/core/common/src/commands/topics/update_topic.rs
deleted file mode 100644
index 6f7bf5a02..000000000
--- a/core/common/src/commands/topics/update_topic.rs
+++ /dev/null
@@ -1,302 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::MAX_NAME_LENGTH;
-use crate::BytesSerializable;
-use crate::CompressionAlgorithm;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::utils::expiry::IggyExpiry;
-use crate::utils::topic_size::MaxTopicSize;
-use crate::{Command, UPDATE_TOPIC_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `UpdateTopic` command is used to update a topic in a stream.
-/// It has additional payload:
-/// - `stream_id` - unique stream ID (numeric or name).
-/// - `topic_id` - unique topic ID (numeric or name).
-/// - `message_expiry` - message expiry, if `NeverExpire` then messages will never expire.
-/// - `max_topic_size` - maximum size of the topic in bytes, if `Unlimited` then topic size is unlimited.
-/// Can't be lower than segment size in the config.
-/// - `replication_factor` - replication factor for the topic.
-/// - `name` - unique topic name, max length is 255 characters.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
-pub struct UpdateTopic {
- /// Unique stream ID (numeric or name).
- #[serde(skip)]
- pub stream_id: Identifier,
- /// Unique topic ID (numeric or name).
- #[serde(skip)]
- pub topic_id: Identifier,
- /// Compression algorithm for the topic.
- pub compression_algorithm: CompressionAlgorithm,
- /// Message expiry, if `NeverExpire` then messages will never expire.
- pub message_expiry: IggyExpiry,
- /// Max topic size, if `Unlimited` then topic size is unlimited.
- /// Can't be lower than segment size in the config.
- pub max_topic_size: MaxTopicSize,
- /// Replication factor for the topic.
- pub replication_factor: Option<u8>,
- /// Unique topic name, max length is 255 characters.
- pub name: String,
-}
-
-impl Command for UpdateTopic {
- fn code(&self) -> u32 {
- UPDATE_TOPIC_CODE
- }
-}
-
-impl Default for UpdateTopic {
- fn default() -> Self {
- UpdateTopic {
- stream_id: Identifier::default(),
- topic_id: Identifier::default(),
- compression_algorithm: Default::default(),
- message_expiry: IggyExpiry::NeverExpire,
- max_topic_size: MaxTopicSize::ServerDefault,
- replication_factor: None,
- name: "topic".to_string(),
- }
- }
-}
-
-impl Validatable<IggyError> for UpdateTopic {
- fn validate(&self) -> Result<(), IggyError> {
- if self.name.is_empty() || self.name.len() > MAX_NAME_LENGTH {
- return Err(IggyError::InvalidTopicName);
- }
-
- if let Some(replication_factor) = self.replication_factor
- && replication_factor == 0
- {
- return Err(IggyError::InvalidReplicationFactor);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for UpdateTopic {
- fn to_bytes(&self) -> Bytes {
- let stream_id_bytes = self.stream_id.to_bytes();
- let topic_id_bytes = self.topic_id.to_bytes();
- let mut bytes = BytesMut::with_capacity(
- 19 + stream_id_bytes.len() + topic_id_bytes.len() + self.name.len(),
- );
- bytes.put_slice(&stream_id_bytes.clone());
- bytes.put_slice(&topic_id_bytes.clone());
- bytes.put_u8(self.compression_algorithm.as_code());
- bytes.put_u64_le(self.message_expiry.into());
- bytes.put_u64_le(self.max_topic_size.into());
- match self.replication_factor {
- Some(replication_factor) => bytes.put_u8(replication_factor),
- None => bytes.put_u8(0),
- }
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.name.len() as u8);
- bytes.put_slice(self.name.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<UpdateTopic, IggyError> {
- if bytes.len() < 21 {
- return Err(IggyError::InvalidCommand);
- }
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone())?;
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..))?;
- position += topic_id.get_size_bytes().as_bytes_usize();
- let compression_algorithm = CompressionAlgorithm::from_code(
- *bytes.get(position).ok_or(IggyError::InvalidCommand)?,
- )?;
- position += 1;
- let message_expiry = u64::from_le_bytes(
- bytes
- .get(position..position + 8)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let message_expiry: IggyExpiry = message_expiry.into();
- let max_topic_size = u64::from_le_bytes(
- bytes
- .get(position + 8..position + 16)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- let max_topic_size: MaxTopicSize = max_topic_size.into();
- let replication_factor = match *bytes.get(position + 16).ok_or(IggyError::InvalidCommand)? {
- 0 => None,
- factor => Some(factor),
- };
- let name_length = *bytes.get(position + 17).ok_or(IggyError::InvalidCommand)? as usize;
- let name = from_utf8(
- bytes
- .get(position + 18..position + 18 + name_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
- let command = UpdateTopic {
- stream_id,
- topic_id,
- compression_algorithm,
- message_expiry,
- max_topic_size,
- replication_factor,
- name,
- };
- Ok(command)
- }
-}
-
-impl Display for UpdateTopic {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}|{}|{}|{}|{}|{}",
- self.stream_id,
- self.topic_id,
- self.message_expiry,
- self.max_topic_size,
- self.replication_factor.unwrap_or(0),
- self.name,
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::utils::byte_size::IggyByteSize;
- use bytes::BufMut;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = UpdateTopic {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- compression_algorithm: CompressionAlgorithm::None,
- message_expiry: IggyExpiry::NeverExpire,
- max_topic_size: MaxTopicSize::ServerDefault,
- replication_factor: Some(1),
- name: "test".to_string(),
- };
-
- let bytes = command.to_bytes();
- let mut position = 0;
- let stream_id = Identifier::from_bytes(bytes.clone()).unwrap();
- position += stream_id.get_size_bytes().as_bytes_usize();
- let topic_id = Identifier::from_bytes(bytes.slice(position..)).unwrap();
- position += topic_id.get_size_bytes().as_bytes_usize();
- let compression_algorithm = CompressionAlgorithm::from_code(bytes[position]).unwrap();
- position += 1;
- let message_expiry = u64::from_le_bytes(bytes[position..position + 8].try_into().unwrap());
- let message_expiry: IggyExpiry = message_expiry.into();
- let max_topic_size =
- u64::from_le_bytes(bytes[position + 8..position + 16].try_into().unwrap());
- let max_topic_size: MaxTopicSize = max_topic_size.into();
- let replication_factor = bytes[position + 16];
- let name_length = bytes[position + 17];
- let name = from_utf8(&bytes[position + 18..position + 18 + name_length as usize])
- .unwrap()
- .to_string();
-
- assert!(!bytes.is_empty());
- assert_eq!(stream_id, command.stream_id);
- assert_eq!(topic_id, command.topic_id);
- assert_eq!(compression_algorithm, command.compression_algorithm);
- assert_eq!(message_expiry, command.message_expiry);
- assert_eq!(max_topic_size, command.max_topic_size);
- assert_eq!(replication_factor, command.replication_factor.unwrap());
- assert_eq!(name.len() as u8, command.name.len() as u8);
- assert_eq!(name, command.name);
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(UpdateTopic::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = UpdateTopic {
- stream_id: Identifier::numeric(1).unwrap(),
- topic_id: Identifier::numeric(2).unwrap(),
- compression_algorithm: CompressionAlgorithm::None,
- message_expiry: IggyExpiry::NeverExpire,
- max_topic_size: MaxTopicSize::ServerDefault,
- replication_factor: Some(1),
- name: "test".to_string(),
- };
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- UpdateTopic::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let stream_id = Identifier::numeric(1).unwrap();
- let topic_id = Identifier::numeric(2).unwrap();
- let compression_algorithm = CompressionAlgorithm::None;
- let name = "test".to_string();
- let message_expiry = IggyExpiry::NeverExpire;
- let max_topic_size = MaxTopicSize::Custom(IggyByteSize::from(100));
- let replication_factor = 1;
-
- let stream_id_bytes = stream_id.to_bytes();
- let topic_id_bytes = topic_id.to_bytes();
- let mut bytes =
- BytesMut::with_capacity(19 + stream_id_bytes.len() + topic_id_bytes.len() + name.len());
- bytes.put_slice(&stream_id_bytes);
- bytes.put_slice(&topic_id_bytes);
- bytes.put_u8(compression_algorithm.as_code());
- bytes.put_u64_le(message_expiry.into());
- bytes.put_u64_le(max_topic_size.as_bytes_u64());
- bytes.put_u8(replication_factor);
-
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(name.len() as u8);
- bytes.put_slice(name.as_bytes());
-
- let command = UpdateTopic::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.stream_id, stream_id);
- assert_eq!(command.topic_id, topic_id);
- assert_eq!(command.compression_algorithm, compression_algorithm);
- assert_eq!(command.message_expiry, message_expiry);
- assert_eq!(command.max_topic_size, max_topic_size);
- assert_eq!(command.replication_factor, Some(replication_factor));
- assert_eq!(command.name, name);
- }
-}
diff --git a/core/common/src/commands/users/change_password.rs b/core/common/src/commands/users/change_password.rs
deleted file mode 100644
index 9d5272433..000000000
--- a/core/common/src/commands/users/change_password.rs
+++ /dev/null
@@ -1,246 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::defaults::*;
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Sizeable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{CHANGE_PASSWORD_CODE, Command};
-use bytes::{BufMut, Bytes, BytesMut};
-use secrecy::{ExposeSecret, SecretString};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `ChangePassword` command is used to change a user's password.
-/// It has additional payload:
-/// - `user_id` - unique user ID (numeric or name).
-/// - `current_password` - current password, must be between 3 and 100 characters long.
-/// - `new_password` - new password, must be between 3 and 100 characters long.
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct ChangePassword {
- /// Unique user ID (numeric or name).
- #[serde(skip)]
- pub user_id: Identifier,
- /// Current password, must be between 3 and 100 characters long.
- #[serde(serialize_with = "crate::utils::serde_secret::serialize_secret")]
- pub current_password: SecretString,
- /// New password, must be between 3 and 100 characters long.
- #[serde(serialize_with = "crate::utils::serde_secret::serialize_secret")]
- pub new_password: SecretString,
-}
-
-impl Command for ChangePassword {
- fn code(&self) -> u32 {
- CHANGE_PASSWORD_CODE
- }
-}
-
-impl Default for ChangePassword {
- fn default() -> Self {
- ChangePassword {
- user_id: Identifier::default(),
- current_password: SecretString::from("secret"),
- new_password: SecretString::from("topsecret"),
- }
- }
-}
-
-impl Validatable<IggyError> for ChangePassword {
- fn validate(&self) -> Result<(), IggyError> {
- let current_password = self.current_password.expose_secret();
- if current_password.is_empty()
- || current_password.len() > MAX_PASSWORD_LENGTH
- || current_password.len() < MIN_PASSWORD_LENGTH
- {
- return Err(IggyError::InvalidPassword);
- }
-
- let new_password = self.new_password.expose_secret();
- if new_password.is_empty()
- || new_password.len() > MAX_PASSWORD_LENGTH
- || new_password.len() < MIN_PASSWORD_LENGTH
- {
- return Err(IggyError::InvalidPassword);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for ChangePassword {
- fn to_bytes(&self) -> Bytes {
- let user_id_bytes = self.user_id.to_bytes();
- let current_password = self.current_password.expose_secret();
- let new_password = self.new_password.expose_secret();
- let mut bytes = BytesMut::new();
- bytes.put_slice(&user_id_bytes);
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(current_password.len() as u8);
- bytes.put_slice(current_password.as_bytes());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(new_password.len() as u8);
- bytes.put_slice(new_password.as_bytes());
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<ChangePassword, IggyError> {
- if bytes.len() < 9 {
- return Err(IggyError::InvalidCommand);
- }
-
- let user_id = Identifier::from_bytes(bytes.clone())?;
- let mut position = user_id.get_size_bytes().as_bytes_usize();
- let current_password_length = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- position += 1;
- let current_password = from_utf8(
- bytes
- .get(position..position + current_password_length as usize)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
- position += current_password_length as usize;
- let new_password_length = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- position += 1;
- let new_password = from_utf8(
- bytes
- .get(position..position + new_password_length as usize)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let command = ChangePassword {
- user_id,
- current_password: SecretString::from(current_password),
- new_password: SecretString::from(new_password),
- };
- Ok(command)
- }
-}
-
-impl Display for ChangePassword {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|******|******", self.user_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = ChangePassword {
- user_id: Identifier::numeric(1).unwrap(),
- current_password: SecretString::from("user"),
- new_password: SecretString::from("secret"),
- };
-
- let bytes = command.to_bytes();
- let user_id = Identifier::from_bytes(bytes.clone()).unwrap();
- let mut position = user_id.get_size_bytes().as_bytes_usize();
- let current_password_length = bytes[position];
- position += 1;
- let current_password =
- from_utf8(&bytes[position..position + current_password_length as usize]).unwrap();
- position += current_password_length as usize;
- let new_password_length = bytes[position];
- position += 1;
- let new_password =
- from_utf8(&bytes[position..position + new_password_length as usize]).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(user_id, command.user_id);
- assert_eq!(current_password, command.current_password.expose_secret());
- assert_eq!(new_password, command.new_password.expose_secret());
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(ChangePassword::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = ChangePassword::default();
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- ChangePassword::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let user_id = Identifier::numeric(1).unwrap();
- let current_password = "secret";
- let new_password = "topsecret";
- let mut bytes = BytesMut::new();
- bytes.put_slice(&user_id.to_bytes());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(current_password.len() as u8);
- bytes.put_slice(current_password.as_bytes());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(new_password.len() as u8);
- bytes.put_slice(new_password.as_bytes());
-
- let command = ChangePassword::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.user_id, user_id);
- assert_eq!(command.current_password.expose_secret(), current_password);
- assert_eq!(command.new_password.expose_secret(), new_password);
- }
-
- #[test]
- fn should_fail_validation_for_invalid_current_password() {
- for password in ["", "ab"] {
- let command = ChangePassword {
- current_password: SecretString::from(password),
- ..ChangePassword::default()
- };
- assert!(
- command.validate().is_err(),
- "expected validation error for current_password: {password:?}"
- );
- }
- }
-
- #[test]
- fn should_fail_validation_for_empty_new_password() {
- let command = ChangePassword {
- new_password: SecretString::from(""),
- ..ChangePassword::default()
- };
- assert!(command.validate().is_err());
- }
-
- #[test]
- fn should_pass_validation_for_valid_command() {
- let command = ChangePassword::default();
- assert!(command.validate().is_ok());
- }
-}
diff --git a/core/common/src/commands/users/create_user.rs b/core/common/src/commands/users/create_user.rs
deleted file mode 100644
index 3405bd486..000000000
--- a/core/common/src/commands/users/create_user.rs
+++ /dev/null
@@ -1,347 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::defaults::*;
-use crate::BytesSerializable;
-use crate::Permissions;
-use crate::UserStatus;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{CREATE_USER_CODE, Command};
-use bytes::{BufMut, Bytes, BytesMut};
-use secrecy::{ExposeSecret, SecretString};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `CreateUser` command is used to create a new user.
-/// It has additional payload:
-/// - `username` - unique name of the user, must be between 3 and 50 characters long.
-/// - `password` - password of the user, must be between 3 and 100 characters long.
-/// - `status` - status of the user, can be either `active` or `inactive`.
-/// - `permissions` - optional permissions of the user. If not provided, user will have no permissions.
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct CreateUser {
- /// Unique name of the user, must be between 3 and 50 characters long.
- pub username: String,
- /// Password of the user, must be between 3 and 100 characters long.
- #[serde(serialize_with = "crate::utils::serde_secret::serialize_secret")]
- pub password: SecretString,
- /// Status of the user, can be either `active` or `inactive`.
- pub status: UserStatus,
- /// Optional permissions of the user. If not provided, user will have no permissions.
- pub permissions: Option<Permissions>,
-}
-
-impl Command for CreateUser {
- fn code(&self) -> u32 {
- CREATE_USER_CODE
- }
-}
-
-impl Default for CreateUser {
- fn default() -> Self {
- CreateUser {
- username: "user".to_string(),
- password: SecretString::from("secret"),
- status: UserStatus::Active,
- permissions: None,
- }
- }
-}
-
-impl Validatable<IggyError> for CreateUser {
- fn validate(&self) -> Result<(), IggyError> {
- if self.username.is_empty()
- || self.username.len() > MAX_USERNAME_LENGTH
- || self.username.len() < MIN_USERNAME_LENGTH
- {
- return Err(IggyError::InvalidUsername);
- }
-
- let password = self.password.expose_secret();
- if password.is_empty()
- || password.len() > MAX_PASSWORD_LENGTH
- || password.len() < MIN_PASSWORD_LENGTH
- {
- return Err(IggyError::InvalidPassword);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for CreateUser {
- fn to_bytes(&self) -> Bytes {
- let password = self.password.expose_secret();
- let mut bytes = BytesMut::with_capacity(2 + self.username.len() + password.len());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.username.len() as u8);
- bytes.put_slice(self.username.as_bytes());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(password.len() as u8);
- bytes.put_slice(password.as_bytes());
- bytes.put_u8(self.status.as_code());
- if let Some(permissions) = &self.permissions {
- bytes.put_u8(1);
- let permissions = permissions.to_bytes();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u32_le(permissions.len() as u32);
- bytes.put_slice(&permissions);
- } else {
- bytes.put_u8(0);
- }
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<CreateUser, IggyError> {
- if bytes.len() < 10 {
- return Err(IggyError::InvalidCommand);
- }
-
- let username_length = *bytes.first().ok_or(IggyError::InvalidCommand)? as usize;
- let username = from_utf8(
- bytes
- .get(1..1 + username_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let mut position = 1 + username_length;
- let password_length = *bytes.get(position).ok_or(IggyError::InvalidCommand)? as usize;
- position += 1;
- let password = from_utf8(
- bytes
- .get(position..position + password_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- position += password_length;
- let status = UserStatus::from_code(*bytes.get(position).ok_or(IggyError::InvalidCommand)?)?;
- position += 1;
- let has_permissions = *bytes.get(position).ok_or(IggyError::InvalidCommand)?;
- if has_permissions > 1 {
- return Err(IggyError::InvalidCommand);
- }
-
- position += 1;
- let permissions = if has_permissions == 1 {
- let permissions_length = u32::from_le_bytes(
- bytes
- .get(position..position + 4)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- position += 4;
- let end = position
- .checked_add(permissions_length as usize)
- .ok_or(IggyError::InvalidCommand)?;
- if end > bytes.len() {
- return Err(IggyError::InvalidCommand);
- }
- Some(Permissions::from_bytes(bytes.slice(position..end))?)
- } else {
- None
- };
-
- let command = CreateUser {
- username,
- password: SecretString::from(password),
- status,
- permissions,
- };
- Ok(command)
- }
-}
-
-impl Display for CreateUser {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let permissions = if let Some(permissions) = &self.permissions {
- permissions.to_string()
- } else {
- "no_permissions".to_string()
- };
- write!(
- f,
- "{}|******|{}|{}",
- self.username, self.status, permissions
- )
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::GlobalPermissions;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = CreateUser {
- username: "user".to_string(),
- password: SecretString::from("secret"),
- status: UserStatus::Active,
- permissions: Some(Permissions {
- global: GlobalPermissions {
- manage_servers: false,
- read_servers: true,
- manage_users: false,
- read_users: true,
- manage_streams: false,
- read_streams: true,
- manage_topics: false,
- read_topics: true,
- poll_messages: true,
- send_messages: true,
- },
- streams: None,
- }),
- };
-
- let bytes = command.to_bytes();
- let username_length = bytes[0];
- let username = from_utf8(&bytes[1..1 + username_length as usize]).unwrap();
- let mut position = 1 + username_length as usize;
- let password_length = bytes[position];
- position += 1;
- let password = from_utf8(&bytes[position..position + password_length as usize]).unwrap();
- position += password_length as usize;
- let status = UserStatus::from_code(bytes[position]).unwrap();
- position += 1;
- let has_permissions = bytes[3 + username_length as usize + password_length as usize];
- position += 1;
-
- let permissions_length =
- u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
- position += 4;
- let permissions =
- Permissions::from_bytes(bytes.slice(position..position + permissions_length as usize))
- .unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(username, command.username);
- assert_eq!(password, command.password.expose_secret());
- assert_eq!(status, command.status);
- assert_eq!(has_permissions, 1);
- assert_eq!(permissions, command.permissions.unwrap());
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(CreateUser::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = CreateUser::default();
- let bytes = command.to_bytes();
- for i in 0..bytes.len() - 1 {
- let truncated = bytes.slice(..i);
- assert!(
- CreateUser::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn from_bytes_should_fail_on_corrupted_username_length() {
- let mut buf = BytesMut::new();
- buf.put_u8(255);
- buf.put_slice(b"short");
- assert!(CreateUser::from_bytes(buf.freeze()).is_err());
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let username = "user";
- let password = "secret";
- let status = UserStatus::Active;
- let has_permissions = 1u8;
- let permissions = Permissions {
- global: GlobalPermissions {
- manage_servers: false,
- read_servers: true,
- manage_users: false,
- read_users: true,
- manage_streams: false,
- read_streams: true,
- manage_topics: false,
- read_topics: true,
- poll_messages: true,
- send_messages: true,
- },
- streams: None,
- };
- let mut bytes = BytesMut::new();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(username.len() as u8);
- bytes.put_slice(username.as_bytes());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(password.len() as u8);
- bytes.put_slice(password.as_bytes());
- bytes.put_u8(status.as_code());
- bytes.put_u8(has_permissions);
- let permissions_bytes = permissions.to_bytes();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u32_le(permissions_bytes.len() as u32);
- bytes.put_slice(&permissions_bytes);
-
- let command = CreateUser::from_bytes(bytes.freeze());
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.username, username);
- assert_eq!(command.password.expose_secret(), password);
- assert_eq!(command.status, status);
- assert!(command.permissions.is_some());
- assert_eq!(command.permissions.unwrap(), permissions);
- }
-
- #[test]
- fn should_fail_validation_for_empty_username() {
- let command = CreateUser {
- username: "".to_string(),
- ..CreateUser::default()
- };
- assert!(command.validate().is_err());
- }
-
- #[test]
- fn should_fail_validation_for_invalid_password() {
- for password in ["", "ab"] {
- let command = CreateUser {
- password: SecretString::from(password),
- ..CreateUser::default()
- };
- assert!(
- command.validate().is_err(),
- "expected validation error for password: {password:?}"
- );
- }
- }
-
- #[test]
- fn should_pass_validation_for_valid_command() {
- let command = CreateUser::default();
- assert!(command.validate().is_ok());
- }
-}
diff --git a/core/common/src/commands/users/delete_user.rs b/core/common/src/commands/users/delete_user.rs
deleted file mode 100644
index 3cc5682a6..000000000
--- a/core/common/src/commands/users/delete_user.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, DELETE_USER_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `DeleteUser` command is used to delete a user by unique ID.
-/// It has additional payload:
-/// - `user_id` - unique user ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default, Clone)]
-pub struct DeleteUser {
- /// Unique user ID (numeric or name).
- #[serde(skip)]
- pub user_id: Identifier,
-}
-
-impl Command for DeleteUser {
- fn code(&self) -> u32 {
- DELETE_USER_CODE
- }
-}
-
-impl Validatable<IggyError> for DeleteUser {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for DeleteUser {
- fn to_bytes(&self) -> Bytes {
- self.user_id.to_bytes()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<DeleteUser, IggyError> {
- if bytes.len() < 3 {
- return Err(IggyError::InvalidCommand);
- }
-
- let user_id = Identifier::from_bytes(bytes)?;
- let command = DeleteUser { user_id };
- Ok(command)
- }
-}
-
-impl Display for DeleteUser {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.user_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = DeleteUser {
- user_id: Identifier::numeric(1).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let user_id = Identifier::from_bytes(bytes.clone()).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(user_id, command.user_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let user_id = Identifier::numeric(1).unwrap();
- let bytes = user_id.to_bytes();
- let command = DeleteUser::from_bytes(bytes);
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.user_id, user_id);
- }
-}
diff --git a/core/common/src/commands/users/get_user.rs b/core/common/src/commands/users/get_user.rs
deleted file mode 100644
index b0082f3b5..000000000
--- a/core/common/src/commands/users/get_user.rs
+++ /dev/null
@@ -1,99 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Identifier;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_USER_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetUser` command is used to retrieve the information about a user by unique ID.
-/// It has additional payload:
-/// - `user_id` - unique user ID (numeric or name).
-#[derive(Debug, Serialize, Deserialize, PartialEq, Default)]
-pub struct GetUser {
- #[serde(skip)]
- /// Unique user ID (numeric or name).
- pub user_id: Identifier,
-}
-
-impl Command for GetUser {
- fn code(&self) -> u32 {
- GET_USER_CODE
- }
-}
-
-impl Validatable<IggyError> for GetUser {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetUser {
- fn to_bytes(&self) -> Bytes {
- self.user_id.to_bytes()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetUser, IggyError> {
- if bytes.len() < 3 {
- return Err(IggyError::InvalidCommand);
- }
-
- let user_id = Identifier::from_bytes(bytes)?;
- let command = GetUser { user_id };
- Ok(command)
- }
-}
-
-impl Display for GetUser {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}", self.user_id)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = GetUser {
- user_id: Identifier::numeric(1).unwrap(),
- };
-
- let bytes = command.to_bytes();
- let user_id = Identifier::from_bytes(bytes.clone()).unwrap();
-
- assert!(!bytes.is_empty());
- assert_eq!(user_id, command.user_id);
- }
-
- #[test]
- fn should_be_deserialized_from_bytes() {
- let user_id = Identifier::numeric(1).unwrap();
- let bytes = user_id.to_bytes();
- let command = GetUser::from_bytes(bytes);
- assert!(command.is_ok());
-
- let command = command.unwrap();
- assert_eq!(command.user_id, user_id);
- }
-}
diff --git a/core/common/src/commands/users/get_users.rs b/core/common/src/commands/users/get_users.rs
deleted file mode 100644
index dc40bd1cb..000000000
--- a/core/common/src/commands/users/get_users.rs
+++ /dev/null
@@ -1,88 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, GET_USERS_CODE};
-use bytes::Bytes;
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-
-/// `GetUsers` command is used to retrieve the information about all users.
-/// It has no additional payload.
-#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
-pub struct GetUsers {}
-
-impl Command for GetUsers {
- fn code(&self) -> u32 {
- GET_USERS_CODE
- }
-}
-
-impl Validatable<IggyError> for GetUsers {
- fn validate(&self) -> Result<(), IggyError> {
- Ok(())
- }
-}
-
-impl BytesSerializable for GetUsers {
- fn to_bytes(&self) -> Bytes {
- Bytes::new()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<GetUsers, IggyError> {
- if !bytes.is_empty() {
- return Err(IggyError::InvalidCommand);
- }
-
- Ok(GetUsers {})
- }
-}
-
-impl Display for GetUsers {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "")
- }
-}
-
-#[cfg(test)]
-mod tests {
- use bytes::Bytes;
-
- use super::*;
-
- #[test]
- fn should_be_serialized_as_empty_bytes() {
- let command = GetUsers {};
- let bytes = command.to_bytes();
- assert!(bytes.is_empty());
- }
-
- #[test]
- fn should_be_deserialized_from_empty_bytes() {
- let command = GetUsers::from_bytes(Bytes::new());
- assert!(command.is_ok());
- }
-
- #[test]
- fn should_not_be_deserialized_from_empty_bytes() {
- let command = GetUsers::from_bytes(Bytes::from_static(&[0]));
- assert!(command.is_err());
- }
-}
diff --git a/core/common/src/commands/users/login_user.rs b/core/common/src/commands/users/login_user.rs
deleted file mode 100644
index ce729d1c0..000000000
--- a/core/common/src/commands/users/login_user.rs
+++ /dev/null
@@ -1,399 +0,0 @@
-/* Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-use super::defaults::*;
-use crate::BytesSerializable;
-use crate::Validatable;
-use crate::error::IggyError;
-use crate::{Command, LOGIN_USER_CODE};
-use bytes::{BufMut, Bytes, BytesMut};
-use secrecy::{ExposeSecret, SecretString};
-use serde::{Deserialize, Serialize};
-use std::fmt::Display;
-use std::str::from_utf8;
-
-/// `LoginUser` command is used to login a user by username and password.
-/// It has additional payload:
-/// - `username` - username, must be between 3 and 50 characters long.
-/// - `password` - password, must be between 3 and 100 characters long.
-#[derive(Debug, Serialize, Deserialize)]
-pub struct LoginUser {
- /// Username, must be between 3 and 50 characters long.
- pub username: String,
- /// Password, must be between 3 and 100 characters long.
- #[serde(serialize_with = "crate::utils::serde_secret::serialize_secret")]
- pub password: SecretString,
- // Version metadata added by SDK.
- pub version: Option<String>,
- // Context metadata added by SDK.
- pub context: Option<String>,
-}
-
-impl Command for LoginUser {
- fn code(&self) -> u32 {
- LOGIN_USER_CODE
- }
-}
-
-impl Default for LoginUser {
- fn default() -> Self {
- LoginUser {
- username: "user".to_string(),
- password: SecretString::from("secret"),
- version: None,
- context: None,
- }
- }
-}
-
-impl Validatable<IggyError> for LoginUser {
- fn validate(&self) -> Result<(), IggyError> {
- if self.username.is_empty()
- || self.username.len() > MAX_USERNAME_LENGTH
- || self.username.len() < MIN_USERNAME_LENGTH
- {
- return Err(IggyError::InvalidUsername);
- }
-
- let password = self.password.expose_secret();
- if password.is_empty()
- || password.len() > MAX_PASSWORD_LENGTH
- || password.len() < MIN_PASSWORD_LENGTH
- {
- return Err(IggyError::InvalidPassword);
- }
-
- Ok(())
- }
-}
-
-impl BytesSerializable for LoginUser {
- fn to_bytes(&self) -> Bytes {
- let password = self.password.expose_secret();
- let mut bytes = BytesMut::with_capacity(2 + self.username.len() + password.len());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(self.username.len() as u8);
- bytes.put_slice(self.username.as_bytes());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(password.len() as u8);
- bytes.put_slice(password.as_bytes());
- match &self.version {
- Some(version) => {
- bytes.put_u32_le(version.len() as u32);
- bytes.put_slice(version.as_bytes());
- }
- None => {
- bytes.put_u32_le(0);
- }
- }
- match &self.context {
- Some(context) => {
- bytes.put_u32_le(context.len() as u32);
- bytes.put_slice(context.as_bytes());
- }
- None => {
- bytes.put_u32_le(0);
- }
- }
- bytes.freeze()
- }
-
- fn from_bytes(bytes: Bytes) -> Result<LoginUser, IggyError> {
- if bytes.len() < 4 {
- return Err(IggyError::InvalidCommand);
- }
-
- let username_length = *bytes.first().ok_or(IggyError::InvalidCommand)? as usize;
- let username = from_utf8(
- bytes
- .get(1..1 + username_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let pos = 1 + username_length;
- let password_length = *bytes.get(pos).ok_or(IggyError::InvalidCommand)? as usize;
- let password = from_utf8(
- bytes
- .get(pos + 1..pos + 1 + password_length)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
-
- let mut position = pos + 1 + password_length;
-
- // Version and context fields are optional for backward compatibility
- // with older SDKs (e.g. v0.8.0) that don't send them.
- // However, 1-3 trailing bytes (incomplete u32 length prefix) are rejected
- // as they indicate a corrupt payload rather than a valid old-SDK format.
- let remaining = bytes.len() - position;
- let version = if remaining == 0 {
- None
- } else if remaining < 4 {
- return Err(IggyError::InvalidCommand);
- } else {
- let version_length = u32::from_le_bytes(
- bytes
- .get(position..position + 4)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- position += 4;
- match version_length {
- 0 => None,
- _ => {
- let version = from_utf8(
- bytes
- .get(position..position + version_length as usize)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
- position += version_length as usize;
- Some(version)
- }
- }
- };
-
- let remaining = bytes.len() - position;
- let context = if remaining == 0 {
- None
- } else if remaining < 4 {
- return Err(IggyError::InvalidCommand);
- } else {
- let context_length = u32::from_le_bytes(
- bytes
- .get(position..position + 4)
- .ok_or(IggyError::InvalidCommand)?
- .try_into()
- .map_err(|_| IggyError::InvalidNumberEncoding)?,
- );
- position += 4;
- match context_length {
- 0 => None,
- _ => {
- let context = from_utf8(
- bytes
- .get(position..position + context_length as usize)
- .ok_or(IggyError::InvalidCommand)?,
- )
- .map_err(|_| IggyError::InvalidUtf8)?
- .to_string();
- Some(context)
- }
- }
- };
-
- let command = LoginUser {
- username,
- password: SecretString::from(password),
- version,
- context,
- };
- Ok(command)
- }
-}
-
-impl Display for LoginUser {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}|******", self.username)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn should_be_serialized_as_bytes() {
- let command = LoginUser {
- username: "user".to_string(),
- password: SecretString::from("secret"),
- version: Some("1.0.0".to_string()),
- context: Some("test".to_string()),
- };
-
- let bytes = command.to_bytes();
- let username_length = bytes[0];
- let username = from_utf8(&bytes[1..=(username_length as usize)]).unwrap();
- let password_length = bytes[1 + username_length as usize];
- let password = from_utf8(
- &bytes[2 + username_length as usize
- ..2 + username_length as usize + password_length as usize],
- )
- .unwrap();
- let position = 2 + username_length as usize + password_length as usize;
- let version_length = u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
- let version = Some(
- from_utf8(&bytes[position + 4..position + 4 + version_length as usize])
- .unwrap()
- .to_string(),
- );
- let position = position + 4 + version_length as usize;
- let context_length = u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
- let context = Some(
- from_utf8(&bytes[position + 4..position + 4 + context_length as usize])
- .unwrap()
- .to_string(),
- );
-
- assert!(!bytes.is_empty());
- assert_eq!(username, command.username);
- assert_eq!(password, command.password.expose_secret());
- assert_eq!(version, command.version);
- assert_eq!(context, command.context);
- }
-
- #[test]
- fn from_bytes_should_fail_on_empty_input() {
- assert!(LoginUser::from_bytes(Bytes::new()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_truncated_input() {
- let command = LoginUser {
- username: "user".to_string(),
- password: SecretString::from("secret"),
- version: Some("1.0.0".to_string()),
- context: Some("test".to_string()),
- };
- let bytes = command.to_bytes();
- // Truncate at every position up to (but not including) the version field.
- // Positions within username/password must error; positions at or past the
- // version boundary are valid old-SDK payloads.
- let version_offset = 2 + command.username.len() + command.password.expose_secret().len();
- for i in 0..version_offset {
- let truncated = bytes.slice(..i);
- assert!(
- LoginUser::from_bytes(truncated).is_err(),
- "expected error for truncation at byte {i}"
- );
- }
- }
-
- #[test]
- fn from_bytes_should_fail_on_corrupted_username_length() {
- let mut buf = BytesMut::new();
- buf.put_u8(255); // username_length = 255
- buf.put_slice(b"short");
- assert!(LoginUser::from_bytes(buf.freeze()).is_err());
- }
-
- #[test]
- fn from_bytes_should_fail_on_trailing_bytes() {
- let username = "user";
- let password = "secret";
- let mut bytes = BytesMut::new();
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(username.len() as u8);
- bytes.put_slice(username.as_bytes());
- #[allow(clippy::cast_possible_truncation)]
- bytes.put_u8(password.len() as u8);
- bytes.put_slice(password.as_bytes());
-
- // 1-3 trailing bytes (incomplete u32 length prefix) must be rejected
- for extra in 1..=3u8 {
- let mut buf = bytes.clone();
- for i in 0..extra {
- buf.put_u8(i);
- }
- assert!(
- LoginUser::from_bytes(buf.freeze()).is_err(),
- "expected error for {extra} trailing byte(s)"
- );
... 14381 lines suppressed ...