From 347df7d865c87bf05f7c2934df43f5017b90baf8 Mon Sep 17 00:00:00 2001 From: bhf Date: Fri, 11 Sep 2026 07:59:50 +0100 Subject: [PATCH 1/2] Update for counters operations --- README.md | 20 ++ src/cacherequests.rs | 8 + src/cacheresponses.rs | 30 +++ src/commands.rs | 77 ++++++- src/counterscommands.rs | 427 ++++++++++++++++++++++++++++++++++++++ src/countersrequests.rs | 43 ++++ src/countersresponses.rs | 205 ++++++++++++++++++ src/main.rs | 190 +++++++++++++++++ tests/integration_test.rs | 142 +++++++++++++ 9 files changed, 1140 insertions(+), 2 deletions(-) create mode 100644 src/counterscommands.rs create mode 100644 src/countersrequests.rs create mode 100644 src/countersresponses.rs diff --git a/README.md b/README.md index 6f66c59..15ac719 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,36 @@ By default, the CLI assumes that your backend cache service is reachable at `htt ## Available commands It exposes the following commands: + +### Cache commands - `create `: Create a new cache - `delete `: Delete a cache - `get-cache `: Get all items from a cache - `clear-cache `: Clear all items from a cache - `insert `: Insert an item into a cache +- `insert-timed `: Insert an item with a time-to-live (milliseconds) into a cache - `get `: Get an item from a cache - `remove `: Remove an item from a cache +- `cancel-removal `: Cancel a scheduled removal of an item - `list-caches`: List all available caches and their item count - `stats`: Get global cache statistics +### Counter commands +- `create-counter-cache `: Create a new counter cache +- `delete-counter-cache `: Delete a counter cache +- `get-counter-cache `: Get all counters from a counter cache +- `clear-counter-cache `: Clear all counters from a counter cache +- `put-counter `: Put a counter into a counter cache +- `put-timed-counter `: Put a counter with a time-to-live (milliseconds) into a counter cache +- `get-counter `: Get a single counter from a counter cache +- `delete-counter `: Delete a single counter from a counter cache +- `increment-counter [amount]`: Increment a counter (defaults to 1) +- `decrement-counter [amount]`: Decrement a counter (defaults to 1) +- `set-counter `: Set a counter to a given value +- `cancel-counter-removal `: Cancel a scheduled removal of a counter +- `list-counter-caches`: List all available counter caches and their item count +- `counter-stats`: Get global counter cache statistics + Use the `--help` flag anytime for more information. Example: `cargo run -- create mycache` diff --git a/src/cacherequests.rs b/src/cacherequests.rs index 0f777b9..43cf66a 100644 --- a/src/cacherequests.rs +++ b/src/cacherequests.rs @@ -11,4 +11,12 @@ pub(crate) struct CreateRequest<'a> { pub(crate) struct PutItemRequest<'a> { pub(crate) key: &'a str, pub(crate) value: &'a str, +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +pub(crate) struct PutTimedItemRequest<'a> { + pub(crate) key: &'a str, + pub(crate) value: &'a str, + pub(crate) ttl: i64, } \ No newline at end of file diff --git a/src/cacheresponses.rs b/src/cacheresponses.rs index cac1b6c..3ecbe4a 100644 --- a/src/cacheresponses.rs +++ b/src/cacheresponses.rs @@ -131,3 +131,33 @@ pub(crate) struct CacheStatsResponse { pub(crate) totalItemsCount: i32, pub(crate) errorCount: i32, } + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct PutTimedItemResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum PutTimedItemResult { + Ok(PutTimedItemResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct CancelItemRemovalResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum CancelItemRemovalResult { + Ok(CancelItemRemovalResponse), + Err(ErrorResponse), +} diff --git a/src/commands.rs b/src/commands.rs index 455d29c..b886ec9 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,7 @@ -use crate::cacherequests::{CreateRequest, PutItemRequest}; +use crate::cacherequests::{CreateRequest, PutItemRequest, PutTimedItemRequest}; use crate::cacheresponses::{ - CreateCacheResult, DeleteCacheResult, DeleteItemResult, GetItemResult, PutItemResult, + CancelItemRemovalResult, CreateCacheResult, DeleteCacheResult, DeleteItemResult, GetItemResult, + PutItemResult, PutTimedItemResult, }; use reqwest::blocking::Client; use reqwest::header::CONTENT_TYPE; @@ -129,6 +130,78 @@ pub(crate) fn process_insert_item( Ok(()) } +pub(crate) fn process_put_timed_item( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, + value: &String, + ttl: i64, +) -> Result<(), Box> { + let put_timed_item_request = PutTimedItemRequest { + key, + value, + ttl, + }; + + let url = &format!("{}/cache/timed/{}", aeron_cache_api_url, cache_name); + + let put_timed_item_response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&put_timed_item_request)?) + .send()?; + + let body = put_timed_item_response.text()?; + let result: PutTimedItemResult = serde_json::from_str(&body)?; + + match result { + PutTimedItemResult::Ok(resp) => println!( + "Put timed item into cache {} on key {} with ttl {}ms", + resp.cacheId, resp.key, ttl + ), + PutTimedItemResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_cancel_item_removal( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, +) -> Result<(), Box> { + let url = &format!( + "{}/cache/{}/{}/cancel-removal", + aeron_cache_api_url, cache_name, key + ); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .send()?; + + let body = response.text()?; + let result: CancelItemRemovalResult = serde_json::from_str(&body)?; + + match result { + CancelItemRemovalResult::Ok(resp) => println!( + "Cancelled scheduled removal of item in cache {} on key {}", + resp.cacheId, resp.key + ), + CancelItemRemovalResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + pub(crate) fn process_create_cache( rest_client: &Client, aeron_cache_api_url: &str, diff --git a/src/counterscommands.rs b/src/counterscommands.rs new file mode 100644 index 0000000..cb4643d --- /dev/null +++ b/src/counterscommands.rs @@ -0,0 +1,427 @@ +use crate::countersrequests::{ + CreateCounterRequest, DecrementCounterRequest, IncrementCounterRequest, PutCounterRequest, + PutTimedCounterRequest, SetCounterRequest, +}; +use crate::countersresponses::{ + CancelCounterRemovalResult, ClearCounterCacheResult, CreateCounterResult, + DecrementCounterResult, DeleteCounterCacheResult, DeleteCounterResult, GetCounterResult, + GetCountersResult, IncrementCounterResult, PutCounterResult, PutTimedCounterResult, + SetCounterResult, +}; +use reqwest::blocking::Client; +use reqwest::header::CONTENT_TYPE; +use std::error::Error; + +pub(crate) fn process_create_counter_cache( + rest_client: &Client, + aeron_cache_api_url: &str, + name: &String, +) -> Result<(), Box> { + let create_counter_request = CreateCounterRequest { cacheId: name }; + + let url = &format!("{}/counters/", aeron_cache_api_url); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&create_counter_request)?) + .send()?; + + let body = response.text()?; + let result: CreateCounterResult = serde_json::from_str(&body)?; + + match result { + CreateCounterResult::Ok(resp) => { + println!("Created counter cache with id: {}", resp.cacheId) + } + CreateCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_put_counter( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, + value: i64, +) -> Result<(), Box> { + let put_counter_request = PutCounterRequest { key, value }; + + let url = &format!("{}/counters/{}", aeron_cache_api_url, cache_name); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&put_counter_request)?) + .send()?; + + let body = response.text()?; + let result: PutCounterResult = serde_json::from_str(&body)?; + + match result { + PutCounterResult::Ok(resp) => println!( + "Put counter into cache {} on key {}", + resp.cacheId, resp.key + ), + PutCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_put_timed_counter( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, + value: i64, + ttl: i64, +) -> Result<(), Box> { + let put_timed_counter_request = PutTimedCounterRequest { key, value, ttl }; + + let url = &format!("{}/counters/timed/{}", aeron_cache_api_url, cache_name); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&put_timed_counter_request)?) + .send()?; + + let body = response.text()?; + let result: PutTimedCounterResult = serde_json::from_str(&body)?; + + match result { + PutTimedCounterResult::Ok(resp) => println!( + "Put timed counter into cache {} on key {} with ttl {}ms", + resp.cacheId, resp.key, ttl + ), + PutTimedCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_get_counter( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, +) -> Result<(), Box> { + let url = &format!("{}/counters/{}/{}", aeron_cache_api_url, cache_name, key); + + let response = rest_client.get(url).send()?; + + let body = response.text()?; + let result: GetCounterResult = serde_json::from_str(&body)?; + + match result { + GetCounterResult::Ok(resp) => println!( + "Got counter from cache {} on key {} with value {}", + resp.cacheId, resp.key, resp.value + ), + GetCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_delete_counter( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, +) -> Result<(), Box> { + let url = &format!("{}/counters/{}/{}", aeron_cache_api_url, cache_name, key); + + let response = rest_client.delete(url).send()?; + + let body = response.text()?; + let result: DeleteCounterResult = serde_json::from_str(&body)?; + + match result { + DeleteCounterResult::Ok(resp) => println!( + "Removed counter from cache {} on key {}", + resp.cacheId, resp.key + ), + DeleteCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_delete_counter_cache( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, +) -> Result<(), Box> { + let url = &format!("{}/counters/{}", aeron_cache_api_url, cache_name); + + let response = rest_client.delete(url).send()?; + + let body = response.text()?; + let result: DeleteCounterCacheResult = serde_json::from_str(&body)?; + + match result { + DeleteCounterCacheResult::Ok(resp) => { + println!("Deleted counter cache: {}", resp.cacheId) + } + DeleteCounterCacheResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_get_counter_cache( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, +) -> Result<(), Box> { + let url = &format!("{}/counters/{}", aeron_cache_api_url, cache_name); + let response = rest_client.get(url).send()?; + let body = response.text()?; + let result: GetCountersResult = serde_json::from_str(&body)?; + + match result { + GetCountersResult::Ok(resp) => { + println!("Counter Cache ID: {}", resp.cacheId); + println!("Status: {}", resp.operationStatus); + if let Some(items) = resp.items { + if items.is_empty() { + println!("No counters in cache."); + } else { + for item in items { + println!("Key: {}, Value: {}", item.key, item.value); + } + } + } else { + println!("No counters in cache."); + } + } + GetCountersResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + Ok(()) +} + +pub(crate) fn process_clear_counter_cache( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, +) -> Result<(), Box> { + let url = &format!("{}/counters/{}", aeron_cache_api_url, cache_name); + let response = rest_client.patch(url).send()?; + let body = response.text()?; + let result: ClearCounterCacheResult = serde_json::from_str(&body)?; + + match result { + ClearCounterCacheResult::Ok(resp) => { + println!("Cleared counter cache: {}", resp.cacheId); + } + ClearCounterCacheResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + Ok(()) +} + +pub(crate) fn process_increment_counter( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, + amount: i64, +) -> Result<(), Box> { + let increment_counter_request = IncrementCounterRequest { key, amount }; + + let url = &format!("{}/counters/increment/{}", aeron_cache_api_url, cache_name); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&increment_counter_request)?) + .send()?; + + let body = response.text()?; + let result: IncrementCounterResult = serde_json::from_str(&body)?; + + match result { + IncrementCounterResult::Ok(resp) => println!( + "Incremented counter in cache {} on key {} to value {}", + resp.cacheId, resp.key, resp.value + ), + IncrementCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_decrement_counter( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, + amount: i64, +) -> Result<(), Box> { + let decrement_counter_request = DecrementCounterRequest { key, amount }; + + let url = &format!("{}/counters/decrement/{}", aeron_cache_api_url, cache_name); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&decrement_counter_request)?) + .send()?; + + let body = response.text()?; + let result: DecrementCounterResult = serde_json::from_str(&body)?; + + match result { + DecrementCounterResult::Ok(resp) => println!( + "Decremented counter in cache {} on key {} to value {}", + resp.cacheId, resp.key, resp.value + ), + DecrementCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_set_counter( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, + value: i64, +) -> Result<(), Box> { + let set_counter_request = SetCounterRequest { key, value }; + + let url = &format!("{}/counters/set/{}", aeron_cache_api_url, cache_name); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .body(serde_json::to_string(&set_counter_request)?) + .send()?; + + let body = response.text()?; + let result: SetCounterResult = serde_json::from_str(&body)?; + + match result { + SetCounterResult::Ok(resp) => println!( + "Set counter in cache {} on key {} to value {}", + resp.cacheId, resp.key, resp.value + ), + SetCounterResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_cancel_counter_removal( + rest_client: &Client, + aeron_cache_api_url: &str, + cache_name: &String, + key: &String, +) -> Result<(), Box> { + let url = &format!( + "{}/counters/{}/{}/cancel-removal", + aeron_cache_api_url, cache_name, key + ); + + let response = rest_client + .post(url) + .header(CONTENT_TYPE, "application/json") + .send()?; + + let body = response.text()?; + let result: CancelCounterRemovalResult = serde_json::from_str(&body)?; + + match result { + CancelCounterRemovalResult::Ok(resp) => println!( + "Cancelled scheduled removal of counter in cache {} on key {}", + resp.cacheId, resp.key + ), + CancelCounterRemovalResult::Err(err) => { + println!("Error: {}", err.errorMsg); + println!("Help: {}", err.helpMsg); + } + } + + Ok(()) +} + +pub(crate) fn process_list_counter_caches( + rest_client: &Client, + aeron_cache_api_url: &str, +) -> Result<(), Box> { + let url = &format!("{}/counters-caches", aeron_cache_api_url); + let response = rest_client.get(url).send()?; + let body = response.text()?; + + if let Ok(caches) = + serde_json::from_str::>(&body) + { + if caches.is_empty() { + println!("No counter caches found."); + } else { + for cache in caches { + println!("Counter Cache ID: {}, Items: {}", cache.cacheId, cache.itemCount); + } + } + } else { + println!("Error parsing response: {}", body); + } + Ok(()) +} + +pub(crate) fn process_get_counter_stats( + rest_client: &Client, + aeron_cache_api_url: &str, +) -> Result<(), Box> { + let url = &format!("{}/counters-stats", aeron_cache_api_url); + let response = rest_client.get(url).send()?; + let body = response.text()?; + if let Ok(stats) = serde_json::from_str::(&body) + { + println!("Counter Cache Statistics:"); + println!("Total Ops: {}", stats.totalOpsCount); + println!("Total Caches: {}", stats.totalCachesCount); + println!("Total Items: {}", stats.totalItemsCount); + println!("Error Count: {}", stats.errorCount); + } else { + println!("Error parsing response: {}", body); + } + Ok(()) +} diff --git a/src/countersrequests.rs b/src/countersrequests.rs new file mode 100644 index 0000000..626bb9c --- /dev/null +++ b/src/countersrequests.rs @@ -0,0 +1,43 @@ +use serde::Serialize; + +#[allow(non_snake_case)] +#[derive(Serialize)] +pub(crate) struct CreateCounterRequest<'a> { + pub(crate) cacheId: &'a str, +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +pub(crate) struct PutCounterRequest<'a> { + pub(crate) key: &'a str, + pub(crate) value: i64, +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +pub(crate) struct PutTimedCounterRequest<'a> { + pub(crate) key: &'a str, + pub(crate) value: i64, + pub(crate) ttl: i64, +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +pub(crate) struct IncrementCounterRequest<'a> { + pub(crate) key: &'a str, + pub(crate) amount: i64, +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +pub(crate) struct DecrementCounterRequest<'a> { + pub(crate) key: &'a str, + pub(crate) amount: i64, +} + +#[allow(non_snake_case)] +#[derive(Serialize)] +pub(crate) struct SetCounterRequest<'a> { + pub(crate) key: &'a str, + pub(crate) value: i64, +} diff --git a/src/countersresponses.rs b/src/countersresponses.rs new file mode 100644 index 0000000..82dae10 --- /dev/null +++ b/src/countersresponses.rs @@ -0,0 +1,205 @@ +use crate::cacheresponses::ErrorResponse; + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct CreateCounterResponse { + pub(crate) cacheId: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum CreateCounterResult { + Ok(CreateCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct PutCounterResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum PutCounterResult { + Ok(PutCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct CounterItem { + pub(crate) key: String, + pub(crate) value: i64, +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct GetCountersResponse { + pub(crate) cacheId: String, + pub(crate) operationStatus: String, + pub(crate) items: Option>, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum GetCountersResult { + Ok(GetCountersResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct GetCounterResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) value: i64, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum GetCounterResult { + Ok(GetCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct DeleteCounterCacheResponse { + pub(crate) cacheId: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum DeleteCounterCacheResult { + Ok(DeleteCounterCacheResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct DeleteCounterResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum DeleteCounterResult { + Ok(DeleteCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct ClearCounterCacheResponse { + pub(crate) cacheId: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum ClearCounterCacheResult { + Ok(ClearCounterCacheResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct IncrementCounterResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) value: i64, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum IncrementCounterResult { + Ok(IncrementCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct DecrementCounterResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) value: i64, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum DecrementCounterResult { + Ok(DecrementCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct SetCounterResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) value: i64, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum SetCounterResult { + Ok(SetCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct PutTimedCounterResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum PutTimedCounterResult { + Ok(PutTimedCounterResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct CancelCounterRemovalResponse { + pub(crate) cacheId: String, + pub(crate) key: String, + pub(crate) operationStatus: String, +} + +#[derive(serde::Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum CancelCounterRemovalResult { + Ok(CancelCounterRemovalResponse), + Err(ErrorResponse), +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct CounterCacheDetails { + pub(crate) cacheId: String, + pub(crate) itemCount: i64, +} + +#[allow(non_snake_case)] +#[derive(serde::Deserialize, Debug)] +pub(crate) struct CounterStatsResponse { + pub(crate) totalOpsCount: i32, + pub(crate) totalCachesCount: i32, + pub(crate) totalItemsCount: i32, + pub(crate) errorCount: i32, +} diff --git a/src/main.rs b/src/main.rs index bda8455..31c83d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ mod commands; mod cacherequests; mod cacheresponses; +mod counterscommands; +mod countersrequests; +mod countersresponses; use clap::{Parser, Subcommand}; use dialoguer::Confirm; @@ -9,6 +12,7 @@ use std::error::Error; use std::env; use commands::*; +use counterscommands::*; #[derive(Parser)] #[command( @@ -45,6 +49,18 @@ enum Commands { value: String, }, + #[command(about = "Insert an item with a time-to-live into a cache")] + InsertTimed { + #[arg(help = "Name of the cache to insert into")] + name: String, + #[arg(help = "Key to insert the item")] + key: String, + #[arg(help = "Value to be inserted")] + value: String, + #[arg(help = "Time-to-live in milliseconds")] + ttl: i64, + }, + #[command(about = "Get an item from a cache")] Get { #[arg(help = "Name of the cache to get the item from")] @@ -61,6 +77,14 @@ enum Commands { key: String, }, + #[command(about = "Cancel a scheduled removal of an item")] + CancelRemoval { + #[arg(help = "Name of the cache")] + name: String, + #[arg(help = "Key of the item")] + key: String, + }, + #[command(about = "Delete a cache")] Delete { #[arg(help = "Name of the cache to delete")] @@ -86,6 +110,114 @@ enum Commands { #[command(about = "Get global cache statistics")] Stats, + + #[command(about = "Create a new counter cache")] + CreateCounterCache { + #[arg(help = "Name of the counter cache to create")] + name: String, + }, + + #[command(about = "Put a counter into a counter cache")] + PutCounter { + #[arg(help = "Name of the counter cache to put into")] + name: String, + #[arg(help = "Key of the counter")] + key: String, + #[arg(help = "Value of the counter")] + value: i64, + }, + + #[command(about = "Put a counter with a time-to-live into a counter cache")] + PutTimedCounter { + #[arg(help = "Name of the counter cache to put into")] + name: String, + #[arg(help = "Key of the counter")] + key: String, + #[arg(help = "Value of the counter")] + value: i64, + #[arg(help = "Time-to-live in milliseconds")] + ttl: i64, + }, + + #[command(about = "Get a single counter from a counter cache")] + GetCounter { + #[arg(help = "Name of the counter cache to get the counter from")] + name: String, + #[arg(help = "Key of the counter we want to get")] + key: String, + }, + + #[command(about = "Delete a single counter from a counter cache")] + DeleteCounter { + #[arg(help = "Name of the counter cache to delete from")] + name: String, + #[arg(help = "Key of the counter we want to delete")] + key: String, + }, + + #[command(about = "Increment a counter")] + IncrementCounter { + #[arg(help = "Name of the counter cache")] + name: String, + #[arg(help = "Key of the counter")] + key: String, + #[arg(default_value_t = 1, help = "Amount to increment by")] + amount: i64, + }, + + #[command(about = "Decrement a counter")] + DecrementCounter { + #[arg(help = "Name of the counter cache")] + name: String, + #[arg(help = "Key of the counter")] + key: String, + #[arg(default_value_t = 1, help = "Amount to decrement by")] + amount: i64, + }, + + #[command(about = "Set a counter to a given value")] + SetCounter { + #[arg(help = "Name of the counter cache")] + name: String, + #[arg(help = "Key of the counter")] + key: String, + #[arg(help = "Value to set the counter to")] + value: i64, + }, + + #[command(about = "Cancel a scheduled removal of a counter")] + CancelCounterRemoval { + #[arg(help = "Name of the counter cache")] + name: String, + #[arg(help = "Key of the counter")] + key: String, + }, + + #[command(about = "Get all counters from a counter cache")] + GetCounterCache { + #[arg(help = "Name of the counter cache")] + name: String, + }, + + #[command(about = "Clear all counters from a counter cache")] + ClearCounterCache { + #[arg(help = "Name of the counter cache to clear")] + name: String, + }, + + #[command(about = "Delete a counter cache")] + DeleteCounterCache { + #[arg(help = "Name of the counter cache to delete")] + name: String, + #[arg(short, long, help = "Automatically confirm deletion")] + yes: bool, + }, + + #[command(about = "List all counter caches")] + ListCounterCaches, + + #[command(about = "Get global counter cache statistics")] + CounterStats, } fn main() -> Result<(), Box> { @@ -108,12 +240,18 @@ fn main() -> Result<(), Box> { Commands::Insert { name: cache_name, key, value } => { process_insert_item(&rest_client, &aeron_cache_api_url, &cache_name, &key, value)?; } + Commands::InsertTimed { name: cache_name, key, value, ttl } => { + process_put_timed_item(&rest_client, &aeron_cache_api_url, &cache_name, &key, &value, ttl)?; + } Commands::Get { name: cache_name, key } => { process_get_item(&rest_client, &aeron_cache_api_url, &cache_name, &key)?; } Commands::Remove { name: cache_name, key } => { process_remove_item(rest_client, &aeron_cache_api_url, &cache_name, &key)?; } + Commands::CancelRemoval { name: cache_name, key } => { + process_cancel_item_removal(&rest_client, &aeron_cache_api_url, &cache_name, &key)?; + } Commands::Delete { name, yes } => { if yes || Confirm::new() .with_prompt(format!( @@ -139,6 +277,58 @@ fn main() -> Result<(), Box> { Commands::Stats => { process_get_stats(&rest_client, &aeron_cache_api_url)?; } + Commands::CreateCounterCache { name } => { + process_create_counter_cache(&rest_client, &aeron_cache_api_url, &name)?; + } + Commands::PutCounter { name, key, value } => { + process_put_counter(&rest_client, &aeron_cache_api_url, &name, &key, value)?; + } + Commands::PutTimedCounter { name, key, value, ttl } => { + process_put_timed_counter(&rest_client, &aeron_cache_api_url, &name, &key, value, ttl)?; + } + Commands::GetCounter { name, key } => { + process_get_counter(&rest_client, &aeron_cache_api_url, &name, &key)?; + } + Commands::DeleteCounter { name, key } => { + process_delete_counter(&rest_client, &aeron_cache_api_url, &name, &key)?; + } + Commands::IncrementCounter { name, key, amount } => { + process_increment_counter(&rest_client, &aeron_cache_api_url, &name, &key, amount)?; + } + Commands::DecrementCounter { name, key, amount } => { + process_decrement_counter(&rest_client, &aeron_cache_api_url, &name, &key, amount)?; + } + Commands::SetCounter { name, key, value } => { + process_set_counter(&rest_client, &aeron_cache_api_url, &name, &key, value)?; + } + Commands::CancelCounterRemoval { name, key } => { + process_cancel_counter_removal(&rest_client, &aeron_cache_api_url, &name, &key)?; + } + Commands::GetCounterCache { name } => { + process_get_counter_cache(&rest_client, &aeron_cache_api_url, &name)?; + } + Commands::ClearCounterCache { name } => { + process_clear_counter_cache(&rest_client, &aeron_cache_api_url, &name)?; + } + Commands::DeleteCounterCache { name, yes } => { + if yes || Confirm::new() + .with_prompt(format!( + "Are you sure you want to delete counter cache '{}'? This action cannot be undone.", + name + )) + .interact()? + { + process_delete_counter_cache(&rest_client, &aeron_cache_api_url, &name)?; + } else { + println!("Counter cache '{}' not deleted", name) + } + } + Commands::ListCounterCaches => { + process_list_counter_caches(&rest_client, &aeron_cache_api_url)?; + } + Commands::CounterStats => { + process_get_counter_stats(&rest_client, &aeron_cache_api_url)?; + } } Ok(()) } diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 16b6518..bdff3b2 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -60,6 +60,39 @@ fn test_cache_lifecycle() -> Result<(), Box> { } +#[test] +fn test_timed_item_and_cancel_removal() -> Result<(), Box> { + let api_url = get_api_url(); + let cache_name = "test-timed-item-cancel-removal"; + + // Create Cache + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "create", cache_name]); + cmd.assert().success(); + + // Insert Timed Item + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "insert-timed", cache_name, "timedkey", "timedvalue", "60000"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Put timed item into cache {} on key timedkey", cache_name))); + + // Cancel Removal + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "cancel-removal", cache_name, "timedkey"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Cancelled scheduled removal of item in cache {} on key timedkey", cache_name))); + + // Cleanup + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "delete", cache_name, "--yes"]); + cmd.assert().success(); + + Ok(()) +} + + #[test] fn test_extended_cache_operations() -> Result<(), Box> { let api_url = get_api_url(); @@ -103,3 +136,112 @@ fn test_extended_cache_operations() -> Result<(), Box> { Ok(()) } + + +#[test] +fn test_counter_lifecycle() -> Result<(), Box> { + let api_url = get_api_url(); + let cache_name = "test-counter-lifecycle"; + + // 1. Create Counter Cache + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "create-counter-cache", cache_name]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Created counter cache with id: {}", cache_name))); + + // 2. Put Counter + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "put-counter", cache_name, "mycounter", "42"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Put counter into cache {} on key mycounter", cache_name))); + + // 3. Get Counter + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "get-counter", cache_name, "mycounter"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Got counter from cache {} on key mycounter with value 42", cache_name))); + + // 4. Increment Counter + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "increment-counter", cache_name, "mycounter", "8"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Incremented counter in cache {} on key mycounter to value 50", cache_name))); + + // 5. Decrement Counter + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "decrement-counter", cache_name, "mycounter", "10"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Decremented counter in cache {} on key mycounter to value 40", cache_name))); + + // 6. Set Counter + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "set-counter", cache_name, "mycounter", "100"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Set counter in cache {} on key mycounter to value 100", cache_name))); + + // 7. Delete Counter + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "delete-counter", cache_name, "mycounter"]); + cmd.assert() + .success() + .stdout(predicate::str::contains(format!("Removed counter from cache {} on key mycounter", cache_name))); + + // 8. Delete Counter Cache (using the --yes flag) + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "delete-counter-cache", cache_name, "--yes"]); + cmd.assert() + .success() + .stdout(predicate::str::contains("Deleted counter cache: ")); + + Ok(()) +} + + +#[test] +fn test_extended_counter_operations() -> Result<(), Box> { + let api_url = get_api_url(); + let cache_name = "test-extended-counter-operations-cache"; + + // Create Counter Cache + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "create-counter-cache", cache_name]); + cmd.assert().success(); + + // Put Counter + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "put-counter", cache_name, "mytestcounter", "7"]); + cmd.assert().success(); + + // Get Counter Cache + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "get-counter-cache", cache_name]); + cmd.assert().success().stdout(predicate::str::contains("Key: mytestcounter, Value: 7")); + + // Clear Counter Cache + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "clear-counter-cache", cache_name]); + cmd.assert().success().stdout(predicate::str::contains("Cleared counter cache:")); + + // List Counter Caches + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "list-counter-caches"]); + cmd.assert().success(); + + // Counter Stats + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "counter-stats"]); + cmd.assert().success().stdout(predicate::str::contains("Counter Cache Statistics:")); + + // Cleanup + let mut cmd = Command::cargo_bin("CacheCLI")?; + cmd.args(&["--api-url", &api_url, "delete-counter-cache", cache_name, "--yes"]); + cmd.assert().success(); + + Ok(()) +} From 78865de8a5cc1a78fcc6330e4356599cd3e16074 Mon Sep 17 00:00:00 2001 From: bhf Date: Fri, 11 Sep 2026 08:02:50 +0100 Subject: [PATCH 2/2] Fix CI --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eecdf9a..9f17d81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,10 @@ jobs: uses: dtolnay/rust-toolchain@stable - name: Install Aeron Cache Backend + env: + HOMEBREW_NO_AUTO_UPDATE: "1" + HOMEBREW_NO_INSTALL_CLEANUP: "1" + HOMEBREW_NO_REQUIRE_TAP_TRUST: "1" run: | brew tap bhf/aeroncache brew install aeron-cache