mod engine;
mod runtime;
mod script;
use anyhow::{Context, Result, bail};
use clap::{CommandFactory, Parser, Subcommand, ValueEnum, ValueHint};
use clap_complete::{
engine::{ArgValueCandidates, CompletionCandidate},
env::CompleteEnv,
};
use std::collections::HashMap;
use std::path::PathBuf;
fn scenario_candidates() -> Vec<CompletionCandidate> {
let args: Vec<String> = std::env::args().collect();
let Some(file) = scenario_file_from_args(&args) else {
return Vec::new();
};
script::scenario_names(&file)
.into_iter()
.map(CompletionCandidate::new)
.collect()
}
fn scenario_file_from_args(args: &[String]) -> Option<PathBuf> {
let mut rest = args.iter().skip_while(|a| *a != "run");
rest.next()?; let mut rest = rest.peekable();
while let Some(a) = rest.next() {
match a.as_str() {
"--set" | "--scenario" => {
rest.next();
}
_ if a.starts_with('-') => {}
_ => return Some(PathBuf::from(a)),
}
}
None
}
#[derive(Parser)]
#[command(
name = "ringo-flow",
version,
about = "Telephony scenario test runner for baresip (Rhai scenarios)"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(required = true, num_args = 1.., value_hint = ValueHint::AnyPath)]
paths: Vec<PathBuf>,
#[arg(long = "set", value_name = "KEY=VALUE")]
set: Vec<String>,
#[arg(long = "env-file", value_name = "FILE", value_hint = ValueHint::FilePath)]
env_file: Vec<PathBuf>,
#[arg(long = "scenario", value_name = "PATTERN", add = ArgValueCandidates::new(scenario_candidates))]
scenario: Option<String>,
#[arg(long = "tag", value_name = "TAG", value_delimiter = ',')]
tag: Vec<String>,
#[arg(long = "exclude-tag", value_name = "TAG", value_delimiter = ',')]
exclude_tag: Vec<String>,
#[arg(long)]
logs: bool,
#[arg(long)]
save_audio: bool,
#[arg(long)]
json: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[arg(short, long)]
quiet: bool,
#[arg(long, visible_alias = "no-ansi")]
no_color: bool,
#[arg(long)]
insecure_http: bool,
},
Check {
#[arg(value_hint = ValueHint::FilePath)]
file: PathBuf,
},
Definitions {
#[arg(default_value = "ringo-flow.d.rhai", value_hint = ValueHint::FilePath)]
out: PathBuf,
},
Docs {
#[arg(default_value = "ringo-flow-api.md", value_hint = ValueHint::FilePath)]
out: PathBuf,
#[arg(long, value_enum, default_value_t = DocFormat::Markdown)]
format: DocFormat,
},
}
#[derive(Clone, Copy, ValueEnum)]
enum DocFormat {
#[value(alias = "md")]
Markdown,
Html,
}
fn parse_overrides(set: &[String]) -> Result<HashMap<String, String>> {
let mut out = HashMap::new();
for s in set {
let (k, v) = s
.split_once('=')
.with_context(|| format!("--set expects key=value, got `{s}`"))?;
if k.is_empty() {
bail!("--set key must not be empty in `{s}`");
}
out.insert(k.to_string(), v.to_string());
}
Ok(out)
}
fn main() -> Result<()> {
CompleteEnv::with_factory(Cli::command).complete();
let cli = Cli::parse();
match cli.command {
Commands::Run {
paths,
set,
env_file,
scenario,
tag,
exclude_tag,
logs,
save_audio,
json,
verbose,
quiet,
no_color,
insecure_http,
} => {
let color = !no_color && std::env::var_os("NO_COLOR").is_none();
runtime::report::set_ansi_enabled(color);
let overrides = parse_overrides(&set)?;
let insecure_http =
insecure_http || std::env::var_os("RINGO_FLOW_INSECURE_HTTP").is_some();
let output = runtime::Output {
json,
quiet,
verbose: verbose > 0,
logs,
save_audio,
insecure_http,
};
let filters = engine::Filters {
name: scenario,
tags: tag,
exclude_tags: exclude_tag,
};
script::run(&paths, output, overrides, filters, &env_file)
}
Commands::Check { file } => script::check(&file),
Commands::Definitions { out } => script::write_definitions(&out),
Commands::Docs { out, format } => match format {
DocFormat::Markdown => script::write_markdown_docs(&out),
DocFormat::Html => script::write_html_docs(&out),
},
}
}