use std::convert::TryFrom; /// /// Query language is simple : a query is a "TOML path", or tpath. /// pub struct Query( pub Vec ); #[ derive( Debug, PartialEq, Eq ) ] pub enum TpathSegment { Name( String ), Num( usize ), } use nom:: { branch::alt, bytes::complete::{ escaped_transform, take_while1, take_while_m_n }, character::complete::{ char, digit1, none_of, one_of }, combinator::{ all_consuming, map, map_res }, error::ErrorKind, multi::many0, sequence::{ delimited, preceded, tuple }, Err, IResult, }; fn hex_unicode_scalar( len : usize, s : &str ) -> IResult<&str, char> { map_res( take_while_m_n( len, len, | c : char | c.is_ascii_hexdigit() ), | s : &str | { char::try_from( u32::from_str_radix( s, 16 ).unwrap() ) })( s ) } fn basic_string_escape( s : &str ) -> IResult<&str, char> { alt (( one_of( "\\\"" ), map( char( 'b' ), | _ | '\x08' ), map( char( 't' ), | _ | '\t' ), map( char( 'n' ), | _ | '\n' ), map( char( 'f' ), | _ | '\x0c' ), map( char( 'r' ), | _ | '\r' ), preceded( char( 'u' ), | s | hex_unicode_scalar( 4, s ) ), preceded( char( 'U' ), | s | hex_unicode_scalar( 8, s ) ), ))( s ) } fn basic_string( s : &str ) -> IResult<&str, String> { let string_body = escaped_transform( none_of( "\\\"" ), '\\', basic_string_escape ); delimited( char( '"' ), string_body, char( '"' ) )( s ) } fn bare_string( s : &str ) -> IResult<&str, &str> { take_while1( | c : char | c.is_ascii_alphanumeric() || c == '-' || c == '_' )( s ) } fn key_string( s : &str ) -> IResult<&str, String> { alt( ( basic_string, map( bare_string, String::from ) ) )( s ) } fn array_index( s : &str ) -> IResult<&str, usize> { map_res( digit1, | i : &str | i.parse::() )( s ) } fn tpath_segment_name( s : &str ) -> IResult<&str, TpathSegment> { map( key_string, TpathSegment::Name )( s ) } fn tpath_segment_num( s : &str ) -> IResult<&str, TpathSegment> { map( delimited( char( '[' ), array_index, char( ']' ) ), TpathSegment::Num )( s ) } fn tpath_segment_rest( s : &str ) -> IResult<&str, TpathSegment> { alt( ( preceded( char( '.' ), tpath_segment_name ), tpath_segment_num ) )( s ) } fn tpath( s : &str ) -> IResult<&str, Vec> { alt (( map( all_consuming( char( '.' ) ), | _ | Vec::new() ), // Must start with a name, because TOML root is always a table. map( tuple( ( tpath_segment_name, many0( tpath_segment_rest ) ) ), | ( hd, mut tl ) | { tl.insert( 0, hd ); tl }), ))( s ) } pub fn parse_query( s : &str ) -> Result> { all_consuming( tpath )( s ).map( | ( trailing, res ) | { assert!( trailing.is_empty() ); Query( res ) }) } #[ test ] fn test_parse_query() { use TpathSegment::{ Name, Num }; let name = | n : &str | Name( n.to_string() ); for ( s, expected ) in vec! [ ( ".", Ok( vec![] ) ), ( "a", Ok( vec![ name( "a" ) ] ) ), ( "a.b", Ok( vec![ name("a"), name( "b" ) ] ) ), ( "\"a.b\"", Ok( vec![ name( "a.b" ) ] ) ), ( "..", Err( () ) ), ( "a[1]", Ok( vec![ name("a"), Num( 1 ) ] ) ), ( "a[b]", Err( () ) ), ( "a[1].b", Ok( vec![ name( "a" ), Num( 1 ), name( "b" ) ] ) ), ( "a.b[1]", Ok( vec![ name( "a" ), name( "b" ), Num( 1 ) ] ) ), ] { let actual = parse_query( s ); // This could use some slicker check that prints the actual on failure. // Also nice would be to proceed to try the other test cases. match expected { Ok( q ) => assert!( q == actual.unwrap().0 ), Err( _ ) => assert!( actual.is_err() ), } } }