use std::ffi::OsString;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use crate::mcp::protocol::PROTOCOL_VERSION;
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
const RUN_TIME_ERROR: &str =
"must be a positive Locust duration using h, m, and s at most once in that order";
const PROTOCOL_VERSION_ERROR: &str = "must use the MCP YYYY-MM-DD version format";
fn parse_positive_usize(value: &str) -> Result<usize, String> {
let parsed = value
.parse::<usize>()
.map_err(|_| String::from("must be an integer greater than zero"))?;
if parsed == 0 {
Err(String::from("must be an integer greater than zero"))
} else {
Ok(parsed)
}
}
fn parse_positive_f64(value: &str) -> Result<f64, String> {
let parsed = value
.parse::<f64>()
.map_err(|_| String::from("must be a finite number greater than zero"))?;
if parsed.is_finite() && parsed > 0.0 {
Ok(parsed)
} else {
Err(String::from("must be a finite number greater than zero"))
}
}
fn parse_run_time(value: &str) -> Result<String, String> {
let bytes = value.as_bytes();
let mut position = 0;
let mut previous_unit = None;
if bytes.is_empty() {
return Err(String::from(RUN_TIME_ERROR));
}
while position < bytes.len() {
let number_start = position;
while position < bytes.len() && bytes[position].is_ascii_digit() {
position += 1;
}
if number_start == position {
return Err(String::from(RUN_TIME_ERROR));
}
let amount = value[number_start..position]
.parse::<u64>()
.map_err(|_| String::from(RUN_TIME_ERROR))?;
if amount == 0 {
return Err(String::from(RUN_TIME_ERROR));
}
let unit = match bytes.get(position) {
Some(b'h') => 0,
Some(b'm') => 1,
Some(b's') => 2,
_ => return Err(String::from(RUN_TIME_ERROR)),
};
if previous_unit.is_some_and(|previous| unit <= previous) {
return Err(String::from(RUN_TIME_ERROR));
}
previous_unit = Some(unit);
position += 1;
}
Ok(value.to_owned())
}
#[derive(Debug, Clone, PartialEq, Parser)]
#[command(name = "cf-integration", version, arg_required_else_help = true)]
pub(crate) struct Cli {
#[command(subcommand)]
pub(crate) command: Command,
}
#[derive(Debug, Clone, PartialEq, Subcommand)]
pub(crate) enum Command {
Stack(StackArgs),
Probe(RoutedWorkflowTargetArgs),
Load(LoadArgs),
Live(LiveArgs),
Conformance(ConformanceArgs),
Debug(DebugArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackArgs {
#[command(subcommand)]
pub(crate) command: StackCommand,
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub(crate) enum StackCommand {
Up(StackUpArgs),
Down(StackDownArgs),
Status(TopologyArgs),
Logs(StackLogsArgs),
Config(TopologyArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackUpArgs {
#[arg(long, value_enum)]
pub(crate) topology: Option<CliTopology>,
#[arg(long)]
pub(crate) fresh: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackDownArgs {
#[arg(long, value_enum)]
pub(crate) topology: Option<TopologySelection>,
#[arg(long)]
pub(crate) volumes: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct TopologyArgs {
#[arg(long, value_enum)]
pub(crate) topology: Option<CliTopology>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct RoutedWorkflowTargetArgs {
#[arg(long, value_enum)]
pub(crate) lane: Option<CliTopology>,
#[arg(long)]
pub(crate) protocol_version: Option<ProtocolVersion>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct WorkflowTargetArgs {
#[arg(long, value_enum)]
pub(crate) lane: Option<CliLane>,
#[arg(long)]
pub(crate) protocol_version: Option<ProtocolVersion>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackLogsArgs {
#[arg(long, value_enum)]
pub(crate) topology: Option<CliTopology>,
#[arg(value_name = "SERVICE")]
pub(crate) services: Vec<OsString>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum CliTopology {
Controlplane,
Dataplane,
}
impl From<CliTopology> for crate::infrastructure::StackMode {
fn from(topology: CliTopology) -> Self {
match topology {
CliTopology::Controlplane => Self::Controlplane,
CliTopology::Dataplane => Self::Dataplane,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum TopologySelection {
Controlplane,
Dataplane,
All,
}
#[derive(Debug, Clone, PartialEq, Args)]
pub(crate) struct LoadArgs {
#[command(flatten)]
pub(crate) target: RoutedWorkflowTargetArgs,
#[arg(long)]
pub(crate) smoke: bool,
#[arg(long, value_parser = parse_positive_usize)]
pub(crate) users: Option<usize>,
#[arg(long, value_parser = parse_positive_f64)]
pub(crate) spawn_rate: Option<f64>,
#[arg(long, value_parser = parse_run_time)]
pub(crate) run_time: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct LiveArgs {
#[command(flatten)]
pub(crate) target: WorkflowTargetArgs,
#[arg(long, value_enum, default_value = "all")]
pub(crate) group: LiveGroup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum CliLane {
FixtureDirect,
BuiltInDataPlane,
ExternalDataPlane,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum LiveGroup {
Mcp,
Rbac,
Protocol,
All,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ProtocolVersion(String);
impl ProtocolVersion {
#[must_use]
pub(crate) fn as_str(&self) -> &str {
&self.0
}
}
impl Default for ProtocolVersion {
fn default() -> Self {
Self(PROTOCOL_VERSION.to_owned())
}
}
impl fmt::Display for ProtocolVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl FromStr for ProtocolVersion {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let bytes = value.as_bytes();
let valid = bytes.len() == 10
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes
.iter()
.enumerate()
.all(|(index, byte)| matches!(index, 4 | 7) || byte.is_ascii_digit());
if valid {
Ok(Self(value.to_owned()))
} else {
Err(String::from(PROTOCOL_VERSION_ERROR))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct ConformanceArgs {
#[command(subcommand)]
pub(crate) command: ConformanceCommand,
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub(crate) enum ConformanceCommand {
Run(ConformanceRunArgs),
Report(ConformanceReportArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct ConformanceRunArgs {
#[arg(long, value_enum, action = ArgAction::Append)]
pub(crate) lane: Vec<CliLane>,
#[arg(long, value_enum, action = ArgAction::Append)]
pub(crate) client_era: Vec<CliConformanceEra>,
#[arg(long, value_enum, action = ArgAction::Append)]
pub(crate) server_era: Vec<CliConformanceEra>,
#[arg(long)]
pub(crate) results_dir: Option<PathBuf>,
#[arg(long)]
pub(crate) baseline_dir: Option<PathBuf>,
#[arg(long)]
pub(crate) bless: bool,
#[arg(long)]
pub(crate) output_dir: Option<PathBuf>,
}
impl From<CliLane> for crate::conformance::results::SemanticLane {
fn from(lane: CliLane) -> Self {
match lane {
CliLane::FixtureDirect => Self::FixtureDirect,
CliLane::BuiltInDataPlane => Self::BuiltInDataPlane,
CliLane::ExternalDataPlane => Self::ExternalDataPlane,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub(crate) enum CliConformanceEra {
Dual,
Legacy,
Modern,
}
impl From<CliConformanceEra> for crate::conformance::results::ConformanceServerEra {
fn from(era: CliConformanceEra) -> Self {
match era {
CliConformanceEra::Dual => Self::Dual,
CliConformanceEra::Legacy => Self::Legacy,
CliConformanceEra::Modern => Self::Modern,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct ConformanceReportArgs {
#[arg(long)]
pub(crate) results_dir: Option<PathBuf>,
#[arg(long)]
pub(crate) output_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct DebugArgs {
#[command(subcommand)]
pub(crate) command: DebugCommand,
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub(crate) enum DebugCommand {
Inspect(InspectArgs),
Token(TokenArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct InspectArgs {
#[command(flatten)]
pub(crate) target: RoutedWorkflowTargetArgs,
#[arg(long, default_value = "tools/list")]
pub(crate) method: String,
#[arg(long)]
pub(crate) server_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct TokenArgs {
#[arg(long, value_enum)]
pub(crate) kind: TokenKind,
#[arg(long)]
pub(crate) server_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum TokenKind {
Scoped,
Admin,
}