mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 17:18:32 +02:00
cli: share HTTP helpers and improve schema guidance
This commit is contained in:
20
.github/workflows/release-cli-npm.yml
vendored
20
.github/workflows/release-cli-npm.yml
vendored
@@ -122,25 +122,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
VERSION="${VERSION#v}"
|
VERSION="${VERSION#v}"
|
||||||
echo "Building yaak version: $VERSION"
|
echo "Building yaak version: $VERSION"
|
||||||
python - "$VERSION" <<'PY'
|
echo "YAAK_CLI_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||||
import pathlib
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
version = sys.argv[1]
|
|
||||||
manifest = pathlib.Path("crates-cli/yaak-cli/Cargo.toml")
|
|
||||||
contents = manifest.read_text()
|
|
||||||
updated, replacements = re.subn(
|
|
||||||
r'(?m)^version = ".*"$',
|
|
||||||
f'version = "{version}"',
|
|
||||||
contents,
|
|
||||||
count=1,
|
|
||||||
)
|
|
||||||
if replacements != 1:
|
|
||||||
raise SystemExit("Failed to update yaak-cli version in Cargo.toml")
|
|
||||||
manifest.write_text(updated)
|
|
||||||
print(f"Updated {manifest} to version {version}")
|
|
||||||
PY
|
|
||||||
|
|
||||||
- name: Build yaak
|
- name: Build yaak
|
||||||
run: cargo build --locked --release -p yaak-cli --bin yaak --target ${{ matrix.target }}
|
run: cargo build --locked --release -p yaak-cli --bin yaak --target ${{ matrix.target }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::path::PathBuf;
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "yaak")]
|
#[command(name = "yaak")]
|
||||||
#[command(about = "Yaak CLI - API client from the command line")]
|
#[command(about = "Yaak CLI - API client from the command line")]
|
||||||
#[command(version)]
|
#[command(version = crate::version::cli_version())]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Use a custom data directory
|
/// Use a custom data directory
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::cli::{AuthArgs, AuthCommands};
|
use crate::cli::{AuthArgs, AuthCommands};
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
|
use crate::utils::http;
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
@@ -136,10 +137,8 @@ async fn whoami() -> CommandResult {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let url = format!("{}/api/v1/whoami", environment.api_base_url());
|
let url = format!("{}/api/v1/whoami", environment.api_base_url());
|
||||||
let response = reqwest::Client::new()
|
let response = http::build_client(Some(&token))?
|
||||||
.get(url)
|
.get(url)
|
||||||
.header("X-Yaak-Session", token)
|
|
||||||
.header(reqwest::header::USER_AGENT, user_agent())
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to call whoami endpoint: {e}"))?;
|
.map_err(|e| format!("Failed to call whoami endpoint: {e}"))?;
|
||||||
@@ -156,7 +155,7 @@ async fn whoami() -> CommandResult {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Err(parse_api_error(status.as_u16(), &body));
|
return Err(http::parse_api_error(status.as_u16(), &body));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{body}");
|
println!("{body}");
|
||||||
@@ -342,9 +341,8 @@ async fn write_redirect(stream: &mut TcpStream, location: &str) -> std::io::Resu
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn exchange_access_token(oauth: &OAuthFlow, code: &str) -> CommandResult<String> {
|
async fn exchange_access_token(oauth: &OAuthFlow, code: &str) -> CommandResult<String> {
|
||||||
let response = reqwest::Client::new()
|
let response = http::build_client(None)?
|
||||||
.post(&oauth.token_url)
|
.post(&oauth.token_url)
|
||||||
.header(reqwest::header::USER_AGENT, user_agent())
|
|
||||||
.form(&[
|
.form(&[
|
||||||
("grant_type", "authorization_code"),
|
("grant_type", "authorization_code"),
|
||||||
("client_id", OAUTH_CLIENT_ID),
|
("client_id", OAUTH_CLIENT_ID),
|
||||||
@@ -406,38 +404,12 @@ fn delete_auth_token(environment: Environment) -> CommandResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_api_error(status: u16, body: &str) -> String {
|
|
||||||
if let Ok(value) = serde_json::from_str::<Value>(body) {
|
|
||||||
if let Some(message) = value.get("message").and_then(Value::as_str) {
|
|
||||||
return message.to_string();
|
|
||||||
}
|
|
||||||
if let Some(error) = value.get("error").and_then(Value::as_str) {
|
|
||||||
return error.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("API error {status}: {body}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_hex(bytes: usize) -> String {
|
fn random_hex(bytes: usize) -> String {
|
||||||
let mut data = vec![0_u8; bytes];
|
let mut data = vec![0_u8; bytes];
|
||||||
OsRng.fill_bytes(&mut data);
|
OsRng.fill_bytes(&mut data);
|
||||||
hex::encode(data)
|
hex::encode(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_agent() -> String {
|
|
||||||
format!("YaakCli/{} ({})", env!("CARGO_PKG_VERSION"), ua_platform())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ua_platform() -> &'static str {
|
|
||||||
match std::env::consts::OS {
|
|
||||||
"windows" => "Win",
|
|
||||||
"darwin" => "Mac",
|
|
||||||
"linux" => "Linux",
|
|
||||||
_ => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm_open_browser() -> CommandResult<bool> {
|
fn confirm_open_browser() -> CommandResult<bool> {
|
||||||
if !io::stdin().is_terminal() {
|
if !io::stdin().is_terminal() {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg};
|
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg};
|
||||||
use crate::ui;
|
use crate::ui;
|
||||||
|
use crate::utils::http;
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rolldown::{
|
use rolldown::{
|
||||||
@@ -7,7 +8,6 @@ use rolldown::{
|
|||||||
WatchOption, Watcher,
|
WatchOption, Watcher,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Value;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, IsTerminal, Read, Write};
|
use std::io::{self, IsTerminal, Read, Write};
|
||||||
@@ -186,10 +186,8 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
|||||||
|
|
||||||
ui::info("Uploading plugin");
|
ui::info("Uploading plugin");
|
||||||
let url = format!("{}/api/v1/plugins/publish", environment.api_base_url());
|
let url = format!("{}/api/v1/plugins/publish", environment.api_base_url());
|
||||||
let response = reqwest::Client::new()
|
let response = http::build_client(Some(&token))?
|
||||||
.post(url)
|
.post(url)
|
||||||
.header("X-Yaak-Session", token)
|
|
||||||
.header(reqwest::header::USER_AGENT, user_agent())
|
|
||||||
.header(reqwest::header::CONTENT_TYPE, "application/zip")
|
.header(reqwest::header::CONTENT_TYPE, "application/zip")
|
||||||
.body(archive)
|
.body(archive)
|
||||||
.send()
|
.send()
|
||||||
@@ -201,7 +199,7 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
|||||||
response.text().await.map_err(|e| format!("Failed reading publish response body: {e}"))?;
|
response.text().await.map_err(|e| format!("Failed reading publish response body: {e}"))?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(parse_api_error(status.as_u16(), &body));
|
return Err(http::parse_api_error(status.as_u16(), &body));
|
||||||
}
|
}
|
||||||
|
|
||||||
let published: PublishResponse = serde_json::from_str(&body)
|
let published: PublishResponse = serde_json::from_str(&body)
|
||||||
@@ -389,32 +387,6 @@ fn get_auth_token(environment: Environment) -> CommandResult<Option<String>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_api_error(status: u16, body: &str) -> String {
|
|
||||||
if let Ok(value) = serde_json::from_str::<Value>(body) {
|
|
||||||
if let Some(message) = value.get("message").and_then(Value::as_str) {
|
|
||||||
return message.to_string();
|
|
||||||
}
|
|
||||||
if let Some(error) = value.get("error").and_then(Value::as_str) {
|
|
||||||
return error.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("API error {status}: {body}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_agent() -> String {
|
|
||||||
format!("YaakCli/{} ({})", env!("CARGO_PKG_VERSION"), ua_platform())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ua_platform() -> &'static str {
|
|
||||||
match std::env::consts::OS {
|
|
||||||
"windows" => "Win",
|
|
||||||
"darwin" => "Mac",
|
|
||||||
"linux" => "Linux",
|
|
||||||
_ => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_name() -> String {
|
fn random_name() -> String {
|
||||||
const ADJECTIVES: &[&str] = &[
|
const ADJECTIVES: &[&str] = &[
|
||||||
"young", "youthful", "yellow", "yielding", "yappy", "yawning", "yummy", "yucky", "yearly",
|
"young", "youthful", "yellow", "yielding", "yappy", "yawning", "yummy", "yucky", "yearly",
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType) -> CommandRes
|
|||||||
.map_err(|e| format!("Failed to serialize WebSocket request schema: {e}"))?,
|
.map_err(|e| format!("Failed to serialize WebSocket request schema: {e}"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enrich_schema_guidance(&mut schema, request_type);
|
||||||
|
|
||||||
if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await {
|
if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await {
|
||||||
eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}");
|
eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}");
|
||||||
}
|
}
|
||||||
@@ -95,6 +97,37 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType) -> CommandRes
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
|
||||||
|
if !matches!(request_type, RequestSchemaType::Http) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(url_schema) = properties.get_mut("url").and_then(Value::as_object_mut) {
|
||||||
|
append_description(
|
||||||
|
url_schema,
|
||||||
|
"For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names without `:` (for example `id`, `commentId`).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_description(schema: &mut Map<String, Value>, extra: &str) {
|
||||||
|
match schema.get_mut("description") {
|
||||||
|
Some(Value::String(existing)) if !existing.trim().is_empty() => {
|
||||||
|
if !existing.ends_with(' ') {
|
||||||
|
existing.push(' ');
|
||||||
|
}
|
||||||
|
existing.push_str(extra);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
schema.insert("description".to_string(), Value::String(extra.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn merge_auth_schema_from_plugins(
|
async fn merge_auth_schema_from_plugins(
|
||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
schema: &mut Value,
|
schema: &mut Value,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod context;
|
|||||||
mod plugin_events;
|
mod plugin_events;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod version;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands, RequestCommands};
|
use cli::{Cli, Commands, RequestCommands};
|
||||||
|
|||||||
47
crates-cli/yaak-cli/src/utils/http.rs
Normal file
47
crates-cli/yaak-cli/src/utils/http.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use reqwest::Client;
|
||||||
|
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
pub fn build_client(session_token: Option<&str>) -> Result<Client, String> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
let user_agent = HeaderValue::from_str(&user_agent())
|
||||||
|
.map_err(|e| format!("Failed to build user-agent header: {e}"))?;
|
||||||
|
headers.insert(USER_AGENT, user_agent);
|
||||||
|
|
||||||
|
if let Some(token) = session_token {
|
||||||
|
let token_value = HeaderValue::from_str(token)
|
||||||
|
.map_err(|e| format!("Failed to build session header: {e}"))?;
|
||||||
|
headers.insert(HeaderName::from_static("x-yaak-session"), token_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Client::builder()
|
||||||
|
.default_headers(headers)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Failed to initialize HTTP client: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_api_error(status: u16, body: &str) -> String {
|
||||||
|
if let Ok(value) = serde_json::from_str::<Value>(body) {
|
||||||
|
if let Some(message) = value.get("message").and_then(Value::as_str) {
|
||||||
|
return message.to_string();
|
||||||
|
}
|
||||||
|
if let Some(error) = value.get("error").and_then(Value::as_str) {
|
||||||
|
return error.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("API error {status}: {body}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_agent() -> String {
|
||||||
|
format!("YaakCli/{} ({})", crate::version::cli_version(), ua_platform())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ua_platform() -> &'static str {
|
||||||
|
match std::env::consts::OS {
|
||||||
|
"windows" => "Win",
|
||||||
|
"darwin" => "Mac",
|
||||||
|
"linux" => "Linux",
|
||||||
|
_ => "Unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod confirm;
|
pub mod confirm;
|
||||||
|
pub mod http;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
|||||||
3
crates-cli/yaak-cli/src/version.rs
Normal file
3
crates-cli/yaak-cli/src/version.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub fn cli_version() -> &'static str {
|
||||||
|
option_env!("YAAK_CLI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||||
|
}
|
||||||
@@ -190,7 +190,9 @@ fn request_schema_http_outputs_json_schema() {
|
|||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(contains("\"type\": \"object\""))
|
.stdout(contains("\"type\": \"object\""))
|
||||||
.stdout(contains("\"authentication\""));
|
.stdout(contains("\"authentication\""))
|
||||||
|
.stdout(contains("/foo/:id/comments/:commentId"))
|
||||||
|
.stdout(contains("put concrete values in `urlParameters`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user