diff --git a/bdd/cpp/features/step_definitions/messaging_steps.cpp b/bdd/cpp/features/step_definitions/messaging_steps.cpp index 90ee3ba7e4..8c2a00d5c7 100644 --- a/bdd/cpp/features/step_definitions/messaging_steps.cpp +++ b/bdd/cpp/features/step_definitions/messaging_steps.cpp @@ -27,10 +27,10 @@ #include #include +#include #include #include -#include "iggy.hpp" #include "world.hpp" GIVEN("^I have no streams in the system$") { @@ -69,18 +69,19 @@ WHEN("^I create a topic with name \"([^\"]{1,255})\" in stream ([0-9]+) with ([0 REGEX_PARAM(int, partitions_count); cucumber::ScenarioScope context; - // Drive the SDK through the typed helpers in iggy.hpp rather than raw strings. Options that - // still take plain strings below (partitioning kind, consumer kind) mark where the C++ SDK - // does not yet expose a typed wrapper. - const auto compression = iggy::CompressionAlgorithm::None(); - const auto message_expiry = iggy::Expiry::NeverExpire(); - const auto max_topic_size = iggy::MaxTopicSize::ServerDefault(); + iggy::ffi::TopicCreateOptions opts{}; + opts.has_partitions_count = true; + opts.partitions_count = static_cast(partitions_count); + opts.has_compression_algorithm = true; + opts.compression_algorithm = "none"; + opts.has_message_expiry = true; + opts.message_expiry_kind = "never_expire"; + opts.message_expiry_value = std::numeric_limits::max(); + opts.has_max_topic_size = true; + opts.max_topic_size = "server_default"; context->client->create_topic(bdd::make_numeric_identifier(static_cast(stream_id)), topic_name, - static_cast(partitions_count), - std::string(compression.CompressionAlgorithmValue()), - std::string(message_expiry.ExpiryKind()), message_expiry.ExpiryValue(), - std::string(max_topic_size.MaxTopicSizeValue()), {}); + std::move(opts)); } THEN("^the topic should be created successfully$") { @@ -146,12 +147,10 @@ WHEN("^I poll messages from stream ([0-9]+), topic ([0-9]+), partition ([0-9]+) REGEX_PARAM(int, offset); cucumber::ScenarioScope context; - const auto polling_strategy = iggy::PollingStrategy::Offset(static_cast(offset)); - const auto polled = context->client->poll_messages( + const auto polled = context->client->poll_messages( bdd::make_numeric_identifier(static_cast(stream_id)), bdd::make_numeric_identifier(static_cast(topic_id)), static_cast(partition_id), - "consumer", bdd::make_numeric_identifier(1), std::string(polling_strategy.PollingStrategyKind()), - polling_strategy.PollingStrategyValue(), 100, false); + "consumer", bdd::make_numeric_identifier(1), "offset", static_cast(offset), 100, false); context->polled.count = polled.count; context->polled.offsets.clear(); diff --git a/foreign/cpp/MODULE.bazel.lock b/foreign/cpp/MODULE.bazel.lock index 55a016bf6e..c6f3c74b75 100644 --- a/foreign/cpp/MODULE.bazel.lock +++ b/foreign/cpp/MODULE.bazel.lock @@ -483,109 +483,6 @@ } } } - }, - "@@rules_rust+//crate_universe/private:internal_extensions.bzl%cu_nr": { - "general": { - "bzlTransitiveDigest": "qQd7ktlamEwHDmeHLMSsEyM7+9Y2dOsIhs3Y/RXmCDY=", - "usagesDigest": "McimNXAztVHDzspa1Kng94T+YeyY/44zx9FkNqIMn2s=", - "recordedInputs": [ - "REPO_MAPPING:apple_support+,bazel_skylib bazel_skylib+", - "REPO_MAPPING:bazel_features+,bazel_features_globals bazel_features++version_extension+bazel_features_globals", - "REPO_MAPPING:bazel_features+,bazel_features_version bazel_features++version_extension+bazel_features_version", - "REPO_MAPPING:rules_cc+,bazel_features bazel_features+", - "REPO_MAPPING:rules_cc+,bazel_skylib bazel_skylib+", - "REPO_MAPPING:rules_cc+,bazel_tools bazel_tools", - "REPO_MAPPING:rules_cc+,cc_compatibility_proxy rules_cc++compatibility_proxy+cc_compatibility_proxy", - "REPO_MAPPING:rules_cc+,platforms platforms", - "REPO_MAPPING:rules_cc+,rules_cc rules_cc+", - "REPO_MAPPING:rules_cc++compatibility_proxy+cc_compatibility_proxy,rules_cc rules_cc+", - "REPO_MAPPING:rules_rust+,apple_support apple_support+", - "REPO_MAPPING:rules_rust+,bazel_features bazel_features+", - "REPO_MAPPING:rules_rust+,bazel_skylib bazel_skylib+", - "REPO_MAPPING:rules_rust+,bazel_tools bazel_tools", - "REPO_MAPPING:rules_rust+,cui rules_rust++cu+cui", - "REPO_MAPPING:rules_rust+,rrc rules_rust++i2+rrc", - "REPO_MAPPING:rules_rust+,rules_cc rules_cc+", - "REPO_MAPPING:rules_rust+,rules_rust rules_rust+" - ], - "generatedRepoSpecs": { - "cargo_bazel_bootstrap": { - "repoRuleId": "@@rules_rust+//cargo/private:cargo_bootstrap.bzl%cargo_bootstrap_repository", - "attributes": { - "srcs": [ - "@@rules_rust+//crate_universe:src/api.rs", - "@@rules_rust+//crate_universe:src/api/lockfile.rs", - "@@rules_rust+//crate_universe:src/cli.rs", - "@@rules_rust+//crate_universe:src/cli/generate.rs", - "@@rules_rust+//crate_universe:src/cli/query.rs", - "@@rules_rust+//crate_universe:src/cli/render.rs", - "@@rules_rust+//crate_universe:src/cli/splice.rs", - "@@rules_rust+//crate_universe:src/cli/vendor.rs", - "@@rules_rust+//crate_universe:src/config.rs", - "@@rules_rust+//crate_universe:src/config/label_injection.rs", - "@@rules_rust+//crate_universe:src/context.rs", - "@@rules_rust+//crate_universe:src/context/crate_context.rs", - "@@rules_rust+//crate_universe:src/context/platforms.rs", - "@@rules_rust+//crate_universe:src/lib.rs", - "@@rules_rust+//crate_universe:src/lockfile.rs", - "@@rules_rust+//crate_universe:src/main.rs", - "@@rules_rust+//crate_universe:src/metadata.rs", - "@@rules_rust+//crate_universe:src/metadata/cargo_bin.rs", - "@@rules_rust+//crate_universe:src/metadata/cargo_tree_resolver.rs", - "@@rules_rust+//crate_universe:src/metadata/cargo_tree_rustc_wrapper.bat", - "@@rules_rust+//crate_universe:src/metadata/cargo_tree_rustc_wrapper.sh", - "@@rules_rust+//crate_universe:src/metadata/dependency.rs", - "@@rules_rust+//crate_universe:src/metadata/metadata_annotation.rs", - "@@rules_rust+//crate_universe:src/rendering.rs", - "@@rules_rust+//crate_universe:src/rendering/template_engine.rs", - "@@rules_rust+//crate_universe:src/rendering/templates/defs_bzl_shim.j2", - "@@rules_rust+//crate_universe:src/rendering/templates/module_bzl.j2", - "@@rules_rust+//crate_universe:src/rendering/templates/partials/header.j2", - "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/aliases_map.j2", - "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/deps_map.j2", - "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/repo_git.j2", - "@@rules_rust+//crate_universe:src/rendering/templates/partials/module/repo_http.j2", - "@@rules_rust+//crate_universe:src/rendering/verbatim/alias_rules.bzl", - "@@rules_rust+//crate_universe:src/select.rs", - "@@rules_rust+//crate_universe:src/splicing.rs", - "@@rules_rust+//crate_universe:src/splicing/cargo_config.rs", - "@@rules_rust+//crate_universe:src/splicing/crate_index_lookup.rs", - "@@rules_rust+//crate_universe:src/splicing/splicer.rs", - "@@rules_rust+//crate_universe:src/test.rs", - "@@rules_rust+//crate_universe:src/utils.rs", - "@@rules_rust+//crate_universe:src/utils/starlark.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/glob.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/label.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/select.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/select_dict.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/select_list.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/select_scalar.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/select_set.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/serialize.rs", - "@@rules_rust+//crate_universe:src/utils/starlark/target_compatible_with.rs", - "@@rules_rust+//crate_universe:src/utils/symlink.rs", - "@@rules_rust+//crate_universe:src/utils/target_triple.rs" - ], - "binary": "cargo-bazel", - "cargo_lockfile": "@@rules_rust+//crate_universe:Cargo.lock", - "cargo_toml": "@@rules_rust+//crate_universe:Cargo.toml", - "version": "1.98.0", - "rust_toolchain_cargo_template": "@rust_host_tools//:bin/{tool}", - "rust_toolchain_rustc_template": "@rust_host_tools//:bin/{tool}", - "compressed_windows_toolchain_names": false, - "timeout": 900 - } - } - }, - "moduleExtensionMetadata": { - "explicitRootModuleDirectDeps": [ - "cargo_bazel_bootstrap" - ], - "explicitRootModuleDirectDevDeps": [], - "useAllRepos": "NO", - "reproducible": false - } - } } }, "facts": {}, diff --git a/foreign/cpp/include/iggy.hpp b/foreign/cpp/include/iggy.hpp index 593815299e..19dc07b233 100644 --- a/foreign/cpp/include/iggy.hpp +++ b/foreign/cpp/include/iggy.hpp @@ -28,20 +28,627 @@ #include #include #include +#include #include #include #include #include #include +#include +#include #include "lib.rs.h" namespace iggy { -using LoginInfo = ffi::LoginInfo; +class IggyBlockingClient; +class LoginInfo; +class Partition; +class Topic; +class TopicDetails; +class Stream; +class StreamDetails; -namespace detail { +/** + * @brief Exception thrown when an Iggy client operation fails. + */ +class IggyException : public std::runtime_error { + public: + explicit IggyException(const char *message) : std::runtime_error(message) {} + explicit IggyException(const std::string &message) : std::runtime_error(message) {} +}; + +/** + * @brief Details returned after a successful login. + * + * The value identifies the authenticated user. HTTP logins also return the + * access token retained by the client for later requests. Stateful transports + * do not return an access token. The token is a credential: callers must not + * write it to logs or expose it to untrusted code. + */ +class LoginInfo final { + public: + /** + * @brief Returns the numeric ID of the authenticated user. + * @return Numeric user ID. + */ + std::uint32_t UserId() const noexcept { return user_id_; } + + /** + * @brief Returns the HTTP access token when the login returned one. + * @return Empty when the selected transport does not use an access token. + * A returned string view remains valid while this LoginInfo object + * is not modified or destroyed. + */ + std::optional AccessToken() const noexcept { + if (!access_token_) { + return std::nullopt; + } + return *access_token_; + } + + /** + * @brief Returns the access-token expiry when a token was returned. + * @return Empty when no access token was returned; otherwise the + * server-provided expiry value. + */ + std::optional AccessTokenExpiry() const noexcept { return access_token_expiry_; } + + private: + LoginInfo(std::uint32_t user_id, + std::optional access_token, + std::optional access_token_expiry) + : user_id_(user_id), access_token_(std::move(access_token)), access_token_expiry_(access_token_expiry) {} + + static LoginInfo FromFfi(ffi::LoginInfo login_info); + + friend class IggyBlockingClient; + + std::uint32_t user_id_; + std::optional access_token_; + std::optional access_token_expiry_; +}; + +/** + * @brief Identifier for a server resource. + * + * An identifier is either a numeric server ID or a name. The factories enforce + * the protocol's non-empty, 255-byte name limit. Numeric zero is valid because + * the server can assign zero as a resource ID. + */ +class Identifier final { + public: + enum class Kind { Numeric, String }; + + /** + * @brief Creates a numeric identifier. + * @param id Numeric server ID. + * @return Identifier that addresses @p id. + */ + static Identifier Numeric(std::uint32_t id) { return Identifier(Kind::Numeric, id); } + + /** + * @brief Creates a name-based identifier. + * @param name Resource name. + * @return Identifier that addresses @p name. + * @throws IggyException if @p name is empty or exceeds 255 bytes. + */ + static Identifier String(std::string name) { + if (name.empty() || name.size() > 255) { + throw IggyException("Identifier name must contain 1 to 255 bytes"); + } + return Identifier(Kind::String, std::move(name)); + } + + /** + * @brief Returns this identifier's representation. + * @return Kind::Numeric or Kind::String. + */ + Kind Type() const noexcept { return kind_; } + + /** + * @brief Returns the identifier payload. + * @return Numeric ID for Kind::Numeric, or a view of the name for + * Kind::String. The string view remains valid while this + * Identifier object is not modified or destroyed. + */ + std::variant Value() const noexcept { + if (kind_ == Kind::Numeric) { + return std::get(value_); + } + return std::string_view(std::get(value_)); + } + + private: + Identifier(Kind kind, std::variant value) : kind_(kind), value_(std::move(value)) {} + + static Identifier FromFfi(ffi::Identifier identifier); + ffi::Identifier ToFfi() const; + + friend class IggyBlockingClient; + + Kind kind_; + std::variant value_; +}; + +/** + * @brief Type tag for a HeaderField payload. + * + * The tag and bytes use Iggy's header codec. Numeric payloads are little + * endian. Create TopicOption values instead of encoding catalog options by + * hand; use HeaderField directly only for application-defined headers. + */ +enum class HeaderKind : std::uint8_t { + Raw = 1, + String = 2, + Bool = 3, + Int8 = 4, + Int16 = 5, + Int32 = 6, + Int64 = 7, + Int128 = 8, + Uint8 = 9, + Uint16 = 10, + Uint32 = 11, + Uint64 = 12, + Uint128 = 13, + Float32 = 14, + Float64 = 15, +}; + +/** + * @brief One typed header key or value. + * + * HeaderField maps directly to Rust's header-field representation. Create() + * preserves the supplied bytes and does not verify that they match @p kind. + * Invalid key or value encodings are rejected when a request is sent. + */ +class HeaderField final { + public: + /** + * @brief Creates a typed header field from wire-encoded bytes. + * @param kind Type tag for @p value. + * @param value Payload encoded according to @p kind. + * @return Header field containing the supplied type and bytes. + */ + static HeaderField Create(HeaderKind kind, std::vector value) { + return HeaderField(kind, std::move(value)); + } + + /** + * @brief Returns the wire type of Value(). + * @return Header type tag. + */ + HeaderKind Kind() const noexcept { return kind_; } + + /** + * @brief Returns bytes owned by this field. + * @return Payload encoded according to Kind(). + */ + const std::vector &Value() const noexcept { return value_; } + + private: + HeaderField(HeaderKind kind, std::vector value) : kind_(kind), value_(std::move(value)) {} + + static HeaderField FromFfi(ffi::HeaderField field); + static ffi::HeaderField ToFfi(HeaderField field); + + friend class HeaderEntry; + + HeaderKind kind_; + std::vector value_; +}; + +/** + * @brief One typed header key-value pair. + * + * This maps directly to Rust's HeaderEntry. Topic options cross the C++ bridge + * as header entries because their keys and values use the same typed codec as + * message user headers. + */ +class HeaderEntry final { + public: + /** + * @brief Creates a header entry from its typed key and value. + * @param key Typed entry key. + * @param value Typed entry value. + * @return Header entry containing @p key and @p value. + */ + static HeaderEntry Create(HeaderField key, HeaderField value) { + return HeaderEntry(std::move(key), std::move(value)); + } + + /** + * @brief Returns the typed key. + * @return Key owned by this entry. + */ + const HeaderField &Key() const noexcept { return key_; } + + /** + * @brief Returns the typed value. + * @return Value owned by this entry. + */ + const HeaderField &Value() const noexcept { return value_; } + private: + HeaderEntry(HeaderField key, HeaderField value) : key_(std::move(key)), value_(std::move(value)) {} + + static HeaderEntry FromFfi(ffi::HeaderEntry entry); + static ffi::HeaderEntry ToFfi(HeaderEntry entry); + + friend class ResourceOptions; + + HeaderField key_; + HeaderField value_; +}; + +/** + * @brief Creation options attached to a stream or topic. + * + * This maps to Rust's ResourceOptions. Explicit() contains entries supplied by + * the creating client. Derived() contains values the server resolved from its + * configuration at admission time. Derived values describe that resource's + * history and may differ if the resource is recreated under another server + * configuration. + * + * Only explicit entries are sent; derived entries returned by Options() are + * not resubmitted. For topic creation prefer `TopicCreateOptions`. + */ +class ResourceOptions final { + public: + /** + * @brief Creates an empty option collection. + * @return Resource options with no explicit or derived entries. + */ + static ResourceOptions Empty() { return ResourceOptions({}); } + + /** + * @brief Creates request options from entries selected by the caller. + * @param entries Explicit option entries. + * @return Resource options that will submit @p entries. + */ + static ResourceOptions Explicit(std::vector entries); + + /** + * @brief Creates request options from explicit map. + * @param entries Explicit option map keyed by option name. + * @return Resource options that will submit @p entries. + */ + static ResourceOptions Explicit(std::map entries); + + /** + * @brief Returns entries supplied explicitly at resource creation. + * @return Explicit entries as map from option name to typed value. + */ + const std::map &Explicit() const noexcept { return explicit_; } + + /** + * @brief Returns entries derived from configured defaults at admission. + * @return Derived entries as map from option name to typed value. + * @note Stream responses currently expose explicit entries only, so this + * collection is empty for Stream and StreamDetails. + */ + const std::map &Derived() const noexcept { return derived_; } + + private: + explicit ResourceOptions(std::map explicit_entries) + : explicit_(std::move(explicit_entries)) {} + ResourceOptions(std::map explicit_entries, + std::map derived_entries) + : explicit_(std::move(explicit_entries)), derived_(std::move(derived_entries)) {} + + static ResourceOptions FromFfi(rust::Vec explicit_entries, + rust::Vec derived_entries); + static rust::Vec ToFfi(ResourceOptions options); + + friend class IggyBlockingClient; + friend class Topic; + friend class TopicDetails; + friend class Stream; + friend class StreamDetails; + + std::map explicit_; + std::map derived_; +}; + +/** + * @brief Topic summary returned within StreamDetails. + * + * A Topic owns its string fields and contains no Rust bridge values. It + * describes server state observed during the enclosing stream read, not a live + * view. Its aggregate statistics and partition count can change immediately + * after that request completes. + * + * Options() distinguishes values selected by the creating client from values + * the server derived during admission. Partition details require GetTopic(). + */ +class Topic final { + public: + /** + * @brief Returns the numeric topic ID assigned within its stream. + * @return Numeric topic ID. + */ + std::uint32_t Id() const noexcept { return id_; } + + /** + * @brief Returns the server creation timestamp. + * @return Timestamp in microseconds. + */ + std::uint64_t CreatedAt() const noexcept { return created_at_; } + + /** + * @brief Returns the topic name. + * @return Name owned by this value. + */ + const std::string &Name() const noexcept { return name_; } + + /** + * @brief Returns the aggregate retained topic size. + * @return Size in bytes. + */ + std::uint64_t SizeBytes() const noexcept { return size_bytes_; } + + /** + * @brief Returns the server-encoded message retention value. + * @return Retention value in microseconds or a protocol sentinel. + */ + std::uint64_t MessageExpiry() const noexcept { return message_expiry_; } + + /** + * @brief Returns the server-selected storage compression algorithm. + * @return Algorithm name owned by this value. + */ + const std::string &CompressionAlgorithm() const noexcept { return compression_algorithm_; } + + /** + * @brief Returns the configured maximum retained topic size. + * @return Maximum size in bytes. + */ + std::uint64_t MaxTopicSize() const noexcept { return max_topic_size_; } + + /** + * @brief Returns the aggregate number of retained messages. + * @return Message count. + */ + std::uint64_t MessagesCount() const noexcept { return messages_count_; } + + /** + * @brief Returns the number of partitions belonging to this topic. + * @return Partition count. + */ + std::uint32_t PartitionsCount() const noexcept { return partitions_count_; } + + /** + * @brief Returns topic creation options and their admission provenance. + * @return Options owned by this value. + */ + const ResourceOptions &Options() const noexcept { return options_; } + + private: + Topic(std::uint32_t id, + std::uint64_t created_at, + std::string name, + std::uint64_t size_bytes, + std::uint64_t message_expiry, + std::string compression_algorithm, + std::uint64_t max_topic_size, + std::uint64_t messages_count, + std::uint32_t partitions_count, + ResourceOptions options) + : id_(id), + created_at_(created_at), + name_(std::move(name)), + size_bytes_(size_bytes), + message_expiry_(message_expiry), + compression_algorithm_(std::move(compression_algorithm)), + max_topic_size_(max_topic_size), + messages_count_(messages_count), + partitions_count_(partitions_count), + options_(std::move(options)) {} + + static Topic FromFfi(ffi::Topic topic); + + friend class IggyBlockingClient; + friend class StreamDetails; + + std::uint32_t id_; + std::uint64_t created_at_; + std::string name_; + std::uint64_t size_bytes_; + std::uint64_t message_expiry_; + std::string compression_algorithm_; + std::uint64_t max_topic_size_; + std::uint64_t messages_count_; + std::uint32_t partitions_count_; + ResourceOptions options_; +}; + +/** + * @brief Partition metadata returned within TopicDetails. + * + * This is an observed summary, not a live partition handle. Offsets and + * statistics can change immediately after GetTopic() returns. + */ +class Partition final { + public: + /** + * @brief Returns the numeric partition ID within its topic. + * @return Numeric partition ID. + */ + std::uint32_t Id() const noexcept { return id_; } + + /** + * @brief Returns the server creation timestamp. + * @return Timestamp in microseconds. + */ + std::uint64_t CreatedAt() const noexcept { return created_at_; } + + /** + * @brief Returns the number of retained storage segments. + * @return Segment count. + */ + std::uint32_t SegmentsCount() const noexcept { return segments_count_; } + + /** + * @brief Returns the current server-observed message offset. + * @return Current message offset. + */ + std::uint64_t CurrentOffset() const noexcept { return current_offset_; } + + /** + * @brief Returns the retained partition size. + * @return Size in bytes. + */ + std::uint64_t SizeBytes() const noexcept { return size_bytes_; } + + /** + * @brief Returns the number of retained messages. + * @return Message count. + */ + std::uint64_t MessagesCount() const noexcept { return messages_count_; } + + private: + Partition(std::uint32_t id, + std::uint64_t created_at, + std::uint32_t segments_count, + std::uint64_t current_offset, + std::uint64_t size_bytes, + std::uint64_t messages_count) + : id_(id), + created_at_(created_at), + segments_count_(segments_count), + current_offset_(current_offset), + size_bytes_(size_bytes), + messages_count_(messages_count) {} + + static Partition FromFfi(ffi::Partition partition); + + friend class TopicDetails; + + std::uint32_t id_; + std::uint64_t created_at_; + std::uint32_t segments_count_; + std::uint64_t current_offset_; + std::uint64_t size_bytes_; + std::uint64_t messages_count_; +}; + +/** + * @brief Topic metadata and partition summaries returned by topic detail calls. + * + * Partitions() contains one observed summary per partition. It does not expose + * segment metadata, messages, consumer offsets, or consumer-group membership. + */ +class TopicDetails final { + public: + /** + * @brief Returns the numeric topic ID within its stream. + * @return Numeric topic ID. + */ + std::uint32_t Id() const noexcept { return id_; } + + /** + * @brief Returns the server creation timestamp. + * @return Timestamp in microseconds. + */ + std::uint64_t CreatedAt() const noexcept { return created_at_; } + + /** + * @brief Returns the topic name. + * @return Name owned by this value. + */ + const std::string &Name() const noexcept { return name_; } + + /** + * @brief Returns the aggregate retained topic size. + * @return Size in bytes. + */ + std::uint64_t SizeBytes() const noexcept { return size_bytes_; } + + /** + * @brief Returns the server-encoded message retention value. + * @return Retention value in microseconds or a protocol sentinel. + */ + std::uint64_t MessageExpiry() const noexcept { return message_expiry_; } + + /** + * @brief Returns the storage compression algorithm selected for this topic. + * @return Algorithm name owned by this value. + */ + const std::string &CompressionAlgorithm() const noexcept { return compression_algorithm_; } + + /** + * @brief Returns the maximum retained size configured for this topic. + * @return Maximum size in bytes. + */ + std::uint64_t MaxTopicSize() const noexcept { return max_topic_size_; } + + /** + * @brief Returns the aggregate number of retained messages. + * @return Message count. + */ + std::uint64_t MessagesCount() const noexcept { return messages_count_; } + + /** + * @brief Returns the number of partitions belonging to this topic. + * @return Partition count. + */ + std::uint32_t PartitionsCount() const noexcept { return partitions_count_; } + + /** + * @brief Returns partition summaries. + * @return Summaries owned by this value. + */ + const std::vector &Partitions() const noexcept { return partitions_; } + + /** + * @brief Returns topic creation options and their admission provenance. + * @return Options owned by this value. + */ + const ResourceOptions &Options() const noexcept { return options_; } + + private: + TopicDetails(std::uint32_t id, + std::uint64_t created_at, + std::string name, + std::uint64_t size_bytes, + std::uint64_t message_expiry, + std::string compression_algorithm, + std::uint64_t max_topic_size, + std::uint64_t messages_count, + std::uint32_t partitions_count, + std::vector partitions, + ResourceOptions options) + : id_(id), + created_at_(created_at), + name_(std::move(name)), + size_bytes_(size_bytes), + message_expiry_(message_expiry), + compression_algorithm_(std::move(compression_algorithm)), + max_topic_size_(max_topic_size), + messages_count_(messages_count), + partitions_count_(partitions_count), + partitions_(std::move(partitions)), + options_(std::move(options)) {} + + static TopicDetails FromFfi(ffi::TopicDetails topic); + + friend class IggyBlockingClient; + + std::uint32_t id_; + std::uint64_t created_at_; + std::string name_; + std::uint64_t size_bytes_; + std::uint64_t message_expiry_; + std::string compression_algorithm_; + std::uint64_t max_topic_size_; + std::uint64_t messages_count_; + std::uint32_t partitions_count_; + std::vector partitions_; + ResourceOptions options_; +}; + +namespace detail { /** @brief Internal base for string-backed option types. */ template class StringTag { @@ -57,6 +664,196 @@ class StringTag { } // namespace detail +/** + * @brief Snapshot of one stream's metadata and aggregate statistics. + * + * CreateStream() and GetStream() return this value after converting the Rust + * bridge response to ordinary C++ fields. It owns its name and topic + * collection, so no bridge-owned string or container leaks through the public + * API. + * + * The value describes the stream state observed by the server for one request. + * It is not a live view or an atomic snapshot of later stream, topic, or + * message activity. SizeBytes(), MessagesCount(), TopicsCount(), and Topics() + * can become stale immediately after the request completes when another client + * changes the stream. + * + * A newly created stream has no topics or messages, so CreateStream() returns + * zero for SizeBytes(), MessagesCount(), and TopicsCount(), with an empty + * Topics() collection. GetStream() returns the same aggregate fields and one + * Topic summary for each observed topic. + * + * Stream IDs identify a stream for its lifetime and remain stable when it is + * renamed. CreatedAt() is the server timestamp, in microseconds, recorded when + * the stream was created. + */ +class StreamDetails final { + public: + /** + * @brief Returns the numeric ID assigned by the server. + * + * This value can be passed to GetStream() while the stream exists. It is + * unchanged by a stream rename. + */ + std::uint32_t Id() const noexcept { return id_; } + + /** + * @brief Returns the server-recorded creation timestamp. + * @return Timestamp in microseconds. + */ + std::uint64_t CreatedAt() const noexcept { return created_at_; } + + /** + * @brief Returns the unique stream name observed by the server. + * @return Reference owned by this value. It remains valid until this + * StreamDetails object is modified or destroyed. + */ + const std::string &Name() const noexcept { return name_; } + + /** + * @brief Returns the aggregate retained size of all stream topics. + * @return Size in bytes observed by the server for this request. + */ + std::uint64_t SizeBytes() const noexcept { return size_bytes_; } + + /** + * @brief Returns the aggregate number of messages in all stream topics. + * @return Message count observed by the server for this request. + */ + std::uint64_t MessagesCount() const noexcept { return messages_count_; } + + /** + * @brief Returns the number of topics belonging to the stream. + * @return Topic count observed by the server for this request. + */ + std::uint32_t TopicsCount() const noexcept { return topics_count_; } + + /** + * @brief Returns the topic summaries observed by the server. + * @return Topic values owned by this StreamDetails object. + */ + const std::vector &Topics() const noexcept { return topics_; } + + /** + * @brief Returns explicit stream creation options. + * @return Options owned by this value. + * @note The current bridge does not return derived stream options. + */ + const ResourceOptions &Options() const noexcept { return options_; } + + private: + StreamDetails(std::uint32_t id, + std::uint64_t created_at, + std::string name, + std::uint64_t size_bytes, + std::uint64_t messages_count, + std::uint32_t topics_count, + std::vector topics, + ResourceOptions options) + : id_(id), + created_at_(created_at), + name_(std::move(name)), + size_bytes_(size_bytes), + messages_count_(messages_count), + topics_count_(topics_count), + topics_(std::move(topics)), + options_(std::move(options)) {} + + static StreamDetails FromFfi(ffi::StreamDetails stream); + + friend class IggyBlockingClient; + + std::uint32_t id_; + std::uint64_t created_at_; + std::string name_; + std::uint64_t size_bytes_; + std::uint64_t messages_count_; + std::uint32_t topics_count_; + std::vector topics_; + ResourceOptions options_; +}; + +/** + * @brief Summary of a stream returned by GetStreams(). + * + * Unlike StreamDetails, this value does not include topic summaries. Its + * aggregate statistics describe the state observed by the server for one + * request and can become stale when another client modifies the stream. + */ +class Stream final { + public: + /** + * @brief Returns the numeric ID assigned by the server. + * @return Numeric stream ID. + */ + std::uint32_t Id() const noexcept { return id_; } + + /** + * @brief Returns the server-recorded creation timestamp. + * @return Timestamp in microseconds. + */ + std::uint64_t CreatedAt() const noexcept { return created_at_; } + + /** + * @brief Returns the stream name. + * @return Name owned by this value. + */ + const std::string &Name() const noexcept { return name_; } + + /** + * @brief Returns the aggregate retained stream size. + * @return Size in bytes. + */ + std::uint64_t SizeBytes() const noexcept { return size_bytes_; } + + /** + * @brief Returns the aggregate number of retained stream messages. + * @return Message count. + */ + std::uint64_t MessagesCount() const noexcept { return messages_count_; } + + /** + * @brief Returns the number of topics belonging to the stream. + * @return Topic count. + */ + std::uint32_t TopicsCount() const noexcept { return topics_count_; } + + /** + * @brief Returns explicit stream creation options. + * @return Options owned by this value. + * @note The current bridge does not return derived stream options. + */ + const ResourceOptions &Options() const noexcept { return options_; } + + private: + Stream(std::uint32_t id, + std::uint64_t created_at, + std::string name, + std::uint64_t size_bytes, + std::uint64_t messages_count, + std::uint32_t topics_count, + ResourceOptions options) + : id_(id), + created_at_(created_at), + name_(std::move(name)), + size_bytes_(size_bytes), + messages_count_(messages_count), + topics_count_(topics_count), + options_(std::move(options)) {} + + static Stream FromFfi(ffi::Stream stream); + + friend class IggyBlockingClient; + + std::uint32_t id_; + std::uint64_t created_at_; + std::string name_; + std::uint64_t size_bytes_; + std::uint64_t messages_count_; + std::uint32_t topics_count_; + ResourceOptions options_; +}; + /** * @brief Compression algorithm used for topic messages. * @@ -210,31 +1007,6 @@ class MaxTopicSize final : private detail::StringTag { explicit MaxTopicSize(std::string max_topic_size) : detail::StringTag(std::move(max_topic_size)) {} }; -// TODO(slbotbm): Add rust bindings for Identifier that will use IdKind -/** - * @brief Identifies whether an identifier is numeric or text-based. - * - * @note Reserved for future identifier bindings and not currently passed - * across the Rust FFI. - */ -class IdKind final : private detail::StringTag { - public: - /** @brief Uses a numeric identifier represented as a 32-bit integer. */ - static IdKind Numeric() { return IdKind("numeric"); } - - /** @brief Uses a string identifier represented by its text value. */ - static IdKind String() { return IdKind("string"); } - - /** - * @brief Returns the value passed to the client implementation. - * @return Identifier kind name. - */ - std::string_view IdKindValue() const { return Value(); } - - private: - explicit IdKind(std::string id_kind) : detail::StringTag(std::move(id_kind)) {} -}; - /** * @brief Message retention policy for a topic. * @@ -281,208 +1053,382 @@ class Expiry final { }; /** - * @brief Starting position for polling messages. + * @brief Options for creating a topic. * - * @note The strategy kind and value are passed across the Rust FFI as a pair. - * The Rust client rejects unsupported kinds. + * Mirrors Rust `TopicCreateOptions` (`core/common/src/types/options/mod.rs:698`). + * Each `std::optional` field corresponds to a catalog key; `std::nullopt` + * means the server default is used and the key is omitted from the request. + * `Raw` carries forward-compatible string keys; a typed field wins on + * collision when encoded. The wire form is the topic options TLV block + * (`core/binary_protocol/src/primitives/options.rs:18`), the same encoding + * used for message user headers. */ -class PollingStrategy final { +class TopicCreateOptions final { public: + TopicCreateOptions() = default; + /** - * @brief Starts polling at a message offset. - * @param value Message offset. - * @return Offset-based polling strategy. + * @brief Number of partitions to create with the topic. + * @return Partition count when set; `nullopt` uses the server default + * `DEFAULT_PARTITIONS_COUNT` (1). + * @note Not an option key. Fills the `CreateTopic` command's fixed field; + * it is consumed to compute assignments and is not stored as a topic + * option. Must be `0..=1000` when set. */ - static PollingStrategy Offset(std::uint64_t value) { return PollingStrategy("offset", value); } + std::optional PartitionsCount() const noexcept { return partitions_count_; } + TopicCreateOptions &SetPartitionsCount(std::uint32_t partitions_count) noexcept { + partitions_count_ = partitions_count; + return *this; + } /** - * @brief Starts polling at a timestamp. - * @param value Timestamp value expected by the Iggy protocol. - * @return Timestamp-based polling strategy. + * @brief Storage compression for the topic. + * @return Compression algorithm when set; `nullopt` uses the server default + * (`none`). + * @note Catalog key `compression_algorithm` (`HeaderKind::String`, values + * `none`, `gzip`). Also updatable via `TopicUpdateOptions`. */ - static PollingStrategy Timestamp(std::uint64_t value) { return PollingStrategy("timestamp", value); } + std::optional<::iggy::CompressionAlgorithm> CompressionAlgorithm() const noexcept { return compression_algorithm_; } + TopicCreateOptions &SetCompressionAlgorithm(::iggy::CompressionAlgorithm compression_algorithm) { + compression_algorithm_ = std::move(compression_algorithm); + return *this; + } - /** @brief Starts polling with the first message in the partition. */ - static PollingStrategy First() { return PollingStrategy("first", 0); } + /** + * @brief Message retention policy for the topic. + * @return Expiry when set; `nullopt` uses the server default + * `IggyExpiry::ServerDefault` (alias `never_expire` with sentinel + * `u64::MAX` on the wire as `Uint64`). + * @note Catalog key `message_expiry` (`Uint64` micros, or `String` like + * `"7 days"` via `Raw`). `ServerDefault()` normalizes to `nullopt`. + * Also updatable. + */ + std::optional<::iggy::Expiry> MessageExpiry() const noexcept { return message_expiry_; } + TopicCreateOptions &SetMessageExpiry(::iggy::Expiry message_expiry) { + if (message_expiry.ExpiryKind() == "server_default") { + message_expiry_.reset(); + } else { + message_expiry_ = std::move(message_expiry); + } + return *this; + } - /** @brief Starts polling with the last available message in the partition. */ - static PollingStrategy Last() { return PollingStrategy("last", 0); } + /** + * @brief Maximum retained topic size. + * @return Max size when set; `nullopt` uses the server default + * `MaxTopicSize::ServerDefault` (`unlimited`, `u64::MAX` on wire). + * @note Catalog key `max_topic_size` (`Uint64` bytes or `String` like + * `"1 GiB"` via `Raw`). `ServerDefault()` normalizes to `nullopt`. + * Must be `>=` the resolved segment size when both are set. Also + * updatable. + */ + std::optional<::iggy::MaxTopicSize> MaxTopicSize() const noexcept { return max_topic_size_; } + TopicCreateOptions &SetMaxTopicSize(::iggy::MaxTopicSize max_topic_size) { + if (max_topic_size.MaxTopicSizeValue() == "server_default") { + max_topic_size_.reset(); + } else { + max_topic_size_ = std::move(max_topic_size); + } + return *this; + } /** - * @brief Returns a strategy that starts after the stored consumer offset. - * @note Typically used with automatic offset commits enabled. + * @brief Size at which a partition segment rotates. + * @return Segment size in bytes when set; `nullopt` uses the server default + * `DEFAULT_SEGMENT_SIZE` (1 GiB). + * @note Catalog key `segment_size` (`Uint64` bytes, `String` `"128MiB"` via + * `Raw`). Constraints: multiple of `512`, `1 MiB` (`MIN_TOPIC_SEGMENT_SIZE`) + * `..=` `1 GiB` (`MAX_TOPIC_SEGMENT_SIZE`, the server segment ceiling + * `core/common/src/types/options/mod.rs:310`). `0` normalizes to + * `nullopt`. Creation-only; `UpdateTopic` rejects it. */ - static PollingStrategy Next() { return PollingStrategy("next", 0); } + std::optional SegmentSize() const noexcept { return segment_size_; } + TopicCreateOptions &SetSegmentSize(std::uint64_t segment_size) noexcept { + segment_size_ = segment_size; + return *this; + } /** - * @brief Returns the polling strategy kind. - * @return One of offset, timestamp, first, last, or next. + * @brief Whether partition writes are fsynced. + * @return Fsync flag when set; `nullopt` uses the server default + * `DEFAULT_ENFORCE_FSYNC` (`false`). + * @note Catalog key `enforce_fsync` (`Bool` `0`/`1`, or `String` + * `"true"`/`"false"` via `Raw`). Creation-only. */ - std::string_view PollingStrategyKind() const { return polling_strategy_kind_; } + std::optional EnforceFsync() const noexcept { return enforce_fsync_; } + TopicCreateOptions &SetEnforceFsync(bool enforce_fsync) noexcept { + enforce_fsync_ = enforce_fsync; + return *this; + } /** - * @brief Returns the value associated with the polling strategy. - * @return Offset or timestamp for parameterized strategies; otherwise zero. + * @brief Journal flush threshold in message count. + * @return Count when set; `nullopt` uses the server default + * `DEFAULT_MESSAGES_REQUIRED_TO_SAVE` (`1024`). + * @note Catalog key `messages_required_to_save` (`Uint32`). Must be + * `1..=16_777_216` (`MAX_MESSAGES_REQUIRED_TO_SAVE`). `0` is rejected. + * Paired with `SizeOfMessagesRequiredToSave`; whichever trips first + * flushes. Creation-only. */ - std::uint64_t PollingStrategyValue() const { return polling_strategy_value_; } + std::optional MessagesRequiredToSave() const noexcept { return messages_required_to_save_; } + TopicCreateOptions &SetMessagesRequiredToSave(std::uint32_t messages_required_to_save) noexcept { + messages_required_to_save_ = messages_required_to_save; + return *this; + } - private: - explicit PollingStrategy(std::string kind, std::uint64_t value) - : polling_strategy_kind_(std::move(kind)), polling_strategy_value_(value) {} + /** + * @brief Journal flush threshold in bytes. + * @return Byte threshold when set; `nullopt` uses the server default + * `DEFAULT_SIZE_OF_MESSAGES_REQUIRED_TO_SAVE` (1 MiB). + * @note Catalog key `size_of_messages_required_to_save` (`Uint64` bytes, + * `String` like `"4KiB"` via `Raw`). Must be `1..=1 GiB` + * (`MAX_SIZE_OF_MESSAGES_REQUIRED_TO_SAVE`). `0` normalizes to + * `nullopt`. Paired with `MessagesRequiredToSave`. Creation-only. + */ + std::optional SizeOfMessagesRequiredToSave() const noexcept { + return size_of_messages_required_to_save_; + } + TopicCreateOptions &SetSizeOfMessagesRequiredToSave(std::uint64_t size_of_messages_required_to_save) noexcept { + size_of_messages_required_to_save_ = size_of_messages_required_to_save; + return *this; + } - std::string polling_strategy_kind_; - std::uint64_t polling_strategy_value_; + /** + * @brief Whether segment bytes are reserved on disk at creation. + * @return Flag when set; `nullopt` uses the server default + * `DEFAULT_PREALLOCATE_SEGMENTS` (`false`). + * @note Catalog key `preallocate_segments` (`Bool`). Creation-only. + * Admission rejects `segment_size * partitions_count > 64 GiB` + * (`MAX_PREALLOCATED_TOPIC_BYTES`). + */ + std::optional PreallocateSegments() const noexcept { return preallocate_segments_; } + TopicCreateOptions &SetPreallocateSegments(bool preallocate_segments) noexcept { + preallocate_segments_ = preallocate_segments; + return *this; + } + + /** + * @brief Forward-compatible string keys not yet covered by a typed field. + * @return Ordered map of `key -> value` strings; values are parsed server-side + * via the same `FromStr` as config file entries. An unknown key is + * rejected with `UnsupportedOptionKey`; a bad value with + * `InvalidOptionValue`. + * @note A typed field wins on collision when both `raw` and the typed setter + * name the same key (`mod.rs:1374` `typed_field_wins_over_raw_entry_for_the_same_key`). + * `partitions_count` is not a key and is rejected if placed in `raw`. + */ + const std::map &RawEntries() const noexcept { return raw_; } + TopicCreateOptions &SetRawEntries(const std::map &entries) { + for (const auto &entry : entries) { + raw_.emplace(entry.first, entry.second); + } + return *this; + } + TopicCreateOptions &SetRawEntries(std::map &&entries) { + for (auto &entry : entries) { + raw_.emplace(std::move(entry.first), std::move(entry.second)); + } + return *this; + } + + private: + std::optional partitions_count_; + std::optional<::iggy::CompressionAlgorithm> compression_algorithm_; + std::optional<::iggy::Expiry> message_expiry_; + std::optional<::iggy::MaxTopicSize> max_topic_size_; + std::optional segment_size_; + std::optional enforce_fsync_; + std::optional messages_required_to_save_; + std::optional size_of_messages_required_to_save_; + std::optional preallocate_segments_; + std::map raw_; + + friend class IggyBlockingClient; }; -namespace detail { +/** + * @brief Options for updating a topic. + * + * Mirrors Rust `TopicUpdateOptions` (`core/common/src/types/options/mod.rs:640`). + * Separate from `TopicCreateOptions` so creation-only keys cannot be set on + * update. Patch semantics: `std::nullopt` keeps the topic's current value; + * only the keys present are changed. Only `compression_algorithm`, + * `message_expiry`, `max_topic_size` and `raw` keys in + * `UPDATABLE_TOPIC_OPTION_KEYS` (`core/common/src/types/options/mod.rs:476`) + * are accepted. Creation-only keys (`segment_size`, `enforce_fsync`, + * `messages_required_to_save`, `size_of_messages_required_to_save`, + * `preallocate_segments`) and unknown keys are rejected with + * `UnsupportedOptionKey`. + */ +class TopicUpdateOptions final { + public: + TopicUpdateOptions() = default; -/// Numeric option values are little-endian on the wire. Encoded byte by byte so -/// a big-endian host produces the same block as a little-endian one. -template -rust::Vec to_little_endian_bytes(const Value value) { - rust::Vec bytes{}; - bytes.reserve(sizeof(Value)); - for (std::size_t index{}; index < sizeof(Value); ++index) { - bytes.push_back(static_cast((value >> (index * 8)) & 0xFF)); + /** + * @brief New storage compression. + * @return Algorithm when set; `nullopt` keeps the current value. + * @note Catalog key `compression_algorithm` (`HeaderKind::String`, `none`/`gzip`). + */ + std::optional<::iggy::CompressionAlgorithm> CompressionAlgorithm() const noexcept { return compression_algorithm_; } + TopicUpdateOptions &SetCompressionAlgorithm(::iggy::CompressionAlgorithm compression_algorithm) { + compression_algorithm_ = std::move(compression_algorithm); + return *this; } - return bytes; -} - -inline rust::Vec to_bool_bytes(const bool value) { - rust::Vec bytes{}; - bytes.push_back(static_cast(value ? 1 : 0)); + /** + * @brief New message retention policy. + * @return Expiry when set; `nullopt` keeps the current value. + * @note Catalog key `message_expiry` (`Uint64` micros or `String` like + * `"7 days"` via `Raw`). `ServerDefault()` normalizes to `nullopt`. + */ + std::optional<::iggy::Expiry> MessageExpiry() const noexcept { return message_expiry_; } + TopicUpdateOptions &SetMessageExpiry(::iggy::Expiry message_expiry) { + if (message_expiry.ExpiryKind() == "server_default") { + message_expiry_.reset(); + } else { + message_expiry_ = std::move(message_expiry); + } + return *this; + } - return bytes; -} + /** + * @brief New maximum retained topic size. + * @return Max size when set; `nullopt` keeps the current value. + * @note Catalog key `max_topic_size` (`Uint64` bytes or `String` like + * `"1 GiB"` via `Raw`). `ServerDefault()` normalizes to `nullopt`. + */ + std::optional<::iggy::MaxTopicSize> MaxTopicSize() const noexcept { return max_topic_size_; } + TopicUpdateOptions &SetMaxTopicSize(::iggy::MaxTopicSize max_topic_size) { + if (max_topic_size.MaxTopicSizeValue() == "server_default") { + max_topic_size_.reset(); + } else { + max_topic_size_ = std::move(max_topic_size); + } + return *this; + } -inline rust::Vec to_key_bytes(const std::string_view key) { - rust::Vec bytes{}; - bytes.reserve(key.size()); - for (const char character : key) { - bytes.push_back(static_cast(character)); + /** + * @brief Forward-compatible updatable string keys. + * @return Ordered map of `key -> value`; only keys in the updatable catalog + * are accepted. A create-only or unknown key is rejected with + * `UnsupportedOptionKey`; a bad value with `InvalidOptionValue`. + * @note Typed field wins on collision. + */ + const std::map &RawEntries() const noexcept { return raw_; } + TopicUpdateOptions &SetRawEntries(const std::map &entries) { + for (const auto &entry : entries) { + raw_.emplace(entry.first, entry.second); + } + return *this; + } + TopicUpdateOptions &SetRawEntries(std::map &&entries) { + for (auto &entry : entries) { + raw_.emplace(std::move(entry.first), std::move(entry.second)); + } + return *this; } - return bytes; -} + private: + std::optional<::iggy::CompressionAlgorithm> compression_algorithm_; + std::optional<::iggy::Expiry> message_expiry_; + std::optional<::iggy::MaxTopicSize> max_topic_size_; + std::map raw_; -inline iggy::ffi::HeaderField to_header_field(const iggy::ffi::HeaderKind kind, rust::Vec value) { - iggy::ffi::HeaderField field{}; - field.kind = static_cast(kind); - field.value = std::move(value); + friend class IggyBlockingClient; +}; - return field; -} +/** + * @brief Options for updating a stream. + * + * Mirrors Rust `StreamUpdateOptions` (`core/common/src/types/options/mod.rs:547`). + * Separate from creation because streams have no catalog keys yet. Patch + * semantics: `std::nullopt` keeps current value; only keys present are + * changed. Currently `UPDATABLE_STREAM_OPTION_KEYS` (`mod.rs:486`) is empty, + * so every key is rejected with `UnsupportedOptionKey` until the first stream + * option lands. `raw` carries forward-compatible string keys. + */ +class StreamUpdateOptions final { + public: + StreamUpdateOptions() = default; -/// An option key is always `String`-kinded. Only the value kind varies per key. -inline iggy::ffi::HeaderEntry to_option_entry(const std::string_view key, - const iggy::ffi::HeaderKind value_kind, - rust::Vec value) { - iggy::ffi::HeaderEntry entry{}; - entry.key = to_header_field(iggy::ffi::HeaderKind::String, to_key_bytes(key)); - entry.value = to_header_field(value_kind, std::move(value)); + /** + * @brief Forward-compatible string keys for stream update. + * @return Ordered map of `key -> value` strings; values are parsed + * server-side via `FromStr`. Unknown keys rejected with + * `UnsupportedOptionKey`. + */ + const std::map &RawEntries() const noexcept { return raw_; } + StreamUpdateOptions &SetRawEntries(const std::map &entries) { + for (const auto &entry : entries) { + raw_.emplace(entry.first, entry.second); + } + return *this; + } + StreamUpdateOptions &SetRawEntries(std::map &&entries) { + for (auto &entry : entries) { + raw_.emplace(std::move(entry.first), std::move(entry.second)); + } + return *this; + } - return entry; -} + private: + std::map raw_; -} // namespace detail + friend class IggyBlockingClient; +}; /** - * @brief Creates topic option entries for `Client::create_topic(...)`. - * - * Each factory encodes one key from the server's topic option catalog using - * that key's required value kind. The server rejects unknown keys and values - * encoded with a different kind. - * - * Use `Client::describe_options("topic")` to retrieve the supported keys, - * value kinds, and defaults. Binary transport errors do not identify a rejected - * key, so the catalog is the discovery mechanism for those transports. + * @brief Starting position for polling messages. * - * @note These options are accepted only during topic creation. - * `Client::update_topic(...)` rejects them because they define how - * partition storage is created. Changing them later could leave existing - * and new segments with different storage settings. + * @note The strategy kind and value are passed across the Rust FFI as a pair. + * The Rust client rejects unsupported kinds. */ -class TopicOption final { +class PollingStrategy final { public: /** - * @brief Set the size at which this topic's segments rotate. - * - * Must be a multiple of 512 bytes, at least 1 MiB, and no larger than the - * server's segment ceiling. - * - * @param bytes Segment size in bytes. - * @return Encoded topic option entry. + * @brief Starts polling at a message offset. + * @param value Message offset. + * @return Offset-based polling strategy. */ - static iggy::ffi::HeaderEntry SegmentSize(const std::uint64_t bytes) { - return detail::to_option_entry("segment_size", iggy::ffi::HeaderKind::Uint64, - detail::to_little_endian_bytes(bytes)); - } + static PollingStrategy Offset(std::uint64_t value) { return PollingStrategy("offset", value); } /** - * @brief Choose whether writes to this topic's partitions are fsynced. - * - * @param enabled Whether partition writes are fsynced. - * @return Encoded topic option entry. + * @brief Starts polling at a timestamp. + * @param value Timestamp value expected by the Iggy protocol. + * @return Timestamp-based polling strategy. */ - static iggy::ffi::HeaderEntry EnforceFsync(const bool enabled) { - return detail::to_option_entry("enforce_fsync", iggy::ffi::HeaderKind::Bool, detail::to_bool_bytes(enabled)); - } + static PollingStrategy Timestamp(std::uint64_t value) { return PollingStrategy("timestamp", value); } + + /** @brief Starts polling with the first message in the partition. */ + static PollingStrategy First() { return PollingStrategy("first", 0); } + + /** @brief Starts polling with the last available message in the partition. */ + static PollingStrategy Last() { return PollingStrategy("last", 0); } /** - * @brief Flush the journal once it holds this many messages. - * - * Must be non-zero. Paired with - * `SizeOfMessagesRequiredToSave(bytes)`: whichever threshold trips - * first flushes. - * - * @param messages Message count at which to flush the journal. - * @return Encoded topic option entry. + * @brief Returns a strategy that starts after the stored consumer offset. + * @note Typically used with automatic offset commits enabled. */ - static iggy::ffi::HeaderEntry MessagesRequiredToSave(const std::uint32_t messages) { - return detail::to_option_entry("messages_required_to_save", iggy::ffi::HeaderKind::Uint32, - detail::to_little_endian_bytes(messages)); - } + static PollingStrategy Next() { return PollingStrategy("next", 0); } /** - * @brief Flush the journal once it holds this many bytes. - * - * Capped at 1 GiB: a threshold above the largest a segment may be never - * trips, and the journal does not survive a crash. - * - * @param bytes Byte count at which to flush the journal. - * @return Encoded topic option entry. + * @brief Returns the polling strategy kind. + * @return One of offset, timestamp, first, last, or next. */ - static iggy::ffi::HeaderEntry SizeOfMessagesRequiredToSave(const std::uint64_t bytes) { - return detail::to_option_entry("size_of_messages_required_to_save", iggy::ffi::HeaderKind::Uint64, - detail::to_little_endian_bytes(bytes)); - } + std::string_view PollingStrategyKind() const { return polling_strategy_kind_; } /** - * @brief Choose whether a segment's bytes are reserved on disk when it is created. - * - * Reserves `segment_size * partitions_count` up front, which the server - * caps at 64 GiB per topic. - * - * @param enabled Whether to reserve segment storage on disk. - * @return Encoded topic option entry. + * @brief Returns the value associated with the polling strategy. + * @return Offset or timestamp for parameterized strategies; otherwise zero. */ - static iggy::ffi::HeaderEntry PreallocateSegments(const bool enabled) { - return detail::to_option_entry("preallocate_segments", iggy::ffi::HeaderKind::Bool, - detail::to_bool_bytes(enabled)); - } + std::uint64_t PollingStrategyValue() const { return polling_strategy_value_; } private: - TopicOption() = delete; -}; + explicit PollingStrategy(std::string kind, std::uint64_t value) + : polling_strategy_kind_(std::move(kind)), polling_strategy_value_(value) {} -/** - * @brief Exception thrown when an Iggy client operation fails. - */ -class IggyException : public std::runtime_error { - public: - explicit IggyException(const char *message) : std::runtime_error(message) {} - explicit IggyException(const std::string &message) : std::runtime_error(message) {} + std::string polling_strategy_kind_; + std::uint64_t polling_strategy_value_; }; /** @@ -709,6 +1655,226 @@ class IggyBlockingClient final { */ void Logout(); + /** + * @brief Creates a top-level stream in the cluster metadata. + * + * A stream is the top-level namespace for topics. This creates no topics, + * partitions, or messages. Its name must be unique, non-empty, and no more + * than 255 UTF-8 bytes. + * + * A transport failure after submission can leave the stream created. Look + * it up by name before retrying or choosing another name. + * + * @param name Unique stream name. + * @return Details of the newly created, topic-less stream. + * @throws IggyException if the client is unavailable or unauthenticated; + * the name is invalid or already in use; the caller lacks + * stream-management permission; or the request fails. + */ + StreamDetails CreateStream(std::string name); + + /** + * @brief Renames a stream. + * + * @param stream Stream to rename, addressed by numeric ID or name. + * @param name New unique stream name. + * @param options Stream update options (currently no updatable keys; `raw` + * carries forward-compatible keys, each rejected until catalogued). + * @throws IggyException if the client is unavailable, the caller lacks + * stream-management permission, either value is invalid, the stream + * does not exist, the name is already taken, or the request fails. + */ + void UpdateStream(const Identifier &stream, std::string name, const StreamUpdateOptions &options = {}); + + /** + * @brief Lists stream summaries visible to the authenticated user. + * + * The summaries exclude per-topic details. Use GetStream() for those. + * @return Stream summaries visible to the authenticated user. + * @throws IggyException if the client is unavailable, the caller lacks + * permission to read streams, or the request fails. + */ + std::vector GetStreams(); + + /** + * @brief Retrieves one stream by numeric ID or name. + * + * The result includes observed aggregate statistics and topic summaries. + * It does not include partition details or messages, and its statistics can + * become stale immediately after the request completes. + * + * @param stream Stream to retrieve. Numeric IDs remain stable if a stream + * is renamed. + * @return Details for the requested stream. + * @throws IggyException if the client is unavailable or unauthenticated; + * the stream does not exist; the caller lacks read permission; or + * the metadata read fails. + */ + StreamDetails GetStream(const Identifier &stream); + + /** + * @brief Deletes a stream and all of its topics, partitions, and messages. + * + * This is irreversible. A transport failure after submission can leave the + * deletion committed, so query the stream before retrying this request. + * + * @param stream Stream to delete, addressed by numeric ID or name. + * @throws IggyException if the client is unavailable, the caller lacks + * stream-management permission, the stream does not exist, or the + * request fails. + */ + void DeleteStream(const Identifier &stream); + + /** + * @brief Removes all messages from every topic in a stream. + * + * The stream, its topics, and topic configuration remain available. A + * transport failure after submission can still leave the purge committed. + * @param stream Stream to purge, addressed by numeric ID or name. + * @throws IggyException if the client is unavailable, the caller lacks + * stream-management permission, the stream does not exist, or the + * request fails. + */ + void PurgeStream(const Identifier &stream); + + /** + * @brief Creates a topic and its initial partitions in a stream. + * + * Mirrors Rust `TopicClient::create_topic` (`core/common/src/traits/topic_client.rs:43`) + * with `TopicCreateOptions` (`core/common/src/types/options/mod.rs:698`). + * Each `std::optional` corresponds to a catalog key; `nullopt` uses the + * server default. `raw` carries forward string keys; a typed field wins + * on collision. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @param name Unique topic name within @p stream. + * @param options Topic creation options. + * @return Metadata and initial partition summaries for the created topic. + * @throws IggyException if the client is unavailable or unauthenticated; + * an identifier, name, partition count, or option is invalid; the + * stream does not exist; the caller lacks topic-management + * permission; or the server rejects or cannot commit the write. + */ + TopicDetails CreateTopic(const Identifier &stream, std::string name, const TopicCreateOptions &options = {}); + + /** + * @brief Renames a topic and updates its mutable configuration. + * + * Mirrors Rust `TopicClient::update_topic` with `TopicUpdateOptions` + * (`core/common/src/types/options/mod.rs:640`). Only `compression_algorithm`, + * `message_expiry`, `max_topic_size` and `raw` keys in + * `UPDATABLE_TOPIC_OPTION_KEYS` are accepted; `nullopt` keeps the current + * value. Creation-only keys (`segment_size`, `enforce_fsync`, etc.) are + * rejected. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @param topic Topic to update, addressed by numeric ID or name. + * @param name New unique topic name within @p stream. + * @param options Topic update options. + * @throws IggyException if the client is unavailable or unauthenticated; + * an identifier, name, setting, or option is invalid; the stream or + * topic does not exist; the caller lacks permission; or the server + * rejects or cannot commit the write. + */ + void UpdateTopic(const Identifier &stream, + const Identifier &topic, + std::string name, + const TopicUpdateOptions &options = {}); + + /** + * @brief Lists topic summaries in a stream. + * + * The returned summaries do not include partition details. Use GetTopic() + * when partition offsets, sizes, and segment counts are needed. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @return Topic summaries visible to the authenticated user. + * @throws IggyException if the client is unavailable or unauthenticated; + * the stream does not exist; the caller lacks read permission; or + * the metadata read fails. + */ + std::vector GetTopics(const Identifier &stream); + + /** + * @brief Retrieves one topic and its partition summaries. + * + * The result is an observed metadata read. Partition offsets and retained + * statistics can change immediately after this call returns. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @param topic Topic to retrieve, addressed by numeric ID or name. + * @return Topic metadata and one summary per partition. + * @throws IggyException if the client is unavailable or unauthenticated; + * the stream or topic does not exist; the caller lacks read + * permission; or the metadata read fails. + */ + TopicDetails GetTopic(const Identifier &stream, const Identifier &topic); + + /** + * @brief Deletes a topic, its partitions, and retained messages. + * + * A failed or unknown transport outcome can leave the deletion committed. + * Query the topic before retrying a destructive request. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @param topic Topic to delete, addressed by numeric ID or name. + * @throws IggyException if the client is unavailable or unauthenticated; + * the stream or topic does not exist; the caller lacks + * topic-management permission; or the server rejects or cannot + * commit the write. + */ + void DeleteTopic(const Identifier &stream, const Identifier &topic); + + /** + * @brief Removes retained messages from every partition of a topic. + * + * The topic, its partitions, names, and configuration remain. New messages + * can be sent after a purge. A failed or unknown transport outcome can + * still leave the purge committed. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @param topic Topic to purge, addressed by numeric ID or name. + * @throws IggyException if the client is unavailable or unauthenticated; + * the stream or topic does not exist; the caller lacks + * topic-management permission; or the server rejects or cannot + * commit the write. + */ + void PurgeTopic(const Identifier &stream, const Identifier &topic); + + /** + * @brief Adds partitions to a topic. + * + * New partitions receive IDs after the topic's existing partitions. The + * requested count must be between 1 and 1000. A transport failure after + * submission can still leave the partitions created. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @param topic Topic to extend, addressed by numeric ID or name. + * @param partitions_count Number of partitions to add. + * @throws IggyException if the client is unavailable or unauthenticated; + * an identifier or count is invalid; the stream or topic does not + * exist; the caller lacks topic-management permission; or the + * request fails. + */ + void CreatePartitions(const Identifier &stream, const Identifier &topic, std::uint32_t partitions_count); + + /** + * @brief Deletes the highest-numbered partitions from a topic. + * + * The deleted partitions and their retained messages are removed. The + * requested count must be between 1 and 1000. A transport failure after + * submission can still leave the deletion committed. + * + * @param stream Parent stream, addressed by numeric ID or name. + * @param topic Topic to shrink, addressed by numeric ID or name. + * @param partitions_count Number of partitions to delete. + * @throws IggyException if the client is unavailable or unauthenticated; + * an identifier or count is invalid; the stream or topic does not + * exist; the caller lacks topic-management permission; or the + * request fails. + */ + void DeletePartitions(const Identifier &stream, const Identifier &topic, std::uint32_t partitions_count); + private: explicit IggyBlockingClient(ffi::Client *client); diff --git a/foreign/cpp/src/client.cpp b/foreign/cpp/src/client.cpp index f81e7956cc..f025dd103a 100644 --- a/foreign/cpp/src/client.cpp +++ b/foreign/cpp/src/client.cpp @@ -17,6 +17,8 @@ * under the License. */ +#include + #include "iggy.hpp" namespace iggy { @@ -54,14 +56,233 @@ void IggyBlockingClient::Shutdown() { } LoginInfo IggyBlockingClient::Login(std::string username, std::string password) { - return RethrowAsIggyException( - [this, &username, &password] { return Handle()->login_user(std::move(username), std::move(password)); }); + return RethrowAsIggyException([this, &username, &password] { + return LoginInfo::FromFfi(Handle()->login_user(std::move(username), std::move(password))); + }); } void IggyBlockingClient::Logout() { RethrowAsIggyException([this] { Handle()->logout_user(); }); } +StreamDetails IggyBlockingClient::CreateStream(std::string name) { + return RethrowAsIggyException( + [this, &name] { return StreamDetails::FromFfi(Handle()->create_stream(std::move(name))); }); +} + +void IggyBlockingClient::UpdateStream(const Identifier &stream, std::string name, const StreamUpdateOptions &options) { + return RethrowAsIggyException([this, &stream, &name, &options] { + // Stream options are currently null-op (UPDATABLE_STREAM_OPTION_KEYS empty), no conversion needed. + (void)options; + Handle()->update_stream(stream.ToFfi(), std::move(name), rust::Vec{}); + }); +} + +std::vector IggyBlockingClient::GetStreams() { + return RethrowAsIggyException([this] { + std::vector streams; + auto ffi_streams = Handle()->get_streams(); + streams.reserve(ffi_streams.size()); + for (auto &stream : ffi_streams) { + streams.push_back(Stream::FromFfi(std::move(stream))); + } + return streams; + }); +} + +StreamDetails IggyBlockingClient::GetStream(const Identifier &stream) { + return RethrowAsIggyException( + [this, &stream] { return StreamDetails::FromFfi(Handle()->get_stream(stream.ToFfi())); }); +} + +void IggyBlockingClient::DeleteStream(const Identifier &stream) { + return RethrowAsIggyException([this, &stream] { Handle()->delete_stream(stream.ToFfi()); }); +} + +void IggyBlockingClient::PurgeStream(const Identifier &stream) { + return RethrowAsIggyException([this, &stream] { Handle()->purge_stream(stream.ToFfi()); }); +} + +TopicDetails IggyBlockingClient::CreateTopic(const Identifier &stream, + std::string name, + const TopicCreateOptions &options) { + return RethrowAsIggyException([this, &stream, &name, &options] { + ffi::TopicCreateOptions ffi_options; + if (auto value = options.PartitionsCount()) { + ffi_options.has_partitions_count = true; + ffi_options.partitions_count = *value; + } else { + ffi_options.has_partitions_count = false; + ffi_options.partitions_count = 0; + } + if (auto value = options.CompressionAlgorithm()) { + ffi_options.has_compression_algorithm = true; + ffi_options.compression_algorithm = std::string(value->CompressionAlgorithmValue()); + } else { + ffi_options.has_compression_algorithm = false; + ffi_options.compression_algorithm = ""; + } + if (auto value = options.MessageExpiry()) { + ffi_options.has_message_expiry = true; + ffi_options.message_expiry_kind = std::string(value->ExpiryKind()); + ffi_options.message_expiry_value = value->ExpiryValue(); + } else { + ffi_options.has_message_expiry = false; + ffi_options.message_expiry_kind = ""; + ffi_options.message_expiry_value = 0; + } + if (auto value = options.MaxTopicSize()) { + ffi_options.has_max_topic_size = true; + ffi_options.max_topic_size = std::string(value->MaxTopicSizeValue()); + } else { + ffi_options.has_max_topic_size = false; + ffi_options.max_topic_size = ""; + } + if (auto value = options.SegmentSize()) { + ffi_options.has_segment_size = true; + ffi_options.segment_size = *value; + } else { + ffi_options.has_segment_size = false; + ffi_options.segment_size = 0; + } + if (auto value = options.EnforceFsync()) { + ffi_options.has_enforce_fsync = true; + ffi_options.enforce_fsync = *value; + } else { + ffi_options.has_enforce_fsync = false; + ffi_options.enforce_fsync = false; + } + if (auto value = options.MessagesRequiredToSave()) { + ffi_options.has_messages_required_to_save = true; + ffi_options.messages_required_to_save = *value; + } else { + ffi_options.has_messages_required_to_save = false; + ffi_options.messages_required_to_save = 0; + } + if (auto value = options.SizeOfMessagesRequiredToSave()) { + ffi_options.has_size_of_messages_required_to_save = true; + ffi_options.size_of_messages_required_to_save = *value; + } else { + ffi_options.has_size_of_messages_required_to_save = false; + ffi_options.size_of_messages_required_to_save = 0; + } + if (auto value = options.PreallocateSegments()) { + ffi_options.has_preallocate_segments = true; + ffi_options.preallocate_segments = *value; + } else { + ffi_options.has_preallocate_segments = false; + ffi_options.preallocate_segments = false; + } + ffi_options.raw_options.reserve(options.RawEntries().size()); + for (const auto &entry : options.RawEntries()) { + ffi::HeaderEntry ffi_entry; + ffi_entry.key.kind = static_cast(HeaderKind::String); + ffi_entry.key.value.reserve(entry.first.size()); + for (char character : entry.first) { + ffi_entry.key.value.push_back(static_cast(character)); + } + ffi_entry.value.kind = static_cast(HeaderKind::String); + ffi_entry.value.value.reserve(entry.second.size()); + for (char character : entry.second) { + ffi_entry.value.value.push_back(static_cast(character)); + } + ffi_options.raw_options.push_back(std::move(ffi_entry)); + } + + return TopicDetails::FromFfi(Handle()->create_topic(stream.ToFfi(), std::move(name), std::move(ffi_options))); + }); +} + +void IggyBlockingClient::UpdateTopic(const Identifier &stream, + const Identifier &topic, + std::string name, + const TopicUpdateOptions &options) { + return RethrowAsIggyException([this, &stream, &topic, &name, &options] { + ffi::TopicUpdateOptions ffi_options; + if (auto value = options.CompressionAlgorithm()) { + ffi_options.has_compression_algorithm = true; + ffi_options.compression_algorithm = std::string(value->CompressionAlgorithmValue()); + } else { + ffi_options.has_compression_algorithm = false; + ffi_options.compression_algorithm = ""; + } + if (auto value = options.MessageExpiry()) { + ffi_options.has_message_expiry = true; + ffi_options.message_expiry_kind = std::string(value->ExpiryKind()); + ffi_options.message_expiry_value = value->ExpiryValue(); + } else { + ffi_options.has_message_expiry = false; + ffi_options.message_expiry_kind = ""; + ffi_options.message_expiry_value = 0; + } + if (auto value = options.MaxTopicSize()) { + ffi_options.has_max_topic_size = true; + ffi_options.max_topic_size = std::string(value->MaxTopicSizeValue()); + } else { + ffi_options.has_max_topic_size = false; + ffi_options.max_topic_size = ""; + } + ffi_options.raw_options.reserve(options.RawEntries().size()); + for (const auto &entry : options.RawEntries()) { + ffi::HeaderEntry ffi_entry; + ffi_entry.key.kind = static_cast(HeaderKind::String); + ffi_entry.key.value.reserve(entry.first.size()); + for (char character : entry.first) { + ffi_entry.key.value.push_back(static_cast(character)); + } + ffi_entry.value.kind = static_cast(HeaderKind::String); + ffi_entry.value.value.reserve(entry.second.size()); + for (char character : entry.second) { + ffi_entry.value.value.push_back(static_cast(character)); + } + ffi_options.raw_options.push_back(std::move(ffi_entry)); + } + + Handle()->update_topic(stream.ToFfi(), topic.ToFfi(), std::move(name), std::move(ffi_options)); + }); +} + +std::vector IggyBlockingClient::GetTopics(const Identifier &stream) { + return RethrowAsIggyException([this, &stream] { + std::vector topics; + auto ffi_topics = Handle()->get_topics(stream.ToFfi()); + topics.reserve(ffi_topics.size()); + for (auto &topic : ffi_topics) { + topics.push_back(Topic::FromFfi(std::move(topic))); + } + return topics; + }); +} + +TopicDetails IggyBlockingClient::GetTopic(const Identifier &stream, const Identifier &topic) { + return RethrowAsIggyException( + [this, &stream, &topic] { return TopicDetails::FromFfi(Handle()->get_topic(stream.ToFfi(), topic.ToFfi())); }); +} + +void IggyBlockingClient::DeleteTopic(const Identifier &stream, const Identifier &topic) { + return RethrowAsIggyException([this, &stream, &topic] { Handle()->delete_topic(stream.ToFfi(), topic.ToFfi()); }); +} + +void IggyBlockingClient::PurgeTopic(const Identifier &stream, const Identifier &topic) { + return RethrowAsIggyException([this, &stream, &topic] { Handle()->purge_topic(stream.ToFfi(), topic.ToFfi()); }); +} + +void IggyBlockingClient::CreatePartitions(const Identifier &stream, + const Identifier &topic, + const std::uint32_t partitions_count) { + return RethrowAsIggyException([this, &stream, &topic, partitions_count] { + Handle()->create_partitions(stream.ToFfi(), topic.ToFfi(), partitions_count); + }); +} + +void IggyBlockingClient::DeletePartitions(const Identifier &stream, + const Identifier &topic, + const std::uint32_t partitions_count) { + return RethrowAsIggyException([this, &stream, &topic, partitions_count] { + Handle()->delete_partitions(stream.ToFfi(), topic.ToFfi(), partitions_count); + }); +} + IggyBlockingClient::IggyBlockingClient(ffi::Client *client) : client_(client) { if (client_ == nullptr) { throw IggyException("Could not create Iggy client"); diff --git a/foreign/cpp/src/client.rs b/foreign/cpp/src/client.rs index 002ead54da..b676072082 100644 --- a/foreign/cpp/src/client.rs +++ b/foreign/cpp/src/client.rs @@ -18,18 +18,16 @@ use crate::{RUNTIME, ffi}; use bytes::Bytes; use iggy::prelude::{ - AutoLogin as RustAutoLogin, Client as IggyConnectionClient, ClusterClient, - CompressionAlgorithm as RustCompressionAlgorithm, Consumer, ConsumerGroupClient, - ConsumerOffsetClient, Identifier as RustIdentifier, IggyClient as RustIggyClient, - IggyClientBuilder as RustIggyClientBuilder, IggyDuration as RustIggyDuration, - IggyExpiry as RustIggyExpiry, IggyMessage, IggyTimestamp, MaxTopicSize as RustMaxTopicSize, - MessageClient, NonZeroIggyDuration as RustNonZeroIggyDuration, - OptionsScope as RustOptionsScope, PartitionClient, Partitioning, - Permissions as RustPermissions, PollingStrategy, SegmentClient, + AutoLogin as RustAutoLogin, Client as IggyConnectionClient, ClusterClient, Consumer, + ConsumerGroupClient, ConsumerOffsetClient, Identifier as RustIdentifier, + IggyClient as RustIggyClient, IggyClientBuilder as RustIggyClientBuilder, + IggyDuration as RustIggyDuration, IggyMessage, IggyTimestamp, MessageClient, + NonZeroIggyDuration as RustNonZeroIggyDuration, OptionsScope as RustOptionsScope, + PartitionClient, Partitioning, Permissions as RustPermissions, PollingStrategy, SegmentClient, SnapshotCompression as RustSnapshotCompression, StreamClient, StreamUpdateOptions, SystemClient as RustSystemClient, SystemSnapshotType as RustSystemSnapshotType, TopicClient, - TopicCreateOptions, TopicUpdateOptions, UserClient, UserStatus as RustUserStatus, - UserUpdateOptions, + TopicCreateOptions as RustTopicCreateOptions, TopicUpdateOptions as RustTopicUpdateOptions, + UserClient, UserStatus as RustUserStatus, UserUpdateOptions, }; use iggy_common::Credentials as RustCredentials; use std::collections::HashSet; @@ -104,9 +102,8 @@ pub fn new_connection(config: ffi::IggyClientConfig) -> Result<*mut Client, Stri ); if config.has_reconnection_interval { let reconnection_interval = - RustNonZeroIggyDuration::try_from(config.reconnection_interval_micros).map_err( - |error| format!("Invalid reconnection interval: {error}"), - )?; + RustNonZeroIggyDuration::try_from(config.reconnection_interval_micros) + .map_err(|error| format!("Invalid reconnection interval: {error}"))?; builder = builder.with_reconnection_interval(reconnection_interval); } if config.has_reestablish_after { @@ -229,13 +226,16 @@ impl Client { &self, stream_id: ffi::Identifier, stream_name: String, + options: Vec, ) -> Result<(), String> { let rust_stream_id = RustIdentifier::try_from(stream_id) .map_err(|error| format!("Could not update stream '{stream_name}': {error}"))?; + // Stream options are currently null-op (UPDATABLE_STREAM_OPTION_KEYS is empty), + // so no conversion is needed. Keep param for surface parity and pass default. + let _ = options; RUNTIME.block_on(async { self.inner - // Streams have no option keys yet. .update_stream( &rust_stream_id, &stream_name, @@ -438,67 +438,17 @@ impl Client { }) } - #[allow(clippy::too_many_arguments)] pub fn create_topic( &self, stream_id: ffi::Identifier, topic_name: String, - partitions_count: u32, - compression_algorithm: String, - message_expiry_kind: String, - message_expiry_value: u64, - max_topic_size: String, - options: Vec, + options: ffi::TopicCreateOptions, ) -> Result { let rust_stream_id = RustIdentifier::try_from(stream_id) .map_err(|error| format!("Could not create topic '{topic_name}': {error}"))?; - let rust_compression_algorithm = match compression_algorithm.to_lowercase().as_str() { - "" | "none" => RustCompressionAlgorithm::None, - _ => RustCompressionAlgorithm::from_str(&compression_algorithm).map_err(|error| { - format!( - "Could not create topic '{topic_name}': invalid compression algorithm '{compression_algorithm}': {error}" - ) - })?, - }; - let rust_message_expiry = match message_expiry_kind.as_str() { - "" | "server_default" | "default" => RustIggyExpiry::ServerDefault, - "never_expire" => RustIggyExpiry::NeverExpire, - "duration" => RustIggyExpiry::ExpireDuration(iggy::prelude::IggyDuration::from( - message_expiry_value, - )), - _ => { - return Err(format!( - "Could not create topic '{topic_name}': invalid message expiry kind '{message_expiry_kind}'" - )); - } - }; - let rust_max_topic_size = match max_topic_size.as_str() { - "" | "server_default" | "0" => RustMaxTopicSize::ServerDefault, - _ => RustMaxTopicSize::from_str(&max_topic_size).map_err(|error| { - format!( - "Could not create topic '{topic_name}': invalid max topic size '{max_topic_size}': {error}" - ) - })?, - }; - - let raw = crate::type_conversion::ffi_options_to_raw(options) + let options = RustTopicCreateOptions::try_from(options) .map_err(|error| format!("Could not create topic '{topic_name}': {error}"))?; - // `None` is what tells admission to resolve the server default, so the - // sentinels the string parsers produce must collapse back to it. - let options = TopicCreateOptions { - partitions_count: Some(partitions_count), - compression_algorithm: (rust_compression_algorithm - != RustCompressionAlgorithm::default()) - .then_some(rust_compression_algorithm), - message_expiry: (rust_message_expiry != RustIggyExpiry::ServerDefault) - .then_some(rust_message_expiry), - max_topic_size: (rust_max_topic_size != RustMaxTopicSize::ServerDefault) - .then_some(rust_max_topic_size), - raw, - ..TopicCreateOptions::default() - }; - RUNTIME.block_on(async { let topic_details = self .inner @@ -558,67 +508,20 @@ impl Client { }) } - #[allow(clippy::too_many_arguments)] pub fn update_topic( &self, stream_id: ffi::Identifier, topic_id: ffi::Identifier, topic_name: String, - compression_algorithm: String, - message_expiry_kind: String, - message_expiry_value: u64, - max_topic_size: String, - options: Vec, + options: ffi::TopicUpdateOptions, ) -> Result<(), String> { let rust_stream_id = RustIdentifier::try_from(stream_id) .map_err(|error| format!("Could not update topic '{topic_name}': {error}"))?; let rust_topic_id = RustIdentifier::try_from(topic_id) .map_err(|error| format!("Could not update topic '{topic_name}': {error}"))?; - let rust_compression_algorithm = match compression_algorithm.to_lowercase().as_str() { - "" | "none" => RustCompressionAlgorithm::None, - _ => RustCompressionAlgorithm::from_str(&compression_algorithm).map_err(|error| { - format!( - "Could not update topic '{topic_name}': invalid compression algorithm '{compression_algorithm}': {error}" - ) - })?, - }; - let rust_message_expiry = match message_expiry_kind.as_str() { - "" | "server_default" | "default" => RustIggyExpiry::ServerDefault, - "never_expire" => RustIggyExpiry::NeverExpire, - "duration" => RustIggyExpiry::ExpireDuration(iggy::prelude::IggyDuration::from( - message_expiry_value, - )), - _ => { - return Err(format!( - "Could not update topic '{topic_name}': invalid message expiry kind '{message_expiry_kind}'" - )); - } - }; - let rust_max_topic_size = match max_topic_size.as_str() { - "" | "server_default" | "0" => RustMaxTopicSize::ServerDefault, - _ => RustMaxTopicSize::from_str(&max_topic_size).map_err(|error| { - format!( - "Could not update topic '{topic_name}': invalid max topic size '{max_topic_size}': {error}" - ) - })?, - }; - - let raw = crate::type_conversion::ffi_options_to_raw(options) + let update_options = RustTopicUpdateOptions::try_from(options) .map_err(|error| format!("Could not update topic '{topic_name}': {error}"))?; - // Settings ride the options block; a server-default sentinel means the - // caller did not set the key, so the topic keeps its current value. - let update_options = TopicUpdateOptions { - compression_algorithm: (rust_compression_algorithm - != RustCompressionAlgorithm::default()) - .then_some(rust_compression_algorithm), - message_expiry: (rust_message_expiry != RustIggyExpiry::ServerDefault) - .then_some(rust_message_expiry), - max_topic_size: (rust_max_topic_size != RustMaxTopicSize::ServerDefault) - .then_some(rust_max_topic_size), - raw, - }; - RUNTIME.block_on(async { self.inner .update_topic(&rust_stream_id, &rust_topic_id, &topic_name, &update_options) diff --git a/foreign/cpp/src/lib.rs b/foreign/cpp/src/lib.rs index 496b0728fc..d7dad80790 100644 --- a/foreign/cpp/src/lib.rs +++ b/foreign/cpp/src/lib.rs @@ -428,6 +428,40 @@ mod ffi { no_delay: bool, } + struct TopicCreateOptions { + has_partitions_count: bool, + partitions_count: u32, + has_compression_algorithm: bool, + compression_algorithm: String, + has_message_expiry: bool, + message_expiry_kind: String, + message_expiry_value: u64, + has_max_topic_size: bool, + max_topic_size: String, + has_segment_size: bool, + segment_size: u64, + has_enforce_fsync: bool, + enforce_fsync: bool, + has_messages_required_to_save: bool, + messages_required_to_save: u32, + has_size_of_messages_required_to_save: bool, + size_of_messages_required_to_save: u64, + has_preallocate_segments: bool, + preallocate_segments: bool, + raw_options: Vec, + } + + struct TopicUpdateOptions { + has_compression_algorithm: bool, + compression_algorithm: String, + has_message_expiry: bool, + message_expiry_kind: String, + message_expiry_value: u64, + has_max_topic_size: bool, + max_topic_size: String, + raw_options: Vec, + } + extern "Rust" { type Client; type Consumer; @@ -440,22 +474,21 @@ mod ffi { fn logout_user(self: &Client) -> Result<()>; fn connect(self: &Client) -> Result<()>; fn create_stream(self: &Client, stream_name: String) -> Result; - fn update_stream(self: &Client, stream_id: Identifier, stream_name: String) -> Result<()>; + fn update_stream( + self: &Client, + stream_id: Identifier, + stream_name: String, + options: Vec, + ) -> Result<()>; fn get_streams(self: &Client) -> Result>; fn get_stream(self: &Client, stream_id: Identifier) -> Result; fn delete_stream(self: &Client, stream_id: Identifier) -> Result<()>; fn purge_stream(self: &Client, stream_id: Identifier) -> Result<()>; - #[allow(clippy::too_many_arguments)] fn create_topic( self: &Client, stream_id: Identifier, topic_name: String, - partitions_count: u32, - compression_algorithm: String, - message_expiry_kind: String, - message_expiry_value: u64, - max_topic_size: String, - options: Vec, + options: TopicCreateOptions, ) -> Result; fn get_topic( self: &Client, @@ -463,17 +496,12 @@ mod ffi { topic_id: Identifier, ) -> Result; fn get_topics(self: &Client, stream_id: Identifier) -> Result>; - #[allow(clippy::too_many_arguments)] fn update_topic( self: &Client, stream_id: Identifier, topic_id: Identifier, topic_name: String, - compression_algorithm: String, - message_expiry_kind: String, - message_expiry_value: u64, - max_topic_size: String, - options: Vec, + options: TopicUpdateOptions, ) -> Result<()>; fn delete_topic(self: &Client, stream_id: Identifier, topic_id: Identifier) -> Result<()>; fn purge_topic(self: &Client, stream_id: Identifier, topic_id: Identifier) -> Result<()>; diff --git a/foreign/cpp/src/type_conversion.rs b/foreign/cpp/src/type_conversion.rs index 83d4c52b6d..97cc106f67 100644 --- a/foreign/cpp/src/type_conversion.rs +++ b/foreign/cpp/src/type_conversion.rs @@ -18,13 +18,16 @@ use crate::ffi; use bytes::Bytes; use iggy::prelude::{ + CompressionAlgorithm as RustCompressionAlgorithm, ConsumerGroupDetails as RustConsumerGroupDetails, IdKind, Identifier as RustIdentifier, - IggyMessage as RustIggyMessage, OptionSpec as RustOptionSpec, Partition as RustPartition, + IggyByteSize as RustIggyByteSize, IggyExpiry as RustIggyExpiry, IggyMessage as RustIggyMessage, + MaxTopicSize as RustMaxTopicSize, OptionSpec as RustOptionSpec, Partition as RustPartition, PolledMessages as RustPolledMessages, SendMessagesConfirmationResponse as RustSendMessagesConfirmationResponse, SendMessagesResponse as RustSendMessagesResponse, Stream as RustStream, - StreamDetails as RustStreamDetails, Topic as RustTopic, TopicDetails as RustTopicDetails, - Validatable, + StreamDetails as RustStreamDetails, Topic as RustTopic, + TopicCreateOptions as RustTopicCreateOptions, TopicDetails as RustTopicDetails, + TopicUpdateOptions as RustTopicUpdateOptions, Validatable, }; use iggy_binary_protocol::WireUserHeaders; use iggy_common::{ @@ -499,8 +502,8 @@ fn resource_options_to_ffi( /// value. A key this build cannot read at all is dropped rather than guessed. pub(crate) fn ffi_options_to_raw( options: Vec, -) -> Result, String> { - let mut raw = std::collections::BTreeMap::new(); +) -> Result, String> { + let mut raw = BTreeMap::new(); for entry in options { let RustHeaderEntry { key, value } = RustHeaderEntry::try_from(entry)?; let key = key @@ -525,6 +528,154 @@ impl From for ffi::OptionSpec { } } +impl TryFrom for RustTopicCreateOptions { + type Error = String; + + fn try_from(options: ffi::TopicCreateOptions) -> Result { + use std::str::FromStr; + let compression_algorithm = if options.has_compression_algorithm { + Some( + RustCompressionAlgorithm::from_str(&options.compression_algorithm).map_err( + |error| { + format!( + "invalid compression algorithm '{}': {error}", + options.compression_algorithm + ) + }, + )?, + ) + } else { + None + }; + let message_expiry = if options.has_message_expiry { + Some(match options.message_expiry_kind.as_str() { + "server_default" | "default" => RustIggyExpiry::ServerDefault, + "never_expire" => RustIggyExpiry::NeverExpire, + "duration" => RustIggyExpiry::ExpireDuration(iggy::prelude::IggyDuration::from( + options.message_expiry_value, + )), + _ => { + return Err(format!( + "invalid message expiry kind '{}'", + options.message_expiry_kind + )); + } + }) + } else { + None + }; + let max_topic_size = if options.has_max_topic_size { + Some( + RustMaxTopicSize::from_str(&options.max_topic_size).map_err(|error| { + format!( + "invalid max topic size '{}': {error}", + options.max_topic_size + ) + })?, + ) + } else { + None + }; + let raw = ffi_options_to_raw(options.raw_options)?; + Ok(RustTopicCreateOptions { + partitions_count: if options.has_partitions_count { + Some(options.partitions_count) + } else { + None + }, + compression_algorithm, + message_expiry, + max_topic_size, + segment_size: if options.has_segment_size { + Some(RustIggyByteSize::from(options.segment_size)) + } else { + None + }, + enforce_fsync: if options.has_enforce_fsync { + Some(options.enforce_fsync) + } else { + None + }, + messages_required_to_save: if options.has_messages_required_to_save { + Some(options.messages_required_to_save) + } else { + None + }, + size_of_messages_required_to_save: if options.has_size_of_messages_required_to_save { + Some(RustIggyByteSize::from( + options.size_of_messages_required_to_save, + )) + } else { + None + }, + preallocate_segments: if options.has_preallocate_segments { + Some(options.preallocate_segments) + } else { + None + }, + raw, + }) + } +} + +impl TryFrom for RustTopicUpdateOptions { + type Error = String; + + fn try_from(options: ffi::TopicUpdateOptions) -> Result { + use std::str::FromStr; + let compression_algorithm = if options.has_compression_algorithm { + Some( + RustCompressionAlgorithm::from_str(&options.compression_algorithm).map_err( + |error| { + format!( + "invalid compression algorithm '{}': {error}", + options.compression_algorithm + ) + }, + )?, + ) + } else { + None + }; + let message_expiry = if options.has_message_expiry { + Some(match options.message_expiry_kind.as_str() { + "server_default" | "default" => RustIggyExpiry::ServerDefault, + "never_expire" => RustIggyExpiry::NeverExpire, + "duration" => RustIggyExpiry::ExpireDuration(iggy::prelude::IggyDuration::from( + options.message_expiry_value, + )), + _ => { + return Err(format!( + "invalid message expiry kind '{}'", + options.message_expiry_kind + )); + } + }) + } else { + None + }; + let max_topic_size = if options.has_max_topic_size { + Some( + RustMaxTopicSize::from_str(&options.max_topic_size).map_err(|error| { + format!( + "invalid max topic size '{}': {error}", + options.max_topic_size + ) + })?, + ) + } else { + None + }; + let raw = ffi_options_to_raw(options.raw_options)?; + Ok(RustTopicUpdateOptions { + compression_algorithm, + message_expiry, + max_topic_size, + raw, + }) + } +} + impl From for ffi::Topic { fn from(topic: RustTopic) -> Self { ffi::Topic { diff --git a/foreign/cpp/src/type_conversions.cpp b/foreign/cpp/src/type_conversions.cpp new file mode 100644 index 0000000000..4a187aca0f --- /dev/null +++ b/foreign/cpp/src/type_conversions.cpp @@ -0,0 +1,185 @@ +/* + * 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. + */ + +#include + +#include "iggy.hpp" + +namespace iggy { + +LoginInfo LoginInfo::FromFfi(ffi::LoginInfo login_info) { + std::optional access_token; + std::optional access_token_expiry; + if (login_info.has_access_token) { + access_token.emplace(login_info.access_token.c_str(), login_info.access_token.size()); + access_token_expiry = login_info.access_token_expiry; + } + + return LoginInfo(login_info.user_id, std::move(access_token), access_token_expiry); +} + +Identifier Identifier::FromFfi(ffi::Identifier identifier) { + const std::string kind(identifier.kind.c_str(), identifier.kind.size()); + if (kind == "numeric") { + if (identifier.length != sizeof(std::uint32_t) || identifier.value.size() != sizeof(std::uint32_t)) { + throw IggyException("Invalid numeric identifier returned by Rust"); + } + const auto id = static_cast(identifier.value[0]) | + (static_cast(identifier.value[1]) << 8U) | + (static_cast(identifier.value[2]) << 16U) | + (static_cast(identifier.value[3]) << 24U); + return Numeric(id); + } + if (kind == "string") { + if (identifier.length != identifier.value.size()) { + throw IggyException("Invalid string identifier returned by Rust"); + } + return String(std::string(identifier.value.begin(), identifier.value.end())); + } + throw IggyException("Invalid identifier kind returned by Rust"); +} + +ffi::Identifier Identifier::ToFfi() const { + ffi::Identifier identifier{}; + if (kind_ == Kind::Numeric) { + identifier.set_numeric(std::get(value_)); + } else { + identifier.set_string(std::get(value_)); + } + return identifier; +} + +HeaderField HeaderField::FromFfi(ffi::HeaderField field) { + return HeaderField(static_cast(field.kind), + std::vector(field.value.begin(), field.value.end())); +} + +ffi::HeaderField HeaderField::ToFfi(HeaderField field) { + ffi::HeaderField ffi_field{}; + ffi_field.kind = static_cast(field.kind_); + ffi_field.value.reserve(field.value_.size()); + for (const auto byte : field.value_) { + ffi_field.value.push_back(byte); + } + return ffi_field; +} + +HeaderEntry HeaderEntry::FromFfi(ffi::HeaderEntry entry) { + return HeaderEntry(HeaderField::FromFfi(std::move(entry.key)), HeaderField::FromFfi(std::move(entry.value))); +} + +ffi::HeaderEntry HeaderEntry::ToFfi(HeaderEntry entry) { + ffi::HeaderEntry ffi_entry{}; + ffi_entry.key = HeaderField::ToFfi(std::move(entry.key_)); + ffi_entry.value = HeaderField::ToFfi(std::move(entry.value_)); + return ffi_entry; +} + +ResourceOptions ResourceOptions::Explicit(std::vector entries) { + std::map explicit_map; + for (auto &entry : entries) { + const auto &key_bytes = entry.Key().Value(); + std::string key(key_bytes.begin(), key_bytes.end()); + explicit_map.emplace(std::move(key), entry.Value()); + } + return ResourceOptions(std::move(explicit_map)); +} + +ResourceOptions ResourceOptions::Explicit(std::map entries) { + return ResourceOptions(std::move(entries)); +} + +ResourceOptions ResourceOptions::FromFfi(rust::Vec explicit_entries, + rust::Vec derived_entries) { + std::map explicit_options; + for (auto &entry : explicit_entries) { + HeaderEntry header_entry = HeaderEntry::FromFfi(std::move(entry)); + const auto &key_bytes = header_entry.Key().Value(); + std::string key(key_bytes.begin(), key_bytes.end()); + explicit_options.emplace(std::move(key), header_entry.Value()); + } + std::map derived_options; + for (auto &entry : derived_entries) { + HeaderEntry header_entry = HeaderEntry::FromFfi(std::move(entry)); + const auto &key_bytes = header_entry.Key().Value(); + std::string key(key_bytes.begin(), key_bytes.end()); + derived_options.emplace(std::move(key), header_entry.Value()); + } + return ResourceOptions(std::move(explicit_options), std::move(derived_options)); +} + +rust::Vec ResourceOptions::ToFfi(ResourceOptions options) { + rust::Vec headers{}; + headers.reserve(options.explicit_.size()); + for (auto &entry : options.explicit_) { + const std::string &key = entry.first; + const HeaderField &value = entry.second; + std::vector key_bytes(key.begin(), key.end()); + headers.push_back(HeaderEntry::ToFfi( + HeaderEntry::Create(HeaderField::Create(HeaderKind::String, std::move(key_bytes)), value))); + } + return headers; +} + +Topic Topic::FromFfi(ffi::Topic topic) { + return Topic(topic.id, topic.created_at, std::string(topic.name.c_str(), topic.name.size()), topic.size_bytes, + topic.message_expiry, + std::string(topic.compression_algorithm.c_str(), topic.compression_algorithm.size()), + topic.max_topic_size, topic.messages_count, topic.partitions_count, + ResourceOptions::FromFfi(std::move(topic.options), std::move(topic.derived_options))); +} + +Partition Partition::FromFfi(ffi::Partition partition) { + return Partition(partition.id, partition.created_at, partition.segments_count, partition.current_offset, + partition.size_bytes, partition.messages_count); +} + +TopicDetails TopicDetails::FromFfi(ffi::TopicDetails topic) { + std::vector partitions; + partitions.reserve(topic.partitions.size()); + for (auto &partition : topic.partitions) { + partitions.push_back(Partition::FromFfi(std::move(partition))); + } + + return TopicDetails(topic.id, topic.created_at, std::string(topic.name.c_str(), topic.name.size()), + topic.size_bytes, topic.message_expiry, + std::string(topic.compression_algorithm.c_str(), topic.compression_algorithm.size()), + topic.max_topic_size, topic.messages_count, topic.partitions_count, std::move(partitions), + ResourceOptions::FromFfi(std::move(topic.options), std::move(topic.derived_options))); +} + +StreamDetails StreamDetails::FromFfi(ffi::StreamDetails stream) { + std::vector topics; + topics.reserve(stream.topics.size()); + for (auto &topic : stream.topics) { + topics.push_back(Topic::FromFfi(std::move(topic))); + } + + return StreamDetails(stream.id, stream.created_at, std::string(stream.name.c_str(), stream.name.size()), + stream.size_bytes, stream.messages_count, stream.topics_count, std::move(topics), + ResourceOptions::FromFfi(std::move(stream.options), rust::Vec{})); +} + +Stream Stream::FromFfi(ffi::Stream stream) { + return Stream(stream.id, stream.created_at, std::string(stream.name.c_str(), stream.name.size()), stream.size_bytes, + stream.messages_count, stream.topics_count, + ResourceOptions::FromFfi(std::move(stream.options), rust::Vec{})); +} + +} // namespace iggy diff --git a/foreign/cpp/tests/e2e/client.cpp b/foreign/cpp/tests/e2e/client.cpp index 5fde598711..a88def7225 100644 --- a/foreign/cpp/tests/e2e/client.cpp +++ b/foreign/cpp/tests/e2e/client.cpp @@ -512,10 +512,12 @@ TEST_F(LowLevelE2E_Client, CreatedUserCanReadOnlyTopicGrantedByPermissions) { iggy::ffi::TopicDetails allowed_topic{}; iggy::ffi::TopicDetails denied_topic{}; ASSERT_NO_THROW({ - allowed_topic = root_client->create_topic(make_numeric_identifier(allowed_stream.id), allowed_topic_name, 1, - "none", "server_default", 0, "server_default", {}); - denied_topic = root_client->create_topic(make_numeric_identifier(denied_stream.id), denied_topic_name, 1, - "none", "server_default", 0, "server_default", {}); + allowed_topic = + root_client->create_topic(make_numeric_identifier(allowed_stream.id), allowed_topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default")); + denied_topic = + root_client->create_topic(make_numeric_identifier(denied_stream.id), denied_topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default")); }); iggy::ffi::Permissions permissions{}; @@ -2101,8 +2103,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferThrowsForExistingPartition) { ASSERT_NO_THROW(client->create_stream(stream_name)); auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); - ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); rust::Vec messages; messages.push_back(iggy::ffi::make_message(to_payload("flush-me"), rust::Vec())); @@ -2124,8 +2126,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferThrowsForExistingEmptyPartition) { ASSERT_NO_THROW(client->create_stream(stream_name)); auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); - ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); ASSERT_THROW(client->flush_unsaved_buffer(make_numeric_identifier(stream.id), make_numeric_identifier(0), 0, true), std::exception); @@ -2156,8 +2158,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferOnNonExistentStreamThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); ASSERT_THROW( client->flush_unsaved_buffer(make_string_identifier(GetRandomName()), make_numeric_identifier(0), 0, true), @@ -2172,8 +2174,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferOnNonExistentTopicThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); ASSERT_THROW(client->flush_unsaved_buffer(make_string_identifier(stream_name), make_string_identifier(GetRandomName()), 0, true), @@ -2189,8 +2191,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferAfterStreamDeletedThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); - ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); const std::uint32_t saved_stream_id = stream.id; ASSERT_NO_THROW(client->delete_stream(make_numeric_identifier(saved_stream_id))); @@ -2210,8 +2212,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferAfterTopicDeletedThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); - ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); ASSERT_NO_THROW(client->delete_topic(make_numeric_identifier(stream.id), make_string_identifier(topic_name))); ASSERT_THROW( @@ -2229,8 +2231,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferTwiceThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); - ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); rust::Vec messages; messages.push_back(iggy::ffi::make_message(to_payload("flush-twice"), rust::Vec())); @@ -2252,8 +2254,8 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferWithInvalidPartitionIdsThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); - ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); const std::uint32_t invalid_partition_ids[] = {1u, 9999u, static_cast(-1)}; for (const std::uint32_t invalid_partition_id : invalid_partition_ids) { @@ -2273,8 +2275,9 @@ TEST_F(LowLevelE2E_Client, DeleteSegmentsBeforeLoginThrows) { ASSERT_NO_THROW(setup_client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "never_expire", 0, "server_default", {})); + ASSERT_NO_THROW( + setup_client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); ASSERT_THROW(unauthenticated_client->delete_segments(make_string_identifier(stream_name), @@ -2300,8 +2303,8 @@ TEST_F(LowLevelE2E_Client, DeleteSegmentsOnNonExistentStreamThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); ASSERT_THROW( client->delete_segments(make_string_identifier(missing_stream_name), make_string_identifier(topic_name), 0, 1), @@ -2317,8 +2320,8 @@ TEST_F(LowLevelE2E_Client, DeleteSegmentsOnNonExistentTopicThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); ASSERT_THROW( client->delete_segments(make_string_identifier(stream_name), make_string_identifier(missing_topic_name), 0, 1), @@ -2333,8 +2336,8 @@ TEST_F(LowLevelE2E_Client, DeleteSegmentsOnNonExistentPartitionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); ASSERT_THROW( client->delete_segments(make_string_identifier(stream_name), make_string_identifier(topic_name), 999, 1), @@ -2349,8 +2352,8 @@ TEST_F(LowLevelE2E_Client, DeleteSegmentsWithZeroCountIsNoOp) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); std::uint32_t stream_id = 0; std::uint32_t topic_id = 0; @@ -2430,8 +2433,8 @@ TEST_F(LowLevelE2E_Client, DeleteSegmentsWhenOnlyActiveSegmentRemainsIsNoOp) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default"))); std::uint32_t stream_id = 0; std::uint32_t topic_id = 0; @@ -2543,12 +2546,12 @@ TEST_F(LowLevelE2E_Client, GetStatsReturnsServerStats) { TrackStream(first_stream_name); ASSERT_NO_THROW(client->create_stream(second_stream_name)); TrackStream(second_stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, 2, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, 3, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), first_topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), second_topic_name, + make_topic_create_options(2, "none", "server_default", 0, "server_default"))); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), third_topic_name, + make_topic_create_options(3, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_partitions(make_string_identifier(first_stream_name), make_string_identifier(first_topic_name), additional_partitions_count)); const auto first_group = client->create_consumer_group(make_string_identifier(first_stream_name), @@ -2696,8 +2699,8 @@ TEST_F(LowLevelE2E_Client, GetMeReflectsConsumerGroupMembershipChanges) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); const auto stream_details = client->get_stream(make_string_identifier(stream_name)); ASSERT_EQ(stream_details.topics.size(), 1u); diff --git a/foreign/cpp/tests/e2e/consumer_group.cpp b/foreign/cpp/tests/e2e/consumer_group.cpp index 5a32d62036..7099e5f226 100644 --- a/foreign/cpp/tests/e2e/consumer_group.cpp +++ b/foreign/cpp/tests/e2e/consumer_group.cpp @@ -36,8 +36,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, CreateConsumerGroupSucceeds) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW({ const auto group = client->create_consumer_group(make_string_identifier(stream_name), @@ -59,8 +59,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, CreateConsumerGroupOnNonExistentResourcesThrow ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_THROW(client->create_consumer_group(make_string_identifier(missing_stream_name), make_string_identifier(topic_name), GetRandomName()), @@ -79,8 +79,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, CreateConsumerGroupTwiceOnSameInputThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -97,8 +97,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, CreateConsumerGroupWithInvalidNamesThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); const std::string invalid_names[] = {"", std::string(256, 'a')}; for (const std::string &invalid_name : invalid_names) { @@ -118,8 +118,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, CreateConsumerGroupAfterStreamDeletionThrows) ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); ForgetTrackedStream(stream_name); @@ -138,8 +138,9 @@ TEST_F(LowLevelE2E_ConsumerGroup, CreateConsumerGroupBeforeLoginThrows) { iggy::ffi::Client *setup_client = GetLoggedInClient(); ASSERT_NO_THROW(setup_client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW( + setup_client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); @@ -167,8 +168,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupReturnsSameInfoAsCreateConsume ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); const auto created_group = client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name); @@ -194,8 +195,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupsReturnsCreatedGroups) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), first_group_name)); @@ -220,8 +221,9 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupsBeforeLoginThrows) { iggy::ffi::Client *setup_client = GetLoggedInClient(); ASSERT_NO_THROW(setup_client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW( + setup_client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); @@ -248,8 +250,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupSucceeds) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -267,8 +269,9 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupBeforeLoginThrows) { iggy::ffi::Client *setup_client = GetLoggedInClient(); ASSERT_NO_THROW(setup_client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW( + setup_client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(setup_client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -304,8 +307,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupOnNonExistentResourcesThrows) ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), created_group_name)); TrackConsumerGroup(stream_name, topic_name, created_group_name); @@ -332,8 +335,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupAfterStreamDeletionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -354,8 +357,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupAfterTopicDeletionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -375,8 +378,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupReflectsInGetConsumerGroup) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); iggy::ffi::ConsumerGroupDetails created_group; ASSERT_NO_THROW({ @@ -407,8 +410,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupTwiceKeepsSingleMember) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -434,8 +437,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupFromTwoClientsIncreasesMember ASSERT_NO_THROW(first->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(first->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", 0, - "server_default", {})); + ASSERT_NO_THROW(first->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(first->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -460,8 +463,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupThenLeaveRestoresMembersCount ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -492,8 +495,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, LeaveConsumerGroupReducesMembersCount) { ASSERT_NO_THROW(first->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(first->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", 0, - "server_default", {})); + ASSERT_NO_THROW(first->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(first->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -526,8 +529,9 @@ TEST_F(LowLevelE2E_ConsumerGroup, LeaveConsumerGroupBeforeLoginThrows) { iggy::ffi::Client *setup_client = GetLoggedInClient(); ASSERT_NO_THROW(setup_client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW( + setup_client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(setup_client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -565,8 +569,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, LeaveConsumerGroupOnNonExistentResourcesThrows ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), created_group_name)); TrackConsumerGroup(stream_name, topic_name, created_group_name); @@ -595,8 +599,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, LeaveConsumerGroupAfterStreamDeletionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -619,8 +623,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, LeaveConsumerGroupAfterTopicDeletionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -643,8 +647,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, LeaveConsumerGroupTwiceThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -667,8 +671,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, LeaveConsumerGroupWithoutJoiningThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -688,8 +692,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupsReflectsJoinedGroupMembersCou ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); iggy::ffi::ConsumerGroupDetails joined_group; iggy::ffi::ConsumerGroupDetails other_group; @@ -758,8 +762,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupsIsStableAcrossBackToBackCalls ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), first_group_name)); @@ -795,8 +799,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupsReturnsCorrectNumberOfGroups) ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), deleted_group_name)); @@ -847,8 +851,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupsAfterStreamDeletionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), first_group_name)); TrackConsumerGroup(stream_name, topic_name, first_group_name); @@ -874,8 +878,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupsAfterTopicDeletionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), first_group_name)); TrackConsumerGroup(stream_name, topic_name, first_group_name); @@ -900,8 +904,9 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupBeforeLoginThrows) { iggy::ffi::Client *setup_client = GetLoggedInClient(); ASSERT_NO_THROW(setup_client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW( + setup_client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(setup_client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -937,8 +942,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupOnNonExistentResourcesThrows) ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), created_group_name)); TrackConsumerGroup(stream_name, topic_name, created_group_name); @@ -965,8 +970,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, GetConsumerGroupAfterStreamDeletionThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); @@ -986,8 +991,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, DeleteConsumerGroupSucceeds) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -1010,8 +1015,9 @@ TEST_F(LowLevelE2E_ConsumerGroup, DeleteConsumerGroupBeforeLoginThrows) { iggy::ffi::Client *setup_client = GetLoggedInClient(); ASSERT_NO_THROW(setup_client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW( + setup_client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(setup_client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -1047,8 +1053,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, DeleteConsumerGroupOnNonExistentResourcesThrow ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), created_group_name)); TrackConsumerGroup(stream_name, topic_name, created_group_name); @@ -1074,8 +1080,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, DeleteConsumerGroupTwiceThrows) { ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -1097,8 +1103,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, DeleteConsumerGroupAfterStreamDeletionThrows) ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); TrackConsumerGroup(stream_name, topic_name, group_name); @@ -1121,8 +1127,8 @@ TEST_F(LowLevelE2E_ConsumerGroup, DeleteConsumerGroupAndRecreateWithSameNameSucc ASSERT_NO_THROW(client->create_stream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, + make_topic_create_options(1, "none", "server_default", 0, "server_default"))); ASSERT_NO_THROW(client->create_consumer_group(make_string_identifier(stream_name), make_string_identifier(topic_name), group_name)); diff --git a/foreign/cpp/tests/e2e/message.cpp b/foreign/cpp/tests/e2e/message.cpp index 09b1e2ae1f..70ce7f0fcf 100644 --- a/foreign/cpp/tests/e2e/message.cpp +++ b/foreign/cpp/tests/e2e/message.cpp @@ -37,8 +37,8 @@ TEST_F(LowLevelE2E_Message, SendAndPollMessagesRoundTrip) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 10; i++) { @@ -79,8 +79,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesVerifyMessageIds) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; auto msg = iggy::ffi::make_message(to_payload("id-test-message"), rust::Vec()); @@ -108,8 +108,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesFromEmptyPartition) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); auto polled = client->poll_messages(make_numeric_identifier(stream.id), make_numeric_identifier(0), 0, "consumer", make_numeric_identifier(1), "offset", 0, 100, false); @@ -181,8 +181,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesWithInvalidPartitioningKind) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; auto msg = iggy::ffi::make_message(to_payload("test"), rust::Vec()); @@ -202,8 +202,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesWithInvalidPartitioningValue) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; auto msg = iggy::ffi::make_message(to_payload("test"), rust::Vec()); @@ -228,8 +228,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesToSpecificPartitionVerified) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 3, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(3, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 5; i++) { @@ -261,8 +261,8 @@ TEST_F(LowLevelE2E_Message, SendEmptyMessageVectorThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec empty_messages; @@ -280,8 +280,8 @@ TEST_F(LowLevelE2E_Message, SendMessageWithEmptyPayloadThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; rust::Vec empty_payload; @@ -302,8 +302,8 @@ TEST_F(LowLevelE2E_Message, SendMessageWithOversizedPayloadThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); // Build a payload one byte over the SDK's max payload size (64 MB). constexpr std::uint32_t kOversizedPayloadBytes = 64'000'001u; @@ -331,8 +331,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesPreservesOrder) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 50; i++) { @@ -365,8 +365,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesWithDuplicateIds) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 3; i++) { @@ -399,8 +399,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesWithVariousPayloads) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec payload_null; payload_null.push_back(0x00); @@ -466,8 +466,8 @@ TEST_F(LowLevelE2E_Message, SendAndPollMessageWithTypedHeadersRoundTrip) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); struct ExpectedHeaderMessage { const char *key; @@ -555,8 +555,8 @@ TEST_F(LowLevelE2E_Message, SendMessageWithDuplicateTypedHeaderKeysThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec headers; headers.push_back(make_header_entry(make_header_field(iggy::ffi::HeaderKind::String, to_payload("dup-key")), @@ -581,8 +581,8 @@ TEST_F(LowLevelE2E_Message, SendMessageWithWrongFixedWidthHeaderBytesThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); struct FixedWidthHeaderCase { const char *key; @@ -627,8 +627,8 @@ TEST_F(LowLevelE2E_Message, SendMessageWithInvalidTypedHeaderKindThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); iggy::ffi::HeaderField invalid_key; invalid_key.kind = 255; @@ -655,8 +655,8 @@ TEST_F(LowLevelE2E_Message, SendMessageWithInvalidTypedHeaderSizesThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec empty_key_headers; empty_key_headers.push_back( @@ -720,8 +720,8 @@ TEST_F(LowLevelE2E_Message, SendMessageAtUserHeadersSizeBoundary) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); constexpr std::uint32_t kMaxUserHeadersBytes = 100'000u; constexpr std::uint32_t kFullHeaderEncodedBytes = 267u; // 10 bytes framing + 2-byte key + 255-byte value @@ -850,8 +850,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithInvalidConsumerKindThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); ASSERT_THROW(client->poll_messages(make_numeric_identifier(stream.id), make_numeric_identifier(0), 0, "invalid", make_numeric_identifier(1), "offset", 0, 10, false), @@ -867,8 +867,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithInvalidStrategyKindThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); ASSERT_THROW(client->poll_messages(make_numeric_identifier(stream.id), make_numeric_identifier(0), 0, "consumer", make_numeric_identifier(1), "invalid", 0, 10, false), @@ -884,8 +884,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesCountLessThanAvailable) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 10; i++) { @@ -912,8 +912,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithLargeOffset) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 5; i++) { @@ -940,8 +940,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesFirstStrategy) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 10; i++) { @@ -975,8 +975,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesLastStrategy) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 10; i++) { @@ -1011,8 +1011,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesNextStrategyNoAutoCommit) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 5; i++) { @@ -1053,8 +1053,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesNextStrategyAutoCommit) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 10; i++) { @@ -1101,8 +1101,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesConsumerIdIndependence) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 5; i++) { @@ -1135,8 +1135,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesMultipleSendsThenPollOrder) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec batch1; for (std::uint32_t i = 0; i < 5; i++) { @@ -1184,8 +1184,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesMultipleCustomIds) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); const std::uint64_t id_values[] = {100, 200, 300, 400, 500}; rust::Vec messages; @@ -1218,8 +1218,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesAfterStreamDeletedThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; auto msg = iggy::ffi::make_message(to_payload("test"), rust::Vec()); @@ -1246,8 +1246,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithInvalidPartitionIdThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); ASSERT_THROW(client->poll_messages(make_numeric_identifier(stream.id), make_numeric_identifier(0), 9999, "consumer", make_numeric_identifier(1), "offset", 0, 10, false), @@ -1263,8 +1263,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithCountZeroThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); ASSERT_THROW(client->poll_messages(make_numeric_identifier(stream.id), make_numeric_identifier(0), 0, "consumer", make_numeric_identifier(1), "offset", 0, 0, false), @@ -1281,8 +1281,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithoutSpecifyingPartition) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 5; i++) { @@ -1317,8 +1317,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesTimestampStrategy) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec batch1; for (std::uint32_t i = 0; i < 5; i++) { @@ -1380,8 +1380,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesMonotonicOffsets) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 20; i++) { @@ -1419,8 +1419,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesLargeBatch) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); rust::Vec messages; for (std::uint32_t i = 0; i < 1000; i++) { @@ -1480,8 +1480,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithInvalidConsumerIdThrows) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); iggy::ffi::Identifier invalid_id; invalid_id.kind = "invalid"; @@ -1502,8 +1502,8 @@ TEST_F(LowLevelE2E_Message, ConsumerGroupCreateJoinAndPollMessages) { auto stream = client->get_stream(make_string_identifier(stream_name)); TrackStream(stream.id); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client->create_topic(make_numeric_identifier(stream.id), topic_name, + make_topic_create_options(1, "none", "never_expire", 0, "server_default")); const std::string group_name = GetRandomName(); auto group = diff --git a/foreign/cpp/tests/e2e/partition.cpp b/foreign/cpp/tests/e2e/partition.cpp index 8f305db971..74efc5ce9d 100644 --- a/foreign/cpp/tests/e2e/partition.cpp +++ b/foreign/cpp/tests/e2e/partition.cpp @@ -19,126 +19,113 @@ #include #include +#include #include -#include "lib.rs.h" +#include "iggy.hpp" #include "tests/e2e/test_helpers.hpp" -class LowLevelE2E_Partition : public E2ETestFixture {}; +class E2E_Partition : public E2ETestFixture {}; -TEST_F(LowLevelE2E_Partition, CreatePartitionsSucceeds) { +TEST_F(E2E_Partition, CreatePartitionsSucceeds) { RecordProperty("description", "Creates partitions for an existing topic and verifies the resulting count."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); ASSERT_NO_THROW( - client->create_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 43)); + client.CreatePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 43)); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); - EXPECT_EQ(stream_details.topics[0].name, topic_name); - EXPECT_EQ(stream_details.topics[0].partitions_count, 44u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); + EXPECT_EQ(stream_details.Topics()[0].Name(), topic_name); + EXPECT_EQ(stream_details.Topics()[0].PartitionsCount(), 44u); }); } -TEST_F(LowLevelE2E_Partition, CreatePartitionsBeforeLoginThrows) { +TEST_F(E2E_Partition, CreatePartitionsBeforeLoginThrows) { RecordProperty("description", "Throws when create_partitions is called before connect, and after connect but before login."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedOutClient(); + auto client = GetLoggedOutHighLevelClient(); - ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1), - std::exception); - ASSERT_NO_THROW(client->connect()); - ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1), - std::exception); - ASSERT_NO_THROW(client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(client->disconnect()); - ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1), - std::exception); + ASSERT_THROW( + client.CreatePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); + ASSERT_NO_THROW(client.Connect()); + ASSERT_THROW( + client.CreatePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); + ASSERT_NO_THROW(client.Login("iggy", "iggy")); + ASSERT_NO_THROW(client.Disconnect()); + ASSERT_THROW( + client.CreatePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); } -TEST_F(LowLevelE2E_Partition, CreatePartitionsOnNonExistentResourcesThrows) { +TEST_F(E2E_Partition, CreatePartitionsOnNonExistentResourcesThrows) { RecordProperty("description", "Throws when create_partitions is called for a stream or topic that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); const std::string missing_stream_name = GetRandomName(); const std::string missing_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); ASSERT_THROW( - client->create_partitions(make_string_identifier(missing_stream_name), make_string_identifier(topic_name), 1), + client.CreatePartitions(iggy::Identifier::String(missing_stream_name), iggy::Identifier::String(topic_name), 1), std::exception); ASSERT_THROW( - client->create_partitions(make_string_identifier(stream_name), make_string_identifier(missing_topic_name), 1), + client.CreatePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(missing_topic_name), 1), std::exception); } -TEST_F(LowLevelE2E_Partition, CreatePartitionsWithInvalidIdentifiersThrows) { - RecordProperty("description", "Rejects create_partitions requests that use invalid stream or topic identifiers."); +TEST_F(E2E_Partition, CreatePartitionsWithInvalidIdentifiersThrows) { + RecordProperty("description", "Rejects invalid stream or topic identifiers before creating partitions."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - - iggy::ffi::Identifier invalid_stream_kind_id; - invalid_stream_kind_id.kind = "invalid"; - invalid_stream_kind_id.length = 4; - invalid_stream_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->create_partitions(std::move(invalid_stream_kind_id), make_string_identifier(topic_name), 1), - std::exception); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - iggy::ffi::Identifier invalid_stream_numeric_id; - invalid_stream_numeric_id.kind = "numeric"; - invalid_stream_numeric_id.length = 1; - invalid_stream_numeric_id.value.push_back(1); - ASSERT_THROW(client->create_partitions(std::move(invalid_stream_numeric_id), make_string_identifier(topic_name), 1), + ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(""), iggy::Identifier::String(topic_name), 1), std::exception); - - iggy::ffi::Identifier invalid_topic_kind_id; - invalid_topic_kind_id.kind = "invalid"; - invalid_topic_kind_id.length = 4; - invalid_topic_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), std::move(invalid_topic_kind_id), 1), + ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(std::string(256, 'a')), + iggy::Identifier::String(topic_name), 1), std::exception); - - iggy::ffi::Identifier invalid_topic_numeric_id; - invalid_topic_numeric_id.kind = "numeric"; - invalid_topic_numeric_id.length = 1; - invalid_topic_numeric_id.value.push_back(1); - ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), std::move(invalid_topic_numeric_id), 1), + ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(""), 1), + std::exception); + ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(stream_name), + iggy::Identifier::String(std::string(256, 'a')), 1), std::exception); } -TEST_F(LowLevelE2E_Partition, CreatePartitionsWithBoundaryPartitionsCountValues) { +TEST_F(E2E_Partition, CreatePartitionsWithBoundaryPartitionsCountValues) { RecordProperty("description", "Accepts supported create_partitions counts and rejects values outside the allowed range."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); struct TestCase { @@ -159,29 +146,29 @@ TEST_F(LowLevelE2E_Partition, CreatePartitionsWithBoundaryPartitionsCountValues) for (const auto &test_case : test_cases) { SCOPED_TRACE(test_case.topic_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), test_case.topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), test_case.topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); if (test_case.should_succeed) { - ASSERT_NO_THROW(client->create_partitions(make_string_identifier(stream_name), - make_string_identifier(test_case.topic_name), - test_case.partitions_count)); + ASSERT_NO_THROW(client.CreatePartitions(iggy::Identifier::String(stream_name), + iggy::Identifier::String(test_case.topic_name), + test_case.partitions_count)); } else { ASSERT_THROW( - client->create_partitions(make_string_identifier(stream_name), - make_string_identifier(test_case.topic_name), test_case.partitions_count), + client.CreatePartitions(iggy::Identifier::String(stream_name), + iggy::Identifier::String(test_case.topic_name), test_case.partitions_count), std::exception); } } ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), test_cases.size()); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), test_cases.size()); for (const auto &test_case : test_cases) { bool found = false; - for (const auto &topic : stream_details.topics) { - if (topic.name == test_case.topic_name) { - EXPECT_EQ(topic.partitions_count, test_case.expected_total_partitions); + for (const auto &topic : stream_details.Topics()) { + if (topic.Name() == test_case.topic_name) { + EXPECT_EQ(topic.PartitionsCount(), test_case.expected_total_partitions); found = true; break; } @@ -191,64 +178,64 @@ TEST_F(LowLevelE2E_Partition, CreatePartitionsWithBoundaryPartitionsCountValues) }); } -TEST_F(LowLevelE2E_Partition, CreatePartitionsWithNumericIdentifiersSucceeds) { +TEST_F(E2E_Partition, CreatePartitionsWithNumericIdentifiersSucceeds) { RecordProperty("description", "Creates partitions successfully when valid numeric stream and topic identifiers are used."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); - ASSERT_NO_THROW(client->create_partitions(make_numeric_identifier(stream_details.id), - make_numeric_identifier(stream_details.topics[0].id), 43)); + ASSERT_NO_THROW(client.CreatePartitions(iggy::Identifier::Numeric(stream_details.Id()), + iggy::Identifier::Numeric(stream_details.Topics()[0].Id()), 43)); ASSERT_NO_THROW({ - const auto updated_stream_details = client->get_stream(make_numeric_identifier(stream_details.id)); - ASSERT_EQ(updated_stream_details.topics.size(), 1u); - EXPECT_EQ(updated_stream_details.topics[0].id, stream_details.topics[0].id); - EXPECT_EQ(updated_stream_details.topics[0].name, topic_name); - EXPECT_EQ(updated_stream_details.topics[0].partitions_count, 44u); + const auto updated_stream_details = client.GetStream(iggy::Identifier::Numeric(stream_details.Id())); + ASSERT_EQ(updated_stream_details.Topics().size(), 1u); + EXPECT_EQ(updated_stream_details.Topics()[0].Id(), stream_details.Topics()[0].Id()); + EXPECT_EQ(updated_stream_details.Topics()[0].Name(), topic_name); + EXPECT_EQ(updated_stream_details.Topics()[0].PartitionsCount(), 44u); }); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsSucceeds) { +TEST_F(E2E_Partition, DeletePartitionsSucceeds) { RecordProperty("description", "Deletes partitions from an existing topic and verifies the resulting count."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 44, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(44))); ASSERT_NO_THROW( - client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 43)); + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 43)); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); - EXPECT_EQ(stream_details.topics[0].name, topic_name); - EXPECT_EQ(stream_details.topics[0].partitions_count, 1u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); + EXPECT_EQ(stream_details.Topics()[0].Name(), topic_name); + EXPECT_EQ(stream_details.Topics()[0].PartitionsCount(), 1u); }); } -TEST_F(LowLevelE2E_Partition, DeleteMorePartitionsThanExistingThrows) { +TEST_F(E2E_Partition, DeleteMorePartitionsThanExistingThrows) { RecordProperty("description", "Rejects delete_partitions counts outside the allowed range and counts greater than existing."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); struct TestCase { @@ -268,30 +255,30 @@ TEST_F(LowLevelE2E_Partition, DeleteMorePartitionsThanExistingThrows) { for (const auto &test_case : test_cases) { SCOPED_TRACE(test_case.topic_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), test_case.topic_name, - test_case.initial_partitions, "none", "server_default", 0, - "server_default", {})); + ASSERT_NO_THROW( + client.CreateTopic(iggy::Identifier::String(stream_name), test_case.topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(test_case.initial_partitions))); if (test_case.should_succeed) { - ASSERT_NO_THROW(client->delete_partitions(make_string_identifier(stream_name), - make_string_identifier(test_case.topic_name), - test_case.partitions_count)); + ASSERT_NO_THROW(client.DeletePartitions(iggy::Identifier::String(stream_name), + iggy::Identifier::String(test_case.topic_name), + test_case.partitions_count)); } else { ASSERT_THROW( - client->delete_partitions(make_string_identifier(stream_name), - make_string_identifier(test_case.topic_name), test_case.partitions_count), + client.DeletePartitions(iggy::Identifier::String(stream_name), + iggy::Identifier::String(test_case.topic_name), test_case.partitions_count), std::exception); } } ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), test_cases.size()); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), test_cases.size()); for (const auto &test_case : test_cases) { bool found = false; - for (const auto &topic : stream_details.topics) { - if (topic.name == test_case.topic_name) { - EXPECT_EQ(topic.partitions_count, test_case.expected_total_partitions); + for (const auto &topic : stream_details.Topics()) { + if (topic.Name() == test_case.topic_name) { + EXPECT_EQ(topic.PartitionsCount(), test_case.expected_total_partitions); found = true; break; } @@ -301,179 +288,184 @@ TEST_F(LowLevelE2E_Partition, DeleteMorePartitionsThanExistingThrows) { }); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsBeforeCreatingAdditionalPartitionsSucceeds) { +TEST_F(E2E_Partition, DeletePartitionsBeforeCreatingAdditionalPartitionsSucceeds) { RecordProperty("description", "Deletes partitions from the initial topic allocation without calling create_partitions first."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 3, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(3))); ASSERT_NO_THROW( - client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1)); + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1)); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); - EXPECT_EQ(stream_details.topics[0].partitions_count, 2u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); + EXPECT_EQ(stream_details.Topics()[0].PartitionsCount(), 2u); }); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsFromTopicWithZeroPartitionsThrows) { +TEST_F(E2E_Partition, DeletePartitionsFromTopicWithZeroPartitionsThrows) { RecordProperty("description", "Throws when delete_partitions is called with count 1 for a topic that currently has 0 partitions."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 0, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW( + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1)); - ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1), - std::exception); + ASSERT_THROW( + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); - EXPECT_EQ(stream_details.topics[0].name, topic_name); - EXPECT_EQ(stream_details.topics[0].partitions_count, 0u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); + EXPECT_EQ(stream_details.Topics()[0].Name(), topic_name); + EXPECT_EQ(stream_details.Topics()[0].PartitionsCount(), 0u); }); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsBeforeLoginThrows) { +TEST_F(E2E_Partition, DeletePartitionsBeforeLoginThrows) { RecordProperty("description", "Throws when delete_partitions is called before connect, and after connect but before login."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedOutClient(); + auto client = GetLoggedOutHighLevelClient(); - ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1), - std::exception); - ASSERT_NO_THROW(client->connect()); - ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1), - std::exception); - ASSERT_NO_THROW(client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(client->disconnect()); - ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 1), - std::exception); + ASSERT_THROW( + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); + ASSERT_NO_THROW(client.Connect()); + ASSERT_THROW( + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); + ASSERT_NO_THROW(client.Login("iggy", "iggy")); + ASSERT_NO_THROW(client.Disconnect()); + ASSERT_THROW( + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsOnNonExistentResourcesThrows) { +TEST_F(E2E_Partition, DeletePartitionsOnNonExistentResourcesThrows) { RecordProperty("description", "Throws when delete_partitions is called for a stream or topic that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); const std::string missing_stream_name = GetRandomName(); const std::string missing_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 3, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(3))); ASSERT_THROW( - client->delete_partitions(make_string_identifier(missing_stream_name), make_string_identifier(topic_name), 1), + client.DeletePartitions(iggy::Identifier::String(missing_stream_name), iggy::Identifier::String(topic_name), 1), std::exception); ASSERT_THROW( - client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(missing_topic_name), 1), + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(missing_topic_name), 1), std::exception); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsWithInvalidIdentifiersThrows) { - RecordProperty("description", "Rejects delete_partitions requests that use invalid stream or topic identifiers."); +TEST_F(E2E_Partition, DeletePartitionsWithInvalidIdentifiersThrows) { + RecordProperty("description", "Rejects invalid stream or topic identifiers before deleting partitions."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 3, "none", "server_default", - 0, "server_default", {})); - - iggy::ffi::Identifier invalid_stream_kind_id; - invalid_stream_kind_id.kind = "invalid"; - invalid_stream_kind_id.length = 4; - invalid_stream_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->delete_partitions(std::move(invalid_stream_kind_id), make_string_identifier(topic_name), 1), - std::exception); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(3))); - iggy::ffi::Identifier invalid_stream_numeric_id; - invalid_stream_numeric_id.kind = "numeric"; - invalid_stream_numeric_id.length = 1; - invalid_stream_numeric_id.value.push_back(1); - ASSERT_THROW(client->delete_partitions(std::move(invalid_stream_numeric_id), make_string_identifier(topic_name), 1), + ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(""), iggy::Identifier::String(topic_name), 1), std::exception); - - iggy::ffi::Identifier invalid_topic_kind_id; - invalid_topic_kind_id.kind = "invalid"; - invalid_topic_kind_id.length = 4; - invalid_topic_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), std::move(invalid_topic_kind_id), 1), + ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(std::string(256, 'a')), + iggy::Identifier::String(topic_name), 1), std::exception); - - iggy::ffi::Identifier invalid_topic_numeric_id; - invalid_topic_numeric_id.kind = "numeric"; - invalid_topic_numeric_id.length = 1; - invalid_topic_numeric_id.value.push_back(1); - ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), std::move(invalid_topic_numeric_id), 1), + ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(""), 1), + std::exception); + ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(stream_name), + iggy::Identifier::String(std::string(256, 'a')), 1), std::exception); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsTwiceForSameTopicSucceeds) { +TEST_F(E2E_Partition, DeletePartitionsTwiceForSameTopicSucceeds) { RecordProperty("description", "Allows delete_partitions to be called twice for the same stream and topic."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 45, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(45))); ASSERT_NO_THROW( - client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 20)); + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 20)); ASSERT_NO_THROW( - client->delete_partitions(make_string_identifier(stream_name), make_string_identifier(topic_name), 20)); + client.DeletePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 20)); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); - EXPECT_EQ(stream_details.topics[0].name, topic_name); - EXPECT_EQ(stream_details.topics[0].partitions_count, 5u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); + EXPECT_EQ(stream_details.Topics()[0].Name(), topic_name); + EXPECT_EQ(stream_details.Topics()[0].PartitionsCount(), 5u); }); } -TEST_F(LowLevelE2E_Partition, DeletePartitionsAfterStreamDeletionThrows) { +TEST_F(E2E_Partition, DeletePartitionsAfterStreamDeletionThrows) { RecordProperty("description", "Throws when delete_partitions is called after the stream has been deleted."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 3, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(3))); - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); - ASSERT_NO_THROW(client->delete_stream(make_numeric_identifier(stream_details.id))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::Numeric(stream_details.Id()))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->delete_partitions(make_numeric_identifier(stream_details.id), - make_numeric_identifier(stream_details.topics[0].id), 1), + ASSERT_THROW(client.DeletePartitions(iggy::Identifier::Numeric(stream_details.Id()), + iggy::Identifier::Numeric(stream_details.Topics()[0].Id()), 1), std::exception); } -// TODO(slbotbm): Add CreatePartitionsAfterTopicDeletionThrows test case after delete_topic function is added. +TEST_F(E2E_Partition, CreatePartitionsAfterTopicDeletionThrows) { + RecordProperty("description", "Throws when creating partitions after the topic has been deleted."); + const std::string stream_name = GetRandomName(); + const std::string topic_name = GetRandomName(); + + auto client = GetLoggedInHighLevelClient(); + + ASSERT_NO_THROW(client.CreateStream(stream_name)); + TrackStream(stream_name); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name))); + + ASSERT_THROW( + client.CreatePartitions(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), 1), + std::exception); +} diff --git a/foreign/cpp/tests/e2e/stream.cpp b/foreign/cpp/tests/e2e/stream.cpp index c4f33e8731..f8efeac3cf 100644 --- a/foreign/cpp/tests/e2e/stream.cpp +++ b/foreign/cpp/tests/e2e/stream.cpp @@ -25,162 +25,151 @@ #include "lib.rs.h" #include "tests/e2e/test_helpers.hpp" -class LowLevelE2E_Stream : public E2ETestFixture {}; +class E2E_Stream : public E2ETestFixture {}; -TEST_F(LowLevelE2E_Stream, CreateStreamAfterLogin) { +TEST_F(E2E_Stream, CreateStreamAfterLogin) { RecordProperty("description", "Creates a stream successfully after authenticating."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); } -TEST_F(LowLevelE2E_Stream, CreateDuplicateStreamThrows) { +TEST_F(E2E_Stream, CreateDuplicateStreamThrows) { RecordProperty("description", "Rejects creating the same stream twice."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_THROW(client->create_stream(stream_name), std::exception); + ASSERT_THROW(client.CreateStream(stream_name), std::exception); } -TEST_F(LowLevelE2E_Stream, CreateStreamBeforeLoginThrows) { +TEST_F(E2E_Stream, CreateStreamBeforeLoginThrows) { RecordProperty("description", "Throws when stream creation is attempted before authentication."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedOutClient(); - - ASSERT_THROW(client->create_stream(stream_name), std::exception); - ASSERT_NO_THROW(client->connect()); - ASSERT_THROW(client->create_stream(stream_name), std::exception); - ASSERT_NO_THROW(client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(client->disconnect()); - ASSERT_THROW(client->create_stream(stream_name), std::exception); + auto client = GetLoggedOutHighLevelClient(); + + ASSERT_THROW(client.CreateStream(stream_name), std::exception); + ASSERT_NO_THROW(client.Connect()); + ASSERT_THROW(client.CreateStream(stream_name), std::exception); + ASSERT_NO_THROW(client.Login("iggy", "iggy")); + ASSERT_NO_THROW(client.Disconnect()); + ASSERT_THROW(client.CreateStream(stream_name), std::exception); } -TEST_F(LowLevelE2E_Stream, CreateStreamValidatesNameConstraintsAndUniqueness) { +TEST_F(E2E_Stream, CreateStreamValidatesNameConstraintsAndUniqueness) { RecordProperty("description", "Validates stream name length constraints and accepts the maximum allowed name length."); - iggy::ffi::Client *client = GetLoggedInClient(); - const std::string illegal_stream_names[] = { "", std::string(256, 'b'), }; + auto client = GetLoggedInHighLevelClient(); for (const auto &stream_name : illegal_stream_names) { SCOPED_TRACE(stream_name); - ASSERT_THROW(client->create_stream(stream_name), std::exception); + ASSERT_THROW(client.CreateStream(stream_name), std::exception); } const std::string max_length_name(255, 'a'); - ASSERT_NO_THROW(client->create_stream(max_length_name)); + ASSERT_NO_THROW(client.CreateStream(max_length_name)); TrackStream(max_length_name); } -TEST_F(LowLevelE2E_Stream, CreateStreamWithEmojiName) { +TEST_F(E2E_Stream, CreateStreamWithEmojiName) { RecordProperty("description", "Creates a stream with a UTF-8 emoji name."); const std::string stream_name = "🚀🚀🚀🚀Apache Iggy🚀🚀🚀🚀"; - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - const std::string returned_stream_name = static_cast(stream_details.name); - EXPECT_EQ(returned_stream_name, stream_name); - EXPECT_EQ(stream_details.topics_count, 0u); - EXPECT_EQ(stream_details.topics.size(), 0u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_details.Name(), stream_name); + EXPECT_EQ(stream_details.TopicsCount(), 0u); + EXPECT_EQ(stream_details.Topics().size(), 0u); }); } -TEST_F(LowLevelE2E_Stream, UpdateStreamWorksCorrectly) { +TEST_F(E2E_Stream, UpdateStreamWorksCorrectly) { RecordProperty("description", "Updates an existing stream name while preserving the stream identity."); const std::string stream_name = GetRandomName(); const std::string updated_stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - iggy::ffi::StreamDetails original_stream_details{}; - ASSERT_NO_THROW({ original_stream_details = client->get_stream(make_string_identifier(stream_name)); }); - const std::uint32_t stream_id = original_stream_details.id; + iggy::StreamDetails original_stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + const std::uint32_t stream_id = original_stream_details.Id(); ForgetTrackedStream(stream_name); TrackStream(stream_id); - ASSERT_NO_THROW(client->update_stream(make_string_identifier(stream_name), updated_stream_name)); + ASSERT_NO_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), updated_stream_name)); - ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), std::exception); + ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), std::exception); - iggy::ffi::StreamDetails updated_stream_details{}; - ASSERT_NO_THROW({ updated_stream_details = client->get_stream(make_numeric_identifier(stream_id)); }); + iggy::StreamDetails updated_stream_details = client.GetStream(iggy::Identifier::Numeric(stream_id)); - EXPECT_EQ(updated_stream_details.id, original_stream_details.id); - EXPECT_EQ(updated_stream_details.created_at, original_stream_details.created_at); - EXPECT_EQ(updated_stream_details.name, updated_stream_name); - EXPECT_EQ(updated_stream_details.size_bytes, original_stream_details.size_bytes); - EXPECT_EQ(updated_stream_details.messages_count, original_stream_details.messages_count); - EXPECT_EQ(updated_stream_details.topics_count, original_stream_details.topics_count); - EXPECT_EQ(updated_stream_details.topics.size(), original_stream_details.topics.size()); + EXPECT_EQ(updated_stream_details.Id(), original_stream_details.Id()); + EXPECT_EQ(updated_stream_details.CreatedAt(), original_stream_details.CreatedAt()); + EXPECT_EQ(updated_stream_details.Name(), updated_stream_name); + EXPECT_EQ(updated_stream_details.SizeBytes(), original_stream_details.SizeBytes()); + EXPECT_EQ(updated_stream_details.MessagesCount(), original_stream_details.MessagesCount()); + EXPECT_EQ(updated_stream_details.TopicsCount(), original_stream_details.TopicsCount()); + EXPECT_EQ(updated_stream_details.Topics().size(), original_stream_details.Topics().size()); } -TEST_F(LowLevelE2E_Stream, UpdateStreamWithSameNameIsIdempotent) { +TEST_F(E2E_Stream, UpdateStreamWithSameNameIsIdempotent) { RecordProperty("description", "Calling update_stream with the current name succeeds without changing stream details."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - iggy::ffi::StreamDetails first_read{}; - iggy::ffi::StreamDetails second_read{}; - ASSERT_NO_THROW({ first_read = client->get_stream(make_string_identifier(stream_name)); }); - ASSERT_NO_THROW(client->update_stream(make_numeric_identifier(first_read.id), stream_name)); - ASSERT_NO_THROW({ second_read = client->get_stream(make_numeric_identifier(first_read.id)); }); - - EXPECT_EQ(second_read.id, first_read.id); - EXPECT_EQ(second_read.created_at, first_read.created_at); - EXPECT_EQ(second_read.name, first_read.name); - EXPECT_EQ(second_read.size_bytes, first_read.size_bytes); - EXPECT_EQ(second_read.messages_count, first_read.messages_count); - EXPECT_EQ(second_read.topics_count, first_read.topics_count); - EXPECT_EQ(second_read.topics.size(), first_read.topics.size()); + auto first_read = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_NO_THROW(client.UpdateStream(iggy::Identifier::Numeric(first_read.Id()), stream_name)); + auto second_read = client.GetStream(iggy::Identifier::Numeric(first_read.Id())); + + EXPECT_EQ(second_read.Id(), first_read.Id()); + EXPECT_EQ(second_read.CreatedAt(), first_read.CreatedAt()); + EXPECT_EQ(second_read.Name(), first_read.Name()); + EXPECT_EQ(second_read.SizeBytes(), first_read.SizeBytes()); + EXPECT_EQ(second_read.MessagesCount(), first_read.MessagesCount()); + EXPECT_EQ(second_read.TopicsCount(), first_read.TopicsCount()); + EXPECT_EQ(second_read.Topics().size(), first_read.Topics().size()); } -TEST_F(LowLevelE2E_Stream, UpdateStreamBeforeLoginThrows) { +TEST_F(E2E_Stream, UpdateStreamBeforeLoginThrows) { RecordProperty("description", "Rejects update_stream before connect, and after connect but before login."); const std::string stream_name = GetRandomName(); const std::string updated_stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); - ASSERT_THROW(unauthenticated_client->update_stream(make_string_identifier(stream_name), updated_stream_name), + ASSERT_THROW(unauthenticated_client.UpdateStream(iggy::Identifier::String(stream_name), updated_stream_name), std::exception); - ASSERT_NO_THROW(unauthenticated_client->connect()); - ASSERT_THROW(unauthenticated_client->update_stream(make_string_identifier(stream_name), updated_stream_name), + ASSERT_NO_THROW(unauthenticated_client.Connect()); + ASSERT_THROW(unauthenticated_client.UpdateStream(iggy::Identifier::String(stream_name), updated_stream_name), std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); - ASSERT_THROW(unauthenticated_client->update_stream(make_string_identifier(stream_name), updated_stream_name), + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); + ASSERT_THROW(unauthenticated_client.UpdateStream(iggy::Identifier::String(stream_name), updated_stream_name), std::exception); } -TEST_F(LowLevelE2E_Stream, UpdateStreamWithVariousUtf8Characters) { +TEST_F(E2E_Stream, UpdateStreamWithVariousUtf8Characters) { RecordProperty("description", "Updates a stream name with various UTF-8 values."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); std::uint32_t stream_id = 0; ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - stream_id = stream_details.id; + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + stream_id = stream_details.Id(); }); ForgetTrackedStream(stream_name); TrackStream(stream_id); @@ -191,106 +180,74 @@ TEST_F(LowLevelE2E_Stream, UpdateStreamWithVariousUtf8Characters) { for (const auto &updated_stream_name : updated_stream_names) { SCOPED_TRACE(updated_stream_name); - ASSERT_NO_THROW(client->update_stream(make_numeric_identifier(stream_id), updated_stream_name)); + ASSERT_NO_THROW(client.UpdateStream(iggy::Identifier::Numeric(stream_id), updated_stream_name)); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_numeric_identifier(stream_id)); - EXPECT_EQ(stream_details.name, updated_stream_name); + const auto stream_details = client.GetStream(iggy::Identifier::Numeric(stream_id)); + EXPECT_EQ(stream_details.Name(), updated_stream_name); }); } } -TEST_F(LowLevelE2E_Stream, UpdateNonExistentStreamThrows) { +TEST_F(E2E_Stream, UpdateNonExistentStreamThrows) { RecordProperty("description", "Throws when updating a stream that does not exist."); const std::string stream_name = GetRandomName(); const std::string updated_stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_THROW(client->update_stream(make_string_identifier(stream_name), updated_stream_name), std::exception); + auto client = GetLoggedInHighLevelClient(); + ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), updated_stream_name), std::exception); } -TEST_F(LowLevelE2E_Stream, UpdateStreamWithDuplicateNameThrows) { +TEST_F(E2E_Stream, UpdateStreamWithDuplicateNameThrows) { RecordProperty("description", "Rejects renaming a stream to another stream's existing name."); const std::string first_stream_name = GetRandomName(); const std::string second_stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(first_stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(first_stream_name)); TrackStream(first_stream_name); - ASSERT_NO_THROW(client->create_stream(second_stream_name)); + ASSERT_NO_THROW(client.CreateStream(second_stream_name)); TrackStream(second_stream_name); - ASSERT_THROW(client->update_stream(make_string_identifier(first_stream_name), second_stream_name), std::exception); + ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(first_stream_name), second_stream_name), std::exception); } -TEST_F(LowLevelE2E_Stream, UpdateDeletedStreamThrows) { +TEST_F(E2E_Stream, UpdateDeletedStreamThrows) { RecordProperty("description", "Throws when updating a stream after it has been deleted."); const std::string stream_name = GetRandomName(); const std::string updated_stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->update_stream(make_string_identifier(stream_name), updated_stream_name), std::exception); -} - -TEST_F(LowLevelE2E_Stream, UpdateStreamFailedValidationDoesNotMutateStream) { - RecordProperty("description", "Keeps the stream unchanged when update_stream fails wrapper validation."); - const std::string stream_name = GetRandomName(); - const std::string updated_stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); - TrackStream(stream_name); - - iggy::ffi::StreamDetails stream_before_failed_update{}; - ASSERT_NO_THROW({ stream_before_failed_update = client->get_stream(make_string_identifier(stream_name)); }); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->update_stream(std::move(invalid_numeric_id), updated_stream_name), std::exception); - - iggy::ffi::StreamDetails stream_after_failed_update{}; - ASSERT_NO_THROW({ stream_after_failed_update = client->get_stream(make_string_identifier(stream_name)); }); - - EXPECT_EQ(stream_after_failed_update.id, stream_before_failed_update.id); - EXPECT_EQ(stream_after_failed_update.created_at, stream_before_failed_update.created_at); - EXPECT_EQ(stream_after_failed_update.name, stream_before_failed_update.name); - EXPECT_EQ(stream_after_failed_update.size_bytes, stream_before_failed_update.size_bytes); - EXPECT_EQ(stream_after_failed_update.messages_count, stream_before_failed_update.messages_count); - EXPECT_EQ(stream_after_failed_update.topics_count, stream_before_failed_update.topics_count); - EXPECT_EQ(stream_after_failed_update.topics.size(), stream_before_failed_update.topics.size()); - - ASSERT_THROW(client->get_stream(make_string_identifier(updated_stream_name)), std::exception); + ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), updated_stream_name), std::exception); } -TEST_F(LowLevelE2E_Stream, UpdateStreamOnlyChangesName) { +TEST_F(E2E_Stream, UpdateStreamOnlyChangesName) { RecordProperty( "description", "Changes only the stream name and leaves stream, topic, message, partition, and segment data intact."); const std::string stream_name = GetRandomName(); const std::string updated_stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + iggy::ffi::Client *ffi_client = GetLoggedInClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); std::uint32_t stream_id = 0; ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - stream_id = stream_details.id; + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + stream_id = stream_details.Id(); }); ForgetTrackedStream(stream_name); TrackStream(stream_id); - ASSERT_NO_THROW(client->create_topic(make_numeric_identifier(stream_id), topic_name, 2, "none", "never_expire", 0, - "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::Numeric(stream_id), topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(2) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::None()) + .SetMessageExpiry(iggy::Expiry::NeverExpire()))); rust::Vec messages; for (std::uint32_t i = 0; i < 3; ++i) { @@ -298,46 +255,40 @@ TEST_F(LowLevelE2E_Stream, UpdateStreamOnlyChangesName) { rust::Vec()); messages.push_back(std::move(message)); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_id), make_numeric_identifier(0), - "partition_id", partition_id_bytes(0), std::move(messages))); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(stream_id), make_numeric_identifier(0), + "partition_id", partition_id_bytes(0), std::move(messages))); - iggy::ffi::StreamDetails stream_before_update{}; - iggy::ffi::StreamDetails stream_after_update{}; + auto stream_before_update = client.GetStream(iggy::Identifier::Numeric(stream_id)); iggy::ffi::Stats stats_before_update{}; - iggy::ffi::Stats stats_after_update{}; - ASSERT_NO_THROW({ - stream_before_update = client->get_stream(make_numeric_identifier(stream_id)); - stats_before_update = client->get_stats(); - }); - - ASSERT_NO_THROW(client->update_stream(make_numeric_identifier(stream_id), updated_stream_name)); + ASSERT_NO_THROW({ stats_before_update = ffi_client->get_stats(); }); - ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW({ - stream_after_update = client->get_stream(make_numeric_identifier(stream_id)); - stats_after_update = client->get_stats(); - }); + ASSERT_NO_THROW(client.UpdateStream(iggy::Identifier::Numeric(stream_id), updated_stream_name)); - EXPECT_EQ(stream_after_update.id, stream_before_update.id); - EXPECT_EQ(stream_after_update.created_at, stream_before_update.created_at); - EXPECT_EQ(stream_after_update.name, updated_stream_name); - EXPECT_EQ(stream_after_update.size_bytes, stream_before_update.size_bytes); - EXPECT_EQ(stream_after_update.messages_count, stream_before_update.messages_count); - EXPECT_EQ(stream_after_update.topics_count, stream_before_update.topics_count); - ASSERT_EQ(stream_before_update.topics.size(), 1u); - ASSERT_EQ(stream_after_update.topics.size(), 1u); - - const auto &before_topic = stream_before_update.topics[0]; - const auto &after_topic = stream_after_update.topics[0]; - EXPECT_EQ(after_topic.id, before_topic.id); - EXPECT_EQ(after_topic.created_at, before_topic.created_at); - EXPECT_EQ(after_topic.name, before_topic.name); - EXPECT_EQ(after_topic.size_bytes, before_topic.size_bytes); - EXPECT_EQ(after_topic.message_expiry, before_topic.message_expiry); - EXPECT_EQ(after_topic.compression_algorithm, before_topic.compression_algorithm); - EXPECT_EQ(after_topic.max_topic_size, before_topic.max_topic_size); - EXPECT_EQ(after_topic.messages_count, before_topic.messages_count); - EXPECT_EQ(after_topic.partitions_count, before_topic.partitions_count); + ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), std::exception); + auto stream_after_update = client.GetStream(iggy::Identifier::Numeric(stream_id)); + iggy::ffi::Stats stats_after_update{}; + ASSERT_NO_THROW({ stats_after_update = ffi_client->get_stats(); }); + + EXPECT_EQ(stream_after_update.Id(), stream_before_update.Id()); + EXPECT_EQ(stream_after_update.CreatedAt(), stream_before_update.CreatedAt()); + EXPECT_EQ(stream_after_update.Name(), updated_stream_name); + EXPECT_EQ(stream_after_update.SizeBytes(), stream_before_update.SizeBytes()); + EXPECT_EQ(stream_after_update.MessagesCount(), stream_before_update.MessagesCount()); + EXPECT_EQ(stream_after_update.TopicsCount(), stream_before_update.TopicsCount()); + ASSERT_EQ(stream_before_update.Topics().size(), 1u); + ASSERT_EQ(stream_after_update.Topics().size(), 1u); + + const auto &before_topic = stream_before_update.Topics()[0]; + const auto &after_topic = stream_after_update.Topics()[0]; + EXPECT_EQ(after_topic.Id(), before_topic.Id()); + EXPECT_EQ(after_topic.CreatedAt(), before_topic.CreatedAt()); + EXPECT_EQ(after_topic.Name(), before_topic.Name()); + EXPECT_EQ(after_topic.SizeBytes(), before_topic.SizeBytes()); + EXPECT_EQ(after_topic.MessageExpiry(), before_topic.MessageExpiry()); + EXPECT_EQ(after_topic.CompressionAlgorithm(), before_topic.CompressionAlgorithm()); + EXPECT_EQ(after_topic.MaxTopicSize(), before_topic.MaxTopicSize()); + EXPECT_EQ(after_topic.MessagesCount(), before_topic.MessagesCount()); + EXPECT_EQ(after_topic.PartitionsCount(), before_topic.PartitionsCount()); EXPECT_EQ(stats_after_update.streams_count, stats_before_update.streams_count); EXPECT_EQ(stats_after_update.topics_count, stats_before_update.topics_count); @@ -346,19 +297,18 @@ TEST_F(LowLevelE2E_Stream, UpdateStreamOnlyChangesName) { EXPECT_EQ(stats_after_update.messages_count, stats_before_update.messages_count); } -TEST_F(LowLevelE2E_Stream, UpdateStreamValidatesNameBounds) { +TEST_F(E2E_Stream, UpdateStreamValidatesNameBounds) { RecordProperty("description", "Rejects invalid stream name lengths during update and accepts the maximum allowed name length."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); std::uint32_t stream_id = 0; ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - stream_id = stream_details.id; + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + stream_id = stream_details.Id(); }); ForgetTrackedStream(stream_name); TrackStream(stream_id); @@ -369,211 +319,155 @@ TEST_F(LowLevelE2E_Stream, UpdateStreamValidatesNameBounds) { }; for (const auto &invalid_stream_name : invalid_stream_names) { SCOPED_TRACE("invalid_stream_name_length=" + std::to_string(invalid_stream_name.size())); - ASSERT_THROW(client->update_stream(make_numeric_identifier(stream_id), invalid_stream_name), std::exception); + ASSERT_THROW(client.UpdateStream(iggy::Identifier::Numeric(stream_id), invalid_stream_name), std::exception); } } -TEST_F(LowLevelE2E_Stream, StreamCreatedAndDeletedSuccessfully) { +TEST_F(E2E_Stream, StreamCreatedAndDeletedSuccessfully) { RecordProperty("description", "Creates a stream and deletes it successfully by string identifier."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); } -TEST_F(LowLevelE2E_Stream, DeleteNotCreatedStreamThrows) { +TEST_F(E2E_Stream, DeleteNotCreatedStreamThrows) { RecordProperty("description", "Throws when deleting a stream that does not exist."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_THROW(client->delete_stream(make_string_identifier(stream_name)), std::exception); + auto client = GetLoggedInHighLevelClient(); + ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, DeleteStreamBeforeLoginThrows) { +TEST_F(E2E_Stream, DeleteStreamBeforeLoginThrows) { RecordProperty("description", "Throws when stream deletion is attempted before authentication."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedOutClient(); + auto client = GetLoggedOutHighLevelClient(); - ASSERT_THROW(client->delete_stream(make_string_identifier(stream_name)), std::exception); + ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), std::exception); - ASSERT_NO_THROW(client->connect()); + ASSERT_NO_THROW(client.Connect()); - ASSERT_THROW(client->delete_stream(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW(client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(client->disconnect()); - ASSERT_THROW(client->delete_stream(make_string_identifier(stream_name)), std::exception); + ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), std::exception); + ASSERT_NO_THROW(client.Login("iggy", "iggy")); + ASSERT_NO_THROW(client.Disconnect()); + ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, DeleteStreamTwiceThrows) { +TEST_F(E2E_Stream, DeleteStreamTwiceThrows) { RecordProperty("description", "Throws when deleting the same stream a second time."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->delete_stream(make_string_identifier(stream_name)), std::exception); -} - -TEST_F(LowLevelE2E_Stream, DeleteStreamWithInvalidIdentifierThrows) { - RecordProperty("description", "Rejects stream deletion requests that use invalid identifier formats."); - iggy::ffi::Client *client = GetLoggedInClient(); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->delete_stream(std::move(invalid_kind_id)), std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->delete_stream(std::move(invalid_numeric_id)), std::exception); -} - -TEST_F(LowLevelE2E_Stream, GetStreamDetailsWithInvalidIdentifierThrows) { - RecordProperty("description", "Rejects stream detail lookups that use invalid identifier formats."); - iggy::ffi::Client *client = GetLoggedInClient(); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->get_stream(std::move(invalid_kind_id)), std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->get_stream(std::move(invalid_numeric_id)), std::exception); + ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, GetStreamByStringIdentifierReturnsStreamDetails) { +TEST_F(E2E_Stream, GetStreamByStringIdentifierReturnsStreamDetails) { RecordProperty("description", "Returns expected stream details when looked up by string identifier."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_details.name, stream_name); - EXPECT_EQ(stream_details.topics_count, 0u); - EXPECT_EQ(stream_details.topics.size(), 0u); - EXPECT_EQ(stream_details.messages_count, 0u); - EXPECT_EQ(stream_details.size_bytes, 0u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_details.Name(), stream_name); + EXPECT_EQ(stream_details.TopicsCount(), 0u); + EXPECT_EQ(stream_details.Topics().size(), 0u); + EXPECT_EQ(stream_details.MessagesCount(), 0u); + EXPECT_EQ(stream_details.SizeBytes(), 0u); }); } -TEST_F(LowLevelE2E_Stream, GetNonExistentStreamDetailsThrows) { +TEST_F(E2E_Stream, GetNonExistentStreamDetailsThrows) { RecordProperty("description", "Throws when requesting details for a stream that does not exist."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), std::exception); + auto client = GetLoggedInHighLevelClient(); + ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, GetStreamDetailsBeforeLoginThrows) { +TEST_F(E2E_Stream, GetStreamDetailsBeforeLoginThrows) { RecordProperty("description", "Throws when stream details are requested before authentication."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedOutClient(); - - ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW(client->connect()); - ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW(client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(client->disconnect()); - ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), std::exception); + auto client = GetLoggedOutHighLevelClient(); + + ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), std::exception); + ASSERT_NO_THROW(client.Connect()); + ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), std::exception); + ASSERT_NO_THROW(client.Login("iggy", "iggy")); + ASSERT_NO_THROW(client.Disconnect()); + ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, GetDeletedStreamDetailsThrows) { +TEST_F(E2E_Stream, GetDeletedStreamDetailsThrows) { RecordProperty("description", "Throws when requesting details for a stream after it has been deleted."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->get_stream(make_string_identifier(stream_name))); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.GetStream(iggy::Identifier::String(stream_name))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), std::exception); -} - -TEST_F(LowLevelE2E_Stream, GetStreamByNumericIdentifierReturnsStreamDetails) { - RecordProperty("description", "Returns expected stream details when looked up by numeric identifier."); - const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); - TrackStream(stream_name); - - std::uint32_t stream_id = 0; - ASSERT_NO_THROW({ - const auto by_string = client->get_stream(make_string_identifier(stream_name)); - stream_id = by_string.id; - }); - - ASSERT_NO_THROW({ - const auto by_numeric = client->get_stream(make_numeric_identifier(stream_id)); - EXPECT_EQ(by_numeric.id, stream_id); - EXPECT_EQ(by_numeric.name, stream_name); - EXPECT_EQ(by_numeric.topics_count, 0u); - EXPECT_EQ(by_numeric.topics.size(), 0u); - }); + ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, GetStreamsReturnsEmptyAfterCleanup) { +TEST_F(E2E_Stream, GetStreamsReturnsEmptyAfterCleanup) { RecordProperty("description", "Verifies get_streams returns empty vector after cleaning up all streams."); - iggy::ffi::Client *client = GetLoggedInClient(); - - auto streams = client->get_streams(); + auto client = GetLoggedInHighLevelClient(); + auto streams = client.GetStreams(); for (const auto &s : streams) { - client->delete_stream(make_numeric_identifier(s.id)); + client.DeleteStream(iggy::Identifier::Numeric(s.Id())); } - streams = client->get_streams(); + streams = client.GetStreams(); ASSERT_EQ(streams.size(), 0u); } -TEST_F(LowLevelE2E_Stream, GetStreamsReturnsStreamAfterCreation) { +TEST_F(E2E_Stream, GetStreamsReturnsStreamAfterCreation) { RecordProperty("description", "Verifies created stream appears in get_streams result."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - client->create_stream(stream_name); + auto client = GetLoggedInHighLevelClient(); + client.CreateStream(stream_name); TrackStream(stream_name); - auto streams = client->get_streams(); + auto streams = client.GetStreams(); ASSERT_GE(streams.size(), 1u); bool found = false; for (const auto &s : streams) { - if (std::string(s.name) == stream_name) { + if (s.Name() == stream_name) { found = true; - EXPECT_GT(s.created_at, static_cast(0)); - EXPECT_EQ(s.size_bytes, static_cast(0)); - EXPECT_EQ(s.messages_count, static_cast(0)); - EXPECT_EQ(s.topics_count, 0u); + EXPECT_GT(s.CreatedAt(), 0u); + EXPECT_EQ(s.SizeBytes(), 0u); + EXPECT_EQ(s.MessagesCount(), 0u); + EXPECT_EQ(s.TopicsCount(), 0u); break; } } ASSERT_TRUE(found) << "Stream '" << stream_name << "' not found in get_streams result"; } -TEST_F(LowLevelE2E_Stream, GetStreamsFieldsVerification) { +TEST_F(E2E_Stream, GetStreamsFieldsVerification) { RecordProperty("description", "Verifies get_streams returns correct field values after creating stream with topic and messages."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - client->create_stream(stream_name); + auto client = GetLoggedInHighLevelClient(); + iggy::ffi::Client *ffi_client = GetLoggedInClient(); + client.CreateStream(stream_name); TrackStream(stream_name); - auto stream = client->get_stream(make_string_identifier(stream_name)); + auto stream = client.GetStream(iggy::Identifier::String(stream_name)); const std::string topic_name = GetRandomName(); - client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, "none", "never_expire", 0, "server_default", - {}); + client.CreateTopic(iggy::Identifier::Numeric(stream.Id()), topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(1) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::None()) + .SetMessageExpiry(iggy::Expiry::NeverExpire())); rust::Vec messages; for (std::uint32_t i = 0; i < 5; i++) { @@ -581,42 +475,41 @@ TEST_F(LowLevelE2E_Stream, GetStreamsFieldsVerification) { rust::Vec()); messages.push_back(std::move(msg)); } - client->send_messages(make_numeric_identifier(stream.id), make_numeric_identifier(0), "partition_id", - partition_id_bytes(0), std::move(messages)); + ffi_client->send_messages(make_numeric_identifier(stream.Id()), make_numeric_identifier(0), "partition_id", + partition_id_bytes(0), std::move(messages)); - auto streams = client->get_streams(); + auto streams = client.GetStreams(); ASSERT_GE(streams.size(), 1u); bool found = false; for (const auto &s : streams) { - if (std::string(s.name) == stream_name) { + if (s.Name() == stream_name) { found = true; - EXPECT_EQ(s.topics_count, 1u); - EXPECT_EQ(s.messages_count, 5u); + EXPECT_EQ(s.TopicsCount(), 1u); + EXPECT_EQ(s.MessagesCount(), 5u); break; } } ASSERT_TRUE(found) << "Stream '" << stream_name << "' not found in get_streams result"; } -TEST_F(LowLevelE2E_Stream, GetStreamsBeforeLoginThrows) { +TEST_F(E2E_Stream, GetStreamsBeforeLoginThrows) { RecordProperty("description", "Throws when get_streams is called before authentication."); - iggy::ffi::Client *client = GetLoggedOutClient(); - - ASSERT_THROW(client->get_streams(), std::exception); - ASSERT_NO_THROW(client->connect()); - ASSERT_THROW(client->get_streams(), std::exception); - ASSERT_NO_THROW(client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(client->disconnect()); - ASSERT_THROW(client->get_streams(), std::exception); + auto client = GetLoggedOutHighLevelClient(); + + ASSERT_THROW(client.GetStreams(), std::exception); + ASSERT_NO_THROW(client.Connect()); + ASSERT_THROW(client.GetStreams(), std::exception); + ASSERT_NO_THROW(client.Login("iggy", "iggy")); + ASSERT_NO_THROW(client.Disconnect()); + ASSERT_THROW(client.GetStreams(), std::exception); } -TEST_F(LowLevelE2E_Stream, GetStreamsConsistentWithGetStream) { +TEST_F(E2E_Stream, GetStreamsConsistentWithGetStream) { RecordProperty("description", "Verifies get_streams result is consistent with get_stream for the same stream."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - client->create_stream(stream_name); + auto client = GetLoggedInHighLevelClient(); + client.CreateStream(stream_name); TrackStream(stream_name); std::string list_name; @@ -624,48 +517,47 @@ TEST_F(LowLevelE2E_Stream, GetStreamsConsistentWithGetStream) { std::uint32_t list_topics_count = 0; std::uint64_t list_created_at = 0; std::uint64_t list_size_bytes = 0; - auto streams = client->get_streams(); + auto streams = client.GetStreams(); for (const auto &s : streams) { - if (std::string(s.name) == stream_name) { - list_name = std::string(s.name); - list_id = s.id; - list_topics_count = s.topics_count; - list_created_at = s.created_at; - list_size_bytes = s.size_bytes; + if (s.Name() == stream_name) { + list_name = s.Name(); + list_id = s.Id(); + list_topics_count = s.TopicsCount(); + list_created_at = s.CreatedAt(); + list_size_bytes = s.SizeBytes(); break; } } ASSERT_FALSE(list_name.empty()) << "Stream '" << stream_name << "' not found in get_streams result"; - auto single = client->get_stream(make_string_identifier(stream_name)); - auto single_name = std::string(single.name); - auto single_topics = single.topics_count; + auto single = client.GetStream(iggy::Identifier::String(stream_name)); + auto single_name = single.Name(); + auto single_topics = single.TopicsCount(); EXPECT_EQ(list_name, single_name); - EXPECT_EQ(list_id, single.id); + EXPECT_EQ(list_id, single.Id()); EXPECT_EQ(list_topics_count, single_topics); - EXPECT_EQ(list_created_at, single.created_at); - EXPECT_EQ(list_size_bytes, single.size_bytes); + EXPECT_EQ(list_created_at, single.CreatedAt()); + EXPECT_EQ(list_size_bytes, single.SizeBytes()); } -TEST_F(LowLevelE2E_Stream, GetStreamsRepeatedCallsReturnSameResult) { +TEST_F(E2E_Stream, GetStreamsRepeatedCallsReturnSameResult) { RecordProperty("description", "Verifies repeated get_streams calls return consistent results."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - client->create_stream(stream_name); + auto client = GetLoggedInHighLevelClient(); + client.CreateStream(stream_name); TrackStream(stream_name); - auto streams1 = client->get_streams(); - auto streams2 = client->get_streams(); - auto streams3 = client->get_streams(); + auto streams1 = client.GetStreams(); + auto streams2 = client.GetStreams(); + auto streams3 = client.GetStreams(); ASSERT_EQ(streams1.size(), streams2.size()); ASSERT_EQ(streams2.size(), streams3.size()); - auto contains_stream = [&](const rust::Vec &vec) { + auto contains_stream = [&](const std::vector &vec) { for (const auto &s : vec) { - if (std::string(s.name) == stream_name) { + if (s.Name() == stream_name) { return true; } } @@ -677,71 +569,60 @@ TEST_F(LowLevelE2E_Stream, GetStreamsRepeatedCallsReturnSameResult) { ASSERT_TRUE(contains_stream(streams3)) << "Stream not found in third call"; } -TEST_F(LowLevelE2E_Stream, PurgeStreamOnNonExistentStreamThrows) { +TEST_F(E2E_Stream, PurgeStreamOnNonExistentStreamThrows) { RecordProperty("description", "Throws when purging a stream that does not exist."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_THROW(client->purge_stream(make_string_identifier(stream_name)), std::exception); + auto client = GetLoggedInHighLevelClient(); + ASSERT_THROW(client.PurgeStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, PurgeStreamAfterStreamDeletionThrows) { +TEST_F(E2E_Stream, PurgeStreamAfterStreamDeletionThrows) { RecordProperty("description", "Throws when purging a stream after it has been deleted."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->purge_stream(make_string_identifier(stream_name)), std::exception); + ASSERT_THROW(client.PurgeStream(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Stream, PurgeStreamWithInvalidIdentifierThrows) { - RecordProperty("description", "Rejects stream purge requests that use invalid identifier formats."); - iggy::ffi::Client *client = GetLoggedInClient(); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->purge_stream(std::move(invalid_kind_id)), std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->purge_stream(std::move(invalid_numeric_id)), std::exception); -} - -TEST_F(LowLevelE2E_Stream, PurgeStreamPreservesStreamMetadata) { +TEST_F(E2E_Stream, PurgeStreamPreservesStreamMetadata) { RecordProperty("description", "Preserves stream identity and topic metadata after purging stream messages."); const std::string stream_name = GetRandomName(); const std::string first_topic_name = GetRandomName(); const std::string second_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + iggy::ffi::Client *ffi_client = GetLoggedInClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 2, "gzip", "duration", - 1000, "1GiB", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 3, "none", - "never_expire", 0, "server_default", {})); - - const auto stream_before_purge = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_before_purge.topics.size(), 2u); + ASSERT_NO_THROW( + client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(2) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(3) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::None()) + .SetMessageExpiry(iggy::Expiry::NeverExpire()))); + + const auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_before_purge.Topics().size(), 2u); rust::Vec first_topic_messages; first_topic_messages.push_back( iggy::ffi::make_message(to_payload("preserve-stream-metadata"), rust::Vec())); - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_before_purge.id), - make_string_identifier(first_topic_name), "partition_id", - partition_id_bytes(0), std::move(first_topic_messages))); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(stream_before_purge.Id()), + make_string_identifier(first_topic_name), "partition_id", + partition_id_bytes(0), std::move(first_topic_messages))); - const auto stream_with_messages = client->get_stream(make_string_identifier(stream_name)); - EXPECT_GT(stream_with_messages.messages_count, 0u); - EXPECT_GT(stream_with_messages.size_bytes, 0u); + const auto stream_with_messages = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_GT(stream_with_messages.MessagesCount(), 0u); + EXPECT_GT(stream_with_messages.SizeBytes(), 0u); struct TopicMetadata { std::uint32_t id; @@ -753,69 +634,68 @@ TEST_F(LowLevelE2E_Stream, PurgeStreamPreservesStreamMetadata) { std::uint32_t partitions_count; }; std::unordered_map topics_before_purge; - for (const auto &topic : stream_with_messages.topics) { - topics_before_purge[static_cast(topic.name)] = { - topic.id, - topic.created_at, - static_cast(topic.name), - topic.message_expiry, - static_cast(topic.compression_algorithm), - topic.max_topic_size, - topic.partitions_count}; + for (const auto &topic : stream_with_messages.Topics()) { + topics_before_purge[topic.Name()] = {topic.Id(), + topic.CreatedAt(), + topic.Name(), + topic.MessageExpiry(), + topic.CompressionAlgorithm(), + topic.MaxTopicSize(), + topic.PartitionsCount()}; } - ASSERT_NO_THROW(client->purge_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.PurgeStream(iggy::Identifier::String(stream_name))); - const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_purge.id, stream_with_messages.id); - EXPECT_EQ(stream_after_purge.created_at, stream_with_messages.created_at); - EXPECT_EQ(stream_after_purge.name, stream_with_messages.name); - EXPECT_EQ(stream_after_purge.topics_count, stream_with_messages.topics_count); - ASSERT_EQ(stream_after_purge.topics.size(), stream_with_messages.topics.size()); + const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_purge.Id(), stream_with_messages.Id()); + EXPECT_EQ(stream_after_purge.CreatedAt(), stream_with_messages.CreatedAt()); + EXPECT_EQ(stream_after_purge.Name(), stream_with_messages.Name()); + EXPECT_EQ(stream_after_purge.TopicsCount(), stream_with_messages.TopicsCount()); + ASSERT_EQ(stream_after_purge.Topics().size(), stream_with_messages.Topics().size()); - for (const auto &topic : stream_after_purge.topics) { - const std::string topic_name = static_cast(topic.name); + for (const auto &topic : stream_after_purge.Topics()) { + const std::string topic_name = topic.Name(); const auto metadata_it = topics_before_purge.find(topic_name); ASSERT_NE(metadata_it, topics_before_purge.end()); const auto &metadata = metadata_it->second; - EXPECT_EQ(topic.id, metadata.id); - EXPECT_EQ(topic.created_at, metadata.created_at); - EXPECT_EQ(topic.name, metadata.name); - EXPECT_EQ(topic.message_expiry, metadata.message_expiry); - EXPECT_EQ(topic.compression_algorithm, metadata.compression_algorithm); - EXPECT_EQ(topic.max_topic_size, metadata.max_topic_size); - EXPECT_EQ(topic.partitions_count, metadata.partitions_count); + EXPECT_EQ(topic.Id(), metadata.id); + EXPECT_EQ(topic.CreatedAt(), metadata.created_at); + EXPECT_EQ(topic.Name(), metadata.name); + EXPECT_EQ(topic.MessageExpiry(), metadata.message_expiry); + EXPECT_EQ(topic.CompressionAlgorithm(), metadata.compression_algorithm); + EXPECT_EQ(topic.MaxTopicSize(), metadata.max_topic_size); + EXPECT_EQ(topic.PartitionsCount(), metadata.partitions_count); } } -TEST_F(LowLevelE2E_Stream, PurgeStreamRemovesMessagesAndPreservesTopics) { +TEST_F(E2E_Stream, PurgeStreamRemovesMessagesAndPreservesTopics) { RecordProperty("description", "Purges all stream messages while keeping the stream and topics intact."); const std::string stream_name = GetRandomName(); const std::string first_topic_name = GetRandomName(); const std::string second_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + iggy::ffi::Client *ffi_client = GetLoggedInClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 2u); + const auto created_stream = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(created_stream.Topics().size(), 2u); std::uint32_t first_topic_id = 0; std::uint32_t second_topic_id = 0; bool first_topic_found = false; bool second_topic_found = false; - for (const auto &topic : created_stream.topics) { - const std::string topic_name = static_cast(topic.name); + for (const auto &topic : created_stream.Topics()) { + const std::string topic_name = topic.Name(); if (topic_name == first_topic_name) { - first_topic_id = topic.id; + first_topic_id = topic.Id(); first_topic_found = true; } else if (topic_name == second_topic_name) { - second_topic_id = topic.id; + second_topic_id = topic.Id(); second_topic_found = true; } } @@ -827,98 +707,98 @@ TEST_F(LowLevelE2E_Stream, PurgeStreamRemovesMessagesAndPreservesTopics) { first_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-first-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(first_topic_id), "partition_id", - partition_id_bytes(0), std::move(first_topic_messages))); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(first_topic_id), "partition_id", + partition_id_bytes(0), std::move(first_topic_messages))); rust::Vec second_topic_messages; for (std::uint32_t i = 0; i < 2; ++i) { second_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-second-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(second_topic_id), "partition_id", - partition_id_bytes(0), std::move(second_topic_messages))); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(second_topic_id), "partition_id", + partition_id_bytes(0), std::move(second_topic_messages))); - const auto stream_before_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_before_purge.topics_count, 2u); - EXPECT_EQ(stream_before_purge.messages_count, 5u); - EXPECT_GT(stream_before_purge.size_bytes, 0u); + const auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_before_purge.TopicsCount(), 2u); + EXPECT_EQ(stream_before_purge.MessagesCount(), 5u); + EXPECT_GT(stream_before_purge.SizeBytes(), 0u); std::unordered_map messages_before_purge; - for (const auto &topic : stream_before_purge.topics) { - messages_before_purge[static_cast(topic.name)] = topic.messages_count; + for (const auto &topic : stream_before_purge.Topics()) { + messages_before_purge[topic.Name()] = topic.MessagesCount(); } EXPECT_EQ(messages_before_purge[first_topic_name], 3u); EXPECT_EQ(messages_before_purge[second_topic_name], 2u); - const auto streams_before_purge = client->get_streams(); + const auto streams_before_purge = client.GetStreams(); bool found_stream_before_purge = false; for (const auto &stream : streams_before_purge) { - if (static_cast(stream.name) == stream_name) { + if (stream.Name() == stream_name) { found_stream_before_purge = true; - EXPECT_EQ(stream.topics_count, 2u); - EXPECT_EQ(stream.messages_count, 5u); - EXPECT_GT(stream.size_bytes, 0u); + EXPECT_EQ(stream.TopicsCount(), 2u); + EXPECT_EQ(stream.MessagesCount(), 5u); + EXPECT_GT(stream.SizeBytes(), 0u); break; } } ASSERT_TRUE(found_stream_before_purge); - ASSERT_NO_THROW(client->purge_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.PurgeStream(iggy::Identifier::String(stream_name))); - const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_purge.topics_count, 2u); - EXPECT_EQ(stream_after_purge.messages_count, 0u); - EXPECT_EQ(stream_after_purge.size_bytes, 0u); - ASSERT_EQ(stream_after_purge.topics.size(), 2u); - for (const auto &topic : stream_after_purge.topics) { - EXPECT_EQ(topic.messages_count, 0u); - EXPECT_EQ(topic.size_bytes, 0u); + const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_purge.TopicsCount(), 2u); + EXPECT_EQ(stream_after_purge.MessagesCount(), 0u); + EXPECT_EQ(stream_after_purge.SizeBytes(), 0u); + ASSERT_EQ(stream_after_purge.Topics().size(), 2u); + for (const auto &topic : stream_after_purge.Topics()) { + EXPECT_EQ(topic.MessagesCount(), 0u); + EXPECT_EQ(topic.SizeBytes(), 0u); } - const auto streams_after_purge = client->get_streams(); + const auto streams_after_purge = client.GetStreams(); bool found_stream_after_purge = false; for (const auto &stream : streams_after_purge) { - if (static_cast(stream.name) == stream_name) { + if (stream.Name() == stream_name) { found_stream_after_purge = true; - EXPECT_EQ(stream.topics_count, 2u); - EXPECT_EQ(stream.messages_count, 0u); - EXPECT_EQ(stream.size_bytes, 0u); + EXPECT_EQ(stream.TopicsCount(), 2u); + EXPECT_EQ(stream.MessagesCount(), 0u); + EXPECT_EQ(stream.SizeBytes(), 0u); break; } } ASSERT_TRUE(found_stream_after_purge); } -TEST_F(LowLevelE2E_Stream, PurgeStreamAcrossMultipleTopicsAndPartitionsClearsEverything) { +TEST_F(E2E_Stream, PurgeStreamAcrossMultipleTopicsAndPartitionsClearsEverything) { RecordProperty("description", "Purges all messages across multiple topics and partitions in the stream."); const std::string stream_name = GetRandomName(); const std::string first_topic_name = GetRandomName(); const std::string second_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + iggy::ffi::Client *ffi_client = GetLoggedInClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 2, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 3, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(2))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(3))); - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 2u); + const auto created_stream = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(created_stream.Topics().size(), 2u); std::uint32_t first_topic_id = 0; std::uint32_t second_topic_id = 0; bool first_topic_found = false; bool second_topic_found = false; - for (const auto &topic : created_stream.topics) { - const std::string topic_name = static_cast(topic.name); + for (const auto &topic : created_stream.Topics()) { + const std::string topic_name = topic.Name(); if (topic_name == first_topic_name) { - first_topic_id = topic.id; + first_topic_id = topic.Id(); first_topic_found = true; } else if (topic_name == second_topic_name) { - second_topic_id = topic.id; + second_topic_id = topic.Id(); second_topic_found = true; } } @@ -932,9 +812,9 @@ TEST_F(LowLevelE2E_Stream, PurgeStreamAcrossMultipleTopicsAndPartitionsClearsEve to_payload("purge-stream-topic-a-" + std::to_string(partition_id) + "-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(first_topic_id), "partition_id", - partition_id_bytes(partition_id), std::move(messages))); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(first_topic_id), "partition_id", + partition_id_bytes(partition_id), std::move(messages))); } for (std::uint32_t partition_id = 0; partition_id < 3; ++partition_id) { rust::Vec messages; @@ -943,118 +823,120 @@ TEST_F(LowLevelE2E_Stream, PurgeStreamAcrossMultipleTopicsAndPartitionsClearsEve to_payload("purge-stream-topic-b-" + std::to_string(partition_id) + "-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(second_topic_id), "partition_id", - partition_id_bytes(partition_id), std::move(messages))); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(second_topic_id), "partition_id", + partition_id_bytes(partition_id), std::move(messages))); } - const auto stream_before_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_before_purge.messages_count, 10u); + const auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_before_purge.MessagesCount(), 10u); - ASSERT_NO_THROW(client->purge_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.PurgeStream(iggy::Identifier::String(stream_name))); - const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_purge.topics_count, 2u); - EXPECT_EQ(stream_after_purge.messages_count, 0u); - EXPECT_EQ(stream_after_purge.size_bytes, 0u); - for (const auto &topic : stream_after_purge.topics) { - EXPECT_EQ(topic.messages_count, 0u); - EXPECT_EQ(topic.size_bytes, 0u); + const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_purge.TopicsCount(), 2u); + EXPECT_EQ(stream_after_purge.MessagesCount(), 0u); + EXPECT_EQ(stream_after_purge.SizeBytes(), 0u); + for (const auto &topic : stream_after_purge.Topics()) { + EXPECT_EQ(topic.MessagesCount(), 0u); + EXPECT_EQ(topic.SizeBytes(), 0u); } } -TEST_F(LowLevelE2E_Stream, PurgeStreamThenSendMessagesAgainSucceeds) { +TEST_F(E2E_Stream, PurgeStreamThenSendMessagesAgainSucceeds) { RecordProperty("description", "Allows sending fresh messages to a topic after purging its parent stream."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + iggy::ffi::Client *ffi_client = GetLoggedInClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 1u); - const std::uint32_t topic_id = created_stream.topics.front().id; + const auto created_stream = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(created_stream.Topics().size(), 1u); + const std::uint32_t topic_id = created_stream.Topics().front().Id(); rust::Vec first_batch; first_batch.push_back(iggy::ffi::make_message(to_payload("before-purge"), rust::Vec())); - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id), - "partition_id", partition_id_bytes(0), std::move(first_batch))); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(topic_id), "partition_id", partition_id_bytes(0), + std::move(first_batch))); - ASSERT_NO_THROW(client->purge_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.PurgeStream(iggy::Identifier::String(stream_name))); rust::Vec second_batch; second_batch.push_back(iggy::ffi::make_message(to_payload("after-purge-0"), rust::Vec())); second_batch.push_back(iggy::ffi::make_message(to_payload("after-purge-1"), rust::Vec())); - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id), - "partition_id", partition_id_bytes(0), std::move(second_batch))); - - const auto stream_after_resend = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_resend.topics_count, 1u); - EXPECT_EQ(stream_after_resend.messages_count, 2u); - EXPECT_GT(stream_after_resend.size_bytes, 0u); - ASSERT_EQ(stream_after_resend.topics.size(), 1u); - EXPECT_EQ(stream_after_resend.topics.front().messages_count, 2u); + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(topic_id), "partition_id", partition_id_bytes(0), + std::move(second_batch))); + + const auto stream_after_resend = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_resend.TopicsCount(), 1u); + EXPECT_EQ(stream_after_resend.MessagesCount(), 2u); + EXPECT_GT(stream_after_resend.SizeBytes(), 0u); + ASSERT_EQ(stream_after_resend.Topics().size(), 1u); + EXPECT_EQ(stream_after_resend.Topics().front().MessagesCount(), 2u); } -TEST_F(LowLevelE2E_Stream, PurgeStreamTwiceKeepsStreamEmptyAndTopicsIntact) { +TEST_F(E2E_Stream, PurgeStreamTwiceKeepsStreamEmptyAndTopicsIntact) { RecordProperty("description", "Allows purging the same stream twice and keeps the stream empty after both calls."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + iggy::ffi::Client *ffi_client = GetLoggedInClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 1u); - const std::uint32_t topic_id = created_stream.topics.front().id; + const auto created_stream = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(created_stream.Topics().size(), 1u); + const std::uint32_t topic_id = created_stream.Topics().front().Id(); rust::Vec messages; for (std::uint32_t i = 0; i < 3; ++i) { messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-twice-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id), - "partition_id", partition_id_bytes(0), std::move(messages))); - - ASSERT_NO_THROW(client->purge_stream(make_string_identifier(stream_name))); - const auto stream_after_first_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_first_purge.topics_count, 1u); - EXPECT_EQ(stream_after_first_purge.messages_count, 0u); - EXPECT_EQ(stream_after_first_purge.size_bytes, 0u); - ASSERT_EQ(stream_after_first_purge.topics.size(), 1u); - EXPECT_EQ(stream_after_first_purge.topics.front().messages_count, 0u); - EXPECT_EQ(stream_after_first_purge.topics.front().size_bytes, 0u); - - ASSERT_NO_THROW(client->purge_stream(make_string_identifier(stream_name))); - const auto stream_after_second_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_second_purge.topics_count, 1u); - EXPECT_EQ(stream_after_second_purge.messages_count, 0u); - EXPECT_EQ(stream_after_second_purge.size_bytes, 0u); - ASSERT_EQ(stream_after_second_purge.topics.size(), 1u); - EXPECT_EQ(stream_after_second_purge.topics.front().messages_count, 0u); - EXPECT_EQ(stream_after_second_purge.topics.front().size_bytes, 0u); -} - -TEST_F(LowLevelE2E_Stream, PurgeStreamBeforeLoginThrows) { + ASSERT_NO_THROW(ffi_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(topic_id), "partition_id", partition_id_bytes(0), + std::move(messages))); + + ASSERT_NO_THROW(client.PurgeStream(iggy::Identifier::String(stream_name))); + const auto stream_after_first_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_first_purge.TopicsCount(), 1u); + EXPECT_EQ(stream_after_first_purge.MessagesCount(), 0u); + EXPECT_EQ(stream_after_first_purge.SizeBytes(), 0u); + ASSERT_EQ(stream_after_first_purge.Topics().size(), 1u); + EXPECT_EQ(stream_after_first_purge.Topics().front().MessagesCount(), 0u); + EXPECT_EQ(stream_after_first_purge.Topics().front().SizeBytes(), 0u); + + ASSERT_NO_THROW(client.PurgeStream(iggy::Identifier::String(stream_name))); + const auto stream_after_second_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_second_purge.TopicsCount(), 1u); + EXPECT_EQ(stream_after_second_purge.MessagesCount(), 0u); + EXPECT_EQ(stream_after_second_purge.SizeBytes(), 0u); + ASSERT_EQ(stream_after_second_purge.Topics().size(), 1u); + EXPECT_EQ(stream_after_second_purge.Topics().front().MessagesCount(), 0u); + EXPECT_EQ(stream_after_second_purge.Topics().front().SizeBytes(), 0u); +} + +TEST_F(E2E_Stream, PurgeStreamBeforeLoginThrows) { RecordProperty("description", "Throws when stream purge is attempted before authentication."); const std::string stream_name = GetRandomName(); - - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); - ASSERT_THROW(unauthenticated_client->purge_stream(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->connect()); - ASSERT_THROW(unauthenticated_client->purge_stream(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); - ASSERT_THROW(unauthenticated_client->purge_stream(make_string_identifier(stream_name)), std::exception); + ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)), std::exception); + ASSERT_NO_THROW(unauthenticated_client.Connect()); + ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)), std::exception); + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); + ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)), std::exception); } diff --git a/foreign/cpp/tests/e2e/test_helpers.hpp b/foreign/cpp/tests/e2e/test_helpers.hpp index 22a4b142a1..e2f43efeca 100644 --- a/foreign/cpp/tests/e2e/test_helpers.hpp +++ b/foreign/cpp/tests/e2e/test_helpers.hpp @@ -29,6 +29,7 @@ #include +#include "iggy.hpp" #include "lib.rs.h" inline iggy::ffi::Identifier make_string_identifier(const std::string &value) { @@ -100,6 +101,25 @@ inline bool has_header(const rust::Vec &headers, return false; } +inline iggy::ffi::TopicCreateOptions make_topic_create_options( + std::uint32_t partitions_count, + const std::string &compression_algorithm = "none", + const std::string &message_expiry_kind = "server_default", + std::uint64_t message_expiry_value = 0, + const std::string &max_topic_size = "server_default") { + iggy::ffi::TopicCreateOptions opts{}; + opts.has_partitions_count = true; + opts.partitions_count = partitions_count; + opts.has_compression_algorithm = true; + opts.compression_algorithm = compression_algorithm; + opts.has_message_expiry = true; + opts.message_expiry_kind = message_expiry_kind; + opts.message_expiry_value = message_expiry_value; + opts.has_max_topic_size = true; + opts.max_topic_size = max_topic_size; + return opts; +} + struct TrackedConsumerGroup { std::string stream_name; std::string topic_name; @@ -141,6 +161,15 @@ class E2ETestFixture : public ::testing::Test { return client; } + iggy::IggyBlockingClient GetLoggedOutHighLevelClient() { return iggy::IggyBlockingClient::Builder().Build(); } + + iggy::IggyBlockingClient GetLoggedInHighLevelClient(std::string username = "iggy", std::string password = "iggy") { + auto client = GetLoggedOutHighLevelClient(); + client.Connect(); + client.Login(std::move(username), std::move(password)); + return client; + } + std::string GetRandomName(const std::size_t max_length = 255) { if (max_length == 0) { return {}; diff --git a/foreign/cpp/tests/e2e/topic.cpp b/foreign/cpp/tests/e2e/topic.cpp index 5bc3bdaa4e..916b433c33 100644 --- a/foreign/cpp/tests/e2e/topic.cpp +++ b/foreign/cpp/tests/e2e/topic.cpp @@ -17,10 +17,12 @@ * under the License. */ +#include #include #include #include #include +#include #include #include #include @@ -31,50 +33,43 @@ #include "lib.rs.h" #include "tests/e2e/test_helpers.hpp" -namespace { +class E2E_Topic : public E2ETestFixture {}; -rust::Vec little_endian_bytes(const std::uint64_t value, const std::size_t width) { - rust::Vec bytes; - bytes.reserve(width); - for (std::size_t index = 0; index < width; ++index) { - bytes.push_back(static_cast((value >> (index * 8)) & 0xFF)); - } - - return bytes; -} - -rust::Vec bool_bytes(const bool value) { - rust::Vec bytes; - bytes.push_back(static_cast(value ? 1 : 0)); - - return bytes; -} - -} // namespace - -class LowLevelE2E_Topic : public E2ETestFixture {}; - -TEST_F(LowLevelE2E_Topic, CreateTopicWithAllOptionCombinations) { +TEST_F(E2E_Topic, CreateTopicWithAllOptionCombinations) { RecordProperty("description", "Creates topics across supported option combinations and verifies they are all returned."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - const std::vector compression_algorithms = {"none", "gzip"}; + struct CompressionOption { + std::string name; + iggy::CompressionAlgorithm value; + }; + const std::vector compression_algorithms = { + {"none", iggy::CompressionAlgorithm::None()}, + {"gzip", iggy::CompressionAlgorithm::Gzip()}, + }; struct ExpiryOption { - std::string kind; - std::uint64_t value; + std::string name; + iggy::Expiry value; }; const std::vector expiry_options = { - {"server_default", 0}, - {"never_expire", 0}, - {"duration", 1000}, + {"server_default", iggy::Expiry::ServerDefault()}, + {"never_expire", iggy::Expiry::NeverExpire()}, + {"duration", iggy::Expiry::Duration(1000)}, + }; + struct MaxTopicSizeOption { + std::string name; + iggy::MaxTopicSize value; + }; + const std::vector max_topic_sizes = { + {"server_default", iggy::MaxTopicSize::ServerDefault()}, + {"unlimited", iggy::MaxTopicSize::Unlimited()}, + {"1GiB", iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)}, }; - const std::vector max_topic_sizes = {"server_default", "unlimited", "1GiB"}; std::size_t expected_topics_count = 0; std::unordered_set expected_topic_names; @@ -82,13 +77,15 @@ TEST_F(LowLevelE2E_Topic, CreateTopicWithAllOptionCombinations) { for (const auto &expiry_option : expiry_options) { for (const auto &max_topic_size : max_topic_sizes) { const std::string topic_name = GetRandomName(); - SCOPED_TRACE("compression=" + compression_algorithm + ", expiry_kind=" + expiry_option.kind + - ", expiry_value=" + std::to_string(expiry_option.value) + - ", max_topic_size=" + max_topic_size); - - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, - compression_algorithm, expiry_option.kind, expiry_option.value, - max_topic_size, {})); + SCOPED_TRACE("compression=" + compression_algorithm.name + ", expiry_kind=" + expiry_option.name + + ", max_topic_size=" + max_topic_size.name); + + iggy::TopicCreateOptions options; + options.SetPartitionsCount(1) + .SetCompressionAlgorithm(compression_algorithm.value) + .SetMessageExpiry(expiry_option.value) + .SetMaxTopicSize(max_topic_size.value); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, options)); ++expected_topics_count; expected_topic_names.insert(topic_name); } @@ -96,60 +93,60 @@ TEST_F(LowLevelE2E_Topic, CreateTopicWithAllOptionCombinations) { } ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_details.name, stream_name); - EXPECT_EQ(stream_details.topics_count, expected_topics_count); - ASSERT_EQ(stream_details.topics.size(), expected_topics_count); - for (const auto &topic : stream_details.topics) { - const std::string topic_name = static_cast(topic.name); - const auto erased = expected_topic_names.erase(topic_name); - EXPECT_EQ(erased, 1u) << "Unexpected topic name returned: " << topic_name; + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_details.Name(), stream_name); + EXPECT_EQ(stream_details.TopicsCount(), expected_topics_count); + ASSERT_EQ(stream_details.Topics().size(), expected_topics_count); + for (const auto &topic : stream_details.Topics()) { + const auto erased = expected_topic_names.erase(topic.Name()); + EXPECT_EQ(erased, 1u) << "Unexpected topic name returned: " << topic.Name(); } EXPECT_TRUE(expected_topic_names.empty()); }); } -TEST_F(LowLevelE2E_Topic, CreateTopicWithBoundaryPartitionsCountValues) { +TEST_F(E2E_Topic, CreateTopicWithBoundaryPartitionsCountValues) { RecordProperty("description", "Accepts boundary partition counts and rejects values above the supported maximum."); const std::string stream_name = GetRandomName(); - const std::string zero_partitions_topic_name = GetRandomName(); + const std::string default_partitions_topic = GetRandomName(); const std::string max_partitions_topic_name = GetRandomName(); const std::string overflow_topic_name = GetRandomName(); + const std::string zero_partitions_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), zero_partitions_topic_name, 0, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), max_partitions_topic_name, 1000, "none", - "server_default", 0, "server_default", {})); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), overflow_topic_name, 1001, "none", - "server_default", 0, "server_default", {}), + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), default_partitions_topic, + iggy::TopicCreateOptions())); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), max_partitions_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1000))); + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), overflow_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1001)), std::exception); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), zero_partitions_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(0))); - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_details.topics_count, 2u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_details.TopicsCount(), 3u); std::unordered_map topic_partitions; - for (const auto &topic : stream_details.topics) { - const std::string topic_name = static_cast(topic.name); - topic_partitions[topic_name] = topic.partitions_count; + for (const auto &topic : stream_details.Topics()) { + topic_partitions[topic.Name()] = topic.PartitionsCount(); } - EXPECT_EQ(topic_partitions.size(), 2u); - EXPECT_EQ(topic_partitions[zero_partitions_topic_name], 0u); + EXPECT_EQ(topic_partitions.size(), 3u); + EXPECT_EQ(topic_partitions[default_partitions_topic], 1u); EXPECT_EQ(topic_partitions[max_partitions_topic_name], 1000u); + EXPECT_EQ(topic_partitions[zero_partitions_topic_name], 0u); } -TEST_F(LowLevelE2E_Topic, CreateTopicWithInvalidNamesThrows) { +TEST_F(E2E_Topic, CreateTopicWithInvalidNamesThrows) { RecordProperty("description", "Rejects invalid topic names and accepts the maximum allowed name length."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); const std::string illegal_topic_names[] = { @@ -158,75 +155,76 @@ TEST_F(LowLevelE2E_Topic, CreateTopicWithInvalidNamesThrows) { }; for (const auto &topic_name : illegal_topic_names) { SCOPED_TRACE(topic_name); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {}), + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1)), std::exception); } const std::string max_length_name(255, 'a'); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), max_length_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), max_length_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); } -TEST_F(LowLevelE2E_Topic, CreateDuplicateTopicThrows) { +TEST_F(E2E_Topic, CreateDuplicateTopicThrows) { RecordProperty("description", "Rejects creating a duplicate topic within the same stream."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", 0, - "server_default", {}), + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1)), std::exception); } -TEST_F(LowLevelE2E_Topic, CreateSameTopicNameInDifferentStreamsSucceeds) { +TEST_F(E2E_Topic, CreateSameTopicNameInDifferentStreamsSucceeds) { RecordProperty("description", "Allows the same topic name to be created in different streams."); const std::string first_stream_name = GetRandomName(); const std::string second_stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(first_stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(first_stream_name)); TrackStream(first_stream_name); - ASSERT_NO_THROW(client->create_stream(second_stream_name)); + ASSERT_NO_THROW(client.CreateStream(second_stream_name)); TrackStream(second_stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(second_stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(first_stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(second_stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); } -TEST_F(LowLevelE2E_Topic, CreateTopicWithInvalidOptionsThrows) { +TEST_F(E2E_Topic, CreateTopicWithInvalidOptionsThrows) { RecordProperty("description", "Rejects topic creation requests that use invalid option values."); const std::string stream_name = GetRandomName(); const std::string invalid_compression_topic_name = GetRandomName(); const std::string invalid_expiry_topic_name = GetRandomName(); const std::string invalid_max_size_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), invalid_compression_topic_name, 1, - "invalid-compression", "server_default", 0, "server_default", {}), + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), invalid_compression_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1).SetRawEntries( + {{"compression_algorithm", "invalid-compression"}})), std::exception); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), invalid_expiry_topic_name, 1, "none", - "invalid-expiry-kind", 0, "server_default", {}), + ASSERT_THROW(client.CreateTopic( + iggy::Identifier::String(stream_name), invalid_expiry_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1).SetRawEntries({{"message_expiry", "invalid"}})), std::exception); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), invalid_max_size_topic_name, 1, "none", - "server_default", 0, "not-a-size", {}), + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), invalid_max_size_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1).SetRawEntries( + {{"max_topic_size", "not-a-size"}})), std::exception); } -TEST_F(LowLevelE2E_Topic, CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRemainder) { +TEST_F(E2E_Topic, CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRemainder) { RecordProperty("description", "Returns an explicitly set option in its canonical kind, derives the keys left unset, and rejects " "an option key outside the server catalog."); @@ -234,96 +232,89 @@ TEST_F(LowLevelE2E_Topic, CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRe const std::string topic_name = GetRandomName(); const std::string unknown_option_topic = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - rust::Vec options; - options.push_back(make_header_entry(make_header_field(iggy::ffi::HeaderKind::String, to_payload("enforce_fsync")), - make_header_field(iggy::ffi::HeaderKind::String, to_payload("true")))); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", std::move(options))); + iggy::TopicCreateOptions options; + options.SetPartitionsCount(1).SetRawEntries({{"enforce_fsync", "true"}}); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, options)); const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); // Admission re-encodes the block from its own parse, so a value comes back // in its key's catalog kind rather than in the kind that was sent. - rust::Vec enforce_fsync_enabled; - enforce_fsync_enabled.push_back(1); - EXPECT_TRUE(has_header(topic_details.options, static_cast(iggy::ffi::HeaderKind::String), - to_payload("enforce_fsync"), static_cast(iggy::ffi::HeaderKind::Bool), - enforce_fsync_enabled)); - - EXPECT_FALSE(topic_details.derived_options.empty()); - std::unordered_set derived_option_keys; - for (const auto &derived_option : topic_details.derived_options) { - derived_option_keys.insert(std::string(derived_option.key.value.begin(), derived_option.key.value.end())); - } - EXPECT_EQ(derived_option_keys.count("max_topic_size"), 1u); - EXPECT_EQ(derived_option_keys.count("enforce_fsync"), 0u); - - rust::Vec unknown_options; - unknown_options.push_back( - make_header_entry(make_header_field(iggy::ffi::HeaderKind::String, to_payload("not_a_real_option")), - make_header_field(iggy::ffi::HeaderKind::String, to_payload("true")))); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), unknown_option_topic, 1, "none", - "server_default", 0, "server_default", std::move(unknown_options)), + const auto &explicit_options = topic_details.Options().Explicit(); + ASSERT_EQ(explicit_options.size(), 1u); + auto explicit_it = explicit_options.find("enforce_fsync"); + ASSERT_NE(explicit_it, explicit_options.end()); + EXPECT_EQ(explicit_it->second.Kind(), iggy::HeaderKind::Bool); + EXPECT_EQ(explicit_it->second.Value(), (std::vector{1})); + + EXPECT_FALSE(topic_details.Options().Derived().empty()); + const auto &derived_options = topic_details.Options().Derived(); + EXPECT_EQ(derived_options.count("max_topic_size"), 1u); + EXPECT_EQ(derived_options.count("enforce_fsync"), 0u); + + iggy::TopicCreateOptions unknown_options; + unknown_options.SetPartitionsCount(1).SetRawEntries({{"not_a_real_option", "true"}}); + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), unknown_option_topic, unknown_options), std::exception); } -TEST_F(LowLevelE2E_Topic, CreateTopicWithTypedOptionHelpersReportsThemAsExplicitOptions) { +TEST_F(E2E_Topic, CreateTopicWithTypedOptionHelpersReportsThemAsExplicitOptions) { RecordProperty("description", "Creates a topic with every typed option helper and verifies each key comes back as an explicit " "option in its catalog kind."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); constexpr std::uint64_t segment_size_bytes = 8ULL * 1024ULL * 1024ULL; constexpr std::uint32_t messages_required_to_save = 512; constexpr std::uint64_t size_of_messages_required_to_save = 2ULL * 1024ULL * 1024ULL; - rust::Vec options; - options.push_back(iggy::TopicOption::SegmentSize(segment_size_bytes)); - options.push_back(iggy::TopicOption::EnforceFsync(true)); - options.push_back(iggy::TopicOption::MessagesRequiredToSave(messages_required_to_save)); - options.push_back(iggy::TopicOption::SizeOfMessagesRequiredToSave(size_of_messages_required_to_save)); - options.push_back(iggy::TopicOption::PreallocateSegments(false)); + iggy::TopicCreateOptions options; + options.SetPartitionsCount(1) + .SetSegmentSize(segment_size_bytes) + .SetEnforceFsync(true) + .SetMessagesRequiredToSave(messages_required_to_save) + .SetSizeOfMessagesRequiredToSave(size_of_messages_required_to_save) + .SetPreallocateSegments(false); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", std::move(options))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, options)); const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); - - const auto key_kind = static_cast(iggy::ffi::HeaderKind::String); - const auto bool_kind = static_cast(iggy::ffi::HeaderKind::Bool); - const auto uint32_kind = static_cast(iggy::ffi::HeaderKind::Uint32); - const auto uint64_kind = static_cast(iggy::ffi::HeaderKind::Uint64); - - EXPECT_TRUE(has_header(topic_details.options, key_kind, to_payload("segment_size"), uint64_kind, - little_endian_bytes(segment_size_bytes, 8))); - EXPECT_TRUE(has_header(topic_details.options, key_kind, to_payload("enforce_fsync"), bool_kind, bool_bytes(true))); - EXPECT_TRUE(has_header(topic_details.options, key_kind, to_payload("messages_required_to_save"), uint32_kind, - little_endian_bytes(messages_required_to_save, 4))); - EXPECT_TRUE(has_header(topic_details.options, key_kind, to_payload("size_of_messages_required_to_save"), - uint64_kind, little_endian_bytes(size_of_messages_required_to_save, 8))); - EXPECT_TRUE( - has_header(topic_details.options, key_kind, to_payload("preallocate_segments"), bool_kind, bool_bytes(false))); - - for (const auto &derived_option : topic_details.derived_options) { - const std::string derived_key(derived_option.key.value.begin(), derived_option.key.value.end()); - EXPECT_NE(derived_key, "segment_size") << "segment_size was set explicitly, so it cannot be derived"; - } + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + + const auto &explicit_options = topic_details.Options().Explicit(); + EXPECT_EQ(explicit_options.count("segment_size"), 1u); + EXPECT_EQ(explicit_options.count("enforce_fsync"), 1u); + EXPECT_EQ(explicit_options.count("messages_required_to_save"), 1u); + EXPECT_EQ(explicit_options.count("size_of_messages_required_to_save"), 1u); + EXPECT_EQ(explicit_options.count("preallocate_segments"), 1u); + + auto check_kind = [&](const std::string &key, iggy::HeaderKind expected) { + auto it = explicit_options.find(key); + ASSERT_NE(it, explicit_options.end()) << "Missing explicit key: " << key; + EXPECT_EQ(it->second.Kind(), expected) << "Wrong kind for key: " << key; + }; + check_kind("segment_size", iggy::HeaderKind::Uint64); + check_kind("enforce_fsync", iggy::HeaderKind::Bool); + check_kind("messages_required_to_save", iggy::HeaderKind::Uint32); + check_kind("size_of_messages_required_to_save", iggy::HeaderKind::Uint64); + check_kind("preallocate_segments", iggy::HeaderKind::Bool); + + EXPECT_EQ(explicit_options.size(), 5u); + EXPECT_EQ(topic_details.Options().Derived().count("segment_size"), 0u) + << "segment_size was set explicitly, so it cannot be derived"; } -TEST_F(LowLevelE2E_Topic, DescribeOptionsServesTopicCatalogAndRejectsUnknownScope) { +TEST_F(E2E_Topic, DescribeOptionsServesTopicCatalogAndRejectsUnknownScope) { RecordProperty("description", "Serves the topic option catalog with each key's kind, default and description, returns an empty " "catalog for the stream scope, and rejects an unknown scope name."); @@ -359,476 +350,408 @@ TEST_F(LowLevelE2E_Topic, DescribeOptionsServesTopicCatalogAndRejectsUnknownScop ASSERT_THROW(client->describe_options("not_a_scope"), std::exception); } -TEST_F(LowLevelE2E_Topic, CreateTopicWithMaxTopicSizeBelowSegmentSizeThrows) { +TEST_F(E2E_Topic, CreateTopicWithMaxTopicSizeBelowSegmentSizeThrows) { RecordProperty("description", "Rejects topic creation when the maximum topic size is smaller than the segment size."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", 0, - "1024", {}), - std::exception); + iggy::TopicCreateOptions options; + options.SetPartitionsCount(1).SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024)); + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, options), std::exception); } -TEST_F(LowLevelE2E_Topic, CreateTopicOnNonExistentStreamThrows) { +TEST_F(E2E_Topic, CreateTopicOnNonExistentStreamThrows) { RecordProperty("description", "Throws when creating a topic on a stream that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", 0, - "server_default", {}), + auto client = GetLoggedInHighLevelClient(); + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1)), std::exception); } -TEST_F(LowLevelE2E_Topic, CreateTopicAfterStreamDeletionThrows) { +TEST_F(E2E_Topic, CreateTopicAfterStreamDeletionThrows) { RecordProperty("description", "Throws when creating a topic after its stream has been deleted."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", 0, - "server_default", {}), + ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1)), std::exception); } -TEST_F(LowLevelE2E_Topic, CreateTopicWithInvalidStreamIdentifierThrows) { - RecordProperty("description", "Rejects topic creation requests that use invalid stream identifier formats."); - const std::string stream_name = GetRandomName(); - const std::string first_topic_name = GetRandomName(); - const std::string second_topic_name = GetRandomName(); - - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); - TrackStream(stream_name); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->create_topic(std::move(invalid_kind_id), first_topic_name, 1, "none", "server_default", 0, - "server_default", {}), - std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->create_topic(std::move(invalid_numeric_id), second_topic_name, 1, "none", "server_default", 0, - "server_default", {}), - std::exception); -} - -TEST_F(LowLevelE2E_Topic, CreateTopicBeforeLoginThrows) { +TEST_F(E2E_Topic, CreateTopicBeforeLoginThrows) { RecordProperty("description", "Throws when topic creation is attempted from an unauthenticated client."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); - ASSERT_NO_THROW(unauthenticated_client->connect()); - ASSERT_THROW(unauthenticated_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {}), + ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); - ASSERT_THROW(unauthenticated_client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {}), + ASSERT_NO_THROW(unauthenticated_client.Connect()); + ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1)), + std::exception); + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); + ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1)), std::exception); } -TEST_F(LowLevelE2E_Topic, DeleteTopicAfterCreate) { +TEST_F(E2E_Topic, DeleteTopicAfterCreate) { RecordProperty("description", "Deletes an existing topic after creating it."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name))); + ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name))); ASSERT_NO_THROW({ - const auto topics = client->get_topics(make_string_identifier(stream_name)); + const auto topics = client.GetTopics(iggy::Identifier::String(stream_name)); EXPECT_TRUE(topics.empty()); }); } -TEST_F(LowLevelE2E_Topic, DeleteTopicOnNonExistentStreamThrows) { +TEST_F(E2E_Topic, DeleteTopicOnNonExistentStreamThrows) { RecordProperty("description", "Throws when deleting a topic from a stream that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + ASSERT_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, DeleteTopicOnNonExistentTopicThrows) { +TEST_F(E2E_Topic, DeleteTopicOnNonExistentTopicThrows) { RecordProperty("description", "Throws when deleting a topic that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + ASSERT_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, DeleteTopicTwiceThrows) { +TEST_F(E2E_Topic, DeleteTopicTwiceThrows) { RecordProperty("description", "Throws when deleting the same topic a second time."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name))); - ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name))); + ASSERT_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, DeleteTopicAfterStreamDeletionThrows) { +TEST_F(E2E_Topic, DeleteTopicAfterStreamDeletionThrows) { RecordProperty("description", "Throws when deleting a topic after its stream has been deleted."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + ASSERT_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, DeleteTopicBeforeLoginThrows) { +TEST_F(E2E_Topic, DeleteTopicBeforeLoginThrows) { RecordProperty("description", "Rejects delete_topic before connect, and after connect but before login."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); ASSERT_THROW( - unauthenticated_client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->connect()); + ASSERT_NO_THROW(unauthenticated_client.Connect()); ASSERT_THROW( - unauthenticated_client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); ASSERT_THROW( - unauthenticated_client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, DeleteTopicWithInvalidStreamIdentifierThrows) { - RecordProperty("description", "Rejects topic deletion requests that use invalid stream identifier formats."); - const std::string stream_name = GetRandomName(); - const std::string topic_name = GetRandomName(); - - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); - TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->delete_topic(std::move(invalid_kind_id), make_string_identifier(topic_name)), std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->delete_topic(std::move(invalid_numeric_id), make_string_identifier(topic_name)), - std::exception); -} - -TEST_F(LowLevelE2E_Topic, DeleteTopicWithInvalidTopicIdentifierThrows) { - RecordProperty("description", "Rejects topic deletion requests that use invalid topic identifier formats."); - const std::string stream_name = GetRandomName(); - const std::string topic_name = GetRandomName(); - - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); - TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), std::move(invalid_kind_id)), std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->delete_topic(make_string_identifier(stream_name), std::move(invalid_numeric_id)), - std::exception); -} - -TEST_F(LowLevelE2E_Topic, GetTopicReturnsTopicForExistingTopic) { +TEST_F(E2E_Topic, GetTopicReturnsTopicForExistingTopic) { RecordProperty("description", "Returns topic details for an existing topic."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW( - client->create_topic(make_string_identifier(stream_name), topic_name, 3, "gzip", "duration", 1000, "1GiB", {})); + client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(3) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); ASSERT_NO_THROW({ const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); - EXPECT_EQ(topic_details.name, topic_name); - EXPECT_EQ(topic_details.partitions_count, 3u); - EXPECT_EQ(topic_details.partitions.size(), 3u); - EXPECT_EQ(topic_details.compression_algorithm, "gzip"); - EXPECT_EQ(topic_details.message_expiry, 1000u); - EXPECT_EQ(topic_details.max_topic_size, 1024ULL * 1024ULL * 1024ULL); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + EXPECT_EQ(topic_details.Name(), topic_name); + EXPECT_EQ(topic_details.PartitionsCount(), 3u); + EXPECT_EQ(topic_details.Partitions().size(), 3u); + EXPECT_EQ(topic_details.CompressionAlgorithm(), "gzip"); + EXPECT_EQ(topic_details.MessageExpiry(), 1000u); + EXPECT_EQ(topic_details.MaxTopicSize(), 1024ULL * 1024ULL * 1024ULL); }); } -TEST_F(LowLevelE2E_Topic, GetTopicBeforeLoginThrows) { +TEST_F(E2E_Topic, GetTopicBeforeLoginThrows) { RecordProperty("description", "Rejects get_topic before connect, and after connect but before login."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); ASSERT_THROW( - unauthenticated_client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->connect()); + ASSERT_NO_THROW(unauthenticated_client.Connect()); ASSERT_THROW( - unauthenticated_client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); ASSERT_THROW( - unauthenticated_client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, GetTopicWithWrongStreamIdThrows) { +TEST_F(E2E_Topic, GetTopicWithWrongStreamIdThrows) { RecordProperty("description", "Rejects get_topic when the topic belongs to a different stream."); const std::string first_stream_name = GetRandomName(); const std::string second_stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(first_stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(first_stream_name)); TrackStream(first_stream_name); - ASSERT_NO_THROW(client->create_stream(second_stream_name)); + ASSERT_NO_THROW(client.CreateStream(second_stream_name)); TrackStream(second_stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(first_stream_name), topic_name, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(first_stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - ASSERT_THROW(client->get_topic(make_string_identifier(second_stream_name), make_string_identifier(topic_name)), + ASSERT_THROW(client.GetTopic(iggy::Identifier::String(second_stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, GetTopicWithWrongTopicThrows) { +TEST_F(E2E_Topic, GetTopicWithWrongTopicThrows) { RecordProperty("description", "Rejects get_topic when the topic does not exist in the stream."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); const std::string wrong_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - ASSERT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(wrong_topic_name)), + ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(wrong_topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, GetTopicAfterStreamDeletionThrows) { +TEST_F(E2E_Topic, GetTopicAfterStreamDeletionThrows) { RecordProperty("description", "Rejects get_topic after the stream has been deleted."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, GetTopicAfterTopicDeletionThrows) { +TEST_F(E2E_Topic, GetTopicAfterTopicDeletionThrows) { RecordProperty("description", "Rejects get_topic after the topic has been deleted."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(topic_name))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name))); - ASSERT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, GetTopicReturnsEmptyPartitionsForZeroPartitionTopic) { +TEST_F(E2E_Topic, GetTopicReturnsEmptyPartitionsForZeroPartitionTopic) { RecordProperty("description", "Returns an empty partitions vector for a topic created with zero partitions."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 0, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(0))); ASSERT_NO_THROW({ const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); - EXPECT_EQ(topic_details.name, topic_name); - EXPECT_EQ(topic_details.partitions_count, 0u); - EXPECT_TRUE(topic_details.partitions.empty()); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + EXPECT_EQ(topic_details.Name(), topic_name); + EXPECT_EQ(topic_details.PartitionsCount(), 0u); + EXPECT_TRUE(topic_details.Partitions().empty()); }); } -TEST_F(LowLevelE2E_Topic, GetTopicReturnsMaxBoundaryPartitionCount) { +TEST_F(E2E_Topic, GetTopicReturnsMaxBoundaryPartitionCount) { RecordProperty("description", "Returns the maximum boundary partition count for a topic created with it."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1000, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1000))); ASSERT_NO_THROW({ const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); - EXPECT_EQ(topic_details.name, topic_name); - EXPECT_EQ(topic_details.partitions_count, 1000u); - EXPECT_EQ(topic_details.partitions.size(), 1000u); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + EXPECT_EQ(topic_details.Name(), topic_name); + EXPECT_EQ(topic_details.PartitionsCount(), 1000u); + EXPECT_EQ(topic_details.Partitions().size(), 1000u); }); } -TEST_F(LowLevelE2E_Topic, GetTopicIsStableAcrossBackToBackCalls) { +TEST_F(E2E_Topic, GetTopicIsStableAcrossBackToBackCalls) { RecordProperty("description", "Returns stable topic details across back-to-back get_topic calls."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW( - client->create_topic(make_string_identifier(stream_name), topic_name, 3, "gzip", "duration", 1000, "1GiB", {})); + client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(3) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); - iggy::ffi::TopicDetails first_topic{}; - iggy::ffi::TopicDetails second_topic{}; ASSERT_NO_THROW({ - first_topic = client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); - second_topic = client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); + const auto first_topic = + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + const auto second_topic = + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + + EXPECT_EQ(second_topic.Name(), first_topic.Name()); + EXPECT_EQ(second_topic.MessageExpiry(), first_topic.MessageExpiry()); + EXPECT_EQ(second_topic.CompressionAlgorithm(), first_topic.CompressionAlgorithm()); + EXPECT_EQ(second_topic.MaxTopicSize(), first_topic.MaxTopicSize()); + EXPECT_EQ(second_topic.PartitionsCount(), first_topic.PartitionsCount()); + EXPECT_EQ(second_topic.Partitions().size(), first_topic.Partitions().size()); }); - - EXPECT_EQ(static_cast(second_topic.name), static_cast(first_topic.name)); - EXPECT_EQ(second_topic.message_expiry, first_topic.message_expiry); - EXPECT_EQ(static_cast(second_topic.compression_algorithm), - static_cast(first_topic.compression_algorithm)); - EXPECT_EQ(second_topic.max_topic_size, first_topic.max_topic_size); - EXPECT_EQ(second_topic.partitions_count, first_topic.partitions_count); - EXPECT_EQ(second_topic.partitions.size(), first_topic.partitions.size()); } -TEST_F(LowLevelE2E_Topic, GetTopicAgreesWithGetStreamTopicSummary) { +TEST_F(E2E_Topic, GetTopicAgreesWithGetStreamTopicSummary) { RecordProperty("description", "Returns topic details that agree with get_stream topic summary fields."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW( - client->create_topic(make_string_identifier(stream_name), topic_name, 3, "gzip", "duration", 1000, "1GiB", {})); + client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(3) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); ASSERT_NO_THROW({ - const auto stream_details = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_details.topics.size(), 1u); + const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_details.Topics().size(), 1u); - const auto topic_summary = stream_details.topics.front(); + const auto &topic_summary = stream_details.Topics().front(); const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); - - EXPECT_EQ(static_cast(topic_details.name), static_cast(topic_summary.name)); - EXPECT_EQ(topic_details.message_expiry, topic_summary.message_expiry); - EXPECT_EQ(static_cast(topic_details.compression_algorithm), - static_cast(topic_summary.compression_algorithm)); - EXPECT_EQ(topic_details.max_topic_size, topic_summary.max_topic_size); - EXPECT_EQ(topic_details.partitions_count, topic_summary.partitions_count); - EXPECT_EQ(topic_details.partitions.size(), topic_summary.partitions_count); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + + EXPECT_EQ(topic_details.Name(), topic_summary.Name()); + EXPECT_EQ(topic_details.MessageExpiry(), topic_summary.MessageExpiry()); + EXPECT_EQ(topic_details.CompressionAlgorithm(), topic_summary.CompressionAlgorithm()); + EXPECT_EQ(topic_details.MaxTopicSize(), topic_summary.MaxTopicSize()); + EXPECT_EQ(topic_details.PartitionsCount(), topic_summary.PartitionsCount()); + EXPECT_EQ(topic_details.Partitions().size(), topic_summary.PartitionsCount()); }); } -TEST_F(LowLevelE2E_Topic, GetTopicsReturnsCreatedTopicInputFields) { +TEST_F(E2E_Topic, GetTopicsReturnsCreatedTopicInputFields) { RecordProperty("description", "Creates topics in a stream, gets them, and verifies user-provided topic fields."); const std::string stream_name = GetRandomName(); const std::string first_topic_name = GetRandomName(); @@ -844,212 +767,232 @@ TEST_F(LowLevelE2E_Topic, GetTopicsReturnsCreatedTopicInputFields) { const std::unordered_map expected_topics = { {first_topic_name, {2, "gzip", 1000, 1024ULL * 1024ULL * 1024ULL}}, {second_topic_name, - {0, "none", std::numeric_limits::max(), std::numeric_limits::max()}}, + {1, "none", std::numeric_limits::max(), std::numeric_limits::max()}}, }; - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 2, "gzip", "duration", - 1000, "1GiB", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 0, "none", - "never_expire", 0, "unlimited", {})); + ASSERT_NO_THROW( + client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(2) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(1) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::None()) + .SetMessageExpiry(iggy::Expiry::NeverExpire()) + .SetMaxTopicSize(iggy::MaxTopicSize::Unlimited()))); ASSERT_NO_THROW({ - const auto topics = client->get_topics(make_string_identifier(stream_name)); + const auto topics = client.GetTopics(iggy::Identifier::String(stream_name)); ASSERT_EQ(topics.size(), expected_topics.size()); - EXPECT_EQ(topics[0].name, first_topic_name); - EXPECT_EQ(topics[1].name, second_topic_name); + EXPECT_EQ(topics[0].Name(), first_topic_name); + EXPECT_EQ(topics[1].Name(), second_topic_name); std::unordered_set found_topic_names; for (const auto &topic : topics) { - const std::string topic_name = static_cast(topic.name); - const auto expected = expected_topics.find(topic_name); - ASSERT_NE(expected, expected_topics.end()) << "Unexpected topic name returned: " << topic_name; - - EXPECT_EQ(topic.name, expected->first); - EXPECT_EQ(topic.partitions_count, expected->second.partitions_count); - EXPECT_EQ(topic.compression_algorithm, expected->second.compression_algorithm); - EXPECT_EQ(topic.message_expiry, expected->second.message_expiry); - EXPECT_EQ(topic.max_topic_size, expected->second.max_topic_size); - found_topic_names.insert(topic_name); + const auto expected = expected_topics.find(topic.Name()); + ASSERT_NE(expected, expected_topics.end()) << "Unexpected topic name returned: " << topic.Name(); + + EXPECT_EQ(topic.Name(), expected->first); + EXPECT_EQ(topic.PartitionsCount(), expected->second.partitions_count); + EXPECT_EQ(topic.CompressionAlgorithm(), expected->second.compression_algorithm); + EXPECT_EQ(topic.MessageExpiry(), expected->second.message_expiry); + EXPECT_EQ(topic.MaxTopicSize(), expected->second.max_topic_size); + found_topic_names.insert(topic.Name()); } EXPECT_EQ(found_topic_names.size(), expected_topics.size()); }); } -TEST_F(LowLevelE2E_Topic, GetTopicsBeforeLoginThrows) { +TEST_F(E2E_Topic, GetTopicsBeforeLoginThrows) { RecordProperty("description", "Rejects get_topics before connect, and after connect but before login."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *setup_client = GetLoggedInClient(); - ASSERT_NO_THROW(setup_client->create_stream(stream_name)); + auto setup_client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(setup_client.CreateStream(stream_name)); TrackStream(stream_name); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); - ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->connect()); - ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); - ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)), std::exception); + ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)), std::exception); + ASSERT_NO_THROW(unauthenticated_client.Connect()); + ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)), std::exception); + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); + ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, GetTopicsReturnsEmptyForStreamWithoutTopics) { +TEST_F(E2E_Topic, GetTopicsReturnsEmptyForStreamWithoutTopics) { RecordProperty("description", "Returns an empty topic list for an existing stream that has no topics."); const std::string stream_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW({ - const auto topics = client->get_topics(make_string_identifier(stream_name)); + const auto topics = client.GetTopics(iggy::Identifier::String(stream_name)); EXPECT_TRUE(topics.empty()); }); } -TEST_F(LowLevelE2E_Topic, GetTopicsAfterTopicDeletionReturnsRemainingTopics) { +TEST_F(E2E_Topic, GetTopicsAfterTopicDeletionReturnsRemainingTopics) { RecordProperty("description", "Returns only non-deleted topics after a topic is deleted from the stream."); const std::string stream_name = GetRandomName(); const std::string deleted_topic = GetRandomName(); const std::string remaining_topic = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), deleted_topic, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), remaining_topic, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->delete_topic(make_string_identifier(stream_name), make_string_identifier(deleted_topic))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), deleted_topic, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), remaining_topic, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(deleted_topic))); ASSERT_NO_THROW({ - const auto topics = client->get_topics(make_string_identifier(stream_name)); + const auto topics = client.GetTopics(iggy::Identifier::String(stream_name)); ASSERT_EQ(topics.size(), 1u); - EXPECT_EQ(topics.front().name, remaining_topic); + EXPECT_EQ(topics.front().Name(), remaining_topic); }); } -TEST_F(LowLevelE2E_Topic, GetTopicsAfterTopicUpdateReturnsUpdatedInputFields) { +TEST_F(E2E_Topic, GetTopicsAfterTopicUpdateReturnsUpdatedInputFields) { RecordProperty("description", "Returns updated user-provided topic fields after a topic update."); const std::string stream_name = GetRandomName(); const std::string original_topic = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), original_topic, 2, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), original_topic, + iggy::TopicCreateOptions().SetPartitionsCount(2))); + ASSERT_NO_THROW(client.UpdateTopic( + iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic), updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); ASSERT_NO_THROW({ - const auto topics = client->get_topics(make_string_identifier(stream_name)); + const auto topics = client.GetTopics(iggy::Identifier::String(stream_name)); ASSERT_EQ(topics.size(), 1u); - EXPECT_EQ(topics.front().name, updated_topic_name); - EXPECT_EQ(topics.front().partitions_count, 2u); - EXPECT_EQ(topics.front().compression_algorithm, "gzip"); - EXPECT_EQ(topics.front().message_expiry, 1000u); - EXPECT_EQ(topics.front().max_topic_size, 1024ULL * 1024ULL * 1024ULL); + EXPECT_EQ(topics.front().Name(), updated_topic_name); + EXPECT_EQ(topics.front().PartitionsCount(), 2u); + EXPECT_EQ(topics.front().CompressionAlgorithm(), "gzip"); + EXPECT_EQ(topics.front().MessageExpiry(), 1000u); + EXPECT_EQ(topics.front().MaxTopicSize(), 1024ULL * 1024ULL * 1024ULL); }); } -TEST_F(LowLevelE2E_Topic, UpdateTopicWorksCorrectly) { +TEST_F(E2E_Topic, UpdateTopicWorksCorrectly) { RecordProperty("description", "Returns a topic summary that matches topic details after updating a topic."); const std::string stream_name = GetRandomName(); const std::string original_topic = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), original_topic, 2, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), original_topic, + iggy::TopicCreateOptions().SetPartitionsCount(2))); + ASSERT_NO_THROW(client.UpdateTopic( + iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic), updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); ASSERT_NO_THROW({ const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(updated_topic_name)); - const auto topics = client->get_topics(make_string_identifier(stream_name)); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(updated_topic_name)); + const auto topics = client.GetTopics(iggy::Identifier::String(stream_name)); ASSERT_EQ(topics.size(), 1u); const auto &topic_summary = topics.front(); - EXPECT_EQ(topic_summary.id, topic_details.id); - EXPECT_EQ(topic_summary.created_at, topic_details.created_at); - EXPECT_EQ(topic_summary.name, topic_details.name); - EXPECT_EQ(topic_summary.size_bytes, topic_details.size_bytes); - EXPECT_EQ(topic_summary.message_expiry, topic_details.message_expiry); - EXPECT_EQ(topic_summary.compression_algorithm, topic_details.compression_algorithm); - EXPECT_EQ(topic_summary.max_topic_size, topic_details.max_topic_size); - EXPECT_EQ(topic_summary.messages_count, topic_details.messages_count); - EXPECT_EQ(topic_summary.partitions_count, topic_details.partitions_count); + EXPECT_EQ(topic_summary.Id(), topic_details.Id()); + EXPECT_EQ(topic_summary.CreatedAt(), topic_details.CreatedAt()); + EXPECT_EQ(topic_summary.Name(), topic_details.Name()); + EXPECT_EQ(topic_summary.SizeBytes(), topic_details.SizeBytes()); + EXPECT_EQ(topic_summary.MessageExpiry(), topic_details.MessageExpiry()); + EXPECT_EQ(topic_summary.CompressionAlgorithm(), topic_details.CompressionAlgorithm()); + EXPECT_EQ(topic_summary.MaxTopicSize(), topic_details.MaxTopicSize()); + EXPECT_EQ(topic_summary.MessagesCount(), topic_details.MessagesCount()); + EXPECT_EQ(topic_summary.PartitionsCount(), topic_details.PartitionsCount()); }); } -TEST_F(LowLevelE2E_Topic, UpdateTopicDoesNotChangePartitionsCount) { +TEST_F(E2E_Topic, UpdateTopicDoesNotChangePartitionsCount) { RecordProperty("description", "Preserves the topic partition count after updating topic metadata."); const std::string stream_name = GetRandomName(); const std::string original_topic = GetRandomName(); const std::string updated_topic_name = GetRandomName(); constexpr std::uint32_t partitions_count = 3; - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), original_topic, partitions_count, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), original_topic, + iggy::TopicCreateOptions().SetPartitionsCount(partitions_count))); - ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {})); + ASSERT_NO_THROW(client.UpdateTopic( + iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic), updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); ASSERT_NO_THROW({ const auto topic_details = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(updated_topic_name)); - EXPECT_EQ(topic_details.partitions_count, partitions_count); - EXPECT_EQ(topic_details.partitions.size(), partitions_count); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(updated_topic_name)); + EXPECT_EQ(topic_details.PartitionsCount(), partitions_count); + EXPECT_EQ(topic_details.Partitions().size(), partitions_count); }); } -TEST_F(LowLevelE2E_Topic, UpdateTopicDoesNotChangeMessages) { +TEST_F(E2E_Topic, UpdateTopicDoesNotChangeMessages) { RecordProperty("description", "Keeps existing messages readable after updating topic metadata."); const std::string stream_name = GetRandomName(); const std::string original_topic = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + iggy::ffi::Client *message_client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), original_topic, 1, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), original_topic, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 1u); - const auto topic_id = created_stream.topics.front().id; + const auto created_stream = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(created_stream.Topics().size(), 1u); + const auto topic_id = created_stream.Topics().front().Id(); rust::Vec messages; messages.push_back( iggy::ffi::make_message(to_payload("message-before-topic-update"), rust::Vec())); - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id), - "partition_id", partition_id_bytes(0), std::move(messages))); + ASSERT_NO_THROW(message_client->send_messages(make_numeric_identifier(created_stream.Id()), + make_numeric_identifier(topic_id), "partition_id", + partition_id_bytes(0), std::move(messages))); - ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(original_topic), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {})); + ASSERT_NO_THROW(client.UpdateTopic( + iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic), updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); ASSERT_NO_THROW({ - const auto polled = client->poll_messages(make_numeric_identifier(created_stream.id), - make_string_identifier(updated_topic_name), 0, "consumer", - make_numeric_identifier(1), "offset", 0, 10, false); + const auto polled = message_client->poll_messages(make_numeric_identifier(created_stream.Id()), + make_string_identifier(updated_topic_name), 0, "consumer", + make_numeric_identifier(1), "offset", 0, 10, false); ASSERT_EQ(polled.count, 1u); ASSERT_EQ(polled.messages.size(), 1u); const std::string actual(polled.messages[0].payload.begin(), polled.messages[0].payload.end()); @@ -1057,113 +1000,138 @@ TEST_F(LowLevelE2E_Topic, UpdateTopicDoesNotChangeMessages) { }); } -TEST_F(LowLevelE2E_Topic, UpdateTopicWithAllOptionCombinationsUpdatesInputFields) { +TEST_F(E2E_Topic, UpdateTopicWithAllOptionCombinationsUpdatesInputFields) { RecordProperty("description", "Updates a topic across supported option combinations and verifies deterministic updated fields."); const std::string stream_name = GetRandomName(); std::string topic_name = GetRandomName(); - const std::vector compression_algorithms = {"none", "gzip"}; + struct CompressionOption { + std::string name; + iggy::CompressionAlgorithm value; + }; + const std::vector compression_algorithms = { + {"none", iggy::CompressionAlgorithm::None()}, + {"gzip", iggy::CompressionAlgorithm::Gzip()}, + }; struct ExpiryOption { - std::string kind; - std::uint64_t value; + std::string name; + iggy::Expiry value; }; const std::vector expiry_options = { - {"server_default", 0}, - {"never_expire", 0}, - {"duration", 1000}, + {"server_default", iggy::Expiry::ServerDefault()}, + {"never_expire", iggy::Expiry::NeverExpire()}, + {"duration", iggy::Expiry::Duration(1000)}, + }; + struct MaxTopicSizeOption { + std::string name; + iggy::MaxTopicSize value; + }; + const std::vector max_topic_sizes = { + {"server_default", iggy::MaxTopicSize::ServerDefault()}, + {"unlimited", iggy::MaxTopicSize::Unlimited()}, + {"1GiB", iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)}, }; - const std::vector max_topic_sizes = {"server_default", "unlimited", "1GiB"}; - - iggy::ffi::Client *client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 2, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(2))); for (const auto &compression_algorithm : compression_algorithms) { for (const auto &expiry_option : expiry_options) { for (const auto &max_topic_size : max_topic_sizes) { const std::string updated_topic_name = GetRandomName(); - SCOPED_TRACE("compression=" + compression_algorithm + ", expiry_kind=" + expiry_option.kind + - ", expiry_value=" + std::to_string(expiry_option.value) + - ", max_topic_size=" + max_topic_size); - - ASSERT_NO_THROW(client->update_topic( - make_string_identifier(stream_name), make_string_identifier(topic_name), updated_topic_name, - compression_algorithm, expiry_option.kind, expiry_option.value, max_topic_size, {})); + SCOPED_TRACE("compression=" + compression_algorithm.name + ", expiry_kind=" + expiry_option.name + + ", max_topic_size=" + max_topic_size.name); + + ASSERT_NO_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), + iggy::Identifier::String(topic_name), updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(compression_algorithm.value) + .SetMessageExpiry(expiry_option.value) + .SetMaxTopicSize(max_topic_size.value))); topic_name = updated_topic_name; } } } } -TEST_F(LowLevelE2E_Topic, UpdateTopicWithSameOptionsIsIdempotent) { +TEST_F(E2E_Topic, UpdateTopicWithSameOptionsIsIdempotent) { RecordProperty("description", "Calling update_topic twice with the same options returns the same topic details."); const std::string stream_name = GetRandomName(); const std::string original_topic = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), original_topic, 2, "none", - "server_default", 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), original_topic, + iggy::TopicCreateOptions().SetPartitionsCount(2))); const auto created_topic = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(original_topic)); - - ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {})); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(original_topic)); + + ASSERT_NO_THROW(client.UpdateTopic( + iggy::Identifier::String(stream_name), iggy::Identifier::Numeric(created_topic.Id()), updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); const auto first_update = - client->get_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id)); - - ASSERT_NO_THROW(client->update_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {})); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::Numeric(created_topic.Id())); + + ASSERT_NO_THROW(client.UpdateTopic( + iggy::Identifier::String(stream_name), iggy::Identifier::Numeric(created_topic.Id()), updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); const auto second_update = - client->get_topic(make_string_identifier(stream_name), make_numeric_identifier(created_topic.id)); - - EXPECT_EQ(second_update.id, first_update.id); - EXPECT_EQ(second_update.name, first_update.name); - EXPECT_EQ(second_update.partitions_count, first_update.partitions_count); - EXPECT_EQ(second_update.compression_algorithm, first_update.compression_algorithm); - EXPECT_EQ(second_update.message_expiry, first_update.message_expiry); - EXPECT_EQ(second_update.max_topic_size, first_update.max_topic_size); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::Numeric(created_topic.Id())); + + EXPECT_EQ(second_update.Id(), first_update.Id()); + EXPECT_EQ(second_update.Name(), first_update.Name()); + EXPECT_EQ(second_update.PartitionsCount(), first_update.PartitionsCount()); + EXPECT_EQ(second_update.CompressionAlgorithm(), first_update.CompressionAlgorithm()); + EXPECT_EQ(second_update.MessageExpiry(), first_update.MessageExpiry()); + EXPECT_EQ(second_update.MaxTopicSize(), first_update.MaxTopicSize()); } -TEST_F(LowLevelE2E_Topic, UpdateTopicWithDuplicateTopicNameThrows) { +TEST_F(E2E_Topic, UpdateTopicWithDuplicateTopicNameThrows) { RecordProperty("description", "Rejects renaming a topic to another topic's existing name."); const std::string stream_name = GetRandomName(); const std::string first_topic_name = GetRandomName(); const std::string second_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - - ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name), - second_topic_name, "gzip", "duration", 1000, "1GiB", {}), + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + + ASSERT_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name), + second_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))), std::exception); } -TEST_F(LowLevelE2E_Topic, UpdateTopicWithInvalidNamesThrows) { +TEST_F(E2E_Topic, UpdateTopicWithInvalidNamesThrows) { RecordProperty("description", "Rejects invalid topic names when updating a topic."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); const std::vector invalid_topic_names = { "", @@ -1173,344 +1141,291 @@ TEST_F(LowLevelE2E_Topic, UpdateTopicWithInvalidNamesThrows) { for (const auto &invalid_topic_name : invalid_topic_names) { SCOPED_TRACE("invalid_topic_name_length=" + std::to_string(invalid_topic_name.size())); - ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name), - invalid_topic_name, "gzip", "duration", 1000, "1GiB", {}), - std::exception); + ASSERT_THROW( + client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), + invalid_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))), + std::exception); } } -TEST_F(LowLevelE2E_Topic, UpdateTopicFailedValidationDoesNotMutateTopic) { +TEST_F(E2E_Topic, UpdateTopicFailedValidationDoesNotMutateTopic) { RecordProperty("description", "Keeps the topic unchanged when update_topic fails wrapper validation."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + auto client = GetLoggedInHighLevelClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW( - client->create_topic(make_string_identifier(stream_name), topic_name, 2, "gzip", "duration", 1000, "1GiB", {})); + client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(2) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); const auto topic_before_update = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); - - ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name), - updated_topic_name, "none", "duration", 2000, "not-a-size", {}), + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); + + ASSERT_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), + updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::None()) + .SetMessageExpiry(iggy::Expiry::Duration(2000)) + .SetMaxTopicSize(iggy::MaxTopicSize::ServerDefault()) + .SetRawEntries({{"max_topic_size", "not-a-size"}})), std::exception); const auto topic_after_failed_update = - client->get_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)); + client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)); - EXPECT_EQ(topic_after_failed_update.id, topic_before_update.id); - EXPECT_EQ(topic_after_failed_update.name, topic_before_update.name); - EXPECT_EQ(topic_after_failed_update.partitions_count, topic_before_update.partitions_count); - EXPECT_EQ(topic_after_failed_update.compression_algorithm, topic_before_update.compression_algorithm); - EXPECT_EQ(topic_after_failed_update.message_expiry, topic_before_update.message_expiry); - EXPECT_EQ(topic_after_failed_update.max_topic_size, topic_before_update.max_topic_size); + EXPECT_EQ(topic_after_failed_update.Id(), topic_before_update.Id()); + EXPECT_EQ(topic_after_failed_update.Name(), topic_before_update.Name()); + EXPECT_EQ(topic_after_failed_update.PartitionsCount(), topic_before_update.PartitionsCount()); + EXPECT_EQ(topic_after_failed_update.CompressionAlgorithm(), topic_before_update.CompressionAlgorithm()); + EXPECT_EQ(topic_after_failed_update.MessageExpiry(), topic_before_update.MessageExpiry()); + EXPECT_EQ(topic_after_failed_update.MaxTopicSize(), topic_before_update.MaxTopicSize()); - EXPECT_THROW(client->get_topic(make_string_identifier(stream_name), make_string_identifier(updated_topic_name)), + EXPECT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(updated_topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, UpdateTopicBeforeLoginThrows) { +TEST_F(E2E_Topic, UpdateTopicBeforeLoginThrows) { RecordProperty("description", "Rejects update_topic before connect, and after connect but before login."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); - ASSERT_THROW( - unauthenticated_client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {}), - std::exception); - ASSERT_NO_THROW(unauthenticated_client->connect()); - ASSERT_THROW( - unauthenticated_client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {}), - std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); - ASSERT_THROW( - unauthenticated_client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {}), - std::exception); + ASSERT_THROW(unauthenticated_client.UpdateTopic(iggy::Identifier::String(stream_name), + iggy::Identifier::String(topic_name), updated_topic_name), + std::exception); + ASSERT_NO_THROW(unauthenticated_client.Connect()); + ASSERT_THROW(unauthenticated_client.UpdateTopic(iggy::Identifier::String(stream_name), + iggy::Identifier::String(topic_name), updated_topic_name), + std::exception); + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); + ASSERT_THROW(unauthenticated_client.UpdateTopic(iggy::Identifier::String(stream_name), + iggy::Identifier::String(topic_name), updated_topic_name), + std::exception); } -TEST_F(LowLevelE2E_Topic, UpdateTopicOnNonExistentStreamThrows) { +TEST_F(E2E_Topic, UpdateTopicOnNonExistentStreamThrows) { RecordProperty("description", "Throws when updating a topic on a stream that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {}), + auto client = GetLoggedInHighLevelClient(); + ASSERT_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), + updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))), std::exception); } -TEST_F(LowLevelE2E_Topic, UpdateTopicOnNonExistentTopicThrows) { +TEST_F(E2E_Topic, UpdateTopicOnNonExistentTopicThrows) { RecordProperty("description", "Throws when updating a topic that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); const std::string updated_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_THROW(client->update_topic(make_string_identifier(stream_name), make_string_identifier(topic_name), - updated_topic_name, "gzip", "duration", 1000, "1GiB", {}), + ASSERT_THROW(client.UpdateTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name), + updated_topic_name, + iggy::TopicUpdateOptions() + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))), std::exception); } -TEST_F(LowLevelE2E_Topic, GetTopicsAfterStreamDeletionReturnsEmpty) { +TEST_F(E2E_Topic, GetTopicsAfterStreamDeletionReturnsEmpty) { RecordProperty("description", "Returns an empty topic list after deleting the stream that owned the topic."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); ASSERT_NO_THROW({ - const auto topics = client->get_topics(make_string_identifier(stream_name)); + const auto topics = client.GetTopics(iggy::Identifier::String(stream_name)); EXPECT_TRUE(topics.empty()); }); } -TEST_F(LowLevelE2E_Topic, PurgeTopicOnNonExistentStreamThrows) { +TEST_F(E2E_Topic, PurgeTopicOnNonExistentStreamThrows) { RecordProperty("description", "Throws when purging a topic on a stream that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + auto client = GetLoggedInHighLevelClient(); + ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, PurgeTopicAfterStreamDeletionThrows) { +TEST_F(E2E_Topic, PurgeTopicAfterStreamDeletionThrows) { RecordProperty("description", "Throws when purging a topic after its stream has been deleted."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - ASSERT_NO_THROW(client->delete_stream(make_string_identifier(stream_name))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name))); ForgetTrackedStream(stream_name); - ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, PurgeTopicOnNonExistentTopicThrows) { +TEST_F(E2E_Topic, PurgeTopicOnNonExistentTopicThrows) { RecordProperty("description", "Throws when purging a topic that does not exist."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), - std::exception); -} - -TEST_F(LowLevelE2E_Topic, PurgeTopicWithInvalidStreamIdentifierThrows) { - RecordProperty("description", "Rejects topic purge requests that use invalid stream identifier formats."); - const std::string stream_name = GetRandomName(); - const std::string topic_name = GetRandomName(); - - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); - TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->purge_topic(std::move(invalid_kind_id), make_string_identifier(topic_name)), std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->purge_topic(std::move(invalid_numeric_id), make_string_identifier(topic_name)), + ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } -TEST_F(LowLevelE2E_Topic, PurgeTopicWithInvalidTopicIdentifierThrows) { - RecordProperty("description", "Rejects topic purge requests that use invalid topic identifier formats."); - const std::string stream_name = GetRandomName(); - const std::string topic_name = GetRandomName(); - - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); - TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - - iggy::ffi::Identifier invalid_kind_id; - invalid_kind_id.kind = "invalid"; - invalid_kind_id.length = 4; - invalid_kind_id.value = {1, 0, 0, 0}; - ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), std::move(invalid_kind_id)), std::exception); - - iggy::ffi::Identifier invalid_numeric_id; - invalid_numeric_id.kind = "numeric"; - invalid_numeric_id.length = 1; - invalid_numeric_id.value.push_back(1); - ASSERT_THROW(client->purge_topic(make_string_identifier(stream_name), std::move(invalid_numeric_id)), - std::exception); -} - -TEST_F(LowLevelE2E_Topic, PurgeTopicPreservesTopicMetadata) { +TEST_F(E2E_Topic, PurgeTopicPreservesTopicMetadata) { RecordProperty("description", "Preserves topic metadata after purging its messages."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + iggy::ffi::Client *message_client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); ASSERT_NO_THROW( - client->create_topic(make_string_identifier(stream_name), topic_name, 3, "gzip", "duration", 1000, "1GiB", {})); + client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions() + .SetPartitionsCount(3) + .SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(1000)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL)))); - auto stream_before_purge = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_before_purge.topics.size(), 1u); - const auto topic_before_messages = stream_before_purge.topics.front(); + auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_before_purge.Topics().size(), 1u); rust::Vec messages; messages.push_back( iggy::ffi::make_message(to_payload("preserve-topic-metadata"), rust::Vec())); - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_before_purge.id), - make_numeric_identifier(topic_before_messages.id), "partition_id", - partition_id_bytes(1), std::move(messages))); - - stream_before_purge = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_before_purge.topics.size(), 1u); - const auto topic_with_messages = stream_before_purge.topics.front(); - EXPECT_GT(topic_with_messages.messages_count, 0u); - EXPECT_GT(topic_with_messages.size_bytes, 0u); - - ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name))); - - const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(stream_after_purge.topics.size(), 1u); - const auto topic_after_purge = stream_after_purge.topics.front(); - - EXPECT_EQ(topic_after_purge.id, topic_with_messages.id); - EXPECT_EQ(topic_after_purge.created_at, topic_with_messages.created_at); - EXPECT_EQ(topic_after_purge.name, topic_with_messages.name); - EXPECT_EQ(topic_after_purge.message_expiry, topic_with_messages.message_expiry); - EXPECT_EQ(topic_after_purge.compression_algorithm, topic_with_messages.compression_algorithm); - EXPECT_EQ(topic_after_purge.max_topic_size, topic_with_messages.max_topic_size); - EXPECT_EQ(topic_after_purge.partitions_count, topic_with_messages.partitions_count); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(topic_name), "partition_id", + partition_id_bytes(1), std::move(messages))); + + stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_before_purge.Topics().size(), 1u); + const auto &topic_with_messages = stream_before_purge.Topics().front(); + EXPECT_GT(topic_with_messages.MessagesCount(), 0u); + EXPECT_GT(topic_with_messages.SizeBytes(), 0u); + + ASSERT_NO_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name))); + + const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name)); + ASSERT_EQ(stream_after_purge.Topics().size(), 1u); + const auto &topic_after_purge = stream_after_purge.Topics().front(); + + EXPECT_EQ(topic_after_purge.Id(), topic_with_messages.Id()); + EXPECT_EQ(topic_after_purge.CreatedAt(), topic_with_messages.CreatedAt()); + EXPECT_EQ(topic_after_purge.Name(), topic_with_messages.Name()); + EXPECT_EQ(topic_after_purge.MessageExpiry(), topic_with_messages.MessageExpiry()); + EXPECT_EQ(topic_after_purge.CompressionAlgorithm(), topic_with_messages.CompressionAlgorithm()); + EXPECT_EQ(topic_after_purge.MaxTopicSize(), topic_with_messages.MaxTopicSize()); + EXPECT_EQ(topic_after_purge.PartitionsCount(), topic_with_messages.PartitionsCount()); } -TEST_F(LowLevelE2E_Topic, PurgeTopicRemovesOnlyTargetTopicMessages) { +TEST_F(E2E_Topic, PurgeTopicRemovesOnlyTargetTopicMessages) { RecordProperty("description", "Purges one topic's messages without affecting the other topics in the stream."); const std::string stream_name = GetRandomName(); const std::string first_topic_name = GetRandomName(); const std::string second_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + iggy::ffi::Client *message_client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 2u); - - std::uint32_t first_topic_id = 0; - std::uint32_t second_topic_id = 0; - bool first_topic_found = false; - bool second_topic_found = false; - for (const auto &topic : created_stream.topics) { - const std::string topic_name = static_cast(topic.name); - if (topic_name == first_topic_name) { - first_topic_id = topic.id; - first_topic_found = true; - } else if (topic_name == second_topic_name) { - second_topic_id = topic.id; - second_topic_found = true; - } - } - ASSERT_TRUE(first_topic_found); - ASSERT_TRUE(second_topic_found); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); rust::Vec first_topic_messages; for (std::uint32_t i = 0; i < 3; ++i) { first_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-first-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(first_topic_id), "partition_id", - partition_id_bytes(0), std::move(first_topic_messages))); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(first_topic_name), "partition_id", + partition_id_bytes(0), std::move(first_topic_messages))); rust::Vec second_topic_messages; for (std::uint32_t i = 0; i < 2; ++i) { second_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-second-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(second_topic_id), "partition_id", - partition_id_bytes(0), std::move(second_topic_messages))); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(second_topic_name), "partition_id", + partition_id_bytes(0), std::move(second_topic_messages))); - const auto stream_before_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_before_purge.messages_count, 5u); - EXPECT_GT(stream_before_purge.size_bytes, 0u); + const auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_before_purge.MessagesCount(), 5u); + EXPECT_GT(stream_before_purge.SizeBytes(), 0u); std::unordered_map messages_before_purge; - for (const auto &topic : stream_before_purge.topics) { - messages_before_purge[static_cast(topic.name)] = topic.messages_count; + for (const auto &topic : stream_before_purge.Topics()) { + messages_before_purge[topic.Name()] = topic.MessagesCount(); } EXPECT_EQ(messages_before_purge[first_topic_name], 3u); EXPECT_EQ(messages_before_purge[second_topic_name], 2u); - ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name))); + ASSERT_NO_THROW( + client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name))); - const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_purge.topics_count, 2u); - EXPECT_EQ(stream_after_purge.messages_count, 2u); - EXPECT_GT(stream_after_purge.size_bytes, 0u); - EXPECT_LT(stream_after_purge.size_bytes, stream_before_purge.size_bytes); + const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_purge.TopicsCount(), 2u); + EXPECT_EQ(stream_after_purge.MessagesCount(), 2u); + EXPECT_GT(stream_after_purge.SizeBytes(), 0u); + EXPECT_LT(stream_after_purge.SizeBytes(), stream_before_purge.SizeBytes()); std::unordered_map messages_after_purge; std::unordered_map sizes_after_purge; - for (const auto &topic : stream_after_purge.topics) { - const std::string topic_name = static_cast(topic.name); - messages_after_purge[topic_name] = topic.messages_count; - sizes_after_purge[topic_name] = topic.size_bytes; + for (const auto &topic : stream_after_purge.Topics()) { + messages_after_purge[topic.Name()] = topic.MessagesCount(); + sizes_after_purge[topic.Name()] = topic.SizeBytes(); } EXPECT_EQ(messages_after_purge[first_topic_name], 0u); EXPECT_EQ(sizes_after_purge[first_topic_name], 0u); @@ -1518,21 +1433,18 @@ TEST_F(LowLevelE2E_Topic, PurgeTopicRemovesOnlyTargetTopicMessages) { EXPECT_GT(sizes_after_purge[second_topic_name], 0u); } -TEST_F(LowLevelE2E_Topic, PurgeTopicAcrossMultiplePartitionsClearsAllPartitions) { +TEST_F(E2E_Topic, PurgeTopicAcrossMultiplePartitionsClearsAllPartitions) { RecordProperty("description", "Purges all messages from every partition in the topic."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + iggy::ffi::Client *message_client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 3, "none", "server_default", - 0, "server_default", {})); - - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 1u); - const std::uint32_t topic_id = created_stream.topics.front().id; + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(3))); for (std::uint32_t partition_id = 0; partition_id < 3; ++partition_id) { rust::Vec messages; @@ -1541,148 +1453,128 @@ TEST_F(LowLevelE2E_Topic, PurgeTopicAcrossMultiplePartitionsClearsAllPartitions) to_payload("purge-topic-partition-" + std::to_string(partition_id) + "-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(topic_id), "partition_id", - partition_id_bytes(partition_id), std::move(messages))); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(topic_name), "partition_id", + partition_id_bytes(partition_id), std::move(messages))); } - const auto stream_before_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_before_purge.messages_count, 6u); - ASSERT_EQ(stream_before_purge.topics.size(), 1u); - EXPECT_EQ(stream_before_purge.topics.front().partitions_count, 3u); - EXPECT_EQ(stream_before_purge.topics.front().messages_count, 6u); + const auto stream_before_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_before_purge.MessagesCount(), 6u); + ASSERT_EQ(stream_before_purge.Topics().size(), 1u); + EXPECT_EQ(stream_before_purge.Topics().front().PartitionsCount(), 3u); + EXPECT_EQ(stream_before_purge.Topics().front().MessagesCount(), 6u); - ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name))); + ASSERT_NO_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name))); - const auto stream_after_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_purge.messages_count, 0u); - ASSERT_EQ(stream_after_purge.topics.size(), 1u); - EXPECT_EQ(stream_after_purge.topics.front().partitions_count, 3u); - EXPECT_EQ(stream_after_purge.topics.front().messages_count, 0u); - EXPECT_EQ(stream_after_purge.topics.front().size_bytes, 0u); + const auto stream_after_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_purge.MessagesCount(), 0u); + ASSERT_EQ(stream_after_purge.Topics().size(), 1u); + EXPECT_EQ(stream_after_purge.Topics().front().PartitionsCount(), 3u); + EXPECT_EQ(stream_after_purge.Topics().front().MessagesCount(), 0u); + EXPECT_EQ(stream_after_purge.Topics().front().SizeBytes(), 0u); } -TEST_F(LowLevelE2E_Topic, PurgeTopicThenSendMessagesAgainSucceeds) { +TEST_F(E2E_Topic, PurgeTopicThenSendMessagesAgainSucceeds) { RecordProperty("description", "Allows sending fresh messages again after purging the topic."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + iggy::ffi::Client *message_client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); - - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 1u); - const std::uint32_t topic_id = created_stream.topics.front().id; + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); rust::Vec first_batch; first_batch.push_back( iggy::ffi::make_message(to_payload("before-topic-purge"), rust::Vec())); - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id), - "partition_id", partition_id_bytes(0), std::move(first_batch))); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(topic_name), "partition_id", + partition_id_bytes(0), std::move(first_batch))); - ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name))); + ASSERT_NO_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name))); rust::Vec second_batch; second_batch.push_back( iggy::ffi::make_message(to_payload("after-topic-purge-0"), rust::Vec())); second_batch.push_back( iggy::ffi::make_message(to_payload("after-topic-purge-1"), rust::Vec())); - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), make_numeric_identifier(topic_id), - "partition_id", partition_id_bytes(0), std::move(second_batch))); - - const auto stream_after_resend = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_resend.messages_count, 2u); - ASSERT_EQ(stream_after_resend.topics.size(), 1u); - EXPECT_EQ(stream_after_resend.topics.front().messages_count, 2u); - EXPECT_GT(stream_after_resend.topics.front().size_bytes, 0u); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(topic_name), "partition_id", + partition_id_bytes(0), std::move(second_batch))); + + const auto stream_after_resend = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_resend.MessagesCount(), 2u); + ASSERT_EQ(stream_after_resend.Topics().size(), 1u); + EXPECT_EQ(stream_after_resend.Topics().front().MessagesCount(), 2u); + EXPECT_GT(stream_after_resend.Topics().front().SizeBytes(), 0u); } -TEST_F(LowLevelE2E_Topic, PurgeTopicTwiceKeepsTargetTopicEmptyAndOtherTopicsUntouched) { +TEST_F(E2E_Topic, PurgeTopicTwiceKeepsTargetTopicEmptyAndOtherTopicsUntouched) { RecordProperty("description", "Allows purging the same topic twice and keeps the target topic empty without affecting siblings."); const std::string stream_name = GetRandomName(); const std::string first_topic_name = GetRandomName(); const std::string second_topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); + iggy::ffi::Client *message_client = GetLoggedInClient(); - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), first_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), second_topic_name, 1, "none", - "server_default", 0, "server_default", {})); - - const auto created_stream = client->get_stream(make_string_identifier(stream_name)); - ASSERT_EQ(created_stream.topics.size(), 2u); - - std::uint32_t first_topic_id = 0; - std::uint32_t second_topic_id = 0; - bool first_topic_found = false; - bool second_topic_found = false; - for (const auto &topic : created_stream.topics) { - const std::string topic_name = static_cast(topic.name); - if (topic_name == first_topic_name) { - first_topic_id = topic.id; - first_topic_found = true; - } else if (topic_name == second_topic_name) { - second_topic_id = topic.id; - second_topic_found = true; - } - } - ASSERT_TRUE(first_topic_found); - ASSERT_TRUE(second_topic_found); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), first_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), second_topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); rust::Vec first_topic_messages; for (std::uint32_t i = 0; i < 3; ++i) { first_topic_messages.push_back(iggy::ffi::make_message( to_payload("purge-topic-twice-first-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(first_topic_id), "partition_id", - partition_id_bytes(0), std::move(first_topic_messages))); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(first_topic_name), "partition_id", + partition_id_bytes(0), std::move(first_topic_messages))); rust::Vec second_topic_messages; for (std::uint32_t i = 0; i < 2; ++i) { second_topic_messages.push_back(iggy::ffi::make_message( to_payload("purge-topic-twice-second-" + std::to_string(i)), rust::Vec())); } - ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id), - make_numeric_identifier(second_topic_id), "partition_id", - partition_id_bytes(0), std::move(second_topic_messages))); + ASSERT_NO_THROW(message_client->send_messages(make_string_identifier(stream_name), + make_string_identifier(second_topic_name), "partition_id", + partition_id_bytes(0), std::move(second_topic_messages))); - ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name))); - const auto stream_after_first_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_first_purge.topics_count, 2u); - EXPECT_EQ(stream_after_first_purge.messages_count, 2u); + ASSERT_NO_THROW( + client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name))); + const auto stream_after_first_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_first_purge.TopicsCount(), 2u); + EXPECT_EQ(stream_after_first_purge.MessagesCount(), 2u); std::unordered_map messages_after_first_purge; std::unordered_map sizes_after_first_purge; - for (const auto &topic : stream_after_first_purge.topics) { - const std::string topic_name = static_cast(topic.name); - messages_after_first_purge[topic_name] = topic.messages_count; - sizes_after_first_purge[topic_name] = topic.size_bytes; + for (const auto &topic : stream_after_first_purge.Topics()) { + messages_after_first_purge[topic.Name()] = topic.MessagesCount(); + sizes_after_first_purge[topic.Name()] = topic.SizeBytes(); } EXPECT_EQ(messages_after_first_purge[first_topic_name], 0u); EXPECT_EQ(sizes_after_first_purge[first_topic_name], 0u); EXPECT_EQ(messages_after_first_purge[second_topic_name], 2u); EXPECT_GT(sizes_after_first_purge[second_topic_name], 0u); - ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), make_string_identifier(first_topic_name))); - const auto stream_after_second_purge = client->get_stream(make_string_identifier(stream_name)); - EXPECT_EQ(stream_after_second_purge.topics_count, 2u); - EXPECT_EQ(stream_after_second_purge.messages_count, 2u); + ASSERT_NO_THROW( + client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(first_topic_name))); + const auto stream_after_second_purge = client.GetStream(iggy::Identifier::String(stream_name)); + EXPECT_EQ(stream_after_second_purge.TopicsCount(), 2u); + EXPECT_EQ(stream_after_second_purge.MessagesCount(), 2u); std::unordered_map messages_after_second_purge; std::unordered_map sizes_after_second_purge; - for (const auto &topic : stream_after_second_purge.topics) { - const std::string topic_name = static_cast(topic.name); - messages_after_second_purge[topic_name] = topic.messages_count; - sizes_after_second_purge[topic_name] = topic.size_bytes; + for (const auto &topic : stream_after_second_purge.Topics()) { + messages_after_second_purge[topic.Name()] = topic.MessagesCount(); + sizes_after_second_purge[topic.Name()] = topic.SizeBytes(); } EXPECT_EQ(messages_after_second_purge[first_topic_name], 0u); EXPECT_EQ(sizes_after_second_purge[first_topic_name], 0u); @@ -1690,30 +1582,29 @@ TEST_F(LowLevelE2E_Topic, PurgeTopicTwiceKeepsTargetTopicEmptyAndOtherTopicsUnto EXPECT_GT(sizes_after_second_purge[second_topic_name], 0u); } -TEST_F(LowLevelE2E_Topic, PurgeTopicBeforeLoginThrows) { +TEST_F(E2E_Topic, PurgeTopicBeforeLoginThrows) { RecordProperty("description", "Throws when topic purge is attempted before authentication."); const std::string stream_name = GetRandomName(); const std::string topic_name = GetRandomName(); - iggy::ffi::Client *client = GetLoggedInClient(); - - ASSERT_NO_THROW(client->create_stream(stream_name)); + auto client = GetLoggedInHighLevelClient(); + ASSERT_NO_THROW(client.CreateStream(stream_name)); TrackStream(stream_name); - ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), topic_name, 1, "none", "server_default", - 0, "server_default", {})); + ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, + iggy::TopicCreateOptions().SetPartitionsCount(1))); - iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient(); + auto unauthenticated_client = GetLoggedOutHighLevelClient(); ASSERT_THROW( - unauthenticated_client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->connect()); + ASSERT_NO_THROW(unauthenticated_client.Connect()); ASSERT_THROW( - unauthenticated_client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); - ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy")); - ASSERT_NO_THROW(unauthenticated_client->disconnect()); + ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy")); + ASSERT_NO_THROW(unauthenticated_client.Disconnect()); ASSERT_THROW( - unauthenticated_client->purge_topic(make_string_identifier(stream_name), make_string_identifier(topic_name)), + unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name)), std::exception); } diff --git a/foreign/cpp/tests/unit/unit_tests.cpp b/foreign/cpp/tests/unit/unit_tests.cpp index 18db58f657..6f3eb10047 100644 --- a/foreign/cpp/tests/unit/unit_tests.cpp +++ b/foreign/cpp/tests/unit/unit_tests.cpp @@ -18,6 +18,8 @@ */ #include +#include +#include #include #include #include @@ -26,22 +28,6 @@ #include "iggy.hpp" -namespace { - -std::string option_key(const iggy::ffi::HeaderEntry &entry) { - return std::string(entry.key.value.begin(), entry.key.value.end()); -} - -std::vector option_value_bytes(const iggy::ffi::HeaderEntry &entry) { - return std::vector(entry.value.value.begin(), entry.value.value.end()); -} - -constexpr std::uint8_t kind_code(const iggy::ffi::HeaderKind kind) { - return static_cast(kind); -} - -} // namespace - TEST(CompressionAlgorithmTest, ReturnsExpectedValues) { EXPECT_EQ(iggy::CompressionAlgorithm::None().CompressionAlgorithmValue(), "none"); EXPECT_EQ(iggy::CompressionAlgorithm::Gzip().CompressionAlgorithmValue(), "gzip"); @@ -66,11 +52,6 @@ TEST(SystemSnapshotTypeTest, ReturnsExpectedValues) { EXPECT_EQ(iggy::SystemSnapshotType::All().SnapshotTypeValue(), "all"); } -TEST(IdKindTest, ReturnsExpectedValues) { - EXPECT_EQ(iggy::IdKind::Numeric().IdKindValue(), "numeric"); - EXPECT_EQ(iggy::IdKind::String().IdKindValue(), "string"); -} - TEST(MaxTopicSizeTest, ReturnsExpectedValues) { EXPECT_EQ(iggy::MaxTopicSize::ServerDefault().MaxTopicSizeValue(), "server_default"); EXPECT_EQ(iggy::MaxTopicSize::Unlimited().MaxTopicSizeValue(), "unlimited"); @@ -116,69 +97,180 @@ TEST(ExpiryTest, ReturnsExpectedKindAndValue) { EXPECT_EQ(duration.ExpiryValue(), static_cast(15)); } -TEST(TopicOptionTest, SegmentSizeEncodesLittleEndianUint64) { - const auto option = iggy::TopicOption::SegmentSize(0x0102030405060708ULL); +TEST(TopicCreateOptionsTest, DefaultHasNoValues) { + const iggy::TopicCreateOptions options; + EXPECT_FALSE(options.PartitionsCount().has_value()); + EXPECT_FALSE(options.CompressionAlgorithm().has_value()); + EXPECT_FALSE(options.MessageExpiry().has_value()); + EXPECT_FALSE(options.MaxTopicSize().has_value()); + EXPECT_FALSE(options.SegmentSize().has_value()); + EXPECT_FALSE(options.EnforceFsync().has_value()); + EXPECT_FALSE(options.MessagesRequiredToSave().has_value()); + EXPECT_FALSE(options.SizeOfMessagesRequiredToSave().has_value()); + EXPECT_FALSE(options.PreallocateSegments().has_value()); + EXPECT_TRUE(options.RawEntries().empty()); +} + +TEST(TopicCreateOptionsTest, PartitionsCountStoresValue) { + iggy::TopicCreateOptions options; + options.SetPartitionsCount(3); + ASSERT_TRUE(options.PartitionsCount().has_value()); + EXPECT_EQ(*options.PartitionsCount(), 3u); + options.SetPartitionsCount(1000); + EXPECT_EQ(*options.PartitionsCount(), 1000u); +} - EXPECT_EQ(option.key.kind, kind_code(iggy::ffi::HeaderKind::String)); - EXPECT_EQ(option_key(option), "segment_size"); - EXPECT_EQ(option.value.kind, kind_code(iggy::ffi::HeaderKind::Uint64)); - EXPECT_EQ(option_value_bytes(option), (std::vector{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01})); +TEST(TopicCreateOptionsTest, SegmentSizeStoresValue) { + iggy::TopicCreateOptions options; + options.SetSegmentSize(0x0102030405060708ULL); + ASSERT_TRUE(options.SegmentSize().has_value()); + EXPECT_EQ(*options.SegmentSize(), 0x0102030405060708ULL); } -TEST(TopicOptionTest, EnforceFsyncEncodesSingleBoolByte) { - const auto enabled = iggy::TopicOption::EnforceFsync(true); +TEST(TopicCreateOptionsTest, EnforceFsyncStoresBool) { + iggy::TopicCreateOptions enabled; + enabled.SetEnforceFsync(true); + ASSERT_TRUE(enabled.EnforceFsync().has_value()); + EXPECT_EQ(*enabled.EnforceFsync(), true); - EXPECT_EQ(enabled.key.kind, kind_code(iggy::ffi::HeaderKind::String)); - EXPECT_EQ(option_key(enabled), "enforce_fsync"); - EXPECT_EQ(enabled.value.kind, kind_code(iggy::ffi::HeaderKind::Bool)); - EXPECT_EQ(option_value_bytes(enabled), (std::vector{1})); + iggy::TopicCreateOptions disabled; + disabled.SetEnforceFsync(false); + ASSERT_TRUE(disabled.EnforceFsync().has_value()); + EXPECT_EQ(*disabled.EnforceFsync(), false); +} - const auto disabled = iggy::TopicOption::EnforceFsync(false); +TEST(TopicCreateOptionsTest, MessagesRequiredToSaveStoresValue) { + iggy::TopicCreateOptions options; + options.SetMessagesRequiredToSave(0x01020304U); + ASSERT_TRUE(options.MessagesRequiredToSave().has_value()); + EXPECT_EQ(*options.MessagesRequiredToSave(), 0x01020304U); +} - EXPECT_EQ(option_key(disabled), "enforce_fsync"); - EXPECT_EQ(disabled.value.kind, kind_code(iggy::ffi::HeaderKind::Bool)); - EXPECT_EQ(option_value_bytes(disabled), (std::vector{0})); +TEST(TopicCreateOptionsTest, SizeOfMessagesRequiredToSaveStoresValue) { + iggy::TopicCreateOptions options; + options.SetSizeOfMessagesRequiredToSave(1024ULL * 1024ULL); + ASSERT_TRUE(options.SizeOfMessagesRequiredToSave().has_value()); + EXPECT_EQ(*options.SizeOfMessagesRequiredToSave(), 1024ULL * 1024ULL); } -TEST(TopicOptionTest, MessagesRequiredToSaveEncodesLittleEndianUint32) { - const auto option = iggy::TopicOption::MessagesRequiredToSave(0x01020304U); +TEST(TopicCreateOptionsTest, PreallocateSegmentsStoresBool) { + iggy::TopicCreateOptions enabled; + enabled.SetPreallocateSegments(true); + ASSERT_TRUE(enabled.PreallocateSegments().has_value()); + EXPECT_EQ(*enabled.PreallocateSegments(), true); - EXPECT_EQ(option.key.kind, kind_code(iggy::ffi::HeaderKind::String)); - EXPECT_EQ(option_key(option), "messages_required_to_save"); - EXPECT_EQ(option.value.kind, kind_code(iggy::ffi::HeaderKind::Uint32)); - EXPECT_EQ(option_value_bytes(option), (std::vector{0x04, 0x03, 0x02, 0x01})); + iggy::TopicCreateOptions disabled; + disabled.SetPreallocateSegments(false); + ASSERT_TRUE(disabled.PreallocateSegments().has_value()); + EXPECT_EQ(*disabled.PreallocateSegments(), false); } -TEST(TopicOptionTest, SizeOfMessagesRequiredToSaveEncodesLittleEndianUint64) { - const auto option = iggy::TopicOption::SizeOfMessagesRequiredToSave(1024ULL * 1024ULL); +TEST(TopicCreateOptionsTest, MaximumValuesPreserved) { + iggy::TopicCreateOptions options; + options.SetSegmentSize(std::numeric_limits::max()); + ASSERT_TRUE(options.SegmentSize().has_value()); + EXPECT_EQ(*options.SegmentSize(), std::numeric_limits::max()); + + options.SetMessagesRequiredToSave(std::numeric_limits::max()); + ASSERT_TRUE(options.MessagesRequiredToSave().has_value()); + EXPECT_EQ(*options.MessagesRequiredToSave(), std::numeric_limits::max()); - EXPECT_EQ(option.key.kind, kind_code(iggy::ffi::HeaderKind::String)); - EXPECT_EQ(option_key(option), "size_of_messages_required_to_save"); - EXPECT_EQ(option.value.kind, kind_code(iggy::ffi::HeaderKind::Uint64)); - EXPECT_EQ(option_value_bytes(option), (std::vector{0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00})); + options.SetSizeOfMessagesRequiredToSave(std::numeric_limits::max()); + ASSERT_TRUE(options.SizeOfMessagesRequiredToSave().has_value()); + EXPECT_EQ(*options.SizeOfMessagesRequiredToSave(), std::numeric_limits::max()); } -TEST(TopicOptionTest, PreallocateSegmentsEncodesSingleBoolByte) { - const auto enabled = iggy::TopicOption::PreallocateSegments(true); +TEST(TopicCreateOptionsTest, ChainingAndOverwrite) { + iggy::TopicCreateOptions options; + options.SetSegmentSize(1024).SetEnforceFsync(true).SetMessagesRequiredToSave(512); + EXPECT_EQ(*options.SegmentSize(), 1024ULL); + EXPECT_EQ(*options.EnforceFsync(), true); + EXPECT_EQ(*options.MessagesRequiredToSave(), 512u); + options.SetSegmentSize(2048); + EXPECT_EQ(*options.SegmentSize(), 2048ULL); +} + +TEST(TopicCreateOptionsTest, CompressionAlgorithmAndExpiryAndMaxTopicSize) { + iggy::TopicCreateOptions options; + options.SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::Duration(15)) + .SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024)); + ASSERT_TRUE(options.CompressionAlgorithm().has_value()); + EXPECT_EQ(options.CompressionAlgorithm()->CompressionAlgorithmValue(), "gzip"); + ASSERT_TRUE(options.MessageExpiry().has_value()); + EXPECT_EQ(options.MessageExpiry()->ExpiryKind(), "duration"); + EXPECT_EQ(options.MessageExpiry()->ExpiryValue(), 15u); + ASSERT_TRUE(options.MaxTopicSize().has_value()); + EXPECT_EQ(options.MaxTopicSize()->MaxTopicSizeValue(), "1024"); +} + +TEST(TopicCreateOptionsTest, ServerDefaultSentinelsClearValues) { + iggy::TopicCreateOptions options; + options.SetMessageExpiry(iggy::Expiry::Duration(15)).SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024)); + ASSERT_TRUE(options.MessageExpiry().has_value()); + ASSERT_TRUE(options.MaxTopicSize().has_value()); - EXPECT_EQ(enabled.key.kind, kind_code(iggy::ffi::HeaderKind::String)); - EXPECT_EQ(option_key(enabled), "preallocate_segments"); - EXPECT_EQ(enabled.value.kind, kind_code(iggy::ffi::HeaderKind::Bool)); - EXPECT_EQ(option_value_bytes(enabled), (std::vector{1})); + options.SetMessageExpiry(iggy::Expiry::ServerDefault()).SetMaxTopicSize(iggy::MaxTopicSize::ServerDefault()); + EXPECT_FALSE(options.MessageExpiry().has_value()); + EXPECT_FALSE(options.MaxTopicSize().has_value()); +} + +TEST(TopicCreateOptionsTest, RawMapStoresForwardCompatibleKeys) { + iggy::TopicCreateOptions options; + options.SetRawEntries({{"custom_key", "custom_value"}}); + EXPECT_EQ(options.RawEntries().count("custom_key"), 1u); + EXPECT_EQ(options.RawEntries().at("custom_key"), "custom_value"); + options.SetRawEntries(std::map{{"a", "1"}, {"b", "2"}}); + EXPECT_EQ(options.RawEntries().size(), 3u); + EXPECT_EQ(options.RawEntries().at("a"), "1"); +} - const auto disabled = iggy::TopicOption::PreallocateSegments(false); +TEST(TopicUpdateOptionsTest, DefaultHasNoValues) { + const iggy::TopicUpdateOptions options; + EXPECT_FALSE(options.CompressionAlgorithm().has_value()); + EXPECT_FALSE(options.MessageExpiry().has_value()); + EXPECT_FALSE(options.MaxTopicSize().has_value()); + EXPECT_TRUE(options.RawEntries().empty()); +} - EXPECT_EQ(option_value_bytes(disabled), (std::vector{0})); +TEST(TopicUpdateOptionsTest, StoresUpdatableFields) { + iggy::TopicUpdateOptions options; + options.SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip()) + .SetMessageExpiry(iggy::Expiry::NeverExpire()) + .SetMaxTopicSize(iggy::MaxTopicSize::Unlimited()); + ASSERT_TRUE(options.CompressionAlgorithm().has_value()); + EXPECT_EQ(options.CompressionAlgorithm()->CompressionAlgorithmValue(), "gzip"); + ASSERT_TRUE(options.MessageExpiry().has_value()); + EXPECT_EQ(options.MessageExpiry()->ExpiryKind(), "never_expire"); + ASSERT_TRUE(options.MaxTopicSize().has_value()); + EXPECT_EQ(options.MaxTopicSize()->MaxTopicSizeValue(), "unlimited"); } -TEST(TopicOptionTest, MaximumValuesFillEveryValueByte) { - const auto segment_size = iggy::TopicOption::SegmentSize(std::numeric_limits::max()); - EXPECT_EQ(option_value_bytes(segment_size), - (std::vector{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})); +TEST(TopicUpdateOptionsTest, ServerDefaultSentinelsClearValues) { + iggy::TopicUpdateOptions options; + options.SetMessageExpiry(iggy::Expiry::Duration(15)).SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024)); + ASSERT_TRUE(options.MessageExpiry().has_value()); + ASSERT_TRUE(options.MaxTopicSize().has_value()); - const auto messages_required_to_save = - iggy::TopicOption::MessagesRequiredToSave(std::numeric_limits::max()); - EXPECT_EQ(option_value_bytes(messages_required_to_save), (std::vector{0xFF, 0xFF, 0xFF, 0xFF})); + options.SetMessageExpiry(iggy::Expiry::ServerDefault()).SetMaxTopicSize(iggy::MaxTopicSize::ServerDefault()); + EXPECT_FALSE(options.MessageExpiry().has_value()); + EXPECT_FALSE(options.MaxTopicSize().has_value()); +} + +TEST(TopicUpdateOptionsTest, RawMapStoresKeys) { + iggy::TopicUpdateOptions options; + options.SetRawEntries({{"message_expiry", "7 days"}}); + EXPECT_EQ(options.RawEntries().count("message_expiry"), 1u); + options.SetRawEntries(std::map{{"compression_algorithm", "gzip"}}); + EXPECT_EQ(options.RawEntries().size(), 2u); +} + +TEST(StreamUpdateOptionsTest, RawMapStoresKeys) { + iggy::StreamUpdateOptions options; + EXPECT_TRUE(options.RawEntries().empty()); + options.SetRawEntries({{"future_key", "future_value"}}); + EXPECT_EQ(options.RawEntries().count("future_key"), 1u); + EXPECT_EQ(options.RawEntries().at("future_key"), "future_value"); } TEST(IggyExceptionTest, StoresMessage) { @@ -190,18 +282,6 @@ TEST(IggyExceptionTest, StoresMessage) { EXPECT_EQ(std::string(from_string.what()), message); } -TEST(IggyBlockingClientTest, MovedFromOperationsThrow) { - auto client = iggy::IggyBlockingClient::Builder().Build(); - auto moved_to = std::move(client); - (void)moved_to; - - EXPECT_THROW(client.Connect(), iggy::IggyException); - EXPECT_THROW(client.Disconnect(), iggy::IggyException); - EXPECT_THROW(client.Shutdown(), iggy::IggyException); - EXPECT_THROW(client.Login("iggy", "iggy"), iggy::IggyException); - EXPECT_THROW(client.Logout(), iggy::IggyException); -} - TEST(AutoLoginKindTest, HasStableDiscriminantsAndZeroInitializedDefault) { EXPECT_EQ(static_cast(iggy::ffi::AutoLoginKind::Disabled), 0u); EXPECT_EQ(static_cast(iggy::ffi::AutoLoginKind::UsernamePassword), 1u);