Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add compression option to replica create, update, and show commands, [PR-261](https://github.com/reductstore/reduct-cli/pull/261) by @vbmade2000
- Improve time parsing for --start and --stop options, [PR-226](https://github.com/reductstore/reduct-cli/pull/226) by @yaslam-dev
- Added `write` command to write a single record to a bucket, [PR-275](https://github.com/reductstore/reduct-cli/pull/275) by @vbmade2000
- Add `--json` flag to `bucket ls`, `bucket show`, `bucket create`, `bucket update`, `bucket rename` and `bucket rm` command, [PR-274](https://github.com/reductstore/reduct-cli/pull/274) by @vbmade2000

### Changed

Expand Down
34 changes: 32 additions & 2 deletions src/cmd/bucket/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,36 @@ pub(super) async fn create_bucket(ctx: &CliContext, args: &ArgMatches) -> anyhow
.unwrap()
.clone()
.pair()?;

let is_json = ctx.json().unwrap_or(false);

let bucket_settings = parse_bucket_settings(args);

let client: ReductClient = build_client(ctx, &alias_or_url).await?;

client
.create_bucket(&bucket_name)
.settings(bucket_settings)
.send()
.await?;

output!(ctx, "Bucket '{}' created", bucket_name);
if !is_json {
output!(ctx, "Bucket '{}' created", bucket_name);
} else {
output!(ctx, "{}", "{}");
}

Ok(())
}

#[cfg(test)]
mod tests {

use super::*;
use crate::context::tests::{bucket, context};
use crate::context::{
tests::{bucket, context, MockOutput},
ContextBuilder,
};
use reduct_rs::QuotaType;
use rstest::*;

Expand Down Expand Up @@ -156,4 +169,21 @@ mod tests {
"Failed because of invalid block records"
);
}

#[rstest]
#[tokio::test]
async fn test_create_bucket_successfully_json(context: CliContext, #[future] bucket: String) {
let args = create_bucket_cmd()
.get_matches_from(vec!["create", format!("local/{}", bucket.await).as_str()]);

let ctx = ContextBuilder::new()
.config_path(context.config_path())
.json(Some(true))
.output(Box::new(MockOutput::new()))
.build();

create_bucket(&ctx, &args).await.unwrap();

assert_eq!(ctx.stdout().history(), vec!["{}"]);
}
}
164 changes: 158 additions & 6 deletions src/cmd/bucket/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use bytesize::ByteSize;
use clap::ArgAction::SetTrue;
use clap::{Arg, ArgMatches, Command};
use reduct_rs::BucketInfoList;
use serde::{Deserialize, Serialize};
use tabled::settings::Style;
use tabled::{Table, Tabled};

Expand All @@ -35,24 +36,35 @@ pub(super) fn ls_bucket_cmd() -> Command {

pub(super) async fn ls_bucket(ctx: &CliContext, args: &ArgMatches) -> anyhow::Result<()> {
let alias_or_url = args.get_one::<String>("ALIAS_OR_URL").unwrap();
let is_json = ctx.json().unwrap_or(false);

let client = build_client(ctx, alias_or_url).await?;

let bucket_list = client.bucket_list().await?;
if args.get_flag("full") {
print_full_list(ctx, bucket_list);
print_full_list(ctx, bucket_list, is_json);
} else {
print_list(ctx, bucket_list);
print_list(ctx, bucket_list, is_json);
}
Ok(())
}

fn print_list(ctx: &CliContext, bucket_list: BucketInfoList) {
fn print_list(ctx: &CliContext, bucket_list: BucketInfoList, is_json: bool) {
if is_json {
let buckets = bucket_list
.buckets
.iter()
.map(|bucket| bucket.name.as_str())
.collect::<Vec<_>>();
output!(ctx, "{}", serde_json::to_string_pretty(&buckets).unwrap());
return;
}
for bucket in bucket_list.buckets {
output!(ctx, "{}", bucket.name);
}
}

#[derive(Tabled)]
#[derive(Deserialize, Serialize, Tabled)]
struct BucketRow {
#[tabled(rename = "Name")]
name: String,
Expand Down Expand Up @@ -85,8 +97,11 @@ fn record_range_values(oldest: u64, latest: u64, is_empty: bool) -> (String, Str
(oldest_value, latest_value)
}

fn print_full_list(ctx: &CliContext, bucket_list: BucketInfoList) {
fn print_full_list(ctx: &CliContext, bucket_list: BucketInfoList, is_json: bool) {
if bucket_list.buckets.is_empty() {
if is_json {
output!(ctx, "{}", "[]");
}
return;
}

Expand Down Expand Up @@ -115,14 +130,23 @@ fn print_full_list(ctx: &CliContext, bucket_list: BucketInfoList) {
})
.collect::<Vec<_>>();

if is_json {
output!(ctx, "{}", serde_json::to_string_pretty(&rows).unwrap());
return;
}
let table = Table::new(rows).with(Style::markdown()).to_string();
output!(ctx, "{}", table);
}

#[cfg(test)]
mod tests {

use super::*;
use crate::context::tests::{bucket, bucket2, context};
use crate::context::{
tests::{bucket, bucket2, context, MockOutput},
ContextBuilder,
};
use reduct_rs::{Bucket, ReductClient};
use rstest::rstest;

#[rstest]
Expand Down Expand Up @@ -210,4 +234,132 @@ mod tests {
]
);
}

#[rstest]
#[tokio::test]
async fn test_ls_bucket_full_json_with_buckets(
context: CliContext,
#[future] bucket: String,
#[future] bucket2: String,
) {
let bucket = bucket.await;
let bucket2 = bucket2.await;

let ctx = ContextBuilder::new()
.config_path(context.config_path())
.json(Some(true))
.output(Box::new(MockOutput::new()))
.build();

let args = ls_bucket_cmd().get_matches_from(vec!["ls", "local", "--full"]);
let client = build_client(&ctx, "local").await.unwrap();

// Create buckets
let _ = create_test_bucket_with_entry(&bucket, &client).await;
let _ = create_test_bucket_with_entry(&bucket2, &client).await;

// List buckets
ls_bucket(&ctx, &args).await.unwrap();

let rows: Vec<BucketRow> = serde_json::from_str(&ctx.stdout().history()[0]).unwrap();

assert!(rows.iter().any(|row| row.name == bucket));
assert!(rows.iter().any(|row| row.name == bucket2));
}

#[rstest]
#[tokio::test]
async fn test_ls_bucket_full_json_with_no_bucket(
context: CliContext,
#[future] bucket: String,
#[future] bucket2: String,
) {
let bucket = bucket.await;
let bucket2 = bucket2.await;

let ctx = ContextBuilder::new()
.config_path(context.config_path())
.json(Some(true))
.output(Box::new(MockOutput::new()))
.build();

let args = ls_bucket_cmd().get_matches_from(vec!["ls", "local", "--full"]);

// List buckets
ls_bucket(&ctx, &args).await.unwrap();

let rows: Vec<BucketRow> = serde_json::from_str(&ctx.stdout().history()[0]).unwrap();

assert_eq!(rows.iter().any(|row| row.name == bucket), false);
assert_eq!(rows.iter().any(|row| row.name == bucket2), false);
}

#[rstest]
#[tokio::test]
async fn test_ls_bucket_json_with_buckets(
context: CliContext,
#[future] bucket: String,
#[future] bucket2: String,
) {
let ctx = ContextBuilder::new()
.config_path(context.config_path())
.json(Some(true))
.output(Box::new(MockOutput::new()))
.build();

let args = ls_bucket_cmd().get_matches_from(vec!["ls", "local"]);
let client = build_client(&ctx, "local").await.unwrap();

// Create buckets
let bucket = create_test_bucket_with_entry(&bucket.await, &client).await;
let bucket2 = create_test_bucket_with_entry(&bucket2.await, &client).await;

// List buckets
ls_bucket(&ctx, &args).await.unwrap();

let rows: Vec<String> = serde_json::from_str(&ctx.stdout().history()[0]).unwrap();

assert!(rows.contains(&bucket.name().to_string()));
assert!(rows.contains(&bucket2.name().to_string()));
}

#[rstest]
#[tokio::test]
async fn test_ls_bucket_json_with_no_bucket(
context: CliContext,
#[future] bucket: String,
#[future] bucket2: String,
) {
let bucket = bucket.await;
let bucket2 = bucket2.await;

let ctx = ContextBuilder::new()
.config_path(context.config_path())
.json(Some(true))
.output(Box::new(MockOutput::new()))
.build();

let args = ls_bucket_cmd().get_matches_from(vec!["ls", "local"]);

// List buckets
ls_bucket(&ctx, &args).await.unwrap();

// We have $system bucket to consider too so len is 1.
let rows: Vec<String> = serde_json::from_str(&ctx.stdout().history()[0]).unwrap();

assert_eq!(rows.contains(&bucket), false);
assert_eq!(rows.contains(&bucket2), false);
}

async fn create_test_bucket_with_entry(bucket_name: &str, client: &ReductClient) -> Bucket {
let bucket = client.create_bucket(bucket_name).send().await.unwrap();
bucket
.write_record("test")
.data("data")
.timestamp_us(0)
.send()
.await
.unwrap();
bucket
}
}
54 changes: 51 additions & 3 deletions src/cmd/bucket/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,29 @@ pub(super) async fn rename_bucket(ctx: &CliContext, args: &ArgMatches) -> anyhow
.pair()?;
let new_name = args.get_one::<String>("NEW_NAME").unwrap();
let entry_name = args.get_one::<String>("only-entry");
let is_json = ctx.json().unwrap_or(false);

let client: ReductClient = build_client(ctx, &alias_or_url).await?;
if let Some(entry_name) = entry_name {
let bucket = client.get_bucket(&bucket_name).await?;
bucket.rename_entry(entry_name, new_name).await?;
output!(ctx, "Entry '{}' renamed to '{}'", entry_name, new_name);
if !is_json {
output!(ctx, "Entry '{}' renamed to '{}'", entry_name, new_name);
} else {
output!(ctx, "{}", "{}");
}
} else {
client
.get_bucket(&bucket_name)
.await?
.rename(new_name)
.await?;

output!(ctx, "Bucket '{}' renamed to '{}'", bucket_name, new_name);
if !is_json {
output!(ctx, "Bucket '{}' renamed to '{}'", bucket_name, new_name);
} else {
output!(ctx, "{}", "{}");
}
}

Ok(())
Expand All @@ -59,7 +68,10 @@ pub(super) async fn rename_bucket(ctx: &CliContext, args: &ArgMatches) -> anyhow
#[cfg(test)]
mod tests {
use super::*;
use crate::context::tests::{bucket, context};
use crate::context::{
tests::{bucket, context, MockOutput},
ContextBuilder,
};
use rstest::*;

#[rstest]
Expand Down Expand Up @@ -166,4 +178,40 @@ mod tests {
"error: invalid value 'local' for '<BUCKET_PATH>'\n\nFor more information, try '--help'.\n"
);
}

#[rstest]
#[tokio::test]
async fn test_rename_bucket_json(context: CliContext, #[future] bucket: String) {
let bucket_name = bucket.await;
let client = build_client(&context, "local").await.unwrap();
client.create_bucket(&bucket_name).send().await.unwrap();

let args = rename_bucket_cmd().get_matches_from(vec![
"rename",
format!("local/{}", bucket_name).as_str(),
"new_renamed_bucket",
]);

let ctx = ContextBuilder::new()
.config_path(context.config_path())
.json(Some(true))
.output(Box::new(MockOutput::new()))
.build();

rename_bucket(&ctx, &args).await.unwrap();

assert_eq!(
ctx.stdout().history().len(),
1,
"JSON output contains one line"
);
assert_eq!(
ctx.stdout().history()[0],
"{}".to_string(),
"JSON output is empty - {{}}"
);

let bucket = client.get_bucket("new_renamed_bucket").await.unwrap();
bucket.remove().await.unwrap();
}
}
Loading
Loading