mod private
{
use crate :: *;
use std ::collections ::HashMap;
use parser :: { Program, ParsedCommand };
use error_tools ::untyped ::Result;
use error_tools ::dependency ::thiserror;
#[ allow( missing_docs ) ]
#[ derive( Debug, error_tools ::typed ::Error ) ]
pub enum ParserError
{
#[ error( "Internal Error: {details}" ) ]
InternalError { details: String },
#[ error( "Unexpected input. Expected: {expected}, found {input}" ) ]
UnexpectedInput { expected: String, input: String },
}
#[ derive( Debug ) ]
pub struct Parser;
type ParsedArgs = ( Vec< String >, HashMap< String, String >, usize );
impl Parser
{
pub fn parse< As, A >( &self, args: As ) -> Result< Program< ParsedCommand >, ParserError >
where
As: IntoIterator< Item = A >,
A: Into< String >,
{
let args: Vec< _ > = args.into_iter().map( Into ::into ).collect();
let mut commands = vec![];
let mut i = 0;
while i < args.len()
{
let ( command, relative_pos ) = Self ::parse_command( &args[ i.. ] )?;
i += relative_pos;
commands.push( command );
}
Ok( Program { commands } )
}
fn valid_command_name( input: &str ) -> bool
{
if let Some( name ) = input.strip_prefix( '.' )
{
name.is_empty() || name.starts_with( '?' ) || name.chars().next().is_some_and( char ::is_alphanumeric )
}
else
{
false
}
}
fn looks_like_path_or_url( input: &str ) -> bool
{
if input.contains( "://" ) { return true; }
if input.len() >= 3
{
let bytes = input.as_bytes();
if bytes[ 0 ].is_ascii_alphabetic() && bytes[ 1 ] == b':'
{
if bytes[ 2 ] == b'\\' || bytes[ 2 ] == b'/' { return true; }
}
}
if input.starts_with( "\\\\" ) { return true; }
if input.len() >= 2 && input.chars().all( | c | c.is_ascii_digit() || c == ':' )
{ return true; }
false
}
fn parse_command( args: &[ String ] ) -> Result< ( ParsedCommand, usize ), ParserError >
{
if args.is_empty()
{
return Err( ParserError ::InternalError { details: "Try to parse command without input".into() } );
}
let mut i = 0;
if !Self ::valid_command_name( &args[ i ] )
{
return Err( ParserError ::UnexpectedInput { expected: "command".into(), input: args[ i ].clone() } );
}
let name = match args[ i ].strip_prefix( '.' ).unwrap()
{
"" => ".",
"?" => ".?",
other => other,
};
i += 1;
let ( subjects, properties, relative_pos ) = Self ::parse_command_args( &args[ i .. ] )?;
i += relative_pos;
Ok(
(
ParsedCommand
{
name: name.to_string(),
subjects,
properties,
},
i,
))
}
fn parse_command_args( args: &[ String ] ) -> Result< ParsedArgs, ParserError >
{
let mut i = 0;
let mut subjects = vec![];
let mut properties = HashMap ::new();
let mut properties_turn = false;
while i < args.len()
{
let item = &args[ i ];
if Self ::valid_command_name( item ) { break; }
if item.contains( " : " )
{
properties_turn = true;
let ( name, value ) = item.split_once( " : " ).unwrap();
if !value.is_empty()
{
properties.insert( name.to_string(), value.to_string() );
}
else if args.len() > i + 1
{
properties.insert( name.to_string(), args[ i + 1 ].clone() );
i += 1;
}
else
{
return Err( ParserError ::UnexpectedInput { expected: "property value".into(), input: "end of input".into() } );
}
}
else if item.contains( ':' ) && !Self ::looks_like_path_or_url( item )
{
properties_turn = true;
if let Some( ( name, value ) ) = item.split_once( ':' )
{
let value = value.trim();
if value.is_empty()
{
if args.len() > i + 1
{
properties.insert( name.to_string(), args[ i + 1 ].clone() );
i += 1;
}
else
{
return Err( ParserError ::UnexpectedInput { expected: "property value".into(), input: "end of input".into() } );
}
}
else
{
properties.insert( name.to_string(), value.to_string() );
}
}
}
else if args.len() > i + 1 && args[ i + 1 ].starts_with( " : " )
{
let stripped = args[ i + 1 ].strip_prefix( " : " ).unwrap();
if !stripped.is_empty()
{
properties.insert( args[ i ].clone(), stripped.to_string() );
i += 1;
}
else if args.len() > i + 2
{
properties.insert( args[ i ].clone(), args[ i + 2 ].clone() );
i += 2;
}
else
{
return Err( ParserError ::UnexpectedInput { expected: "property value".into(), input: "end of input".into() } );
}
}
else if !properties_turn
{
subjects.push( item.clone() );
}
else
{
return Err( ParserError ::UnexpectedInput { expected: "`command` or `property`".into(), input: item.into() } );
}
i += 1;
}
Ok( ( subjects, properties, i ) )
}
}
}
crate ::mod_interface!
{
exposed use Parser;
exposed use ParserError;
}