Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
- reductstore_version: "main"
exclude_api_version_tag: ""
- reductstore_version: "latest"
exclude_api_version_tag: "~[1_17]"
exclude_api_version_tag: "~[1_18]"
- license_file: ""
exclude_license_tag: "~[license]"

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.cmake
31 changes: 31 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Repository Guidelines

## Project Structure & Modules
- `src/reduct`: public SDK surface (`client`, `bucket`, `error`, `result`, `http_options`); `src/reduct/internal`: HTTP transport, serialization, and time parsing helpers.
- `tests`: Catch2 runner in `tests/test.cc` with API scenarios in `tests/reduct/*_test.cc`; `tests/fixture.h` handles cleanup/fixtures against a live server.
- `examples`: minimal client usage samples; use them as templates for docs or quick checks.
- `cmake/`: dependency setup and install helpers; `conanfile.py`, `vcpkg*.json`: package manager configs.
- CMake builds default to `build/`; keep generated artifacts out of `src/` and `tests/`.

## Build, Test & Dev Commands
- Configure: `cmake -S . -B build [-DREDUCT_CPP_USE_FETCHCONTENT=ON] [-DREDUCT_CPP_ENABLE_TESTS=ON]`.
- Build: `cmake --build build [--config Release]`.
- Tests: after enabling tests, `cmake --build build --target reduct-tests` then `ctest --test-dir build` or run `./build/bin/reduct-tests`.
- Conan flow: `conan install . --build=missing && cmake --preset conan-release && cmake --build --preset conan-release --config Release`.
- Install SDK system-wide when needed: `sudo cmake --install build`.

## Coding Style & Naming
- C++20, 2-space indentation, keep lines ≤120 chars (`CPPLINT.cfg`); prefer initializer lists and `auto` only when it aids readability.
- Classes/types/methods use `CamelCase` (e.g., `IClient::GetBucket`); locals/parameters stay `snake_case`.
- Keep headers self-contained; minimize headers in `.cc` files to what is required; avoid raw `new`/`delete` in favor of RAII utilities.

## Testing Guidelines
- Framework: Catch2 v2 via `FetchContent`.
- Tests live in `tests/reduct/*_test.cc`; name cases to mirror API behaviors being verified.
- Integration tests expect a reachable ReductStore at `http://127.0.0.1:8383`; set `REDUCT_CPP_TOKEN_API` for auth. Fixture purges `test_*` buckets/tokens/replications before each run.
- Add or extend tests with new APIs, bug fixes, and boundary conditions that touch storage state.

## Commit & PR Guidelines
- Commits: concise, imperative summaries (`Fix parsing server url`), optionally append issue refs like `(#102)`.
- PRs: describe intent and behavior changes, link issues, call out API impacts, include test commands/output, and note any server or env requirements.
- Ensure builds/tests pass before review; include screenshots only when modifying docs or examples.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add communication timeouts to client settings, [PR-101](https://github.com/reductstore/reduct-cpp/pull/101)
- Support replication modes (`enabled`, `paused`, `disabled`) and mode updates via PATCH, [PR-103](https://github.com/reductstore/reduct-cpp/pull/103)

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,9 @@ The library is backward compatible with the previous versions. However, some met
removed in the future releases. Please refer to the [Changelog](CHANGELOG.md) for more details.
The SDK supports the following ReductStore API versions:

* v1.18
* v1.17
* v1.16
* v1.15

It can work with newer and older versions, but it is not guaranteed that all features will work as expected because
the API may change and some features may be deprecated or the SDK may not support them yet.
24 changes: 22 additions & 2 deletions src/reduct/client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <nlohmann/json.hpp>

#include <iostream>
#include <stdexcept>

#include "internal/time_parse.h"
#include "reduct/internal/http_client.h"
Expand Down Expand Up @@ -236,15 +237,34 @@ class Client : public IClient {
}

Error CreateReplication(std::string_view name, ReplicationSettings settings) const noexcept override {
auto json_data = internal::ReplicationSettingsToJsonString(std::move(settings));
auto [json_data, json_err] = internal::ReplicationSettingsToJsonString(std::move(settings));
if (json_err) {
return json_err;
}

return client_->Post(fmt::format("/replications/{}", name), json_data.dump());
}

Error UpdateReplication(std::string_view name, ReplicationSettings settings) const noexcept override {
auto json_data = internal::ReplicationSettingsToJsonString(std::move(settings));
auto [json_data, json_err] = internal::ReplicationSettingsToJsonString(std::move(settings));
if (json_err) {
return json_err;
}

return client_->Put(fmt::format("/replications/{}", name), json_data.dump());
}

Error SetReplicationMode(std::string_view name, ReplicationMode mode) const noexcept override {
try {
nlohmann::json payload = {{"mode", internal::ReplicationModeToString(mode)}};
auto patch_result = client_->Patch(fmt::format("/replications/{}/mode", name), payload.dump(),
{{"Content-Type", "application/json"}});
return patch_result.error;
} catch (const std::exception& ex) {
return Error{.code = -1, .message = ex.what()};
}
}

Error RemoveReplication(std::string_view name) const noexcept override {
return client_->Delete(fmt::format("/replications/{}", name));
}
Expand Down
12 changes: 12 additions & 0 deletions src/reduct/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,11 @@ class IClient {
/**
* Replication information
*/
enum class ReplicationMode { kEnabled, kPaused, kDisabled };

struct ReplicationInfo {
std::string name; // Replication name
ReplicationMode mode = ReplicationMode::kEnabled; // Replication mode
bool is_active; // Remote instance is available and replication is active
bool is_provisioned; // Replication settings
uint64_t pending_records; // Number of records pending replication
Expand All @@ -203,6 +206,7 @@ class IClient {
[[deprecated("Use when instead. Will be removed in v1.18.0")]]
std::optional<uint64_t> each_n; // Replicate every Nth record if not empty
std::optional<std::string> when; // Replication condition
ReplicationMode mode = ReplicationMode::kEnabled; // Replication mode

auto operator<=>(const ReplicationSettings&) const = default;
};
Expand Down Expand Up @@ -247,6 +251,14 @@ class IClient {
*/
[[nodiscard]] virtual Error UpdateReplication(std::string_view name, ReplicationSettings settings) const noexcept = 0;

/**
* @brief Update replication mode without changing settings
* @param name name of replication
* @param mode replication mode
* @return error
*/
[[nodiscard]] virtual Error SetReplicationMode(std::string_view name, ReplicationMode mode) const noexcept = 0;

/**
* @brief Remove replication
* @param name name of replication
Expand Down
117 changes: 86 additions & 31 deletions src/reduct/internal/serialisation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,50 @@

#include "reduct/internal/serialisation.h"

#include <stdexcept>

#include "time_parse.h"

namespace reduct::internal {

namespace {

IClient::ReplicationMode ParseReplicationMode(const nlohmann::json& mode_json) {
if (mode_json.is_null()) {
return IClient::ReplicationMode::kEnabled;
}

const auto mode = mode_json.get<std::string>();
if (mode == "enabled") {
return IClient::ReplicationMode::kEnabled;
}

if (mode == "paused") {
return IClient::ReplicationMode::kPaused;
}

if (mode == "disabled") {
return IClient::ReplicationMode::kDisabled;
}

throw std::invalid_argument("Invalid replication mode: " + mode);
}

} // namespace

std::string ReplicationModeToString(IClient::ReplicationMode mode) {
switch (mode) {
case IClient::ReplicationMode::kEnabled:
return "enabled";
case IClient::ReplicationMode::kPaused:
return "paused";
case IClient::ReplicationMode::kDisabled:
return "disabled";
}

throw std::invalid_argument("Invalid replication mode");
}

nlohmann::json BucketSettingToJsonString(const IBucket::Settings& settings) {
nlohmann::json data;
const auto& [max_block_size, quota_type, quota_size, max_record_size] = settings;
Expand Down Expand Up @@ -86,53 +126,66 @@ Result<IClient::FullTokenInfo> ParseTokenInfo(const nlohmann::json& json) {
Result<std::vector<IClient::ReplicationInfo>> ParseReplicationList(const nlohmann::json& data) {
std::vector<IClient::ReplicationInfo> replication_list;

auto json_replications = data.at("replications");
replication_list.reserve(json_replications.size());
for (const auto& replication : json_replications) {
replication_list.push_back(IClient::ReplicationInfo{
.name = replication.at("name"),
.is_active = replication.at("is_active"),
.is_provisioned = replication.at("is_provisioned"),
.pending_records = replication.at("pending_records"),
});
try {
auto json_replications = data.at("replications");
replication_list.reserve(json_replications.size());
for (const auto& replication : json_replications) {
replication_list.push_back(IClient::ReplicationInfo{
.name = replication.at("name"),
.mode = replication.contains("mode") ? ParseReplicationMode(replication.at("mode"))
: IClient::ReplicationMode::kEnabled,
.is_active = replication.at("is_active"),
.is_provisioned = replication.at("is_provisioned"),
.pending_records = replication.at("pending_records"),
});
}
} catch (const std::exception& ex) {
return {{}, Error{.code = -1, .message = ex.what()}};
}

return {replication_list, Error::kOk};
}

nlohmann::json ReplicationSettingsToJsonString(IClient::ReplicationSettings settings) {
nlohmann::json json_data;
json_data["src_bucket"] = settings.src_bucket;
json_data["dst_bucket"] = settings.dst_bucket;
json_data["dst_host"] = settings.dst_host;
if (settings.dst_token) {
json_data["dst_token"] = *settings.dst_token;
}
json_data["entries"] = settings.entries;
if (settings.each_s) {
json_data["each_s"] = *settings.each_s;
}
Result<nlohmann::json> ReplicationSettingsToJsonString(IClient::ReplicationSettings settings) {
try {
nlohmann::json json_data;
json_data["src_bucket"] = settings.src_bucket;
json_data["dst_bucket"] = settings.dst_bucket;
json_data["dst_host"] = settings.dst_host;
if (settings.dst_token) {
json_data["dst_token"] = *settings.dst_token;
}
json_data["entries"] = settings.entries;
json_data["mode"] = ReplicationModeToString(settings.mode);
if (settings.each_s) {
json_data["each_s"] = *settings.each_s;
}

if (settings.each_n) {
json_data["each_n"] = *settings.each_n;
}
if (settings.each_n) {
json_data["each_n"] = *settings.each_n;
}

if (settings.when) {
try {
json_data["when"] = nlohmann::json::parse(*settings.when);
} catch (const std::exception& ex) {
return {{}, Error{.code = -1, .message = ex.what()}};
if (settings.when) {
try {
json_data["when"] = nlohmann::json::parse(*settings.when);
} catch (const std::exception& ex) {
return {{}, Error{.code = -1, .message = ex.what()}};
}
}
}

return json_data;
return {json_data, Error::kOk};
} catch (const std::exception& ex) {
return {{}, Error{.code = -1, .message = ex.what()}};
}
}

Result<IClient::FullReplicationInfo> ParseFullReplicationInfo(const nlohmann::json& data) {
IClient::FullReplicationInfo info;
try {
info.info = IClient::ReplicationInfo{
.name = data.at("info").at("name"),
.mode = data.at("info").contains("mode") ? ParseReplicationMode(data.at("info").at("mode"))
: IClient::ReplicationMode::kEnabled,
.is_active = data.at("info").at("is_active"),
.is_provisioned = data.at("info").at("is_provisioned"),
.pending_records = data.at("info").at("pending_records"),
Expand All @@ -144,6 +197,8 @@ Result<IClient::FullReplicationInfo> ParseFullReplicationInfo(const nlohmann::js
.dst_bucket = settings.at("dst_bucket"),
.dst_host = settings.at("dst_host"),
.entries = settings.at("entries"),
.mode = settings.contains("mode") ? ParseReplicationMode(settings.at("mode"))
: IClient::ReplicationMode::kEnabled,
};

if (settings.contains("dst_token") && !settings.at("dst_token").is_null()) {
Expand Down
11 changes: 10 additions & 1 deletion src/reduct/internal/serialisation.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#ifndef REDUCTCPP_SERIALISATION_H
#define REDUCTCPP_SERIALISATION_H

#include <string>

#include <nlohmann/json.hpp>

#include "reduct/bucket.h"
Expand Down Expand Up @@ -37,12 +39,19 @@ Result<IClient::FullTokenInfo> ParseTokenInfo(const nlohmann::json& json);
*/
Result<std::vector<IClient::ReplicationInfo>> ParseReplicationList(const nlohmann::json& data);

/**
* @brief Convert replication mode to string representation
* @param mode replication mode
* @return string value expected by API
*/
std::string ReplicationModeToString(IClient::ReplicationMode mode);

/**
* @brief Serialize replication settings
* @param settings to serialize
* @return json
*/
nlohmann::json ReplicationSettingsToJsonString(IClient::ReplicationSettings settings);
Result<nlohmann::json> ReplicationSettingsToJsonString(IClient::ReplicationSettings settings);

/**
* @brief Parse full replication info from JSON string
Expand Down
Loading
Loading