2022-12-24 21:16:23 +11:00
use nom ::{
2022-12-31 00:59:14 +11:00
bytes ::complete ::{ take_till , take_till1 , take_while } ,
2023-03-13 15:23:07 +11:00
character ::{ complete ::{ space0 , space1 , alpha1 , one_of , char , u8 , u16 } } ,
2022-12-26 01:30:59 +11:00
combinator ::{ recognize , fail , eof } ,
sequence ::terminated ,
branch ::alt ,
error ::{ context , VerboseError , VerboseErrorKind } ,
2022-12-24 21:16:23 +11:00
IResult ,
} ;
2023-03-13 15:23:07 +11:00
use super ::allow ::{ AllowCommand , ConsentTarget , ConsentDetails } ;
use ansi ::{ ansi , strip_special_characters } ;
use crate ::models ::consent ::ConsentType ;
2022-12-24 21:16:23 +11:00
pub fn parse_command_name ( input : & str ) -> ( & str , & str ) {
fn parse ( input : & str ) -> IResult < & str , & str > {
let ( input , _ ) = space0 ( input ) ? ;
2022-12-29 18:44:50 +11:00
let ( input , cmd ) = alt ( (
recognize ( one_of ( " - \" ':. " ) ) ,
take_till1 ( | c | c = = ' ' | | c = = '\t' )
) ) ( input ) ? ;
2022-12-24 21:16:23 +11:00
let ( input , _ ) = space0 ( input ) ? ;
Ok ( ( input , cmd ) )
}
match parse ( input ) {
/* This parser only fails on empty / whitespace only strings. */
Err ( _ ) = > ( " " , " " ) ,
Ok ( ( rest , command ) ) = > ( command , rest )
}
}
2022-12-26 01:30:59 +11:00
2022-12-31 00:59:14 +11:00
pub fn parse_to_space ( input : & str ) -> ( & str , & str ) {
fn parser ( input : & str ) -> IResult < & str , & str > {
terminated ( take_till ( | c | c = = ' ' | | c = = '\t' ) , alt ( ( space1 , eof ) ) ) ( input )
}
match parser ( input ) {
Err ( _ ) = > ( " " , " " ) , /* Impossible? */
Ok ( ( rest , token ) ) = > ( token , rest )
}
}
pub fn parse_offset ( input : & str ) -> ( Option < u8 > , & str ) {
fn parser ( input : & str ) -> IResult < & str , u8 > {
terminated ( u8 , char ( '.' ) ) ( input )
}
match parser ( input ) {
Err ( _ ) = > ( None , input ) ,
Ok ( ( rest , result ) ) = > ( Some ( result ) , rest )
}
}
2023-02-20 22:27:43 +11:00
pub fn parse_count ( input : & str ) -> ( Option < u8 > , & str ) {
fn parser ( input : & str ) -> IResult < & str , u8 > {
terminated ( u8 , char ( ' ' ) ) ( input )
}
match parser ( input ) {
Err ( _ ) = > ( None , input ) ,
Ok ( ( rest , result ) ) = > ( Some ( result ) , rest )
}
}
2022-12-26 01:30:59 +11:00
pub fn parse_username ( input : & str ) -> Result < ( & str , & str ) , & 'static str > {
const CATCHALL_ERROR : & 'static str = " Must only contain alphanumeric characters or _ " ;
fn parse_valid ( input : & str ) -> IResult < & str , ( ) , VerboseError < & str > > {
let ( input , l1 ) = context ( " Must start with a letter " , alpha1 ) ( input ) ? ;
let ( input , l2 ) = context ( CATCHALL_ERROR ,
take_while ( | c : char | c . is_alphanumeric ( ) | | c = = '_' ) ) ( input ) ? ;
if l1 . len ( ) + l2 . len ( ) > 20 {
context ( " Limit of 20 characters " , fail ::< & str , & str , VerboseError < & str > > ) ( input ) ? ;
}
Ok ( ( input , ( ) ) )
}
match terminated ( recognize ( parse_valid ) , alt ( ( space1 , eof ) ) ) ( input ) {
Ok ( ( input , username ) ) = > Ok ( ( username , input ) ) ,
Err ( nom ::Err ::Error ( e ) ) | Err ( nom ::Err ::Failure ( e ) ) = >
Err ( e . errors . into_iter ( ) . find_map ( | k | match k . 1 {
VerboseErrorKind ::Context ( s ) = > Some ( s ) ,
_ = > None
} ) . unwrap_or ( CATCHALL_ERROR ) ) ,
Err ( _ ) = > Err ( CATCHALL_ERROR )
}
}
2023-02-25 23:49:46 +11:00
pub fn parse_on_or_default < ' l > ( input : & ' l str , default_on : & ' l str ) -> ( & ' l str , & ' l str ) {
if let Some ( ( a , b ) ) = input . split_once ( " on " ) {
( a , b )
} else {
( input , default_on )
}
}
2023-03-13 15:23:07 +11:00
pub fn parse_duration_mins < ' l > ( input : & ' l str ) -> Result < ( u64 , & ' l str ) , String > {
let ( input , number ) = match u16 ::< & ' l str , ( ) > ( input ) {
Err ( _ ) = > Err ( " Invalid number - duration should start with a number, e.g. 5 minutes " ) ? ,
Ok ( n ) = > n
} ;
let ( tok , input ) = match input . trim_start ( ) . split_once ( " " ) {
None = > ( input , " " ) ,
Some ( v ) = > v
} ;
Ok ( ( match tok . to_lowercase ( ) . as_str ( ) {
" min " | " mins " | " minute " | " minutes " = > number as u64 ,
" h " | " hr " | " hrs " | " hour " | " hours " = > ( number as u64 ) * 60 ,
" d " | " day " | " days " = > ( number as u64 ) * 60 * 24 ,
" w " | " wk " | " wks " | " week " | " weeks " = > ( number as u64 ) * 60 * 24 * 7 ,
_ = > Err ( " Duration number needs to be followed by a valid unit - minutes, hours, days or weeks " ) ?
} , input ) )
}
pub fn parse_allow < ' l > ( input : & ' l str , is_explicit : bool ) -> Result < AllowCommand , String > {
let usage : & 'static str =
ansi! ( " Usage: allow <lt>action> from <lt>user> <lt>options> | allow <lt>action> against <lt>corp> by <lt>corp> <lt>options>. Try <bold>help allow<reset> for more. " ) ;
let ( consent_type_s , input ) = match input . trim_start ( ) . split_once ( " " ) {
None = > Err ( usage ) ,
Some ( v ) = > Ok ( v )
} ? ;
let consent_type = match ConsentType ::from_str ( & consent_type_s . trim ( ) . to_lowercase ( ) ) {
None = > Err (
if is_explicit { " Invalid consent type - options are fight, medicine, gifts, visit and sex " } else {
" Invalid consent type - options are fight, medicine, gifts and visit "
} ) ,
Some ( ct ) = > Ok ( ct )
} ? ;
let ( tok , mut input ) = match input . trim_start ( ) . split_once ( " " ) {
None = > Err ( usage ) ,
Some ( v ) = > Ok ( v )
} ? ;
let tok_trim = tok . trim_start ( ) . to_lowercase ( ) ;
let consent_target =
if tok_trim = = " against " {
if consent_type ! = ConsentType ::Fight {
Err ( " corps can only currently consent to fight, no other actions " ) ?
} else {
let ( my_corp_raw , new_input ) = match input . trim_start ( ) . split_once ( " " ) {
None = > Err ( usage ) ,
Some ( v ) = > Ok ( v )
} ? ;
let my_corp = my_corp_raw . trim_start ( ) ;
let ( tok , new_input ) = match new_input . trim_start ( ) . split_once ( " " ) {
None = > Err ( usage ) ,
Some ( v ) = > Ok ( v )
} ? ;
if tok . trim_start ( ) . to_lowercase ( ) ! = " by " {
Err ( usage ) ? ;
}
let ( target_corp_raw , new_input ) = match new_input . trim_start ( ) . split_once ( " " ) {
None = > ( new_input . trim_start ( ) , " " ) ,
Some ( v ) = > v
} ;
input = new_input ;
ConsentTarget ::CorpTarget { from_corp : my_corp , to_corp : target_corp_raw . trim_start ( ) }
}
} else if tok_trim = = " from " {
let ( target_user_raw , new_input ) = match input . trim_start ( ) . split_once ( " " ) {
None = > ( input . trim_start ( ) , " " ) ,
Some ( v ) = > v
} ;
input = new_input ;
ConsentTarget ::UserTarget { to_user : target_user_raw . trim_start ( ) }
} else {
Err ( usage ) ?
} ;
let mut consent_details = ConsentDetails ::default_for ( & consent_type ) ;
loop {
input = input . trim_start ( ) ;
if input = = " " {
break ;
}
let ( tok , new_input ) = match input . split_once ( " " ) {
None = > ( input , " " ) ,
Some ( v ) = > v
} ;
match tok . to_lowercase ( ) . as_str ( ) {
" for " = > {
let ( minutes , new_input ) = parse_duration_mins ( new_input ) ? ;
input = new_input ;
consent_details . duration_minutes = Some ( minutes ) ;
}
" until " = > {
let ( tok , new_input ) = match new_input . split_once ( " " ) {
2023-03-13 22:38:54 +11:00
None = > ( new_input , " " ) ,
2023-03-13 15:23:07 +11:00
Some ( v ) = > v
} ;
if tok . trim_start ( ) . to_lowercase ( ) ! = " death " {
Err ( " Option until needs to be followed with death - until death " ) ?
}
consent_details . until_death = true ;
input = new_input ;
}
" allow " = > {
let ( tok , new_input ) = match new_input . split_once ( " " ) {
None = > ( new_input , " " ) ,
Some ( v ) = > v
} ;
match tok . trim_start ( ) . to_lowercase ( ) . as_str ( ) {
" private " = > {
consent_details . allow_private = true ;
} ,
" pick " = > {
consent_details . allow_pick = true ;
} ,
" revoke " = > {
consent_details . freely_revoke = true ;
} ,
_ = > Err ( " Option allow needs to be followed with private, pick or revoke - allow private | allow pick | allow revoke " ) ?
}
input = new_input ;
}
" disallow " = > {
let ( tok , new_input ) = match new_input . split_once ( " " ) {
None = > ( new_input , " " ) ,
Some ( v ) = > v
} ;
match tok . trim_start ( ) . to_lowercase ( ) . as_str ( ) {
" private " = > {
consent_details . allow_private = false ;
} ,
" pick " = > {
consent_details . allow_pick = false ;
} ,
_ = > Err ( " Option disallow needs to be followed with private or pick - disallow private | disallow pick " ) ?
}
input = new_input ;
}
" in " = > {
let ( tok , new_input ) = match new_input . split_once ( " " ) {
None = > ( new_input , " " ) ,
Some ( v ) = > v
} ;
consent_details . only_in . push ( tok ) ;
input = new_input ;
}
_ = > Err ( format! ( " I don't understand the option \" {} \" " , strip_special_characters ( tok ) ) ) ?
}
}
Ok ( AllowCommand { consent_type : consent_type , consent_target : consent_target , consent_details : consent_details } )
}
2022-12-26 01:30:59 +11:00
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn it_parses_normal_command ( ) {
assert_eq! ( parse_command_name ( " help " ) ,
( " help " , " " ) ) ;
}
#[ test ]
fn it_parses_normal_command_with_arg ( ) {
assert_eq! ( parse_command_name ( " help \t testing stuff " ) ,
( " help " , " testing stuff " ) ) ;
}
#[ test ]
fn it_parses_commands_with_leading_whitespace ( ) {
assert_eq! ( parse_command_name ( " \t \t help \t testing stuff " ) ,
( " help " , " testing stuff " ) ) ;
}
#[ test ]
fn it_parses_empty_command_names ( ) {
assert_eq! ( parse_command_name ( " " ) ,
( " " , " " ) ) ;
assert_eq! ( parse_command_name ( " \t " ) ,
( " " , " " ) ) ;
}
#[ test ]
fn it_parses_usernames ( ) {
assert_eq! ( parse_username ( " Wizard123 " ) , Ok ( ( " Wizard123 " , " " ) ) ) ;
}
#[ test ]
fn it_parses_usernames_with_further_args ( ) {
assert_eq! ( parse_username ( " Wizard_123 with cat " ) , Ok ( ( " Wizard_123 " , " with cat " ) ) ) ;
}
#[ test ]
fn it_parses_alpha_only_usernames ( ) {
assert_eq! ( parse_username ( " W " ) , Ok ( ( " W " , " " ) ) ) ;
}
#[ test ]
fn it_fails_on_empty_usernames ( ) {
assert_eq! ( parse_username ( " " ) , Err ( " Must start with a letter " ) ) ;
}
#[ test ]
fn it_fails_on_usernames_with_invalid_start ( ) {
assert_eq! ( parse_username ( " #hack " ) , Err ( " Must start with a letter " ) ) ;
}
#[ test ]
fn it_fails_on_usernames_with_underscore_start ( ) {
assert_eq! ( parse_username ( " _hack " ) , Err ( " Must start with a letter " ) ) ;
}
#[ test ]
fn it_fails_on_usernames_with_number_start ( ) {
assert_eq! ( parse_username ( " 31337 # " ) , Err ( " Must start with a letter " ) ) ;
}
#[ test ]
fn it_fails_on_usernames_with_bad_characters ( ) {
assert_eq! ( parse_username ( " Wizard! " ) , Err ( " Must only contain alphanumeric characters or _ " ) ) ;
}
#[ test ]
fn it_fails_on_long_usernames ( ) {
assert_eq! ( parse_username ( " A23456789012345678901 " ) , Err ( " Limit of 20 characters " ) ) ;
}
2022-12-31 00:59:14 +11:00
#[ test ]
fn parse_to_space_splits_on_whitespace ( ) {
assert_eq! ( parse_to_space ( " hello world " ) , ( " hello " , " world " ) ) ;
assert_eq! ( parse_to_space ( " hello \t world " ) , ( " hello " , " world " ) ) ;
assert_eq! ( parse_to_space ( " hello world " ) , ( " hello " , " world " ) ) ;
}
#[ test ]
fn parse_to_space_supports_missing_rest ( ) {
assert_eq! ( parse_to_space ( " hello " ) , ( " hello " , " " ) ) ;
assert_eq! ( parse_to_space ( " " ) , ( " " , " " ) ) ;
}
#[ test ]
fn parse_offset_supports_no_offset ( ) {
assert_eq! ( parse_offset ( " hello world " ) , ( None , " hello world " ) )
}
#[ test ]
fn parse_offset_supports_offset ( ) {
assert_eq! ( parse_offset ( " 2.hello world " ) , ( Some ( 2 ) , " hello world " ) )
}
2023-03-13 15:23:07 +11:00
#[ test ]
fn parse_consent_works_default_options_user ( ) {
assert_eq! ( super ::parse_allow ( " medicine From Athorina " , false ) ,
Ok ( AllowCommand {
consent_type : ConsentType ::Medicine ,
consent_target : ConsentTarget ::UserTarget { to_user : " Athorina " } ,
consent_details : ConsentDetails ::default_for ( & ConsentType ::Medicine )
} ) )
}
#[ test ]
fn parse_consent_works_default_options_corp ( ) {
assert_eq! ( super ::parse_allow ( " Fight Against megacorp By supercorp " , false ) ,
Ok ( AllowCommand {
consent_type : ConsentType ::Fight ,
consent_target : ConsentTarget ::CorpTarget { from_corp : " megacorp " , to_corp : " supercorp " } ,
consent_details : ConsentDetails ::default_for ( & ConsentType ::Fight )
} ) )
}
#[ test ]
fn parse_consent_handles_options ( ) {
assert_eq! ( super ::parse_allow ( " fighT fRom athorina For 2 hOurs unTil deAth allOw priVate Disallow pIck alLow revoKe iN here in pit " , false ) ,
Ok ( AllowCommand {
consent_type : ConsentType ::Fight ,
consent_target : ConsentTarget ::UserTarget { to_user : " athorina " } ,
consent_details : ConsentDetails {
duration_minutes : Some ( 120 ) ,
until_death : true ,
allow_private : true ,
allow_pick : false ,
freely_revoke : true ,
only_in : vec ! ( " here " , " pit " ) ,
.. ConsentDetails ::default_for ( & ConsentType ::Fight )
}
} ) )
}
2022-12-26 01:30:59 +11:00
}