add json create/update workflows across cli resources

This commit is contained in:
Gregory Schier
2026-02-16 09:08:05 -08:00
parent 570676dffb
commit 0d57f91ca4
15 changed files with 622 additions and 50 deletions

View File

@@ -13,6 +13,7 @@ clap = { version = "4", features = ["derive"] }
dirs = "6"
env_logger = "0.11"
log = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
yaak-crypto = { workspace = true }

View File

@@ -11,21 +11,20 @@ Current branch state:
- Modular CLI structure with command modules and shared `CliContext`
- Resource/action hierarchy in place for:
- `workspace list`
- `request list`
- `request send`
- `request create`
- `workspace list|show|create|update|delete`
- `request list|show|create|update|send|delete`
- `folder list|show|create|update|delete`
- `environment list|show|create|update|delete`
- Top-level `send` exists as a request-send shortcut (not yet flexible request/folder/workspace resolution)
- Legacy `get` command removed
- No folder/workspace/environment CRUD surface yet
- No JSON merge/update flow yet
- JSON create/update flow implemented (`--json` and positional JSON shorthand)
- No `request schema` command yet
Progress checklist:
- [x] Phase 1 complete
- [ ] Phase 2 complete
- [ ] Phase 3 complete
- [x] Phase 2 complete
- [x] Phase 3 complete
- [ ] Phase 4 complete
- [ ] Phase 5 complete
- [ ] Phase 6 complete
@@ -145,7 +144,7 @@ Existing behavior stays the same, just reorganized. Remove the `get` command.
### Phase 2: Add missing CRUD commands
Status: in progress (`show`/`create`/`delete` implemented for workspace, request, folder, environment; JSON update flow pending)
Status: complete
1. `workspace show <id>`
2. `workspace create --name <name>` (and `--json`)

View File

@@ -11,19 +11,31 @@ yaakcli send <request_id>
yaakcli workspace list
yaakcli workspace show <workspace_id>
yaakcli workspace create --name <name>
yaakcli workspace create --json '{"name":"My Workspace"}'
yaakcli workspace create '{"name":"My Workspace"}'
yaakcli workspace update --json '{"id":"wk_abc","description":"Updated"}'
yaakcli workspace delete <workspace_id> [--yes]
yaakcli request list <workspace_id>
yaakcli request show <request_id>
yaakcli request send <request_id>
yaakcli request create <workspace_id> --name <name> --url <url> [--method GET]
yaakcli request create --json '{"workspaceId":"wk_abc","name":"Users","url":"https://api.example.com/users"}'
yaakcli request create '{"workspaceId":"wk_abc","name":"Users","url":"https://api.example.com/users"}'
yaakcli request update --json '{"id":"rq_abc","name":"Users v2"}'
yaakcli request delete <request_id> [--yes]
yaakcli folder list <workspace_id>
yaakcli folder show <folder_id>
yaakcli folder create <workspace_id> --name <name>
yaakcli folder create --json '{"workspaceId":"wk_abc","name":"Auth"}'
yaakcli folder create '{"workspaceId":"wk_abc","name":"Auth"}'
yaakcli folder update --json '{"id":"fl_abc","name":"Auth v2"}'
yaakcli folder delete <folder_id> [--yes]
yaakcli environment list <workspace_id>
yaakcli environment show <environment_id>
yaakcli environment create <workspace_id> --name <name>
yaakcli environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
yaakcli environment create '{"workspaceId":"wk_abc","name":"Production"}'
yaakcli environment update --json '{"id":"ev_abc","color":"#00ff00"}'
yaakcli environment delete <environment_id> [--yes]
```
@@ -38,6 +50,8 @@ Notes:
- `send` is currently a shortcut for sending an HTTP request ID.
- `delete` commands prompt for confirmation unless `--yes` is provided.
- In non-interactive mode, `delete` commands require `--yes`.
- `create` and `update` commands support `--json` and positional JSON shorthand.
- `update` uses JSON Merge Patch semantics (RFC 7386) for partial updates.
## Examples
@@ -45,18 +59,22 @@ Notes:
yaakcli workspace list
yaakcli workspace create --name "My Workspace"
yaakcli workspace show wk_abc
yaakcli workspace update --json '{"id":"wk_abc","description":"Team workspace"}'
yaakcli request list wk_abc
yaakcli request show rq_abc
yaakcli request create wk_abc --name "Users" --url "https://api.example.com/users"
yaakcli request update --json '{"id":"rq_abc","name":"Users v2"}'
yaakcli request send rq_abc -e ev_abc
yaakcli request delete rq_abc --yes
yaakcli folder create wk_abc --name "Auth"
yaakcli folder update --json '{"id":"fl_abc","name":"Auth v2"}'
yaakcli environment create wk_abc --name "Production"
yaakcli environment update --json '{"id":"ev_abc","color":"#00ff00"}'
```
## Roadmap
Planned command expansion (JSON create/update, request schema, and polymorphic send) is tracked in `PLAN.md`.
Planned command expansion (request schema and polymorphic send) is tracked in `PLAN.md`.
When command behavior changes, update this README and verify with:

View File

@@ -66,7 +66,26 @@ pub enum WorkspaceCommands {
Create {
/// Workspace name
#[arg(short, long)]
name: String,
name: Option<String>,
/// JSON payload
#[arg(long, conflicts_with = "json_input")]
json: Option<String>,
/// JSON payload shorthand
#[arg(value_name = "JSON", conflicts_with = "json")]
json_input: Option<String>,
},
/// Update a workspace
Update {
/// JSON payload
#[arg(long, conflicts_with = "json_input")]
json: Option<String>,
/// JSON payload shorthand
#[arg(value_name = "JSON", conflicts_with = "json")]
json_input: Option<String>,
},
/// Delete a workspace
@@ -108,20 +127,35 @@ pub enum RequestCommands {
/// Create a new HTTP request
Create {
/// Workspace ID
workspace_id: String,
/// Workspace ID (or positional JSON payload shorthand)
workspace_id: Option<String>,
/// Request name
#[arg(short, long)]
name: String,
name: Option<String>,
/// HTTP method
#[arg(short, long, default_value = "GET")]
method: String,
#[arg(short, long)]
method: Option<String>,
/// URL
#[arg(short, long)]
url: String,
url: Option<String>,
/// JSON payload
#[arg(long)]
json: Option<String>,
},
/// Update an HTTP request
Update {
/// JSON payload
#[arg(long, conflicts_with = "json_input")]
json: Option<String>,
/// JSON payload shorthand
#[arg(value_name = "JSON", conflicts_with = "json")]
json_input: Option<String>,
},
/// Delete a request
@@ -157,12 +191,27 @@ pub enum FolderCommands {
/// Create a folder
Create {
/// Workspace ID
workspace_id: String,
/// Workspace ID (or positional JSON payload shorthand)
workspace_id: Option<String>,
/// Folder name
#[arg(short, long)]
name: String,
name: Option<String>,
/// JSON payload
#[arg(long)]
json: Option<String>,
},
/// Update a folder
Update {
/// JSON payload
#[arg(long, conflicts_with = "json_input")]
json: Option<String>,
/// JSON payload shorthand
#[arg(value_name = "JSON", conflicts_with = "json")]
json_input: Option<String>,
},
/// Delete a folder
@@ -198,12 +247,27 @@ pub enum EnvironmentCommands {
/// Create an environment
Create {
/// Workspace ID
workspace_id: String,
/// Workspace ID (or positional JSON payload shorthand)
workspace_id: Option<String>,
/// Environment name
#[arg(short, long)]
name: String,
name: Option<String>,
/// JSON payload
#[arg(long)]
json: Option<String>,
},
/// Update an environment
Update {
/// JSON payload
#[arg(long, conflicts_with = "json_input")]
json: Option<String>,
/// JSON payload shorthand
#[arg(value_name = "JSON", conflicts_with = "json")]
json_input: Option<String>,
},
/// Delete an environment

View File

@@ -1,5 +1,9 @@
use crate::cli::{EnvironmentArgs, EnvironmentCommands};
use crate::commands::confirm::confirm_delete;
use crate::commands::json::{
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
validate_create_id,
};
use crate::context::CliContext;
use yaak_models::models::Environment;
use yaak_models::util::UpdateSource;
@@ -8,7 +12,10 @@ pub fn run(ctx: &CliContext, args: EnvironmentArgs) {
match args.command {
EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id),
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
EnvironmentCommands::Create { workspace_id, name } => create(ctx, workspace_id, name),
EnvironmentCommands::Create { workspace_id, name, json } => {
create(ctx, workspace_id, name, json)
}
EnvironmentCommands::Update { json, json_input } => update(ctx, json, json_input),
EnvironmentCommands::Delete { environment_id, yes } => delete(ctx, &environment_id, yes),
}
}
@@ -33,7 +40,55 @@ fn show(ctx: &CliContext, environment_id: &str) {
println!("{output}");
}
fn create(ctx: &CliContext, workspace_id: String, name: String) {
fn create(
ctx: &CliContext,
workspace_id: Option<String>,
name: Option<String>,
json: Option<String>,
) {
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
panic!("environment create cannot combine workspace_id with --json payload");
}
let payload = parse_optional_json(
json,
workspace_id.clone().filter(|v| is_json_shorthand(v)),
"environment create",
);
if let Some(payload) = payload {
if name.is_some() {
panic!("environment create cannot combine --name with JSON payload");
}
validate_create_id(&payload, "environment");
let mut environment: Environment =
serde_json::from_value(payload).expect("Failed to parse environment create JSON");
if environment.workspace_id.is_empty() {
panic!("environment create JSON requires non-empty \"workspaceId\"");
}
if environment.parent_model.is_empty() {
environment.parent_model = "environment".to_string();
}
let created = ctx
.db()
.upsert_environment(&environment, &UpdateSource::Sync)
.expect("Failed to create environment");
println!("Created environment: {}", created.id);
return;
}
let workspace_id = workspace_id.unwrap_or_else(|| {
panic!("environment create requires workspace_id unless JSON payload is provided")
});
let name = name.unwrap_or_else(|| {
panic!("environment create requires --name unless JSON payload is provided")
});
let environment = Environment {
workspace_id,
name,
@@ -49,6 +104,21 @@ fn create(ctx: &CliContext, workspace_id: String, name: String) {
println!("Created environment: {}", created.id);
}
fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) {
let patch = parse_required_json(json, json_input, "environment update");
let id = require_id(&patch, "environment update");
let existing = ctx.db().get_environment(&id).expect("Failed to get environment for update");
let updated = apply_merge_patch(&existing, &patch, &id, "environment update");
let saved = ctx
.db()
.upsert_environment(&updated, &UpdateSource::Sync)
.expect("Failed to update environment");
println!("Updated environment: {}", saved.id);
}
fn delete(ctx: &CliContext, environment_id: &str, yes: bool) {
if !yes && !confirm_delete("environment", environment_id) {
println!("Aborted");

View File

@@ -1,5 +1,9 @@
use crate::cli::{FolderArgs, FolderCommands};
use crate::commands::confirm::confirm_delete;
use crate::commands::json::{
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
validate_create_id,
};
use crate::context::CliContext;
use yaak_models::models::Folder;
use yaak_models::util::UpdateSource;
@@ -8,7 +12,10 @@ pub fn run(ctx: &CliContext, args: FolderArgs) {
match args.command {
FolderCommands::List { workspace_id } => list(ctx, &workspace_id),
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
FolderCommands::Create { workspace_id, name } => create(ctx, workspace_id, name),
FolderCommands::Create { workspace_id, name, json } => {
create(ctx, workspace_id, name, json)
}
FolderCommands::Update { json, json_input } => update(ctx, json, json_input),
FolderCommands::Delete { folder_id, yes } => delete(ctx, &folder_id, yes),
}
}
@@ -30,7 +37,48 @@ fn show(ctx: &CliContext, folder_id: &str) {
println!("{output}");
}
fn create(ctx: &CliContext, workspace_id: String, name: String) {
fn create(
ctx: &CliContext,
workspace_id: Option<String>,
name: Option<String>,
json: Option<String>,
) {
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
panic!("folder create cannot combine workspace_id with --json payload");
}
let payload = parse_optional_json(
json,
workspace_id.clone().filter(|v| is_json_shorthand(v)),
"folder create",
);
if let Some(payload) = payload {
if name.is_some() {
panic!("folder create cannot combine --name with JSON payload");
}
validate_create_id(&payload, "folder");
let folder: Folder =
serde_json::from_value(payload).expect("Failed to parse folder create JSON");
if folder.workspace_id.is_empty() {
panic!("folder create JSON requires non-empty \"workspaceId\"");
}
let created =
ctx.db().upsert_folder(&folder, &UpdateSource::Sync).expect("Failed to create folder");
println!("Created folder: {}", created.id);
return;
}
let workspace_id = workspace_id.unwrap_or_else(|| {
panic!("folder create requires workspace_id unless JSON payload is provided")
});
let name = name
.unwrap_or_else(|| panic!("folder create requires --name unless JSON payload is provided"));
let folder = Folder { workspace_id, name, ..Default::default() };
let created =
@@ -39,6 +87,19 @@ fn create(ctx: &CliContext, workspace_id: String, name: String) {
println!("Created folder: {}", created.id);
}
fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) {
let patch = parse_required_json(json, json_input, "folder update");
let id = require_id(&patch, "folder update");
let existing = ctx.db().get_folder(&id).expect("Failed to get folder for update");
let updated = apply_merge_patch(&existing, &patch, &id, "folder update");
let saved =
ctx.db().upsert_folder(&updated, &UpdateSource::Sync).expect("Failed to update folder");
println!("Updated folder: {}", saved.id);
}
fn delete(ctx: &CliContext, folder_id: &str, yes: bool) {
if !yes && !confirm_delete("folder", folder_id) {
println!("Aborted");

View File

@@ -0,0 +1,108 @@
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Map, Value};
pub fn is_json_shorthand(input: &str) -> bool {
input.trim_start().starts_with('{')
}
pub fn parse_json_object(raw: &str, context: &str) -> Value {
let value: Value = serde_json::from_str(raw)
.unwrap_or_else(|error| panic!("Invalid JSON for {context}: {error}"));
if !value.is_object() {
panic!("JSON payload for {context} must be an object");
}
value
}
pub fn parse_optional_json(
json_flag: Option<String>,
json_shorthand: Option<String>,
context: &str,
) -> Option<Value> {
match (json_flag, json_shorthand) {
(Some(_), Some(_)) => {
panic!("Cannot provide both --json and positional JSON for {context}")
}
(Some(raw), None) => Some(parse_json_object(&raw, context)),
(None, Some(raw)) => Some(parse_json_object(&raw, context)),
(None, None) => None,
}
}
pub fn parse_required_json(
json_flag: Option<String>,
json_shorthand: Option<String>,
context: &str,
) -> Value {
parse_optional_json(json_flag, json_shorthand, context).unwrap_or_else(|| {
panic!("Missing JSON payload for {context}. Use --json or positional JSON")
})
}
pub fn require_id(payload: &Value, context: &str) -> String {
payload
.get("id")
.and_then(|value| value.as_str())
.filter(|value| !value.is_empty())
.map(|value| value.to_string())
.unwrap_or_else(|| panic!("{context} requires a non-empty \"id\" field"))
}
pub fn validate_create_id(payload: &Value, context: &str) {
let Some(id_value) = payload.get("id") else {
return;
};
match id_value {
Value::String(id) if id.is_empty() => {}
_ => panic!("{context} create JSON must omit \"id\" or set it to an empty string"),
}
}
pub fn apply_merge_patch<T>(existing: &T, patch: &Value, id: &str, context: &str) -> T
where
T: Serialize + DeserializeOwned,
{
let mut base = serde_json::to_value(existing).unwrap_or_else(|error| {
panic!("Failed to serialize existing model for {context}: {error}")
});
merge_patch(&mut base, patch);
let Some(base_object) = base.as_object_mut() else {
panic!("Merged payload for {context} must be an object");
};
base_object.insert("id".to_string(), Value::String(id.to_string()));
serde_json::from_value(base).unwrap_or_else(|error| {
panic!("Failed to deserialize merged payload for {context}: {error}")
})
}
fn merge_patch(target: &mut Value, patch: &Value) {
match patch {
Value::Object(patch_map) => {
if !target.is_object() {
*target = Value::Object(Map::new());
}
let target_map =
target.as_object_mut().expect("merge_patch target expected to be object");
for (key, patch_value) in patch_map {
if patch_value.is_null() {
target_map.remove(key);
continue;
}
let target_entry = target_map.entry(key.clone()).or_insert(Value::Null);
merge_patch(target_entry, patch_value);
}
}
_ => {
*target = patch.clone();
}
}
}

View File

@@ -1,6 +1,7 @@
pub mod confirm;
pub mod environment;
pub mod folder;
pub mod json;
pub mod request;
pub mod send;
pub mod workspace;

View File

@@ -1,5 +1,9 @@
use crate::cli::{RequestArgs, RequestCommands};
use crate::commands::confirm::confirm_delete;
use crate::commands::json::{
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
validate_create_id,
};
use crate::context::CliContext;
use log::info;
use serde_json::Value;
@@ -22,9 +26,10 @@ pub async fn run(ctx: &CliContext, args: RequestArgs, environment: Option<&str>,
RequestCommands::Send { request_id } => {
send_request_by_id(ctx, &request_id, environment, verbose).await;
}
RequestCommands::Create { workspace_id, name, method, url } => {
create(ctx, workspace_id, name, method, url)
RequestCommands::Create { workspace_id, name, method, url, json } => {
create(ctx, workspace_id, name, method, url, json)
}
RequestCommands::Update { json, json_input } => update(ctx, json, json_input),
RequestCommands::Delete { request_id, yes } => delete(ctx, &request_id, yes),
}
}
@@ -40,7 +45,56 @@ fn list(ctx: &CliContext, workspace_id: &str) {
}
}
fn create(ctx: &CliContext, workspace_id: String, name: String, method: String, url: String) {
fn create(
ctx: &CliContext,
workspace_id: Option<String>,
name: Option<String>,
method: Option<String>,
url: Option<String>,
json: Option<String>,
) {
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
panic!("request create cannot combine workspace_id with --json payload");
}
let payload = parse_optional_json(
json,
workspace_id.clone().filter(|v| is_json_shorthand(v)),
"request create",
);
if let Some(payload) = payload {
if name.is_some() || method.is_some() || url.is_some() {
panic!("request create cannot combine simple flags with JSON payload");
}
validate_create_id(&payload, "request");
let request: HttpRequest =
serde_json::from_value(payload).expect("Failed to parse request create JSON");
if request.workspace_id.is_empty() {
panic!("request create JSON requires non-empty \"workspaceId\"");
}
let created = ctx
.db()
.upsert_http_request(&request, &UpdateSource::Sync)
.expect("Failed to create request");
println!("Created request: {}", created.id);
return;
}
let workspace_id = workspace_id.unwrap_or_else(|| {
panic!("request create requires workspace_id unless JSON payload is provided")
});
let name = name.unwrap_or_else(|| {
panic!("request create requires --name unless JSON payload is provided")
});
let url = url
.unwrap_or_else(|| panic!("request create requires --url unless JSON payload is provided"));
let method = method.unwrap_or_else(|| "GET".to_string());
let request = HttpRequest {
workspace_id,
name,
@@ -57,6 +111,21 @@ fn create(ctx: &CliContext, workspace_id: String, name: String, method: String,
println!("Created request: {}", created.id);
}
fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) {
let patch = parse_required_json(json, json_input, "request update");
let id = require_id(&patch, "request update");
let existing = ctx.db().get_http_request(&id).expect("Failed to get request for update");
let updated = apply_merge_patch(&existing, &patch, &id, "request update");
let saved = ctx
.db()
.upsert_http_request(&updated, &UpdateSource::Sync)
.expect("Failed to update request");
println!("Updated request: {}", saved.id);
}
fn show(ctx: &CliContext, request_id: &str) {
let request = ctx.db().get_http_request(request_id).expect("Failed to get request");
let output = serde_json::to_string_pretty(&request).expect("Failed to serialize request");

View File

@@ -1,5 +1,8 @@
use crate::cli::{WorkspaceArgs, WorkspaceCommands};
use crate::commands::confirm::confirm_delete;
use crate::commands::json::{
apply_merge_patch, parse_optional_json, parse_required_json, require_id, validate_create_id,
};
use crate::context::CliContext;
use yaak_models::models::Workspace;
use yaak_models::util::UpdateSource;
@@ -8,7 +11,8 @@ pub fn run(ctx: &CliContext, args: WorkspaceArgs) {
match args.command {
WorkspaceCommands::List => list(ctx),
WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id),
WorkspaceCommands::Create { name } => create(ctx, name),
WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input),
WorkspaceCommands::Update { json, json_input } => update(ctx, json, json_input),
WorkspaceCommands::Delete { workspace_id, yes } => delete(ctx, &workspace_id, yes),
}
}
@@ -30,7 +34,35 @@ fn show(ctx: &CliContext, workspace_id: &str) {
println!("{output}");
}
fn create(ctx: &CliContext, name: String) {
fn create(
ctx: &CliContext,
name: Option<String>,
json: Option<String>,
json_input: Option<String>,
) {
let payload = parse_optional_json(json, json_input, "workspace create");
if let Some(payload) = payload {
if name.is_some() {
panic!("workspace create cannot combine --name with JSON payload");
}
validate_create_id(&payload, "workspace");
let workspace: Workspace =
serde_json::from_value(payload).expect("Failed to parse workspace create JSON");
let created = ctx
.db()
.upsert_workspace(&workspace, &UpdateSource::Sync)
.expect("Failed to create workspace");
println!("Created workspace: {}", created.id);
return;
}
let name = name.unwrap_or_else(|| {
panic!("workspace create requires --name unless JSON payload is provided")
});
let workspace = Workspace { name, ..Default::default() };
let created = ctx
.db()
@@ -39,6 +71,21 @@ fn create(ctx: &CliContext, name: String) {
println!("Created workspace: {}", created.id);
}
fn update(ctx: &CliContext, json: Option<String>, json_input: Option<String>) {
let patch = parse_required_json(json, json_input, "workspace update");
let id = require_id(&patch, "workspace update");
let existing = ctx.db().get_workspace(&id).expect("Failed to get workspace for update");
let updated = apply_merge_patch(&existing, &patch, &id, "workspace update");
let saved = ctx
.db()
.upsert_workspace(&updated, &UpdateSource::Sync)
.expect("Failed to update workspace");
println!("Updated workspace: {}", saved.id);
}
fn delete(ctx: &CliContext, workspace_id: &str, yes: bool) {
if !yes && !confirm_delete("workspace", workspace_id) {
println!("Aborted");

View File

@@ -20,8 +20,7 @@ fn create_list_show_delete_round_trip() {
.args(["environment", "create", "wk_test", "--name", "Production"])
.assert()
.success();
let environment_id =
parse_created_id(&create_assert.get_output().stdout, "environment create");
let environment_id = parse_created_id(&create_assert.get_output().stdout, "environment create");
cli_cmd(data_dir)
.args(["environment", "list", "wk_test"])
@@ -43,8 +42,39 @@ fn create_list_show_delete_round_trip() {
.success()
.stdout(contains(format!("Deleted environment: {environment_id}")));
assert!(query_manager(data_dir)
.connect()
.get_environment(&environment_id)
.is_err());
assert!(query_manager(data_dir).connect().get_environment(&environment_id).is_err());
}
#[test]
fn json_create_and_update_merge_patch_round_trip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let data_dir = temp_dir.path();
seed_workspace(data_dir, "wk_test");
let create_assert = cli_cmd(data_dir)
.args([
"environment",
"create",
r#"{"workspaceId":"wk_test","name":"Json Environment"}"#,
])
.assert()
.success();
let environment_id = parse_created_id(&create_assert.get_output().stdout, "environment create");
cli_cmd(data_dir)
.args([
"environment",
"update",
&format!(r##"{{"id":"{}","color":"#00ff00"}}"##, environment_id),
])
.assert()
.success()
.stdout(contains(format!("Updated environment: {environment_id}")));
cli_cmd(data_dir)
.args(["environment", "show", &environment_id])
.assert()
.success()
.stdout(contains("\"name\": \"Json Environment\""))
.stdout(contains("\"color\": \"#00ff00\""));
}

View File

@@ -38,3 +38,37 @@ fn create_list_show_delete_round_trip() {
assert!(query_manager(data_dir).connect().get_folder(&folder_id).is_err());
}
#[test]
fn json_create_and_update_merge_patch_round_trip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let data_dir = temp_dir.path();
seed_workspace(data_dir, "wk_test");
let create_assert = cli_cmd(data_dir)
.args([
"folder",
"create",
r#"{"workspaceId":"wk_test","name":"Json Folder"}"#,
])
.assert()
.success();
let folder_id = parse_created_id(&create_assert.get_output().stdout, "folder create");
cli_cmd(data_dir)
.args([
"folder",
"update",
&format!(r#"{{"id":"{}","description":"Folder Description"}}"#, folder_id),
])
.assert()
.success()
.stdout(contains(format!("Updated folder: {folder_id}")));
cli_cmd(data_dir)
.args(["folder", "show", &folder_id])
.assert()
.success()
.stdout(contains("\"name\": \"Json Folder\""))
.stdout(contains("\"description\": \"Folder Description\""));
}

View File

@@ -55,8 +55,53 @@ fn delete_without_yes_fails_in_non_interactive_mode() {
.code(1)
.stderr(contains("Refusing to delete in non-interactive mode without --yes"));
assert!(query_manager(data_dir)
.connect()
.get_http_request("rq_seed_delete_noninteractive")
.is_ok());
assert!(
query_manager(data_dir).connect().get_http_request("rq_seed_delete_noninteractive").is_ok()
);
}
#[test]
fn json_create_and_update_merge_patch_round_trip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let data_dir = temp_dir.path();
seed_workspace(data_dir, "wk_test");
let create_assert = cli_cmd(data_dir)
.args([
"request",
"create",
r#"{"workspaceId":"wk_test","name":"Json Request","url":"https://example.com"}"#,
])
.assert()
.success();
let request_id = parse_created_id(&create_assert.get_output().stdout, "request create");
cli_cmd(data_dir)
.args([
"request",
"update",
&format!(r#"{{"id":"{}","name":"Renamed Request"}}"#, request_id),
])
.assert()
.success()
.stdout(contains(format!("Updated request: {request_id}")));
cli_cmd(data_dir)
.args(["request", "show", &request_id])
.assert()
.success()
.stdout(contains("\"name\": \"Renamed Request\""))
.stdout(contains("\"url\": \"https://example.com\""));
}
#[test]
fn update_requires_id_in_json_payload() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let data_dir = temp_dir.path();
cli_cmd(data_dir)
.args(["request", "update", r#"{"name":"No ID"}"#])
.assert()
.failure()
.stderr(contains("request update requires a non-empty \"id\" field"));
}

View File

@@ -9,10 +9,8 @@ fn create_show_delete_round_trip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let data_dir = temp_dir.path();
let create_assert = cli_cmd(data_dir)
.args(["workspace", "create", "--name", "WS One"])
.assert()
.success();
let create_assert =
cli_cmd(data_dir).args(["workspace", "create", "--name", "WS One"]).assert().success();
let workspace_id = parse_created_id(&create_assert.get_output().stdout, "workspace create");
cli_cmd(data_dir)
@@ -28,8 +26,34 @@ fn create_show_delete_round_trip() {
.success()
.stdout(contains(format!("Deleted workspace: {workspace_id}")));
assert!(query_manager(data_dir)
.connect()
.get_workspace(&workspace_id)
.is_err());
assert!(query_manager(data_dir).connect().get_workspace(&workspace_id).is_err());
}
#[test]
fn json_create_and_update_merge_patch_round_trip() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let data_dir = temp_dir.path();
let create_assert = cli_cmd(data_dir)
.args(["workspace", "create", r#"{"name":"Json Workspace"}"#])
.assert()
.success();
let workspace_id = parse_created_id(&create_assert.get_output().stdout, "workspace create");
cli_cmd(data_dir)
.args([
"workspace",
"update",
&format!(r#"{{"id":"{}","description":"Updated via JSON"}}"#, workspace_id),
])
.assert()
.success()
.stdout(contains(format!("Updated workspace: {workspace_id}")));
cli_cmd(data_dir)
.args(["workspace", "show", &workspace_id])
.assert()
.success()
.stdout(contains("\"name\": \"Json Workspace\""))
.stdout(contains("\"description\": \"Updated via JSON\""));
}