mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-28 12:11:53 +01:00
add json create/update workflows across cli resources
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
108
crates-cli/yaak-cli/src/commands/json.rs
Normal file
108
crates-cli/yaak-cli/src/commands/json.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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\""));
|
||||
}
|
||||
|
||||
@@ -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\""));
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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\""));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user