use std::ffi::OsString;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use crate::mcp::protocol::{LEGACY_PROTOCOL_VERSION, 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 be modern or legacy";
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 {
#[arg(long, global = true)]
pub(crate) standalone: bool,
#[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),
#[command(hide = true)]
Ci(CiArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct CiArgs {
#[command(subcommand)]
pub(crate) command: CiCommand,
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub(crate) enum CiCommand {
PrepareImage(CiPrepareImageArgs),
PrepareRelease,
SelectRelease,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct CiPrepareImageArgs {
#[arg(long)]
pub(crate) artifact: String,
#[arg(long)]
pub(crate) binary: PathBuf,
#[arg(long)]
pub(crate) image: String,
#[arg(long)]
pub(crate) repository: Option<String>,
#[arg(long)]
pub(crate) revision: Option<String>,
#[arg(long, default_value = "docker/Dockerfile")]
pub(crate) dockerfile: PathBuf,
#[arg(long, default_value = "conformance-prebuilt")]
pub(crate) target: String,
#[arg(long, default_value = ".integration/ci/prebuilt")]
pub(crate) download_dir: PathBuf,
}
#[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(StackLaneArgs),
Logs(StackLogsArgs),
Config(StackLaneArgs),
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackUpArgs {
#[command(flatten)]
pub(crate) target: RoutedWorkflowTargetArgs,
#[arg(long)]
pub(crate) fresh: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackDownArgs {
#[arg(long, value_enum)]
pub(crate) lane: Option<LaneSelection>,
#[arg(long)]
pub(crate) volumes: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackLaneArgs {
#[arg(long, value_enum)]
pub(crate) lane: Option<CliRoutedLane>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct RoutedWorkflowTargetArgs {
#[arg(long, value_enum)]
pub(crate) lane: Option<CliRoutedLane>,
#[arg(long, value_enum)]
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, value_enum)]
pub(crate) protocol_version: Option<ProtocolVersion>,
}
#[derive(Debug, Clone, PartialEq, Eq, Args)]
pub(crate) struct StackLogsArgs {
#[arg(long, value_enum)]
pub(crate) lane: Option<CliRoutedLane>,
#[arg(value_name = "SERVICE")]
pub(crate) services: Vec<OsString>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum CliRoutedLane {
Builtin,
External,
}
impl From<CliRoutedLane> for crate::infrastructure::StackMode {
fn from(lane: CliRoutedLane) -> Self {
match lane {
CliRoutedLane::Builtin => Self::Controlplane,
CliRoutedLane::External => Self::Dataplane,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum LaneSelection {
Builtin,
External,
All,
}
#[derive(Debug, Clone, PartialEq, Args)]
pub(crate) struct LoadArgs {
#[command(flatten)]
pub(crate) target: RoutedWorkflowTargetArgs,
#[arg(long)]
pub(crate) observability: bool,
#[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,
Builtin,
External,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum LiveGroup {
Mcp,
Rbac,
Protocol,
All,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
pub(crate) enum ProtocolVersion {
#[default]
Modern,
Legacy,
}
impl ProtocolVersion {
#[must_use]
pub(crate) const fn wire_version(self) -> &'static str {
match self {
Self::Modern => PROTOCOL_VERSION,
Self::Legacy => LEGACY_PROTOCOL_VERSION,
}
}
}
impl fmt::Display for ProtocolVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Modern => "modern",
Self::Legacy => "legacy",
})
}
}
impl FromStr for ProtocolVersion {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"modern" => Ok(Self::Modern),
"legacy" => Ok(Self::Legacy),
_ => 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::Builtin => Self::BuiltInDataPlane,
CliLane::External => 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,
}