Skip to content

Commit 555bd3a

Browse files
bhfCopilot
andcommitted
Implement extended cache operations and update API URL handling in CLI
Co-authored-by: Copilot <copilot@github.com>
1 parent 0755e7c commit 555bd3a

6 files changed

Lines changed: 252 additions & 13 deletions

File tree

.github/workflows/test.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ jobs:
4646
# Extract the API URL and ensure it ends with /cache
4747
if [ -f "$ENV_FILE" ]; then
4848
API_URL=$(grep AERON_CACHE_API "$ENV_FILE" | cut -d'=' -f2)
49-
if [[ "$API_URL" != */cache ]]; then
50-
API_URL="${API_URL%/}/cache"
51-
fi
5249
echo "Found API URL: $API_URL"
5350
echo "AERON_CACHE_API_URL=$API_URL" >> $GITHUB_ENV
5451
else

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Aeron Cache CLI
22

3+
34
[![CI](https://github.com/bhf/aeron-cache-cli/actions/workflows/test.yml/badge.svg)](https://github.com/bhf/aeron-cache-cli/actions/workflows/test.yml)
45

56
A CLI on top of the [Aeron Cache](https://github.com/bhf/aeron-cache) REST API.
@@ -12,6 +13,25 @@ Built in Rust 🦀 using:
1213
* [Serde](https://github.com/serde-rs/serde)
1314

1415

15-
You can also specify the URL of the Aeron-Cache API in an environment variable called ```AERON_CACHE_API_URL``` for convenience.
16-
1716
[![asciinema demo](https://asciinema.org/a/JPbih6gJuwFHZxhj9Op64UKgN.svg)](https://asciinema.org/a/JPbih6gJuwFHZxhj9Op64UKgN)
17+
18+
19+
By default, the CLI assumes that your backend cache service is reachable at `http://localhost:7070/api/v1`. If you need to override the API base URL, use the `--api-url` flag or the `AERON_CACHE_API_URL` environment variable.
20+
21+
## Available commands
22+
23+
It exposes the following commands:
24+
- `create <name>`: Create a new cache
25+
- `delete <name>`: Delete a cache
26+
- `get-cache <name>`: Get all items from a cache
27+
- `clear-cache <name>`: Clear all items from a cache
28+
- `insert <name> <key> <value>`: Insert an item into a cache
29+
- `get <name> <key>`: Get an item from a cache
30+
- `remove <name> <key>`: Remove an item from a cache
31+
- `list-caches`: List all available caches and their item count
32+
- `stats`: Get global cache statistics
33+
34+
Use the `--help` flag anytime for more information.
35+
36+
Example: `cargo run -- create mycache`
37+
Example: `cargo run -- --api-url http://otherhost:7070/api/v1 stats`

src/cacheresponses.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,55 @@ pub(crate) enum DeleteItemResult {
7575
Ok(DeleteItemResponse),
7676
Err(ErrorResponse),
7777
}
78+
79+
#[allow(non_snake_case)]
80+
#[derive(serde::Deserialize, Debug)]
81+
pub(crate) struct CacheItem {
82+
pub(crate) key: String,
83+
pub(crate) value: String,
84+
}
85+
86+
#[allow(non_snake_case)]
87+
#[derive(serde::Deserialize, Debug)]
88+
pub(crate) struct GetCacheResponse {
89+
pub(crate) cacheId: String,
90+
pub(crate) status: String,
91+
pub(crate) items: Vec<CacheItem>,
92+
}
93+
94+
#[derive(serde::Deserialize, Debug)]
95+
#[serde(untagged)]
96+
pub(crate) enum GetCacheResult {
97+
Ok(GetCacheResponse),
98+
Err(ErrorResponse),
99+
}
100+
101+
#[allow(non_snake_case)]
102+
#[derive(serde::Deserialize, Debug)]
103+
pub(crate) struct ClearCacheResponse {
104+
pub(crate) cacheId: String,
105+
pub(crate) status: String,
106+
}
107+
108+
#[derive(serde::Deserialize, Debug)]
109+
#[serde(untagged)]
110+
pub(crate) enum ClearCacheResult {
111+
Ok(ClearCacheResponse),
112+
Err(ErrorResponse),
113+
}
114+
115+
#[allow(non_snake_case)]
116+
#[derive(serde::Deserialize, Debug)]
117+
pub(crate) struct CacheDetails {
118+
pub(crate) cacheId: String,
119+
pub(crate) itemCount: i64,
120+
}
121+
122+
#[allow(non_snake_case)]
123+
#[derive(serde::Deserialize, Debug)]
124+
pub(crate) struct CacheStatsResponse {
125+
pub(crate) totalOps: i32,
126+
pub(crate) totalCaches: i32,
127+
pub(crate) totalItems: i32,
128+
pub(crate) errorCount: i32,
129+
}

src/commands.rs

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub(crate) fn process_get_item(
1414
) -> Result<(), Box<dyn Error>> {
1515

1616
let url = &format!(
17-
"{}/{}/{}",
17+
"{}/cache/{}/{}",
1818
aeron_cache_api_url, cache_name, key
1919
);
2020

@@ -47,7 +47,7 @@ pub(crate) fn process_remove_item(
4747
) -> Result<(), Box<dyn Error>> {
4848

4949
let url = &format!(
50-
"{}/{}/{}",
50+
"{}/cache/{}/{}",
5151
aeron_cache_api_url, cache_name, key
5252
);
5353

@@ -107,7 +107,7 @@ pub(crate) fn process_insert_item(
107107
value: &value,
108108
};
109109

110-
let url = &format!("{}/{}", aeron_cache_api_url, cache_name);
110+
let url = &format!("{}/cache/{}", aeron_cache_api_url, cache_name);
111111

112112
let put_item_response = rest_client
113113
.post(url)
@@ -136,7 +136,7 @@ pub(crate) fn process_create_cache(
136136
) -> Result<(), Box<dyn Error>> {
137137
let create_cache_request = CreateRequest { cacheId: name };
138138

139-
let formatted_url = &format!("{}", aeron_cache_api_url);
139+
let formatted_url = &format!("{}/cache", aeron_cache_api_url);
140140

141141
let create_cache_response = rest_client
142142
.post(formatted_url)
@@ -157,3 +157,99 @@ pub(crate) fn process_create_cache(
157157

158158
Ok(())
159159
}
160+
161+
pub(crate) fn process_get_cache(
162+
rest_client: &Client,
163+
aeron_cache_api_url: &str,
164+
cache_name: &String,
165+
) -> Result<(), Box<dyn Error>> {
166+
let url = &format!("{}/cache/{}", aeron_cache_api_url, cache_name);
167+
let response = rest_client.get(url).send()?;
168+
let body = response.text()?;
169+
let result: crate::cacheresponses::GetCacheResult = serde_json::from_str(&body)?;
170+
171+
match result {
172+
crate::cacheresponses::GetCacheResult::Ok(resp) => {
173+
println!("Cache ID: {}", resp.cacheId);
174+
println!("Status: {}", resp.status);
175+
if resp.items.is_empty() {
176+
println!("No items in cache.");
177+
} else {
178+
for item in resp.items {
179+
println!("Key: {}, Value: {}", item.key, item.value);
180+
}
181+
}
182+
}
183+
crate::cacheresponses::GetCacheResult::Err(err) => {
184+
println!("Error: {}", err.errorMsg);
185+
println!("Help: {}", err.helpMsg);
186+
}
187+
}
188+
Ok(())
189+
}
190+
191+
pub(crate) fn process_clear_cache(
192+
rest_client: &Client,
193+
aeron_cache_api_url: &str,
194+
cache_name: &String,
195+
) -> Result<(), Box<dyn Error>> {
196+
let url = &format!("{}/cache/{}", aeron_cache_api_url, cache_name);
197+
let response = rest_client.patch(url).send()?;
198+
let body = response.text()?;
199+
let result: crate::cacheresponses::ClearCacheResult = serde_json::from_str(&body)?;
200+
201+
match result {
202+
crate::cacheresponses::ClearCacheResult::Ok(resp) => {
203+
println!("Cleared cache: {}", resp.cacheId);
204+
}
205+
crate::cacheresponses::ClearCacheResult::Err(err) => {
206+
println!("Error: {}", err.errorMsg);
207+
println!("Help: {}", err.helpMsg);
208+
}
209+
}
210+
Ok(())
211+
}
212+
213+
pub(crate) fn process_list_caches(
214+
rest_client: &Client,
215+
aeron_cache_api_url: &str,
216+
) -> Result<(), Box<dyn Error>> {
217+
let url = &format!("{}/caches", aeron_cache_api_url);
218+
let response = rest_client.get(url).send()?;
219+
let body = response.text()?;
220+
221+
// According to openapi.yaml, GET /api/v1/caches returns an array of CacheDetails directly.
222+
// If it's an error, it might not be ErrorResponse? The spec doesn't list 400 for GetCaches, just 200.
223+
// We can try to parse as array or ErrorResponse. Let's just parse as array.
224+
if let Ok(caches) = serde_json::from_str::<Vec<crate::cacheresponses::CacheDetails>>(&body) {
225+
if caches.is_empty() {
226+
println!("No caches found.");
227+
} else {
228+
for cache in caches {
229+
println!("Cache ID: {}, Items: {}", cache.cacheId, cache.itemCount);
230+
}
231+
}
232+
} else {
233+
println!("Error parsing response: {}", body);
234+
}
235+
Ok(())
236+
}
237+
238+
pub(crate) fn process_get_stats(
239+
rest_client: &Client,
240+
aeron_cache_api_url: &str,
241+
) -> Result<(), Box<dyn Error>> {
242+
let url = &format!("{}/stats", aeron_cache_api_url);
243+
let response = rest_client.get(url).send()?;
244+
let body = response.text()?;
245+
if let Ok(stats) = serde_json::from_str::<crate::cacheresponses::CacheStatsResponse>(&body) {
246+
println!("Cache Statistics:");
247+
println!("Total Ops: {}", stats.totalOps);
248+
println!("Total Caches: {}", stats.totalCaches);
249+
println!("Total Items: {}", stats.totalItems);
250+
println!("Error Count: {}", stats.errorCount);
251+
} else {
252+
println!("Error parsing response: {}", body);
253+
}
254+
Ok(())
255+
}

src/main.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use commands::*;
1818
struct Cli {
1919
#[arg(
2020
long,
21-
default_value = "http://localhost:7070/api/v1/cache",
21+
default_value = "http://localhost:7070/api/v1",
2222
help = "Aeron Cache API base URL"
2323
)]
2424
api_url: String,
@@ -68,19 +68,37 @@ enum Commands {
6868
#[arg(short, long, help = "Automatically confirm deletion")]
6969
yes: bool,
7070
},
71+
72+
#[command(about = "Get all items from a cache")]
73+
GetCache {
74+
#[arg(help = "Name of the cache")]
75+
name: String,
76+
},
77+
78+
#[command(about = "Clear all items from a cache")]
79+
ClearCache {
80+
#[arg(help = "Name of the cache to clear")]
81+
name: String,
82+
},
83+
84+
#[command(about = "List all caches")]
85+
ListCaches,
86+
87+
#[command(about = "Get global cache statistics")]
88+
Stats,
7189
}
7290

7391
fn main() -> Result<(), Box<dyn Error>> {
7492
let cli = Cli::parse();
7593
let rest_client = Client::new();
7694

7795
// Check CLI arg, then env var, then default
78-
let aeron_cache_api_url = if cli.api_url != "http://localhost:7070/api/v1/cache" {
96+
let aeron_cache_api_url = if cli.api_url != "http://localhost:7070/api/v1" {
7997
cli.api_url
8098
} else if let Ok(val) = env::var("AERON_CACHE_API_URL") {
8199
val
82100
} else {
83-
"http://localhost:7070/api/v1/cache".to_string()
101+
"http://localhost:7070/api/v1".to_string()
84102
};
85103

86104
match cli.command {
@@ -109,6 +127,18 @@ fn main() -> Result<(), Box<dyn Error>> {
109127
println!("Cache '{}' not deleted", name)
110128
}
111129
}
130+
Commands::GetCache { name: cache_name } => {
131+
process_get_cache(&rest_client, &aeron_cache_api_url, &cache_name)?;
132+
}
133+
Commands::ClearCache { name: cache_name } => {
134+
process_clear_cache(&rest_client, &aeron_cache_api_url, &cache_name)?;
135+
}
136+
Commands::ListCaches => {
137+
process_list_caches(&rest_client, &aeron_cache_api_url)?;
138+
}
139+
Commands::Stats => {
140+
process_get_stats(&rest_client, &aeron_cache_api_url)?;
141+
}
112142
}
113143
Ok(())
114144
}

tests/integration_test.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use predicates::prelude::*;
33
use std::env;
44

55
fn get_api_url() -> String {
6-
env::var("AERON_CACHE_API_URL").unwrap_or_else(|_| "http://localhost:7070/api/v1/cache".to_string())
6+
env::var("AERON_CACHE_API_URL").unwrap_or_else(|_| "http://localhost:7070/api/v1".to_string())
77
}
88

99
#[test]
@@ -59,3 +59,47 @@ fn test_cache_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
5959
Ok(())
6060
}
6161

62+
63+
#[test]
64+
fn test_extended_cache_operations() -> Result<(), Box<dyn std::error::Error>> {
65+
let api_url = get_api_url();
66+
let cache_name = "test-extended-cache-operations-cache";
67+
68+
// Create Cache
69+
let mut cmd = Command::cargo_bin("CacheCLI")?;
70+
cmd.args(&["--api-url", &api_url, "create", cache_name]);
71+
cmd.assert().success();
72+
73+
// Insert Item
74+
let mut cmd = Command::cargo_bin("CacheCLI")?;
75+
cmd.args(&["--api-url", &api_url, "insert", cache_name, "mytestkey", "mytestvalue"]);
76+
cmd.assert().success();
77+
78+
// Get Cache
79+
let mut cmd = Command::cargo_bin("CacheCLI")?;
80+
cmd.args(&["--api-url", &api_url, "get-cache", cache_name]);
81+
// Might contain "Key: mytestkey, Value: mytestvalue"
82+
cmd.assert().success().stdout(predicate::str::contains("Key: mytestkey, Value: mytestvalue"));
83+
84+
// Clear Cache
85+
let mut cmd = Command::cargo_bin("CacheCLI")?;
86+
cmd.args(&["--api-url", &api_url, "clear-cache", cache_name]);
87+
cmd.assert().success().stdout(predicate::str::contains("Cleared cache:"));
88+
89+
// List Caches
90+
let mut cmd = Command::cargo_bin("CacheCLI")?;
91+
cmd.args(&["--api-url", &api_url, "list-caches"]);
92+
cmd.assert().success();
93+
94+
// Stats
95+
let mut cmd = Command::cargo_bin("CacheCLI")?;
96+
cmd.args(&["--api-url", &api_url, "stats"]);
97+
cmd.assert().success().stdout(predicate::str::contains("Cache Statistics:"));
98+
99+
// Cleanup
100+
let mut cmd = Command::cargo_bin("CacheCLI")?;
101+
cmd.args(&["--api-url", &api_url, "delete", cache_name, "--yes"]);
102+
cmd.assert().success();
103+
104+
Ok(())
105+
}

0 commit comments

Comments
 (0)