automerge/automerge-cli/src/change.rs
Orion Henry 0141bcdc8f import cli
2022-03-02 14:05:10 -05:00

238 lines
8.3 KiB
Rust

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<Input>(s: &'static str) -> impl Parser<Input, Output = String>
where
Input: combine::Stream<Token = char>,
Input::Error: combine::ParseError<Input::Token, Input::Range, Input::Position>,
{
charparser::string_cmp(s, |l, r| l.eq_ignore_ascii_case(&r)).map(|s| s.to_lowercase())
}
fn op_parser<Input>() -> impl combine::Parser<Input, Output = Op>
where
Input: combine::Stream<Token = char>,
{
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<Input>() -> impl Parser<Input, Output = String>
where
Input: combine::Stream<Token = char>,
{
let key_char_parser = combine::choice::<Input, _>((
charparser::alpha_num(),
charparser::char('-'),
charparser::char('_'),
));
combine::many1(key_char_parser).map(|chars: Vec<char>| chars.into_iter().collect())
}
fn index_parser<Input>() -> impl Parser<Input, Output = u32>
where
Input: combine::Stream<Token = char>,
{
combine::many1::<Vec<char>, Input, _>(charparser::digit()).map(|digits| {
let num_string: String = digits.iter().collect();
num_string.parse::<u32>().unwrap()
})
}
combine::parser! {
fn path_segment_parser[Input](path_so_far: amf::Path)(Input) -> amf::Path
where [Input: combine::Stream<Token=char>]
{
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<dyn combine::Parser<Input, Output = amf::Value, PartialState = ()> + 'a>
where
Input: 'a,
Input: combine::Stream<Token = char>,
Input::Error: combine::ParseError<Input::Token, Input::Range, Input::Position>,
{
combine::parser::combinator::no_partial(
//combine::position().and(combine::many1::<Vec<char>, _, _>(combine::any())).and_then(
combine::position().and(combine::many1::<Vec<char>, _, _>(combine::any())).flat_map(
|(position, chars): (Input::Position, Vec<char>)| -> Result<amf::Value, Input::Error> {
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 = <Input::Error as ParseError<_, _, _>>::StreamError::message::<combine::error::Format<String>>(combine::error::Format(e.to_string()));
//let pe = <Input::Error as ParseError<Input::Token, Input::Range, Input::Position>>::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<Input, Output = amf::LocalChange> + 'a
where
Input: 'a,
Input: combine::stream::Stream<Token = char>,
Input::Error: combine::ParseError<Input::Token, Input::Range, Input::Position>,
{
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<Input, Output = amf::LocalChange, PartialState = _>,
> = 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<amf::LocalChange, ChangeError> {
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<u8> = 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,
);
}
}
}