This is an automated email from the ASF dual-hosted git repository.

slbotbm pushed a commit to branch cpp-high-level-client-1
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/cpp-high-level-client-1 by 
this push:
     new d698374b0 fixes
d698374b0 is described below

commit d698374b04eae05f91d4ef4fa77643b90e1e0de4
Author: Rimuksh Kansal <[email protected]>
AuthorDate: Sat Sep 12 01:24:47 2026 +0900

    fixes
---
 foreign/cpp/MODULE.bazel.lock         | 103 +++++++++++++++++++++++
 foreign/cpp/include/iggy.hpp          | 152 ++--------------------------------
 foreign/cpp/src/client.cpp            | 142 +++++++++++--------------------
 foreign/cpp/src/type_conversion.rs    | 127 ++++++++++++++--------------
 foreign/cpp/src/type_conversions.cpp  |  21 -----
 foreign/cpp/tests/e2e/partition.cpp   | 112 ++++++++++++++++---------
 foreign/cpp/tests/e2e/stream.cpp      |  70 ++++++++--------
 foreign/cpp/tests/e2e/topic.cpp       | 132 ++++++++++++++++++-----------
 foreign/cpp/tests/unit/unit_tests.cpp |  60 +++++++-------
 9 files changed, 440 insertions(+), 479 deletions(-)

diff --git a/foreign/cpp/MODULE.bazel.lock b/foreign/cpp/MODULE.bazel.lock
index c6f3c74b7..55a016bf6 100644
--- a/foreign/cpp/MODULE.bazel.lock
+++ b/foreign/cpp/MODULE.bazel.lock
@@ -483,6 +483,109 @@
           }
         }
       }
+    },
+    "@@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 9a1c133e0..230905fb5 100644
--- a/foreign/cpp/include/iggy.hpp
+++ b/foreign/cpp/include/iggy.hpp
@@ -167,7 +167,6 @@ class Identifier final {
   private:
     Identifier(Kind kind, std::variant<std::uint32_t, std::string> value) : 
kind_(kind), value_(std::move(value)) {}
 
-    static Identifier FromFfi(ffi::Identifier identifier);
     ffi::Identifier ToFfi() const;
 
     friend class IggyBlockingClient;
@@ -1004,8 +1003,14 @@ class Expiry final {
      * @brief Creates a time-based expiry policy.
      * @param micros Message lifetime in microseconds.
      * @return Time-based expiry policy.
+     * @throws std::invalid_argument if @p micros is zero.
      */
-    static Expiry Duration(std::uint64_t micros) { return Expiry("duration", 
micros); }
+    static Expiry Duration(std::uint64_t micros) {
+        if (micros == 0) {
+            throw std::invalid_argument("Expiry duration must be greater than 
zero");
+        }
+        return Expiry("duration", micros);
+    }
 
     /**
      * @brief Returns the expiry policy kind.
@@ -1054,8 +1059,7 @@ class TopicCreateOptions final {
 
     /**
      * @brief Returns the number of partitions to create.
-     * @return Configured partition count, or `std::nullopt` to use the server
-     *         default.
+     * @return Configured partition count, or `std::nullopt` to default to 1.
      */
     [[nodiscard]] std::optional<std::uint32_t> PartitionsCount() const 
noexcept { return partitions_count_; }
 
@@ -1539,146 +1543,6 @@ class PollingStrategy final {
     std::uint64_t polling_strategy_value_;
 };
 
-namespace detail {
-
-/// 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 <typename Value>
-std::vector<std::uint8_t> to_little_endian_bytes(const Value value) {
-    std::vector<std::uint8_t> bytes{};
-    bytes.reserve(sizeof(Value));
-    for (std::size_t index{}; index < sizeof(Value); ++index) {
-        bytes.push_back(static_cast<std::uint8_t>((value >> (index * 8)) & 
0xFF));
-    }
-
-    return bytes;
-}
-
-inline std::vector<std::uint8_t> to_bool_bytes(const bool value) {
-    std::vector<std::uint8_t> bytes{};
-    bytes.push_back(static_cast<std::uint8_t>(value ? 1 : 0));
-
-    return bytes;
-}
-
-inline std::vector<std::uint8_t> to_key_bytes(const std::string_view key) {
-    std::vector<std::uint8_t> bytes{};
-    bytes.reserve(key.size());
-    for (const char character : key) {
-        bytes.push_back(static_cast<std::uint8_t>(character));
-    }
-
-    return bytes;
-}
-
-/// An option key is always `String`-kinded. Only the value kind varies per 
key.
-inline HeaderEntry to_option_entry(const std::string_view key,
-                                   const HeaderKind value_kind,
-                                   std::vector<std::uint8_t> value) {
-    return HeaderEntry::Create(HeaderField::Create(HeaderKind::String, 
to_key_bytes(key)),
-                               HeaderField::Create(value_kind, 
std::move(value)));
-}
-
-}  // namespace detail
-
-/**
- * @brief Creates catalog entries for ResourceOptions::Explicit().
- *
- * 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.
- *
- * For example, pass
- * `ResourceOptions::Explicit({TopicOption::SegmentSize(1024 * 1024)})` to
- * CreateTopic(). The server rejects unknown keys and values encoded with a
- * different kind.
- *
- * @note These options are accepted only during topic creation. UpdateTopic()
- *       rejects them because they define how partition storage is created.
- *       Changing them later could leave existing and new segments with
- *       different storage settings.
- */
-class TopicOption 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.
-     */
-    static HeaderEntry SegmentSize(const std::uint64_t bytes) {
-        return detail::to_option_entry("segment_size", HeaderKind::Uint64, 
detail::to_little_endian_bytes(bytes));
-    }
-
-    /**
-     * @brief Choose the message completion policy.
-     *
-     * @param value The policy, defaulting to Replicated.
-     * @return Encoded topic option entry.
-     */
-    static HeaderEntry Durability(const ::iggy::Durability value = 
::iggy::Durability::Replicated) {
-        return detail::to_option_entry("durability", HeaderKind::String, 
detail::to_key_bytes(to_string(value)));
-    }
-
-    /**
-     * @brief Choose explicit offset completion independently of message 
durability.
-     * @param value The policy, defaulting to Replicated.
-     * @return Encoded topic option entry.
-     */
-    static HeaderEntry ConsumerOffsetDurability(const ::iggy::Durability value 
= ::iggy::Durability::Replicated) {
-        return detail::to_option_entry("consumer_offset_durability", 
HeaderKind::String,
-                                       detail::to_key_bytes(to_string(value)));
-    }
-
-    /**
-     * @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.
-     */
-    static HeaderEntry MessagesRequiredToSave(const std::uint32_t messages) {
-        return detail::to_option_entry("messages_required_to_save", 
HeaderKind::Uint32,
-                                       
detail::to_little_endian_bytes(messages));
-    }
-
-    /**
-     * @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.
-     */
-    static HeaderEntry SizeOfMessagesRequiredToSave(const std::uint64_t bytes) 
{
-        return detail::to_option_entry("size_of_messages_required_to_save", 
HeaderKind::Uint64,
-                                       detail::to_little_endian_bytes(bytes));
-    }
-
-    /**
-     * @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.
-     */
-    static HeaderEntry PreallocateSegments(const bool enabled) {
-        return detail::to_option_entry("preallocate_segments", 
HeaderKind::Bool, detail::to_bool_bytes(enabled));
-    }
-
-  private:
-    TopicOption() = delete;
-};
-
 /**
  * @brief Owning client connection to an Apache Iggy server.
  *
diff --git a/foreign/cpp/src/client.cpp b/foreign/cpp/src/client.cpp
index c2596a8f5..1a8546282 100644
--- a/foreign/cpp/src/client.cpp
+++ b/foreign/cpp/src/client.cpp
@@ -21,6 +21,48 @@
 
 namespace iggy {
 
+namespace {
+
+rust::Vec<ffi::HeaderEntry> ToFfiRawOptions(const std::map<std::string, 
std::string> &raw_options) {
+    rust::Vec<ffi::HeaderEntry> ffi_options;
+    ffi_options.reserve(raw_options.size());
+    for (const auto &[key, value] : raw_options) {
+        ffi::HeaderEntry ffi_entry;
+        ffi_entry.key.kind = static_cast<std::uint8_t>(HeaderKind::String);
+        ffi_entry.key.value.reserve(key.size());
+        for (const char character : key) {
+            
ffi_entry.key.value.push_back(static_cast<std::uint8_t>(character));
+        }
+        ffi_entry.value.kind = static_cast<std::uint8_t>(HeaderKind::String);
+        ffi_entry.value.value.reserve(value.size());
+        for (const char character : value) {
+            
ffi_entry.value.value.push_back(static_cast<std::uint8_t>(character));
+        }
+        ffi_options.push_back(std::move(ffi_entry));
+    }
+    return ffi_options;
+}
+
+template <typename FfiOptions, typename Options>
+void SetMutableTopicOptions(FfiOptions &ffi_options, const Options &options) {
+    if (const auto value = options.CompressionAlgorithm()) {
+        ffi_options.has_compression_algorithm = true;
+        ffi_options.compression_algorithm     = std::string(value->Value());
+    }
+    if (const auto value = options.MessageExpiry()) {
+        ffi_options.has_message_expiry   = true;
+        ffi_options.message_expiry_kind  = std::string(value->Kind());
+        ffi_options.message_expiry_value = value->Value();
+    }
+    if (const auto value = options.MaxTopicSize()) {
+        ffi_options.has_max_topic_size = true;
+        ffi_options.max_topic_size     = std::string(value->Value());
+    }
+    ffi_options.raw_options = ToFfiRawOptions(options.RawEntries());
+}
+
+}  // namespace
+
 IggyBlockingClient::IggyBlockingClient(IggyBlockingClient &&other) noexcept
     : client_(std::exchange(other.client_, nullptr)) {}
 
@@ -68,22 +110,7 @@ StreamDetails IggyBlockingClient::CreateStream(std::string 
name) {
 
 void IggyBlockingClient::UpdateStream(const Identifier &stream, std::string 
name, const StreamUpdateOptions &options) {
     RethrowAsIggyException([this, &stream, &name, &options] {
-        rust::Vec<ffi::HeaderEntry> ffi_options;
-        ffi_options.reserve(options.RawEntries().size());
-        for (const auto &entry : options.RawEntries()) {
-            ffi::HeaderEntry ffi_entry;
-            ffi_entry.key.kind = static_cast<std::uint8_t>(HeaderKind::String);
-            ffi_entry.key.value.reserve(entry.first.size());
-            for (char character : entry.first) {
-                
ffi_entry.key.value.push_back(static_cast<std::uint8_t>(character));
-            }
-            ffi_entry.value.kind = 
static_cast<std::uint8_t>(HeaderKind::String);
-            ffi_entry.value.value.reserve(entry.second.size());
-            for (char character : entry.second) {
-                
ffi_entry.value.value.push_back(static_cast<std::uint8_t>(character));
-            }
-            ffi_options.push_back(std::move(ffi_entry));
-        }
+        auto ffi_options = ToFfiRawOptions(options.RawEntries());
         Handle()->update_stream(stream.ToFfi(), name, std::move(ffi_options));
     });
 }
@@ -117,7 +144,8 @@ TopicDetails IggyBlockingClient::CreateTopic(const 
Identifier &stream,
                                              std::string name,
                                              const TopicCreateOptions 
&options) {
     return RethrowAsIggyException([this, &stream, &name, &options] {
-        ffi::TopicCreateOptions ffi_options;
+        ffi::TopicCreateOptions ffi_options{};
+        SetMutableTopicOptions(ffi_options, options);
         if (auto value = options.PartitionsCount()) {
             ffi_options.has_partitions_count = true;
             ffi_options.partitions_count     = *value;
@@ -125,29 +153,6 @@ TopicDetails IggyBlockingClient::CreateTopic(const 
Identifier &stream,
             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->Value());
-        } 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->Kind());
-            ffi_options.message_expiry_value = value->Value();
-        } 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->Value());
-        } 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;
@@ -190,22 +195,6 @@ TopicDetails IggyBlockingClient::CreateTopic(const 
Identifier &stream,
             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<std::uint8_t>(HeaderKind::String);
-            ffi_entry.key.value.reserve(entry.first.size());
-            for (char character : entry.first) {
-                
ffi_entry.key.value.push_back(static_cast<std::uint8_t>(character));
-            }
-            ffi_entry.value.kind = 
static_cast<std::uint8_t>(HeaderKind::String);
-            ffi_entry.value.value.reserve(entry.second.size());
-            for (char character : entry.second) {
-                
ffi_entry.value.value.push_back(static_cast<std::uint8_t>(character));
-            }
-            ffi_options.raw_options.push_back(std::move(ffi_entry));
-        }
-
         return TopicDetails::FromFfi(Handle()->create_topic(stream.ToFfi(), 
name, std::move(ffi_options)));
     });
 }
@@ -215,45 +204,8 @@ void IggyBlockingClient::UpdateTopic(const Identifier 
&stream,
                                      std::string name,
                                      const TopicUpdateOptions &options) {
     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->Value());
-        } 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->Kind());
-            ffi_options.message_expiry_value = value->Value();
-        } 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->Value());
-        } 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<std::uint8_t>(HeaderKind::String);
-            ffi_entry.key.value.reserve(entry.first.size());
-            for (char character : entry.first) {
-                
ffi_entry.key.value.push_back(static_cast<std::uint8_t>(character));
-            }
-            ffi_entry.value.kind = 
static_cast<std::uint8_t>(HeaderKind::String);
-            ffi_entry.value.value.reserve(entry.second.size());
-            for (char character : entry.second) {
-                
ffi_entry.value.value.push_back(static_cast<std::uint8_t>(character));
-            }
-            ffi_options.raw_options.push_back(std::move(ffi_entry));
-        }
+        ffi::TopicUpdateOptions ffi_options{};
+        SetMutableTopicOptions(ffi_options, options);
 
         Handle()->update_topic(stream.ToFfi(), topic.ToFfi(), name, 
std::move(ffi_options));
     });
diff --git a/foreign/cpp/src/type_conversion.rs 
b/foreign/cpp/src/type_conversion.rs
index e1eeae7b8..0d95a692f 100644
--- a/foreign/cpp/src/type_conversion.rs
+++ b/foreign/cpp/src/type_conversion.rs
@@ -15,7 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use crate::ffi;
+use std::{collections::BTreeMap, str::FromStr};
+
 use bytes::Bytes;
 use iggy::prelude::{
     CompressionAlgorithm as RustCompressionAlgorithm,
@@ -42,7 +43,8 @@ use iggy_common::{
     TopicPermissions as RustTopicPermissions, TransportEndpoints as 
RustTransportEndpoints,
     UserInfo as RustUserInfo, UserInfoDetails as RustUserInfoDetails, 
UserStatus as RustUserStatus,
 };
-use std::collections::BTreeMap;
+
+use crate::ffi;
 
 impl From<RustIdentifier> for ffi::Identifier {
     fn from(identifier: RustIdentifier) -> Self {
@@ -517,6 +519,30 @@ pub(crate) fn ffi_options_to_raw(
     Ok(raw)
 }
 
+fn parse_message_expiry(kind: &str, value: u64) -> Result<RustIggyExpiry, 
String> {
+    match kind {
+        "server_default" | "default" => Ok(RustIggyExpiry::ServerDefault),
+        "never_expire" => Ok(RustIggyExpiry::NeverExpire),
+        "duration" if value == 0 => {
+            Err("message expiry duration must be greater than 
zero".to_string())
+        }
+        "duration" => Ok(RustIggyExpiry::ExpireDuration(
+            iggy::prelude::IggyDuration::from(value),
+        )),
+        _ => Err(format!("invalid message expiry kind '{kind}'")),
+    }
+}
+
+fn parse_compression_algorithm(value: &str) -> 
Result<RustCompressionAlgorithm, String> {
+    RustCompressionAlgorithm::from_str(value)
+        .map_err(|error| format!("invalid compression algorithm '{value}': 
{error}"))
+}
+
+fn parse_max_topic_size(value: &str) -> Result<RustMaxTopicSize, String> {
+    RustMaxTopicSize::from_str(value)
+        .map_err(|error| format!("invalid max topic size '{value}': {error}"))
+}
+
 impl From<RustOptionSpec> for ffi::OptionSpec {
     fn from(spec: RustOptionSpec) -> Self {
         ffi::OptionSpec {
@@ -532,47 +558,21 @@ impl TryFrom<ffi::TopicCreateOptions> for 
RustTopicCreateOptions {
     type Error = String;
 
     fn try_from(options: ffi::TopicCreateOptions) -> Result<Self, Self::Error> 
{
-        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
-                        )
-                    },
-                )?,
-            )
+            Some(parse_compression_algorithm(&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
-                    ));
-                }
-            })
+            Some(parse_message_expiry(
+                &options.message_expiry_kind,
+                options.message_expiry_value,
+            )?)
         } 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
-                    )
-                })?,
-            )
+            Some(parse_max_topic_size(&options.max_topic_size)?)
         } else {
             None
         };
@@ -592,7 +592,13 @@ impl TryFrom<ffi::TopicCreateOptions> for 
RustTopicCreateOptions {
         } else {
             RustDurability::default()
         };
-        let raw = ffi_options_to_raw(options.raw_options)?;
+        let mut raw = ffi_options_to_raw(options.raw_options)?;
+        if options.has_durability {
+            raw.remove("durability");
+        }
+        if options.has_consumer_offset_durability {
+            raw.remove("consumer_offset_durability");
+        }
         Ok(RustTopicCreateOptions {
             partitions_count: if options.has_partitions_count {
                 Some(options.partitions_count)
@@ -635,47 +641,21 @@ impl TryFrom<ffi::TopicUpdateOptions> for 
RustTopicUpdateOptions {
     type Error = String;
 
     fn try_from(options: ffi::TopicUpdateOptions) -> Result<Self, Self::Error> 
{
-        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
-                        )
-                    },
-                )?,
-            )
+            Some(parse_compression_algorithm(&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
-                    ));
-                }
-            })
+            Some(parse_message_expiry(
+                &options.message_expiry_kind,
+                options.message_expiry_value,
+            )?)
         } 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
-                    )
-                })?,
-            )
+            Some(parse_max_topic_size(&options.max_topic_size)?)
         } else {
             None
         };
@@ -1039,3 +1019,16 @@ impl From<RustSendMessagesResponse> for 
ffi::SendMessagesResponse {
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn should_reject_zero_message_expiry_duration() {
+        assert_eq!(
+            parse_message_expiry("duration", 0).err().as_deref(),
+            Some("message expiry duration must be greater than zero")
+        );
+    }
+}
diff --git a/foreign/cpp/src/type_conversions.cpp 
b/foreign/cpp/src/type_conversions.cpp
index c3b08fd08..31a1d33b9 100644
--- a/foreign/cpp/src/type_conversions.cpp
+++ b/foreign/cpp/src/type_conversions.cpp
@@ -32,27 +32,6 @@ LoginInfo LoginInfo::FromFfi(ffi::LoginInfo login_info) {
     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<std::uint32_t>(identifier.value[0]) |
-                        (static_cast<std::uint32_t>(identifier.value[1]) << 
8U) |
-                        (static_cast<std::uint32_t>(identifier.value[2]) << 
16U) |
-                        (static_cast<std::uint32_t>(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) {
diff --git a/foreign/cpp/tests/e2e/partition.cpp 
b/foreign/cpp/tests/e2e/partition.cpp
index 74efc5ce9..d72442710 100644
--- a/foreign/cpp/tests/e2e/partition.cpp
+++ b/foreign/cpp/tests/e2e/partition.cpp
@@ -60,16 +60,16 @@ TEST_F(E2E_Partition, CreatePartitionsBeforeLoginThrows) {
 
     ASSERT_THROW(
         client.CreatePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(client.Connect());
     ASSERT_THROW(
         client.CreatePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
     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);
+        iggy::IggyException);
 }
 
 TEST_F(E2E_Partition, CreatePartitionsOnNonExistentResourcesThrows) {
@@ -88,33 +88,50 @@ TEST_F(E2E_Partition, 
CreatePartitionsOnNonExistentResourcesThrows) {
 
     ASSERT_THROW(
         client.CreatePartitions(iggy::Identifier::String(missing_stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
     ASSERT_THROW(
         client.CreatePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(missing_topic_name), 1),
-        std::exception);
+        iggy::IggyException);
 }
 
 TEST_F(E2E_Partition, CreatePartitionsWithInvalidIdentifiersThrows) {
-    RecordProperty("description", "Rejects invalid stream or topic identifiers 
before creating partitions.");
+    RecordProperty("description", "Rejects create_partitions requests that use 
malformed stream or topic identifiers.");
     const std::string stream_name = GetRandomName();
     const std::string topic_name  = GetRandomName();
 
-    auto client = GetLoggedInHighLevelClient();
+    iggy::ffi::Client *client = GetLoggedInClient();
 
-    ASSERT_NO_THROW(client.CreateStream(stream_name));
+    ASSERT_NO_THROW(client->create_stream(stream_name));
     TrackStream(stream_name);
-    ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name,
-                                       
iggy::TopicCreateOptions().SetPartitionsCount(1)));
-
-    ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(""), 
iggy::Identifier::String(topic_name), 1),
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), 
topic_name,
+                                         make_topic_create_options(1, "none", 
"never_expire", 0, "server_default")));
+
+    iggy::ffi::Identifier invalid_stream_kind;
+    invalid_stream_kind.kind   = "invalid";
+    invalid_stream_kind.length = 4;
+    invalid_stream_kind.value  = {1, 0, 0, 0};
+    ASSERT_THROW(client->create_partitions(std::move(invalid_stream_kind), 
make_string_identifier(topic_name), 1),
                  std::exception);
-    
ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(std::string(256, 
'a')),
-                                         iggy::Identifier::String(topic_name), 
1),
+
+    iggy::ffi::Identifier invalid_stream_length;
+    invalid_stream_length.kind   = "numeric";
+    invalid_stream_length.length = 1;
+    invalid_stream_length.value.push_back(1);
+    ASSERT_THROW(client->create_partitions(std::move(invalid_stream_length), 
make_string_identifier(topic_name), 1),
                  std::exception);
-    
ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(""), 1),
+
+    iggy::ffi::Identifier invalid_topic_kind;
+    invalid_topic_kind.kind   = "invalid";
+    invalid_topic_kind.length = 4;
+    invalid_topic_kind.value  = {1, 0, 0, 0};
+    
ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), 
std::move(invalid_topic_kind), 1),
                  std::exception);
-    ASSERT_THROW(client.CreatePartitions(iggy::Identifier::String(stream_name),
-                                         
iggy::Identifier::String(std::string(256, 'a')), 1),
+
+    iggy::ffi::Identifier invalid_topic_length;
+    invalid_topic_length.kind   = "numeric";
+    invalid_topic_length.length = 1;
+    invalid_topic_length.value.push_back(1);
+    
ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), 
std::move(invalid_topic_length), 1),
                  std::exception);
 }
 
@@ -157,7 +174,7 @@ TEST_F(E2E_Partition, 
CreatePartitionsWithBoundaryPartitionsCountValues) {
             ASSERT_THROW(
                 client.CreatePartitions(iggy::Identifier::String(stream_name),
                                         
iggy::Identifier::String(test_case.topic_name), test_case.partitions_count),
-                std::exception);
+                iggy::IggyException);
         }
     }
 
@@ -267,7 +284,7 @@ TEST_F(E2E_Partition, 
DeleteMorePartitionsThanExistingThrows) {
             ASSERT_THROW(
                 client.DeletePartitions(iggy::Identifier::String(stream_name),
                                         
iggy::Identifier::String(test_case.topic_name), test_case.partitions_count),
-                std::exception);
+                iggy::IggyException);
         }
     }
 
@@ -327,7 +344,7 @@ TEST_F(E2E_Partition, 
DeletePartitionsFromTopicWithZeroPartitionsThrows) {
 
     ASSERT_THROW(
         client.DeletePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
 
     ASSERT_NO_THROW({
         const auto stream_details = 
client.GetStream(iggy::Identifier::String(stream_name));
@@ -347,16 +364,16 @@ TEST_F(E2E_Partition, DeletePartitionsBeforeLoginThrows) {
 
     ASSERT_THROW(
         client.DeletePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(client.Connect());
     ASSERT_THROW(
         client.DeletePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
     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);
+        iggy::IggyException);
 }
 
 TEST_F(E2E_Partition, DeletePartitionsOnNonExistentResourcesThrows) {
@@ -375,33 +392,50 @@ TEST_F(E2E_Partition, 
DeletePartitionsOnNonExistentResourcesThrows) {
 
     ASSERT_THROW(
         client.DeletePartitions(iggy::Identifier::String(missing_stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
     ASSERT_THROW(
         client.DeletePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(missing_topic_name), 1),
-        std::exception);
+        iggy::IggyException);
 }
 
 TEST_F(E2E_Partition, DeletePartitionsWithInvalidIdentifiersThrows) {
-    RecordProperty("description", "Rejects invalid stream or topic identifiers 
before deleting partitions.");
+    RecordProperty("description", "Rejects delete_partitions requests that use 
malformed stream or topic identifiers.");
     const std::string stream_name = GetRandomName();
     const std::string topic_name  = GetRandomName();
 
-    auto client = GetLoggedInHighLevelClient();
+    iggy::ffi::Client *client = GetLoggedInClient();
 
-    ASSERT_NO_THROW(client.CreateStream(stream_name));
+    ASSERT_NO_THROW(client->create_stream(stream_name));
     TrackStream(stream_name);
-    ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name,
-                                       
iggy::TopicCreateOptions().SetPartitionsCount(3)));
-
-    ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(""), 
iggy::Identifier::String(topic_name), 1),
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), 
topic_name,
+                                         make_topic_create_options(3, "none", 
"never_expire", 0, "server_default")));
+
+    iggy::ffi::Identifier invalid_stream_kind;
+    invalid_stream_kind.kind   = "invalid";
+    invalid_stream_kind.length = 4;
+    invalid_stream_kind.value  = {1, 0, 0, 0};
+    ASSERT_THROW(client->delete_partitions(std::move(invalid_stream_kind), 
make_string_identifier(topic_name), 1),
                  std::exception);
-    
ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(std::string(256, 
'a')),
-                                         iggy::Identifier::String(topic_name), 
1),
+
+    iggy::ffi::Identifier invalid_stream_length;
+    invalid_stream_length.kind   = "numeric";
+    invalid_stream_length.length = 1;
+    invalid_stream_length.value.push_back(1);
+    ASSERT_THROW(client->delete_partitions(std::move(invalid_stream_length), 
make_string_identifier(topic_name), 1),
                  std::exception);
-    
ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(""), 1),
+
+    iggy::ffi::Identifier invalid_topic_kind;
+    invalid_topic_kind.kind   = "invalid";
+    invalid_topic_kind.length = 4;
+    invalid_topic_kind.value  = {1, 0, 0, 0};
+    
ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), 
std::move(invalid_topic_kind), 1),
                  std::exception);
-    ASSERT_THROW(client.DeletePartitions(iggy::Identifier::String(stream_name),
-                                         
iggy::Identifier::String(std::string(256, 'a')), 1),
+
+    iggy::ffi::Identifier invalid_topic_length;
+    invalid_topic_length.kind   = "numeric";
+    invalid_topic_length.length = 1;
+    invalid_topic_length.value.push_back(1);
+    
ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), 
std::move(invalid_topic_length), 1),
                  std::exception);
 }
 
@@ -449,7 +483,7 @@ TEST_F(E2E_Partition, 
DeletePartitionsAfterStreamDeletionThrows) {
 
     
ASSERT_THROW(client.DeletePartitions(iggy::Identifier::Numeric(stream_details.Id()),
                                          
iggy::Identifier::Numeric(stream_details.Topics()[0].Id()), 1),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Partition, CreatePartitionsAfterTopicDeletionThrows) {
@@ -467,5 +501,5 @@ TEST_F(E2E_Partition, 
CreatePartitionsAfterTopicDeletionThrows) {
 
     ASSERT_THROW(
         client.CreatePartitions(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name), 1),
-        std::exception);
+        iggy::IggyException);
 }
diff --git a/foreign/cpp/tests/e2e/stream.cpp b/foreign/cpp/tests/e2e/stream.cpp
index bb49e88b3..d6c633b26 100644
--- a/foreign/cpp/tests/e2e/stream.cpp
+++ b/foreign/cpp/tests/e2e/stream.cpp
@@ -41,7 +41,7 @@ TEST_F(E2E_Stream, CreateDuplicateStreamThrows) {
     auto client                   = GetLoggedInHighLevelClient();
     ASSERT_NO_THROW(client.CreateStream(stream_name));
     TrackStream(stream_name);
-    ASSERT_THROW(client.CreateStream(stream_name), std::exception);
+    ASSERT_THROW(client.CreateStream(stream_name), iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, CreateStreamBeforeLoginThrows) {
@@ -49,12 +49,12 @@ TEST_F(E2E_Stream, CreateStreamBeforeLoginThrows) {
     const std::string stream_name = GetRandomName();
     auto client                   = GetLoggedOutHighLevelClient();
 
-    ASSERT_THROW(client.CreateStream(stream_name), std::exception);
+    ASSERT_THROW(client.CreateStream(stream_name), iggy::IggyException);
     ASSERT_NO_THROW(client.Connect());
-    ASSERT_THROW(client.CreateStream(stream_name), std::exception);
+    ASSERT_THROW(client.CreateStream(stream_name), iggy::IggyException);
     ASSERT_NO_THROW(client.Login("iggy", "iggy"));
     ASSERT_NO_THROW(client.Disconnect());
-    ASSERT_THROW(client.CreateStream(stream_name), std::exception);
+    ASSERT_THROW(client.CreateStream(stream_name), iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, CreateStreamValidatesNameConstraintsAndUniqueness) {
@@ -67,7 +67,7 @@ TEST_F(E2E_Stream, 
CreateStreamValidatesNameConstraintsAndUniqueness) {
     auto client = GetLoggedInHighLevelClient();
     for (const auto &stream_name : illegal_stream_names) {
         SCOPED_TRACE(stream_name);
-        ASSERT_THROW(client.CreateStream(stream_name), std::exception);
+        ASSERT_THROW(client.CreateStream(stream_name), iggy::IggyException);
     }
 
     const std::string max_length_name(255, 'a');
@@ -104,7 +104,7 @@ TEST_F(E2E_Stream, UpdateStreamWorksCorrectly) {
 
     ASSERT_NO_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), 
updated_stream_name));
 
-    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 
     iggy::StreamDetails updated_stream_details = 
client.GetStream(iggy::Identifier::Numeric(stream_id));
 
@@ -148,11 +148,11 @@ TEST_F(E2E_Stream, 
UpdateStreamWithUnsupportedOptionsRejectsAndPreservesName) {
 
     const auto options = 
iggy::StreamUpdateOptions().SetRawEntries({{"not_a_real_option", "true"}});
     ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), 
updated_stream_name, options),
-                 std::exception);
+                 iggy::IggyException);
 
     const auto stream = 
client.GetStream(iggy::Identifier::String(stream_name));
     EXPECT_EQ(stream.Name(), stream_name);
-    
ASSERT_THROW(client.GetStream(iggy::Identifier::String(updated_stream_name)), 
std::exception);
+    
ASSERT_THROW(client.GetStream(iggy::Identifier::String(updated_stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, UpdateStreamBeforeLoginThrows) {
@@ -166,14 +166,14 @@ TEST_F(E2E_Stream, UpdateStreamBeforeLoginThrows) {
     auto unauthenticated_client = GetLoggedOutHighLevelClient();
 
     
ASSERT_THROW(unauthenticated_client.UpdateStream(iggy::Identifier::String(stream_name),
 updated_stream_name),
-                 std::exception);
+                 iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Connect());
     
ASSERT_THROW(unauthenticated_client.UpdateStream(iggy::Identifier::String(stream_name),
 updated_stream_name),
-                 std::exception);
+                 iggy::IggyException);
     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);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, UpdateStreamWithVariousUtf8Characters) {
@@ -210,7 +210,7 @@ TEST_F(E2E_Stream, UpdateNonExistentStreamThrows) {
     const std::string stream_name         = GetRandomName();
     const std::string updated_stream_name = GetRandomName();
     auto client                           = GetLoggedInHighLevelClient();
-    ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), 
updated_stream_name), std::exception);
+    ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), 
updated_stream_name), iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, UpdateStreamWithDuplicateNameThrows) {
@@ -223,7 +223,8 @@ TEST_F(E2E_Stream, UpdateStreamWithDuplicateNameThrows) {
     ASSERT_NO_THROW(client.CreateStream(second_stream_name));
     TrackStream(second_stream_name);
 
-    
ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(first_stream_name), 
second_stream_name), std::exception);
+    
ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(first_stream_name), 
second_stream_name),
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, UpdateDeletedStreamThrows) {
@@ -237,7 +238,7 @@ TEST_F(E2E_Stream, UpdateDeletedStreamThrows) {
     
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)));
     ForgetTrackedStream(stream_name);
 
-    ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), 
updated_stream_name), std::exception);
+    ASSERT_THROW(client.UpdateStream(iggy::Identifier::String(stream_name), 
updated_stream_name), iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, UpdateStreamOnlyChangesName) {
@@ -281,7 +282,7 @@ TEST_F(E2E_Stream, UpdateStreamOnlyChangesName) {
 
     ASSERT_NO_THROW(client.UpdateStream(iggy::Identifier::Numeric(stream_id), 
updated_stream_name));
 
-    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
     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(); });
@@ -336,7 +337,8 @@ TEST_F(E2E_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.UpdateStream(iggy::Identifier::Numeric(stream_id), 
invalid_stream_name), std::exception);
+        ASSERT_THROW(client.UpdateStream(iggy::Identifier::Numeric(stream_id), 
invalid_stream_name),
+                     iggy::IggyException);
     }
 }
 
@@ -355,7 +357,7 @@ TEST_F(E2E_Stream, DeleteNotCreatedStreamThrows) {
     RecordProperty("description", "Throws when deleting a stream that does not 
exist.");
     const std::string stream_name = GetRandomName();
     auto client                   = GetLoggedInHighLevelClient();
-    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, DeleteStreamBeforeLoginThrows) {
@@ -364,14 +366,14 @@ TEST_F(E2E_Stream, DeleteStreamBeforeLoginThrows) {
 
     auto client = GetLoggedOutHighLevelClient();
 
-    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 
     ASSERT_NO_THROW(client.Connect());
 
-    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
     ASSERT_NO_THROW(client.Login("iggy", "iggy"));
     ASSERT_NO_THROW(client.Disconnect());
-    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, DeleteStreamTwiceThrows) {
@@ -383,7 +385,7 @@ TEST_F(E2E_Stream, DeleteStreamTwiceThrows) {
 
     
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)));
     ForgetTrackedStream(stream_name);
-    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, GetStreamByStringIdentifierReturnsStreamDetails) {
@@ -407,7 +409,7 @@ TEST_F(E2E_Stream, GetNonExistentStreamDetailsThrows) {
     RecordProperty("description", "Throws when requesting details for a stream 
that does not exist.");
     const std::string stream_name = GetRandomName();
     auto client                   = GetLoggedInHighLevelClient();
-    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, GetStreamDetailsBeforeLoginThrows) {
@@ -415,12 +417,12 @@ TEST_F(E2E_Stream, GetStreamDetailsBeforeLoginThrows) {
     const std::string stream_name = GetRandomName();
     auto client                   = GetLoggedOutHighLevelClient();
 
-    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
     ASSERT_NO_THROW(client.Connect());
-    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
     ASSERT_NO_THROW(client.Login("iggy", "iggy"));
     ASSERT_NO_THROW(client.Disconnect());
-    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, GetDeletedStreamDetailsThrows) {
@@ -432,7 +434,7 @@ TEST_F(E2E_Stream, GetDeletedStreamDetailsThrows) {
     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.GetStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.GetStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, GetStreamsReturnsEmptyAfterCleanup) {
@@ -514,12 +516,12 @@ TEST_F(E2E_Stream, GetStreamsBeforeLoginThrows) {
     RecordProperty("description", "Throws when get_streams is called before 
authentication.");
     auto client = GetLoggedOutHighLevelClient();
 
-    ASSERT_THROW(client.GetStreams(), std::exception);
+    ASSERT_THROW(client.GetStreams(), iggy::IggyException);
     ASSERT_NO_THROW(client.Connect());
-    ASSERT_THROW(client.GetStreams(), std::exception);
+    ASSERT_THROW(client.GetStreams(), iggy::IggyException);
     ASSERT_NO_THROW(client.Login("iggy", "iggy"));
     ASSERT_NO_THROW(client.Disconnect());
-    ASSERT_THROW(client.GetStreams(), std::exception);
+    ASSERT_THROW(client.GetStreams(), iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, GetStreamsConsistentWithGetStream) {
@@ -590,7 +592,7 @@ TEST_F(E2E_Stream, PurgeStreamOnNonExistentStreamThrows) {
     RecordProperty("description", "Throws when purging a stream that does not 
exist.");
     const std::string stream_name = GetRandomName();
     auto client                   = GetLoggedInHighLevelClient();
-    ASSERT_THROW(client.PurgeStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.PurgeStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, PurgeStreamAfterStreamDeletionThrows) {
@@ -602,7 +604,7 @@ TEST_F(E2E_Stream, PurgeStreamAfterStreamDeletionThrows) {
     
ASSERT_NO_THROW(client.DeleteStream(iggy::Identifier::String(stream_name)));
     ForgetTrackedStream(stream_name);
 
-    ASSERT_THROW(client.PurgeStream(iggy::Identifier::String(stream_name)), 
std::exception);
+    ASSERT_THROW(client.PurgeStream(iggy::Identifier::String(stream_name)), 
iggy::IggyException);
 }
 
 TEST_F(E2E_Stream, PurgeStreamPreservesStreamMetadata) {
@@ -950,10 +952,10 @@ TEST_F(E2E_Stream, PurgeStreamBeforeLoginThrows) {
 
     auto unauthenticated_client = GetLoggedOutHighLevelClient();
 
-    
ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)),
 std::exception);
+    
ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)),
 iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Connect());
-    
ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)),
 std::exception);
+    
ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)),
 iggy::IggyException);
     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);
+    
ASSERT_THROW(unauthenticated_client.PurgeStream(iggy::Identifier::String(stream_name)),
 iggy::IggyException);
 }
diff --git a/foreign/cpp/tests/e2e/topic.cpp b/foreign/cpp/tests/e2e/topic.cpp
index 7e10a7c97..027c0cde8 100644
--- a/foreign/cpp/tests/e2e/topic.cpp
+++ b/foreign/cpp/tests/e2e/topic.cpp
@@ -123,7 +123,7 @@ TEST_F(E2E_Topic, 
CreateTopicWithBoundaryPartitionsCountValues) {
                                        
iggy::TopicCreateOptions().SetPartitionsCount(1000)));
     ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
overflow_topic_name,
                                     
iggy::TopicCreateOptions().SetPartitionsCount(1001)),
-                 std::exception);
+                 iggy::IggyException);
     ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
zero_partitions_topic_name,
                                        
iggy::TopicCreateOptions().SetPartitionsCount(0)));
 
@@ -157,7 +157,7 @@ TEST_F(E2E_Topic, CreateTopicWithInvalidNamesThrows) {
         SCOPED_TRACE(topic_name);
         ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name,
                                         
iggy::TopicCreateOptions().SetPartitionsCount(1)),
-                     std::exception);
+                     iggy::IggyException);
     }
 
     const std::string max_length_name(255, 'a');
@@ -177,7 +177,7 @@ TEST_F(E2E_Topic, CreateDuplicateTopicThrows) {
                                        
iggy::TopicCreateOptions().SetPartitionsCount(1)));
     ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name,
                                     
iggy::TopicCreateOptions().SetPartitionsCount(1)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, CreateSameTopicNameInDifferentStreamsSucceeds) {
@@ -213,15 +213,15 @@ TEST_F(E2E_Topic, CreateTopicWithInvalidOptionsThrows) {
     ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
invalid_compression_topic_name,
                                     
iggy::TopicCreateOptions().SetPartitionsCount(1).SetRawEntries(
                                         {{"compression_algorithm", 
"invalid-compression"}})),
-                 std::exception);
+                 iggy::IggyException);
     ASSERT_THROW(client.CreateTopic(
                      iggy::Identifier::String(stream_name), 
invalid_expiry_topic_name,
                      
iggy::TopicCreateOptions().SetPartitionsCount(1).SetRawEntries({{"message_expiry",
 "invalid"}})),
-                 std::exception);
+                 iggy::IggyException);
     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);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, 
CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRemainder) {
@@ -237,7 +237,10 @@ TEST_F(E2E_Topic, 
CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRemainder)
     TrackStream(stream_name);
 
     iggy::TopicCreateOptions options;
-    options.SetPartitionsCount(1).SetDurability(iggy::Durability::Persisted);
+    options.SetPartitionsCount(1)
+        .SetDurability(iggy::Durability::Replicated)
+        .SetConsumerOffsetDurability(iggy::Durability::Persisted)
+        .SetRawEntries({{"durability", "persisted"}, 
{"consumer_offset_durability", "replicated"}});
     ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name, options));
 
     const auto topic_details =
@@ -245,18 +248,18 @@ TEST_F(E2E_Topic, 
CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRemainder)
 
     // 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.
-    // The SDK sends both durability fields, including the independently
-    // defaulted offset policy.
+    // Typed setters take precedence over conflicting raw entries.
     const auto &explicit_options = topic_details.Options().Explicit();
     ASSERT_EQ(explicit_options.size(), 2u);
     auto explicit_it = explicit_options.find("durability");
     ASSERT_NE(explicit_it, explicit_options.end());
     EXPECT_EQ(explicit_it->second.Kind(), iggy::HeaderKind::String);
-    EXPECT_EQ(explicit_it->second.Value(), (std::vector<std::uint8_t>{'p', 
'e', 'r', 's', 'i', 's', 't', 'e', 'd'}));
+    EXPECT_EQ(explicit_it->second.Value(),
+              (std::vector<std::uint8_t>{'r', 'e', 'p', 'l', 'i', 'c', 'a', 
't', 'e', 'd'}));
     auto offset_it = explicit_options.find("consumer_offset_durability");
     ASSERT_NE(offset_it, explicit_options.end());
     EXPECT_EQ(offset_it->second.Kind(), iggy::HeaderKind::String);
-    EXPECT_EQ(offset_it->second.Value(), (std::vector<std::uint8_t>{'r', 'e', 
'p', 'l', 'i', 'c', 'a', 't', 'e', 'd'}));
+    EXPECT_EQ(offset_it->second.Value(), (std::vector<std::uint8_t>{'p', 'e', 
'r', 's', 'i', 's', 't', 'e', 'd'}));
 
     EXPECT_FALSE(topic_details.Options().Derived().empty());
     const auto &derived_options = topic_details.Options().Derived();
@@ -267,7 +270,7 @@ TEST_F(E2E_Topic, 
CreateTopicWithOptionsReturnsCanonicalKindAndDerivedRemainder)
     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);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, 
CreateTopicWithTypedOptionHelpersReportsThemAsExplicitOptions) {
@@ -318,6 +321,14 @@ TEST_F(E2E_Topic, 
CreateTopicWithTypedOptionHelpersReportsThemAsExplicitOptions)
     check_kind("size_of_messages_required_to_save", iggy::HeaderKind::Uint64);
     check_kind("preallocate_segments", iggy::HeaderKind::Bool);
 
+    EXPECT_EQ(explicit_options.at("segment_size").Value(),
+              (std::vector<std::uint8_t>{0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 
0x00, 0x00}));
+    EXPECT_EQ(explicit_options.at("messages_required_to_save").Value(),
+              (std::vector<std::uint8_t>{0x00, 0x02, 0x00, 0x00}));
+    EXPECT_EQ(explicit_options.at("size_of_messages_required_to_save").Value(),
+              (std::vector<std::uint8_t>{0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 
0x00, 0x00}));
+    EXPECT_EQ(explicit_options.at("preallocate_segments").Value(), 
(std::vector<std::uint8_t>{0x00}));
+
     EXPECT_EQ(explicit_options.size(), 6u);
     EXPECT_EQ(topic_details.Options().Derived().count("segment_size"), 0u)
         << "segment_size was set explicitly, so it cannot be derived";
@@ -370,7 +381,7 @@ TEST_F(E2E_Topic, 
CreateTopicWithMaxTopicSizeBelowSegmentSizeThrows) {
     TrackStream(stream_name);
     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);
+    ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name, options), iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, CreateTopicOnNonExistentStreamThrows) {
@@ -381,7 +392,7 @@ TEST_F(E2E_Topic, CreateTopicOnNonExistentStreamThrows) {
     auto client = GetLoggedInHighLevelClient();
     ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name,
                                     
iggy::TopicCreateOptions().SetPartitionsCount(1)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, CreateTopicAfterStreamDeletionThrows) {
@@ -397,7 +408,7 @@ TEST_F(E2E_Topic, CreateTopicAfterStreamDeletionThrows) {
 
     ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name,
                                     
iggy::TopicCreateOptions().SetPartitionsCount(1)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, CreateTopicBeforeLoginThrows) {
@@ -413,16 +424,16 @@ TEST_F(E2E_Topic, CreateTopicBeforeLoginThrows) {
 
     
ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name),
 topic_name,
                                                     
iggy::TopicCreateOptions().SetPartitionsCount(1)),
-                 std::exception);
+                 iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Connect());
     
ASSERT_THROW(unauthenticated_client.CreateTopic(iggy::Identifier::String(stream_name),
 topic_name,
                                                     
iggy::TopicCreateOptions().SetPartitionsCount(1)),
-                 std::exception);
+                 iggy::IggyException);
     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);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, DeleteTopicAfterCreate) {
@@ -453,7 +464,7 @@ TEST_F(E2E_Topic, DeleteTopicOnNonExistentStreamThrows) {
     auto client = GetLoggedInHighLevelClient();
 
     ASSERT_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, DeleteTopicOnNonExistentTopicThrows) {
@@ -467,7 +478,7 @@ TEST_F(E2E_Topic, DeleteTopicOnNonExistentTopicThrows) {
     TrackStream(stream_name);
 
     ASSERT_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, DeleteTopicTwiceThrows) {
@@ -484,7 +495,7 @@ TEST_F(E2E_Topic, DeleteTopicTwiceThrows) {
 
     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);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, DeleteTopicAfterStreamDeletionThrows) {
@@ -502,7 +513,7 @@ TEST_F(E2E_Topic, DeleteTopicAfterStreamDeletionThrows) {
     ForgetTrackedStream(stream_name);
 
     ASSERT_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, DeleteTopicBeforeLoginThrows) {
@@ -521,16 +532,16 @@ TEST_F(E2E_Topic, DeleteTopicBeforeLoginThrows) {
 
     ASSERT_THROW(
         
unauthenticated_client.DeleteTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Connect());
     ASSERT_THROW(
         
unauthenticated_client.DeleteTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
     ASSERT_NO_THROW(unauthenticated_client.Disconnect());
     ASSERT_THROW(
         
unauthenticated_client.DeleteTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicReturnsTopicForExistingTopic) {
@@ -577,16 +588,16 @@ TEST_F(E2E_Topic, GetTopicBeforeLoginThrows) {
 
     ASSERT_THROW(
         unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Connect());
     ASSERT_THROW(
         unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
     ASSERT_NO_THROW(unauthenticated_client.Disconnect());
     ASSERT_THROW(
         unauthenticated_client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicWithWrongStreamIdThrows) {
@@ -604,7 +615,7 @@ TEST_F(E2E_Topic, GetTopicWithWrongStreamIdThrows) {
                                        
iggy::TopicCreateOptions().SetPartitionsCount(1)));
 
     ASSERT_THROW(client.GetTopic(iggy::Identifier::String(second_stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicWithWrongTopicThrows) {
@@ -620,7 +631,7 @@ TEST_F(E2E_Topic, GetTopicWithWrongTopicThrows) {
                                        
iggy::TopicCreateOptions().SetPartitionsCount(1)));
 
     ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(wrong_topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicAfterStreamDeletionThrows) {
@@ -637,7 +648,7 @@ TEST_F(E2E_Topic, GetTopicAfterStreamDeletionThrows) {
     ForgetTrackedStream(stream_name);
 
     ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicAfterTopicDeletionThrows) {
@@ -653,7 +664,7 @@ TEST_F(E2E_Topic, GetTopicAfterTopicDeletionThrows) {
     ASSERT_NO_THROW(client.DeleteTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)));
 
     ASSERT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicReturnsEmptyPartitionsForZeroPartitionTopic) {
@@ -828,12 +839,12 @@ TEST_F(E2E_Topic, GetTopicsBeforeLoginThrows) {
 
     auto unauthenticated_client = GetLoggedOutHighLevelClient();
 
-    
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)),
 std::exception);
+    
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)),
 iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Connect());
-    
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)),
 std::exception);
+    
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)),
 iggy::IggyException);
     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);
+    
ASSERT_THROW(unauthenticated_client.GetTopics(iggy::Identifier::String(stream_name)),
 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicsReturnsEmptyForStreamWithoutTopics) {
@@ -1048,6 +1059,11 @@ TEST_F(E2E_Topic, 
UpdateTopicWithAllOptionCombinationsUpdatesInputFields) {
     ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), 
topic_name,
                                        
iggy::TopicCreateOptions().SetPartitionsCount(2)));
 
+    const auto initial_topic =
+        client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name));
+    std::uint64_t expected_message_expiry = initial_topic.MessageExpiry();
+    std::uint64_t expected_max_topic_size = initial_topic.MaxTopicSize();
+
     for (const auto &compression_algorithm : compression_algorithms) {
         for (const auto &expiry_option : expiry_options) {
             for (const auto &max_topic_size : max_topic_sizes) {
@@ -1062,6 +1078,22 @@ TEST_F(E2E_Topic, 
UpdateTopicWithAllOptionCombinationsUpdatesInputFields) {
                                                        
.SetMessageExpiry(expiry_option.value)
                                                        
.SetMaxTopicSize(max_topic_size.value)));
                 topic_name = updated_topic_name;
+
+                if (expiry_option.value.Kind() != "server_default") {
+                    expected_message_expiry = expiry_option.value.Value();
+                }
+                if (max_topic_size.value.Value() == "unlimited") {
+                    expected_max_topic_size = 
std::numeric_limits<std::uint64_t>::max();
+                } else if (max_topic_size.value.Value() != "server_default") {
+                    expected_max_topic_size = 1024ULL * 1024ULL * 1024ULL;
+                }
+
+                const auto updated_topic =
+                    client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name));
+                EXPECT_EQ(updated_topic.Name(), updated_topic_name);
+                EXPECT_EQ(updated_topic.CompressionAlgorithm(), 
compression_algorithm.name);
+                EXPECT_EQ(updated_topic.MessageExpiry(), 
expected_message_expiry);
+                EXPECT_EQ(updated_topic.MaxTopicSize(), 
expected_max_topic_size);
             }
         }
     }
@@ -1128,7 +1160,7 @@ TEST_F(E2E_Topic, 
UpdateTopicWithDuplicateTopicNameThrows) {
                                         
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip())
                                         
.SetMessageExpiry(iggy::Expiry::Duration(1000))
                                         
.SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, UpdateTopicWithInvalidNamesThrows) {
@@ -1157,7 +1189,7 @@ TEST_F(E2E_Topic, UpdateTopicWithInvalidNamesThrows) {
                                    
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip())
                                    
.SetMessageExpiry(iggy::Expiry::Duration(1000))
                                    
.SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))),
-            std::exception);
+            iggy::IggyException);
     }
 }
 
@@ -1189,7 +1221,7 @@ TEST_F(E2E_Topic, 
UpdateTopicFailedValidationDoesNotMutateTopic) {
                                         
.SetMessageExpiry(iggy::Expiry::Duration(2000))
                                         
.SetMaxTopicSize(iggy::MaxTopicSize::ServerDefault())
                                         .SetRawEntries({{"max_topic_size", 
"not-a-size"}})),
-                 std::exception);
+                 iggy::IggyException);
 
     const auto topic_after_failed_update =
         client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name));
@@ -1202,7 +1234,7 @@ TEST_F(E2E_Topic, 
UpdateTopicFailedValidationDoesNotMutateTopic) {
     EXPECT_EQ(topic_after_failed_update.MaxTopicSize(), 
topic_before_update.MaxTopicSize());
 
     EXPECT_THROW(client.GetTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(updated_topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, UpdateTopicBeforeLoginThrows) {
@@ -1221,16 +1253,16 @@ TEST_F(E2E_Topic, UpdateTopicBeforeLoginThrows) {
 
     
ASSERT_THROW(unauthenticated_client.UpdateTopic(iggy::Identifier::String(stream_name),
                                                     
iggy::Identifier::String(topic_name), updated_topic_name),
-                 std::exception);
+                 iggy::IggyException);
     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);
+                 iggy::IggyException);
     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);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, UpdateTopicOnNonExistentStreamThrows) {
@@ -1246,7 +1278,7 @@ TEST_F(E2E_Topic, UpdateTopicOnNonExistentStreamThrows) {
                                         
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip())
                                         
.SetMessageExpiry(iggy::Expiry::Duration(1000))
                                         
.SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, UpdateTopicOnNonExistentTopicThrows) {
@@ -1265,7 +1297,7 @@ TEST_F(E2E_Topic, UpdateTopicOnNonExistentTopicThrows) {
                                         
.SetCompressionAlgorithm(iggy::CompressionAlgorithm::Gzip())
                                         
.SetMessageExpiry(iggy::Expiry::Duration(1000))
                                         
.SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024ULL * 1024ULL * 1024ULL))),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, GetTopicsAfterStreamDeletionReturnsEmpty) {
@@ -1294,7 +1326,7 @@ TEST_F(E2E_Topic, PurgeTopicOnNonExistentStreamThrows) {
 
     auto client = GetLoggedInHighLevelClient();
     ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, PurgeTopicAfterStreamDeletionThrows) {
@@ -1311,7 +1343,7 @@ TEST_F(E2E_Topic, PurgeTopicAfterStreamDeletionThrows) {
     ForgetTrackedStream(stream_name);
 
     ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, PurgeTopicOnNonExistentTopicThrows) {
@@ -1324,7 +1356,7 @@ TEST_F(E2E_Topic, PurgeTopicOnNonExistentTopicThrows) {
     TrackStream(stream_name);
 
     ASSERT_THROW(client.PurgeTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-                 std::exception);
+                 iggy::IggyException);
 }
 
 TEST_F(E2E_Topic, PurgeTopicPreservesTopicMetadata) {
@@ -1606,14 +1638,14 @@ TEST_F(E2E_Topic, PurgeTopicBeforeLoginThrows) {
 
     ASSERT_THROW(
         
unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Connect());
     ASSERT_THROW(
         
unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
     ASSERT_NO_THROW(unauthenticated_client.Login("iggy", "iggy"));
     ASSERT_NO_THROW(unauthenticated_client.Disconnect());
     ASSERT_THROW(
         
unauthenticated_client.PurgeTopic(iggy::Identifier::String(stream_name), 
iggy::Identifier::String(topic_name)),
-        std::exception);
+        iggy::IggyException);
 }
diff --git a/foreign/cpp/tests/unit/unit_tests.cpp 
b/foreign/cpp/tests/unit/unit_tests.cpp
index 5d86cf738..840247c86 100644
--- a/foreign/cpp/tests/unit/unit_tests.cpp
+++ b/foreign/cpp/tests/unit/unit_tests.cpp
@@ -28,18 +28,6 @@
 
 #include "iggy.hpp"
 
-namespace {
-
-std::string option_key(const iggy::HeaderEntry &entry) {
-    return std::string(entry.Key().Value().begin(), entry.Key().Value().end());
-}
-
-const std::vector<std::uint8_t> &option_value_bytes(const iggy::HeaderEntry 
&entry) {
-    return entry.Value().Value();
-}
-
-}  // namespace
-
 TEST(CompressionAlgorithmTest, ReturnsExpectedValues) {
     EXPECT_EQ(iggy::CompressionAlgorithm::None().Value(), "none");
     EXPECT_EQ(iggy::CompressionAlgorithm::Gzip().Value(), "gzip");
@@ -106,6 +94,7 @@ TEST(ExpiryTest, ReturnsExpectedKindAndValue) {
     const auto duration = iggy::Expiry::Duration(15);
     EXPECT_EQ(duration.Kind(), "duration");
     EXPECT_EQ(duration.Value(), static_cast<std::uint64_t>(15));
+    EXPECT_THROW(iggy::Expiry::Duration(0), std::invalid_argument);
 }
 
 TEST(TopicCreateOptionsTest, DefaultHasNoValues) {
@@ -123,23 +112,6 @@ TEST(TopicCreateOptionsTest, DefaultHasNoValues) {
     EXPECT_TRUE(options.RawEntries().empty());
 }
 
-TEST(TopicOptionTest, DurabilityEncodesCanonicalStrings) {
-    const auto persisted = 
iggy::TopicOption::Durability(iggy::Durability::Persisted);
-    EXPECT_EQ(persisted.Key().Kind(), iggy::HeaderKind::String);
-    EXPECT_EQ(option_key(persisted), "durability");
-    EXPECT_EQ(persisted.Value().Kind(), iggy::HeaderKind::String);
-    EXPECT_EQ(option_value_bytes(persisted), (std::vector<std::uint8_t>{'p', 
'e', 'r', 's', 'i', 's', 't', 'e', 'd'}));
-
-    const auto offset = iggy::TopicOption::ConsumerOffsetDurability();
-    EXPECT_EQ(offset.Key().Kind(), iggy::HeaderKind::String);
-    EXPECT_EQ(option_key(offset), "consumer_offset_durability");
-    EXPECT_EQ(offset.Value().Kind(), iggy::HeaderKind::String);
-    EXPECT_EQ(option_value_bytes(offset),
-              (std::vector<std::uint8_t>{'r', 'e', 'p', 'l', 'i', 'c', 'a', 
't', 'e', 'd'}));
-
-    
EXPECT_THROW(iggy::TopicOption::Durability(static_cast<iggy::Durability>(99)), 
std::invalid_argument);
-}
-
 TEST(TopicCreateOptionsTest, PartitionsCountStoresValue) {
     iggy::TopicCreateOptions options;
     options.SetPartitionsCount(3);
@@ -366,6 +338,36 @@ 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;
+
+    const auto stream = iggy::Identifier::String("stream");
+    const auto topic  = iggy::Identifier::String("topic");
+
+    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);
+    EXPECT_THROW(client.CreateStream("stream"), iggy::IggyException);
+    EXPECT_THROW(client.UpdateStream(stream, "updated-stream"), 
iggy::IggyException);
+    EXPECT_THROW(client.GetStreams(), iggy::IggyException);
+    EXPECT_THROW(client.GetStream(stream), iggy::IggyException);
+    EXPECT_THROW(client.DeleteStream(stream), iggy::IggyException);
+    EXPECT_THROW(client.PurgeStream(stream), iggy::IggyException);
+    EXPECT_THROW(client.CreateTopic(stream, "topic", 
iggy::TopicCreateOptions().SetPartitionsCount(1)),
+                 iggy::IggyException);
+    EXPECT_THROW(client.UpdateTopic(stream, topic, "updated-topic"), 
iggy::IggyException);
+    EXPECT_THROW(client.GetTopics(stream), iggy::IggyException);
+    EXPECT_THROW(client.GetTopic(stream, topic), iggy::IggyException);
+    EXPECT_THROW(client.DeleteTopic(stream, topic), iggy::IggyException);
+    EXPECT_THROW(client.PurgeTopic(stream, topic), iggy::IggyException);
+    EXPECT_THROW(client.CreatePartitions(stream, topic, 1), 
iggy::IggyException);
+    EXPECT_THROW(client.DeletePartitions(stream, topic, 1), 
iggy::IggyException);
+}
+
 TEST(AutoLoginKindTest, HasStableDiscriminantsAndZeroInitializedDefault) {
     EXPECT_EQ(static_cast<std::uint8_t>(iggy::ffi::AutoLoginKind::Disabled), 
0u);
     
EXPECT_EQ(static_cast<std::uint8_t>(iggy::ffi::AutoLoginKind::UsernamePassword),
 1u);

Reply via email to