diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8702bec..7d3b7b9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1,7 @@ add_executable(minimal minimal.cpp) -target_link_libraries(minimal couchbase_cxx_client::couchbase_cxx_client - taocpp::json) +target_link_libraries(minimal + PRIVATE couchbase_cxx_client::couchbase_cxx_client taocpp::json) + +add_executable(transactions_basic_transfer transactions_basic_transfer.cpp) +target_link_libraries(transactions_basic_transfer + PRIVATE couchbase_cxx_client::couchbase_cxx_client taocpp::json) diff --git a/examples/minimal.cpp b/examples/minimal.cpp index 44c6505..64e474d 100644 --- a/examples/minimal.cpp +++ b/examples/minimal.cpp @@ -89,8 +89,16 @@ program_config::from_env() -> program_config if (const auto* val = getenv("PROFILE"); val != nullptr) { config.profile = val; // e.g. "wan_development" } - if (const auto* val = getenv("PROFILE"); val != nullptr) { - config.profile = val; // e.g. "wan_development" + if (const auto* val = getenv("VERBOSE"); val != nullptr) { + const std::array truthy_values = { + "yes", "y", "on", "true", "1", + }; + for (const auto& truth : truthy_values) { + if (val == truth) { + config.verbose = true; + break; + } + } } return config; @@ -111,6 +119,6 @@ program_config::dump() std::cout << " BUCKET_NAME: " << quote(bucket_name) << "\n"; std::cout << " SCOPE_NAME: " << quote(scope_name) << "\n"; std::cout << " COLLECTION_NAME: " << quote(collection_name) << "\n"; - std::cout << " verbose: " << std::boolalpha << verbose << "\n"; + std::cout << " VERBOSE: " << std::boolalpha << verbose << "\n"; std::cout << " PROFILE: " << (profile ? quote(*profile) : "[NONE]") << "\n\n"; } diff --git a/examples/transactions_basic_transfer.cpp b/examples/transactions_basic_transfer.cpp new file mode 100644 index 0000000..fd5ad39 --- /dev/null +++ b/examples/transactions_basic_transfer.cpp @@ -0,0 +1,285 @@ +#include "couchbase/durability_level.hxx" +#include "couchbase/transactions/attempt_context.hxx" +#include +#include + +#include +#include + +#include +#include + +struct program_config { + std::string connection_string{ "couchbase://127.0.0.1" }; + std::string username{ "Administrator" }; + std::string password{ "password" }; + std::string bucket_name{ "default" }; + std::string scope_name{ couchbase::scope::default_name }; + std::string collection_name{ couchbase::collection::default_name }; + std::optional profile{}; + bool verbose{ false }; + + static auto from_env() -> program_config; + static auto quote(std::string val) -> std::string; + void dump(); +}; + +enum bank_error : int { + insufficient_funds = 1, +}; + +namespace +{ +struct bank_error_category : std::error_category { + [[nodiscard]] auto name() const noexcept -> const char* override + { + return "bank_error"; + } + + [[nodiscard]] auto message(int ev) const noexcept -> std::string override + { + switch (static_cast(ev)) { + case insufficient_funds: + return "insufficient_funds (1): not enough funds on the account"; + break; + } + return "unexpected error code in \"bank_error\" category, ev=" + std::to_string(ev); + } +}; + +const static bank_error_category instance{}; +const std::error_category& +bank_error_category_instance() noexcept +{ + return instance; +} +} // namespace + +template<> +struct std::is_error_code_enum : std::true_type { +}; + +auto +make_error_code(bank_error e) -> std::error_code +{ + return { static_cast(e), bank_error_category_instance() }; +} + +struct bank_account { + std::string name; + std::int64_t balance; +}; + +std::ostream& +operator<<(std::ostream& os, const bank_account& a) +{ + os << "bank_account(name: \"" << a.name << "\", balance: " << a.balance << " USD)"; + return os; +} + +template<> +struct tao::json::traits { + template class Traits> + static void assign(tao::json::basic_value& v, const bank_account& p) + { + v = { + { "name", p.name }, + { "balance", p.balance }, + }; + } + + template class Traits> + static bank_account as(const tao::json::basic_value& v) + { + bank_account result; + const auto& object = v.get_object(); + result.name = object.at("name").template as(); + result.balance = object.at("balance").template as(); + return result; + } +}; + +int +main() +{ + auto config = program_config::from_env(); + config.dump(); + + if (config.verbose) { + couchbase::logger::initialize_console_logger(); + couchbase::logger::set_level(couchbase::logger::log_level::trace); + } + + auto options = couchbase::cluster_options(config.username, config.password); + if (config.profile) { + options.apply_profile(config.profile.value()); + } + + auto [connect_err, cluster] = + couchbase::cluster::connect(config.connection_string, options).get(); + if (connect_err) { + std::cout << "Unable to connect to the cluster. ec: " << connect_err.message() << "\n"; + return EXIT_FAILURE; + } + + auto collection = + cluster.bucket(config.bucket_name).scope(config.scope_name).collection(config.collection_name); + + auto upsert_options = + couchbase::upsert_options{}.durability(couchbase::durability_level::majority); + { + bank_account alice{ "Alice", 124'000 }; + std::cout << "Initialize account for Alice: " << alice << "\n"; + auto [err, resp] = collection.upsert("alice", alice, upsert_options).get(); + if (err.ec()) { + std::cout << "Unable to create an account for Alice: " << err.message() << "\n"; + return EXIT_FAILURE; + } + std::cout << "Stored account for Alice (CAS=" << resp.cas().value() << ")\n"; + } + { + bank_account bob{ "Bob", 42'000 }; + std::cout << "Initialize account for Bob: " << bob << "\n"; + auto [err, resp] = collection.upsert("bob", bob, upsert_options).get(); + if (err.ec()) { + std::cout << "Unable to create an account for Bob: " << err.message() << "\n"; + return EXIT_FAILURE; + } + std::cout << "Stored account for Bob (CAS=" << resp.cas().value() << ")\n"; + } + + { + auto [err, res] = cluster.transactions()->run( + [collection]( + std::shared_ptr ctx) -> couchbase::error { + auto [e1, alice] = ctx->get(collection, "alice"); + if (e1.ec()) { + std::cout << "Unable to read account for Alice: " << e1.ec().message() << "\n"; + return e1; + } + auto alice_content = alice.content_as(); + + auto [e2, bob] = ctx->get(collection, "bob"); + if (e2.ec()) { + std::cout << "Unable to read account for Bob: " << e2.ec().message() << "\n"; + return e2; + } + auto bob_content = bob.content_as(); + + const std::int64_t money_to_transfer = 1'234; + if (alice_content.balance < money_to_transfer) { + std::cout << "Alice does not have enough money to transfer " << money_to_transfer + << " USD to Bob\n"; + return { + bank_error::insufficient_funds, + "not enough funds on Alice's account", + }; + } + alice_content.balance -= money_to_transfer; + bob_content.balance += money_to_transfer; + + { + auto [e3, a] = ctx->replace(alice, alice_content); + if (e3.ec()) { + std::cout << "Unable to read account for Alice: " << e3.ec().message() << "\n"; + } + } + { + auto [e4, b] = ctx->replace(bob, bob_content); + if (e4.ec()) { + std::cout << "Unable to update account for Bob: " << e4.ec().message() << "\n"; + } + } + return {}; + }); + + if (err.ec()) { + std::cout << "Transaction has failed: " << err.ec().message() << "\n"; + if (auto cause = err.cause(); cause.has_value()) { + std::cout << "Cause: " << cause->ec().message() << "\n"; + } + return EXIT_FAILURE; + } + } + + { + auto [err, resp] = collection.get("alice", {}).get(); + if (err.ec()) { + std::cout << "Unable to read account for Alice: " << err.message() << "\n"; + return EXIT_FAILURE; + } + std::cout << "Alice (CAS=" << resp.cas().value() << "): " << resp.content_as() << "\n"; + } + { + auto [err, resp] = collection.get("bob", {}).get(); + if (err.ec()) { + std::cout << "Unable to read account for Bob: " << err.message() << "\n"; + return EXIT_FAILURE; + } + std::cout << "Bob (CAS=" << resp.cas().value() << "): " << resp.content_as() << "\n"; + } + + cluster.close().get(); + + return 0; +} + +auto +program_config::from_env() -> program_config +{ + program_config config{}; + + if (const auto* val = getenv("CONNECTION_STRING"); val != nullptr) { + config.connection_string = val; + } + if (const auto* val = getenv("USERNAME"); val != nullptr) { + config.username = val; + } + if (const auto* val = getenv("PASSWORD"); val != nullptr) { + config.password = val; + } + if (const auto* val = getenv("BUCKET_NAME"); val != nullptr) { + config.bucket_name = val; + } + if (const auto* val = getenv("SCOPE_NAME"); val != nullptr) { + config.scope_name = val; + } + if (const auto* val = getenv("COLLECTION_NAME"); val != nullptr) { + config.collection_name = val; + } + if (const auto* val = getenv("PROFILE"); val != nullptr) { + config.profile = val; // e.g. "wan_development" + } + if (const auto* val = getenv("VERBOSE"); val != nullptr) { + const std::array truthy_values = { + "yes", "y", "on", "true", "1", + }; + for (const auto& truth : truthy_values) { + if (val == truth) { + config.verbose = true; + break; + } + } + } + + return config; +} + +auto +program_config::quote(std::string val) -> std::string +{ + return "\"" + val + "\""; +} + +void +program_config::dump() +{ + std::cout << " CONNECTION_STRING: " << quote(connection_string) << "\n"; + std::cout << " USERNAME: " << quote(username) << "\n"; + std::cout << " PASSWORD: [HIDDEN]\n"; + std::cout << " BUCKET_NAME: " << quote(bucket_name) << "\n"; + std::cout << " SCOPE_NAME: " << quote(scope_name) << "\n"; + std::cout << " COLLECTION_NAME: " << quote(collection_name) << "\n"; + std::cout << " VERBOSE: " << std::boolalpha << verbose << "\n"; + std::cout << " PROFILE: " << (profile ? quote(*profile) : "[NONE]") << "\n\n"; +}