use automerge as am; use combine::{parser::char as charparser, EasyParser, ParseError, Parser}; use thiserror::Error; #[derive(Error, Debug)] pub enum ChangeError { #[error("Invalid change script: {message}")] InvalidChangeScript { message: String }, #[error("Error reading changes: {:?}", source)] ErrReadingChanges { #[source] source: std::io::Error, }, #[error("Error loading changes: {:?}", source)] ErrApplyingInitialChanges { #[source] source: am::AutomergeError, }, #[error("Error writing changes to output file: {:?}", source)] ErrWritingChanges { #[source] source: std::io::Error, }, } #[derive(Debug)] enum Op { Set, Insert, Delete, Increment, } fn case_insensitive_string(s: &'static str) -> impl Parser where Input: combine::Stream, Input::Error: combine::ParseError, { charparser::string_cmp(s, |l, r| l.eq_ignore_ascii_case(&r)).map(|s| s.to_lowercase()) } fn op_parser() -> impl combine::Parser where Input: combine::Stream, { combine::choice(( combine::attempt(case_insensitive_string("set")).map(|_| Op::Set), combine::attempt(case_insensitive_string("insert")).map(|_| Op::Insert), combine::attempt(case_insensitive_string("delete")).map(|_| Op::Delete), combine::attempt(case_insensitive_string("increment")).map(|_| Op::Increment), )) } fn key_parser() -> impl Parser where Input: combine::Stream, { let key_char_parser = combine::choice::(( charparser::alpha_num(), charparser::char('-'), charparser::char('_'), )); combine::many1(key_char_parser).map(|chars: Vec| chars.into_iter().collect()) } fn index_parser() -> impl Parser where Input: combine::Stream, { combine::many1::, Input, _>(charparser::digit()).map(|digits| { let num_string: String = digits.iter().collect(); num_string.parse::().unwrap() }) } combine::parser! { fn path_segment_parser[Input](path_so_far: amf::Path)(Input) -> amf::Path where [Input: combine::Stream] { let key_path_so_far = path_so_far.clone(); let key_segment_parser = charparser::string("[\"") .with(key_parser()) .skip(charparser::string("\"]")) .then(move |key| path_segment_parser(key_path_so_far.clone().key(key))); let index_path_so_far = path_so_far.clone(); let index_segment_parser = charparser::char('[') .with(index_parser()) .skip(charparser::char(']')) .then(move |index| path_segment_parser(index_path_so_far.clone().index(index))); combine::choice(( combine::attempt(key_segment_parser), combine::attempt(index_segment_parser), combine::value(path_so_far.clone()) )) } } fn value_parser<'a, Input>( ) -> Box + 'a> where Input: 'a, Input: combine::Stream, Input::Error: combine::ParseError, { combine::parser::combinator::no_partial( //combine::position().and(combine::many1::, _, _>(combine::any())).and_then( combine::position().and(combine::many1::, _, _>(combine::any())).flat_map( |(position, chars): (Input::Position, Vec)| -> Result { let json_str: String = chars.into_iter().collect(); let json: serde_json::Value = serde_json::from_str(json_str.as_str()).map_err(|e| { //let pe = >::StreamError::message::>(combine::error::Format(e.to_string())); //let pe = >::StreamError::message(e.to_string().into()); let mut pe = Input::Error::empty(position); pe.add_message(combine::error::Format(e.to_string())); //let pe = combine::ParseError:::wmpty(position); pe })?; Ok(amf::Value::from_json(&json)) }, ) ).boxed() } fn change_parser<'a, Input: 'a>() -> impl combine::Parser + 'a where Input: 'a, Input: combine::stream::Stream, Input::Error: combine::ParseError, { charparser::spaces() .with( op_parser() .skip(charparser::spaces()) .skip(charparser::string("$")) .and(path_segment_parser(am::Path::root())), ) .skip(charparser::spaces()) .then(|(operation, path)| { let onwards: Box< dyn combine::Parser, > = match operation { Op::Set => value_parser::<'a>() .map(move |value| amf::LocalChange::set(path.clone(), value)) .boxed(), Op::Insert => value_parser::<'a>() .map(move |value| amf::LocalChange::insert(path.clone(), value)) .boxed(), Op::Delete => combine::value(amf::LocalChange::delete(path)).boxed(), Op::Increment => combine::value(amf::LocalChange::increment(path)).boxed(), }; onwards }) } fn parse_change_script(input: &str) -> Result { let (change, _) = change_parser() .easy_parse(input) .map_err(|e| ChangeError::InvalidChangeScript { message: e.to_string(), })?; Ok(change) } pub fn change( mut reader: impl std::io::Read, mut writer: impl std::io::Write, script: &str, ) -> Result<(), ChangeError> { let mut buf: Vec = Vec::new(); reader .read_to_end(&mut buf) .map_err(|e| ChangeError::ErrReadingChanges { source: e })?; let backend = am::Automerge::load(&buf) .map_err(|e| ChangeError::ErrApplyingInitialChanges { source: e })?; let local_change = parse_change_script(script)?; let ((), new_changes) = frontend.change::<_, _, amf::InvalidChangeRequest>(None, |d| { d.add_change(local_change)?; Ok(()) })?; let change_bytes = backend.save().unwrap(); writer .write_all(&change_bytes) .map_err(|e| ChangeError::ErrWritingChanges { source: e })?; Ok(()) } #[cfg(test)] mod tests { use maplit::hashmap; use super::*; #[test] fn test_parse_change_script() { struct Scenario { input: &'static str, expected: amf::LocalChange, } let scenarios = vec![ Scenario { input: "set $[\"map\"][0] {\"some\": \"value\"}", expected: amf::LocalChange::set( amf::Path::root().key("map").index(0), amf::Value::from(hashmap! {"some" => "value"}), ), }, Scenario { input: "insert $[\"map\"][0] {\"some\": \"value\"}", expected: amf::LocalChange::insert( amf::Path::root().key("map").index(0), hashmap! {"some" => "value"}.into(), ), }, Scenario { input: "delete $[\"map\"][0]", expected: amf::LocalChange::delete(amf::Path::root().key("map").index(0)), }, Scenario { input: "increment $[\"map\"][0]", expected: amf::LocalChange::increment(amf::Path::root().key("map").index(0)), }, ]; for (index, scenario) in scenarios.into_iter().enumerate() { let result: Result<(amf::LocalChange, _), _> = change_parser().easy_parse(scenario.input); let change = result.unwrap().0; assert_eq!( change, scenario.expected, "Failed on scenario: {0}", index + 1, ); } } }