@@ -98,6 +98,18 @@ pub struct Cli {
9898 /// List supported formats
9999 #[ arg( long = "formats" ) ]
100100 pub formats : bool ,
101+
102+ /// Apply a mapping file (.morph)
103+ #[ arg( short = 'm' , long = "mapping" ) ]
104+ pub mapping : Option < PathBuf > ,
105+
106+ /// Inline mapping expression (can be repeated; applied in order after -m)
107+ #[ arg( short = 'e' , long = "expr" , action = clap:: ArgAction :: Append ) ]
108+ pub expr : Vec < String > ,
109+
110+ /// Parse and validate the mapping without executing
111+ #[ arg( long = "dry-run" ) ]
112+ pub dry_run : bool ,
101113}
102114
103115impl Cli {
@@ -211,6 +223,43 @@ pub fn write_output(cli: &Cli, output: &str) -> crate::error::Result<()> {
211223 }
212224}
213225
226+ /// Build a combined mapping program from -m and -e flags.
227+ /// Returns Ok(None) if no mapping flags were given.
228+ pub fn build_mapping_program (
229+ cli : & Cli ,
230+ ) -> crate :: error:: Result < Option < crate :: mapping:: ast:: Program > > {
231+ let has_mapping = cli. mapping . is_some ( ) ;
232+ let has_exprs = !cli. expr . is_empty ( ) ;
233+
234+ if !has_mapping && !has_exprs {
235+ return Ok ( None ) ;
236+ }
237+
238+ let mut all_statements = Vec :: new ( ) ;
239+
240+ // Load mapping file first
241+ if let Some ( ref path) = cli. mapping {
242+ let source = std:: fs:: read_to_string ( path) . map_err ( |e| {
243+ crate :: error:: MorphError :: Io ( std:: io:: Error :: new (
244+ e. kind ( ) ,
245+ format ! ( "{}: {e}" , path. display( ) ) ,
246+ ) )
247+ } ) ?;
248+ let program = crate :: mapping:: parser:: parse_str ( & source) ?;
249+ all_statements. extend ( program. statements ) ;
250+ }
251+
252+ // Then append inline expressions
253+ for expr_str in & cli. expr {
254+ let program = crate :: mapping:: parser:: parse_str ( expr_str) ?;
255+ all_statements. extend ( program. statements ) ;
256+ }
257+
258+ Ok ( Some ( crate :: mapping:: ast:: Program {
259+ statements : all_statements,
260+ } ) )
261+ }
262+
214263/// Run the full pipeline based on CLI args.
215264pub fn run ( cli : & Cli ) -> crate :: error:: Result < ( ) > {
216265 if cli. formats {
@@ -221,12 +270,35 @@ pub fn run(cli: &Cli) -> crate::error::Result<()> {
221270 return Ok ( ( ) ) ;
222271 }
223272
273+ // Build mapping program (if any flags given)
274+ let mapping_program = build_mapping_program ( cli) ?;
275+
276+ // --dry-run: validate mapping and exit
277+ if cli. dry_run {
278+ match & mapping_program {
279+ Some ( _) => {
280+ println ! ( "mapping valid" ) ;
281+ return Ok ( ( ) ) ;
282+ }
283+ None => {
284+ println ! ( "mapping valid" ) ;
285+ return Ok ( ( ) ) ;
286+ }
287+ }
288+ }
289+
224290 let in_fmt = cli. resolve_input_format ( ) ?;
225291 let out_fmt = cli. resolve_output_format ( ) ?;
226292
227293 let input_data = read_input ( cli) ?;
228294 let value = parse_input ( & input_data, in_fmt) ?;
229295
296+ // Apply mapping if present
297+ let value = match mapping_program {
298+ Some ( ref program) => crate :: mapping:: eval:: eval ( program, & value) ?,
299+ None => value,
300+ } ;
301+
230302 // Determine pretty-printing: explicit flags > default based on TTY
231303 let pretty = if cli. pretty {
232304 true
@@ -417,4 +489,155 @@ mod tests {
417489 assert_eq ! ( Format :: Toml . to_string( ) , "toml" ) ;
418490 assert_eq ! ( Format :: Csv . to_string( ) , "csv" ) ;
419491 }
492+
493+ // -- Mapping CLI flags --------------------------------------------------
494+
495+ #[ test]
496+ fn arg_parsing_mapping_file ( ) {
497+ let cli = Cli :: try_parse_from ( [
498+ "morph" ,
499+ "-i" ,
500+ "in.json" ,
501+ "-o" ,
502+ "out.json" ,
503+ "-m" ,
504+ "transform.morph" ,
505+ ] )
506+ . unwrap ( ) ;
507+ assert_eq ! ( cli. mapping, Some ( PathBuf :: from( "transform.morph" ) ) ) ;
508+ }
509+
510+ #[ test]
511+ fn arg_parsing_single_expr ( ) {
512+ let cli =
513+ Cli :: try_parse_from ( [ "morph" , "-f" , "json" , "-t" , "json" , "-e" , "rename .x -> .y" ] )
514+ . unwrap ( ) ;
515+ assert_eq ! ( cli. expr, vec![ "rename .x -> .y" ] ) ;
516+ }
517+
518+ #[ test]
519+ fn arg_parsing_multiple_expr ( ) {
520+ let cli = Cli :: try_parse_from ( [
521+ "morph" ,
522+ "-f" ,
523+ "json" ,
524+ "-t" ,
525+ "json" ,
526+ "-e" ,
527+ "rename .x -> .y" ,
528+ "-e" ,
529+ "drop .z" ,
530+ ] )
531+ . unwrap ( ) ;
532+ assert_eq ! ( cli. expr, vec![ "rename .x -> .y" , "drop .z" ] ) ;
533+ }
534+
535+ #[ test]
536+ fn arg_parsing_dry_run ( ) {
537+ let cli = Cli :: try_parse_from ( [
538+ "morph" ,
539+ "--dry-run" ,
540+ "-e" ,
541+ "drop .x" ,
542+ "-f" ,
543+ "json" ,
544+ "-t" ,
545+ "json" ,
546+ ] )
547+ . unwrap ( ) ;
548+ assert ! ( cli. dry_run) ;
549+ }
550+
551+ #[ test]
552+ fn arg_parsing_mapping_and_expr_combined ( ) {
553+ let cli = Cli :: try_parse_from ( [
554+ "morph" ,
555+ "-m" ,
556+ "base.morph" ,
557+ "-e" ,
558+ "drop .extra" ,
559+ "-f" ,
560+ "json" ,
561+ "-t" ,
562+ "yaml" ,
563+ ] )
564+ . unwrap ( ) ;
565+ assert_eq ! ( cli. mapping, Some ( PathBuf :: from( "base.morph" ) ) ) ;
566+ assert_eq ! ( cli. expr, vec![ "drop .extra" ] ) ;
567+ }
568+
569+ #[ test]
570+ fn no_mapping_flags_returns_none ( ) {
571+ let cli = Cli :: try_parse_from ( [ "morph" , "-f" , "json" , "-t" , "yaml" ] ) . unwrap ( ) ;
572+ let program = build_mapping_program ( & cli) . unwrap ( ) ;
573+ assert ! ( program. is_none( ) ) ;
574+ }
575+
576+ #[ test]
577+ fn build_mapping_from_expr ( ) {
578+ let cli = Cli :: try_parse_from ( [
579+ "morph" ,
580+ "-f" ,
581+ "json" ,
582+ "-t" ,
583+ "json" ,
584+ "-e" ,
585+ "rename .old -> .new" ,
586+ ] )
587+ . unwrap ( ) ;
588+ let program = build_mapping_program ( & cli) . unwrap ( ) ;
589+ assert ! ( program. is_some( ) ) ;
590+ assert_eq ! ( program. unwrap( ) . statements. len( ) , 1 ) ;
591+ }
592+
593+ #[ test]
594+ fn build_mapping_multiple_exprs_in_order ( ) {
595+ let cli = Cli :: try_parse_from ( [
596+ "morph" ,
597+ "-f" ,
598+ "json" ,
599+ "-t" ,
600+ "json" ,
601+ "-e" ,
602+ "rename .a -> .b" ,
603+ "-e" ,
604+ "drop .c" ,
605+ ] )
606+ . unwrap ( ) ;
607+ let program = build_mapping_program ( & cli) . unwrap ( ) ;
608+ assert ! ( program. is_some( ) ) ;
609+ assert_eq ! ( program. unwrap( ) . statements. len( ) , 2 ) ;
610+ }
611+
612+ #[ test]
613+ fn build_mapping_invalid_expr_returns_error ( ) {
614+ let cli = Cli :: try_parse_from ( [
615+ "morph" ,
616+ "-f" ,
617+ "json" ,
618+ "-t" ,
619+ "json" ,
620+ "-e" ,
621+ "invalid!!!syntax" ,
622+ ] )
623+ . unwrap ( ) ;
624+ let result = build_mapping_program ( & cli) ;
625+ assert ! ( result. is_err( ) ) ;
626+ }
627+
628+ #[ test]
629+ fn build_mapping_nonexistent_file_returns_error ( ) {
630+ let cli = Cli :: try_parse_from ( [
631+ "morph" ,
632+ "-f" ,
633+ "json" ,
634+ "-t" ,
635+ "json" ,
636+ "-m" ,
637+ "/nonexistent/path/transform.morph" ,
638+ ] )
639+ . unwrap ( ) ;
640+ let result = build_mapping_program ( & cli) ;
641+ assert ! ( result. is_err( ) ) ;
642+ }
420643}
0 commit comments