use unilang::semantic::VerifiedCommand;
use unilang::types::Value;
use unilang::data::{ ErrorCode, ErrorData };
use super::types::{ UsageParams, SortStrategy, PreferStrategy, ColsVisibility, SubprocessModel, SubprocessEffort, UsageOutputFormat, GetField, validate_set_model };
#[ allow( clippy::too_many_lines ) ]
pub( super ) fn parse_usage_params( cmd : &VerifiedCommand ) -> Result< UsageParams, ErrorData >
{
let refresh = crate::output::parse_int_flag( cmd, "refresh", 1 )?;
let live = crate::output::parse_int_flag( cmd, "live", 0 )?;
let trace = crate::output::parse_int_flag( cmd, "trace", 0 )? != 0;
let touch = crate::output::parse_int_flag( cmd, "touch", 1 )?;
let rotate = crate::output::parse_int_flag( cmd, "rotate", 0 )? != 0;
let who = match crate::output::parse_int_flag( cmd, "who", -1 )?
{
-1 => None,
0 => Some( false ),
1 => Some( true ),
_ => unreachable!(), };
let force = match cmd.arguments.get( "force" )
{
None => false,
Some( Value::String( s ) ) => match s.as_str()
{
"1" | "true" => true,
"0" | "false" => false,
other => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "force:: must be 0 or 1, got {other:?}" ),
) ),
},
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"force:: must be a string (0 or 1)".to_string(),
) ),
};
let solo = crate::output::parse_int_flag( cmd, "solo", 0 )? != 0;
if solo && rotate
{
return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"solo::1 and rotate::1 are mutually exclusive".to_string(),
) );
}
if rotate && live != 0
{
return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"rotate::1 and live::1 are mutually exclusive".to_string(),
) );
}
let interval = match cmd.arguments.get( "interval" )
{
None => 30_u64,
Some( Value::Integer( n ) ) => u64::try_from( *n ).unwrap_or( 0 ),
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"interval:: must be a non-negative integer".to_string(),
) ),
};
let jitter = match cmd.arguments.get( "jitter" )
{
None => 0_u64,
Some( Value::Integer( n ) ) => u64::try_from( *n ).unwrap_or( 0 ),
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"jitter:: must be a non-negative integer".to_string(),
) ),
};
let sort = match cmd.arguments.get( "sort" )
{
None => SortStrategy::Renew,
Some( Value::String( s ) ) => SortStrategy::parse( s ).map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?,
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"sort:: must be a string".to_string(),
) ),
};
let desc_param = match cmd.arguments.get( "desc" )
{
None => None,
Some( Value::Integer( 0 ) ) => Some( false ),
Some( Value::Integer( 1 ) ) => Some( true ),
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"desc:: must be 0 or 1".to_string(),
) ),
};
let prefer = match cmd.arguments.get( "prefer" )
{
None => PreferStrategy::Any,
Some( Value::String( s ) ) => PreferStrategy::parse( s ).map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?,
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"prefer:: must be a string".to_string(),
) ),
};
if cmd.arguments.contains_key( "next" )
{
return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"next:: parameter has been removed; use sort:: instead (valid values: `name`, `renew`, `renews`)".to_string(),
) );
}
let cols = match cmd.arguments.get( "cols" )
{
None => ColsVisibility::default_set(),
Some( Value::String( s ) ) => ColsVisibility::parse( s ).map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?,
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"cols:: must be a string".to_string(),
) ),
};
let imodel = match cmd.arguments.get( "imodel" )
{
None => SubprocessModel::Auto,
Some( Value::String( s ) ) => SubprocessModel::parse( s ).map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?,
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"imodel:: must be a string".to_string(),
) ),
};
let effort = match cmd.arguments.get( "effort" )
{
None => SubprocessEffort::Auto,
Some( Value::String( s ) ) => SubprocessEffort::parse( s ).map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?,
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"effort:: must be a string".to_string(),
) ),
};
let count = match cmd.arguments.get( "count" )
{
None => 0_u64,
Some( Value::Integer( n ) ) => u64::try_from( *n ).map_err( |_| ErrorData::new( ErrorCode::ArgumentTypeMismatch, "count:: must be a non-negative integer".to_string() ) )?,
_ => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "count:: must be a non-negative integer".to_string() ) ),
};
let offset = match cmd.arguments.get( "offset" )
{
None => 0_u64,
Some( Value::Integer( n ) ) => u64::try_from( *n ).map_err( |_| ErrorData::new( ErrorCode::ArgumentTypeMismatch, "offset:: must be a non-negative integer".to_string() ) )?,
_ => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "offset:: must be a non-negative integer".to_string() ) ),
};
let only_active = crate::output::parse_int_flag( cmd, "only_active", 0 )? != 0;
let only_next = crate::output::parse_int_flag( cmd, "only_next", 0 )? != 0;
let only_valid = crate::output::parse_int_flag( cmd, "only_valid", 0 )? != 0;
let exclude_exhausted = crate::output::parse_int_flag( cmd, "exclude_exhausted", 0 )? != 0;
let h5_min = match cmd.arguments.get( "min_5h" )
{
None => 0_u8,
Some( Value::Integer( n ) ) =>
{
u8::try_from( *n ).map_err( |_| ErrorData::new( ErrorCode::ArgumentTypeMismatch, "min_5h:: must be 0–100".to_string() ) )
.and_then( |v| if v <= 100 { Ok( v ) } else { Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "min_5h:: must be 0–100".to_string() ) ) } )?
}
_ => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "min_5h:: must be an integer 0–100".to_string() ) ),
};
let d7_min = match cmd.arguments.get( "min_7d" )
{
None => 0_u8,
Some( Value::Integer( n ) ) =>
{
u8::try_from( *n ).map_err( |_| ErrorData::new( ErrorCode::ArgumentTypeMismatch, "min_7d:: must be 0–100".to_string() ) )
.and_then( |v| if v <= 100 { Ok( v ) } else { Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "min_7d:: must be 0–100".to_string() ) ) } )?
}
_ => return Err( ErrorData::new( ErrorCode::ArgumentTypeMismatch, "min_7d:: must be an integer 0–100".to_string() ) ),
};
let format = match cmd.arguments.get( "format" )
{
None => UsageOutputFormat::Text,
Some( Value::String( s ) ) => match s.as_str()
{
"text" => UsageOutputFormat::Text,
"json" => UsageOutputFormat::Json,
"tsv" => UsageOutputFormat::Tsv,
"plain" => UsageOutputFormat::Plain,
"value" => UsageOutputFormat::Value,
"table" => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"format::table is only supported by .accounts".to_string(),
) ),
other => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
format!( "unknown format {other:?}: valid values are `text`, `json`, `tsv`, `plain`, `value`" ),
) ),
},
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"format:: must be a string".to_string(),
) ),
};
let get = match cmd.arguments.get( "get" )
{
None => None,
Some( Value::String( s ) ) => Some(
GetField::parse( s ).map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?
),
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"get:: must be a string".to_string(),
) ),
};
let abs = crate::output::parse_int_flag( cmd, "abs", 0 )? != 0;
let no_color = crate::output::parse_int_flag( cmd, "no_color", 0 )? != 0;
let set_model = match cmd.arguments.get( "set_model" )
{
None => None,
Some( Value::String( s ) ) =>
{
validate_set_model( s ).map_err( |e| ErrorData::new( ErrorCode::ArgumentTypeMismatch, e ) )?;
Some( s.clone() )
}
_ => return Err( ErrorData::new(
ErrorCode::ArgumentTypeMismatch,
"set_model:: must be a string".to_string(),
) ),
};
Ok( UsageParams
{
refresh, live, interval, jitter, trace, sort, desc : desc_param, prefer, cols, touch, imodel, effort,
count, offset, only_active, only_next, min_5h : h5_min, min_7d : d7_min, only_valid, exclude_exhausted,
format, get, abs, no_color, set_model,
rotate, force, who, solo,
} )
}
#[ cfg( test ) ]
mod tests
{
use super::*;
use std::collections::HashMap;
use unilang::data::CommandDefinition;
use unilang::semantic::VerifiedCommand;
use unilang::types::Value;
fn make_cmd( args : Vec< ( &str, Value ) > ) -> VerifiedCommand
{
let mut arguments = HashMap::new();
for ( k, v ) in args { arguments.insert( k.to_string(), v ); }
let definition = CommandDefinition::former().name( ".usage" ).description( "test" ).end();
VerifiedCommand { definition, arguments }
}
#[ test ]
fn rotate_field_parses_true()
{
let cmd = make_cmd( vec![ ( "rotate", Value::Integer( 1 ) ) ] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert!( params.rotate, "rotate::1 must set params.rotate to true" );
}
#[ test ]
fn rotate_field_parses_false()
{
let cmd = make_cmd( vec![ ( "rotate", Value::Integer( 0 ) ) ] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert!( !params.rotate, "rotate::0 must set params.rotate to false" );
}
#[ test ]
fn rotate_field_default_false()
{
let cmd = make_cmd( vec![] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert!( !params.rotate, "default rotate must be false" );
}
#[ test ]
fn force_field_parses_true()
{
let cmd = make_cmd( vec![ ( "force", Value::String( "1".to_string() ) ) ] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert!( params.force, "force::1 must set params.force to true" );
}
#[ test ]
fn who_field_default_is_none()
{
let cmd = make_cmd( vec![] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert_eq!( params.who, None, "default who must be None (auto)" );
}
#[ test ]
fn who_field_parses_one_to_some_true()
{
let cmd = make_cmd( vec![ ( "who", Value::Integer( 1 ) ) ] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert_eq!( params.who, Some( true ), "who::1 must set params.who to Some(true)" );
}
#[ test ]
fn who_field_parses_zero_to_some_false()
{
let cmd = make_cmd( vec![ ( "who", Value::Integer( 0 ) ) ] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert_eq!( params.who, Some( false ), "who::0 must set params.who to Some(false)" );
}
#[ test ]
fn ec7_who_rejects_integer_two()
{
let cmd = make_cmd( vec![ ( "who", Value::Integer( 2 ) ) ] );
let result = parse_usage_params( &cmd );
assert!(
result.is_err(),
"EC-7: who::2 must be rejected (integer outside {{0, 1}}); got Ok",
);
}
#[ test ]
fn solo_field_default_false()
{
let cmd = make_cmd( vec![] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert!( !params.solo, "default solo must be false (token conservation off)" );
}
#[ test ]
fn solo_field_parses_true()
{
let cmd = make_cmd( vec![ ( "solo", Value::Integer( 1 ) ) ] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert!( params.solo, "solo::1 must set params.solo to true" );
}
#[ test ]
fn solo_field_parses_false()
{
let cmd = make_cmd( vec![ ( "solo", Value::Integer( 0 ) ) ] );
let params = parse_usage_params( &cmd ).expect( "parse must succeed" );
assert!( !params.solo, "solo::0 must set params.solo to false" );
}
#[ test ]
fn ec5_solo_and_rotate_mutual_exclusion()
{
let cmd = make_cmd( vec![
( "solo", Value::Integer( 1 ) ),
( "rotate", Value::Integer( 1 ) ),
] );
let result = parse_usage_params( &cmd );
assert!( result.is_err(), "EC-5: solo::1 + rotate::1 must return Err (mutual exclusion)" );
let err_msg = result.unwrap_err().message;
assert!(
err_msg.contains( "solo" ) && err_msg.contains( "rotate" ),
"EC-5: error message must reference both params, got: {err_msg}",
);
}
#[ test ]
fn ec12_solo_rejects_integer_two()
{
let cmd = make_cmd( vec![ ( "solo", Value::Integer( 2 ) ) ] );
let result = parse_usage_params( &cmd );
assert!(
result.is_err(),
"EC-12: solo::2 must be rejected (integer outside {{0, 1}}); got Ok",
);
}
#[ test ]
fn rotate_and_live_mutual_exclusion()
{
let cmd = make_cmd( vec![
( "rotate", Value::Integer( 1 ) ),
( "live", Value::Integer( 1 ) ),
] );
let result = parse_usage_params( &cmd );
assert!( result.is_err(), "rotate::1 + live::1 must return Err (mutual exclusion)" );
let err_msg = result.unwrap_err().message;
assert!(
err_msg.contains( "rotate" ) && err_msg.contains( "live" ),
"error message must reference both params, got: {err_msg}",
);
}
}