use std::path::{Path, PathBuf};
use crate::Result;
use crate::cli_util;
use crate::suite::resolve::{resolve_path_from_repo_root, write_file};
use crate::suite::runtime::{
path_relative_to_repo_or_abs, plan_case_output_paths, resolve_grpc_token_profile,
resolve_grpc_url,
};
use crate::suite::schema::{SuiteCase, SuiteDefaults};
pub(super) enum PrepareOutcome<T> {
Ready(T),
}
pub(super) struct GrpcCasePlan {
pub(super) request_abs: PathBuf,
pub(super) request_file: crate::grpc::schema::GrpcRequestFile,
pub(super) config_dir: String,
pub(super) url: String,
pub(super) token: String,
}
pub(super) struct GrpcCaseRunOutput {
pub(super) status: String,
pub(super) message: Option<String>,
pub(super) assertions: Option<serde_json::Value>,
pub(super) command_snippet: Option<String>,
pub(super) stdout_path: PathBuf,
pub(super) stderr_path: PathBuf,
}
pub(super) fn prepare_grpc_case(
repo_root: &Path,
case: &SuiteCase,
id: &str,
defaults: &SuiteDefaults,
) -> Result<PrepareOutcome<GrpcCasePlan>> {
let request_rel = case.request.trim();
if request_rel.is_empty() {
anyhow::bail!("gRPC case '{id}' is missing request");
}
let request_abs = resolve_path_from_repo_root(repo_root, request_rel);
if !request_abs.is_file() {
anyhow::bail!("gRPC case '{id}' request not found: {request_rel}");
}
let config_dir = cli_util::trim_non_empty(&case.config_dir)
.unwrap_or_else(|| defaults.grpc.config_dir.clone());
let token =
cli_util::trim_non_empty(&case.token).unwrap_or_else(|| defaults.grpc.token.clone());
let url = cli_util::trim_non_empty(&case.url).unwrap_or_else(|| defaults.grpc.url.clone());
let request_file = crate::grpc::schema::GrpcRequestFile::load(&request_abs)?;
Ok(PrepareOutcome::Ready(GrpcCasePlan {
request_abs,
request_file,
config_dir,
url,
token,
}))
}
#[allow(clippy::too_many_arguments)]
pub(super) fn run_grpc_case(
repo_root: &Path,
run_dir_abs: &Path,
safe_id: &str,
effective_no_history: bool,
effective_env: &str,
defaults: &SuiteDefaults,
env_grpc_url: &str,
grpc_config_dir: &str,
grpc_url: &str,
grpc_token: &str,
request_abs: &Path,
request_file: &crate::grpc::schema::GrpcRequestFile,
) -> Result<GrpcCaseRunOutput> {
let outputs = plan_case_output_paths(run_dir_abs, safe_id);
let stdout_path = outputs.stdout_path;
let stderr_path = outputs.stderr_path;
write_file(&stdout_path, b"")?;
write_file(&stderr_path, b"")?;
let mut status = "pending".to_string();
let mut message: Option<String> = None;
let target = match resolve_grpc_url(
repo_root,
grpc_config_dir,
grpc_url,
effective_env,
defaults,
env_grpc_url,
) {
Ok(v) => v,
Err(err) => {
write_file(&stderr_path, format!("{err:#}\n").as_bytes())?;
status = "failed".to_string();
message = Some("grpc_runner_failed".to_string());
String::new()
}
};
let mut assertions: Option<serde_json::Value> = None;
if status != "failed" {
let setup_dir_abs = resolve_path_from_repo_root(repo_root, grpc_config_dir);
let bearer = if !grpc_token.trim().is_empty() {
match resolve_grpc_token_profile(&setup_dir_abs, grpc_token) {
Ok(t) => Some(t),
Err(err) => {
write_file(&stderr_path, format!("{err:#}\n").as_bytes())?;
status = "failed".to_string();
message = Some("grpc_runner_failed".to_string());
None
}
}
} else {
None
};
if status != "failed" {
match crate::grpc::runner::execute_grpc_request(
request_file,
&target,
bearer.as_deref(),
) {
Ok(executed) => {
write_file(&stdout_path, &executed.response_body)?;
if !executed.stderr.trim().is_empty() {
write_file(&stderr_path, executed.stderr.as_bytes())?;
}
let mut assert_rows: Vec<serde_json::Value> = Vec::new();
if let Some(expect) = request_file.request.expect.as_ref() {
if let Some(status_expect) = expect.status {
let state = if executed.grpc_status == status_expect {
"passed"
} else {
"failed"
};
assert_rows.push(serde_json::json!({
"label": format!("expect.status: {status_expect}"),
"state": state
}));
}
if let Some(jq_expr) = expect.jq.as_deref() {
let jq_state = match serde_json::from_slice::<serde_json::Value>(
&executed.response_body,
) {
Ok(v) => {
if crate::jq::eval_exit_status(&v, jq_expr).unwrap_or(false) {
"passed"
} else {
"failed"
}
}
Err(_) => "failed",
};
assert_rows.push(serde_json::json!({
"label": format!("expect.jq: {jq_expr}"),
"state": jq_state
}));
}
}
if !assert_rows.is_empty() {
assertions = Some(serde_json::json!({"checks": assert_rows}));
}
if let Err(err) = crate::grpc::expect::evaluate_main_response(
&request_file.request,
&executed,
) {
write_file(&stderr_path, format!("{err:#}\n").as_bytes())?;
status = "failed".to_string();
message = Some("grpc_runner_failed".to_string());
} else {
status = "passed".to_string();
}
}
Err(err) => {
write_file(&stderr_path, format!("{err:#}\n").as_bytes())?;
status = "failed".to_string();
message = Some("grpc_runner_failed".to_string());
}
}
}
}
let mut argv: Vec<String> = vec![
"api-grpc".to_string(),
"call".to_string(),
"--config-dir".to_string(),
grpc_config_dir.to_string(),
];
if effective_no_history {
argv.push("--no-history".to_string());
}
if !grpc_url.trim().is_empty() {
argv.push("--url".to_string());
argv.push(grpc_url.to_string());
} else if !effective_env.trim().is_empty() {
argv.push("--env".to_string());
argv.push(effective_env.to_string());
}
if !grpc_token.trim().is_empty() {
argv.push("--token".to_string());
argv.push(grpc_token.to_string());
}
argv.push(path_relative_to_repo_or_abs(repo_root, request_abs));
let args = super::mask_args_for_command_snippet(&argv[1..]);
let snippet = format!("{} {}", cli_util::shell_quote("api-grpc"), args);
Ok(GrpcCaseRunOutput {
status,
message,
assertions,
command_snippet: Some(snippet),
stdout_path,
stderr_path,
})
}