diff --git a/Cargo.lock b/Cargo.lock index 3896a60..0567f0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,8 +349,10 @@ dependencies = [ "ctor", "fsutils", "itertools", + "lazy_static", "log", "predicates", + "regex", "serde", "simple_logger", "sxd-document", diff --git a/Cargo.toml b/Cargo.toml index 18b517e..add801d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ categories = ["graph"] [dependencies] anyhow = "1.0.65" log = "0.4.17" +regex = "1.6.0" +lazy_static = "1.4.0" simple_logger = "2.3.0" sxd-xpath = "0.4.2" sxd-document = "0.3.2" diff --git a/src/sot.rs b/src/sot.rs index 4ef0196..995e307 100644 --- a/src/sot.rs +++ b/src/sot.rs @@ -26,6 +26,7 @@ mod inconsistencies; mod inspect; mod merge; mod ops; +mod parse; mod serialization; mod slice; mod xml; diff --git a/src/sot/ops.rs b/src/sot/ops.rs index b5d4acf..8b4ddec 100644 --- a/src/sot/ops.rs +++ b/src/sot/ops.rs @@ -74,7 +74,7 @@ impl Sot { } /// Read vertex data. - pub fn data(&mut self, v: u32) -> Result> { + pub fn data(&self, v: u32) -> Result> { let vtx = self .vertices .get(&v) @@ -83,10 +83,6 @@ impl Sot { } /// Find all kids of a vertex. - /// @todo #1:30min Let's implement this method. It has to find - /// all edges departing from the given one and return a vector - /// of tuples, where first element is the label of the edge - /// and the second one is the vertex this edge points to. pub fn kids(&self, _v: u32) -> Result> { Err(anyhow!("Not implemented yet")) } @@ -246,3 +242,19 @@ fn finds_root() -> Result<()> { assert_eq!(0, sot.find(0, "")?); Ok(()) } + +/// @todo #1:30min Let's implement this method. It has to find +/// all edges departing from the given one and return a vector +/// of tuples, where first element is the label of the edge +/// and the second one is the vertex this edge points to. +#[test] +#[ignore] +fn finds_all_kids() -> Result<()> { + let mut sot = Sot::empty(); + sot.add(0)?; + sot.add(1)?; + sot.bind(0, 1, "one")?; + sot.bind(0, 1, "two")?; + assert_eq!(2, sot.kids(0).iter().count()); + Ok(()) +} diff --git a/src/sot/parse.rs b/src/sot/parse.rs new file mode 100644 index 0000000..f97cab0 --- /dev/null +++ b/src/sot/parse.rs @@ -0,0 +1,165 @@ +// Copyright (c) 2022 Yegor Bugayenko +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +use crate::sot::Sot; +use anyhow::{anyhow, Context, Result}; +use lazy_static::lazy_static; +use log::trace; +use regex::Regex; +use std::collections::HashMap; +use std::str::FromStr; + +struct Script { + txt: String, + vars: HashMap, +} + +impl Script { + /// Make a new one. + pub fn new(s: &str) -> Script { + Script { + txt: s.to_string(), + vars: HashMap::new(), + } + } + + /// Deploy the entire script to the Sot. + pub fn deploy_to(&mut self, sot: &mut Sot) -> Result { + let mut pos = 0; + for cmd in self.commands().iter() { + trace!("#deploy_to: deploying command no.{} '{}'...", pos + 1, cmd); + self.deploy_one(cmd, sot) + .context(format!("Failure at the command no.{}: '{}'", pos, cmd))?; + pos += 1; + } + Ok(pos) + } + + /// Get all commands + fn commands(&self) -> Vec { + lazy_static! { + static ref STRIP_COMMENTS: Regex = Regex::new("#.*\n").unwrap(); + } + let text = self.txt.as_str(); + let clean: &str = &STRIP_COMMENTS.replace_all(text, ""); + clean + .split(";") + .map(|t| t.trim()) + .filter(|t| !t.is_empty()) + .map(|t| t.to_string()) + .collect() + } + + /// Deploy a single command to the sot. + fn deploy_one(&mut self, cmd: &str, sot: &mut Sot) -> Result<()> { + lazy_static! { + static ref LINE: Regex = Regex::new("^([A-Z]+) *\\(([^)]*)\\)$").unwrap(); + } + let cap = LINE + .captures(cmd) + .context(format!("Can't parse '{}'", cmd))?; + let args: Vec = (&cap[2]) + .split(",") + .map(|t| t.trim()) + .filter(|t| !t.is_empty()) + .map(|t| t.to_string()) + .collect(); + match &cap[1] { + "ADD" => { + let v = self.parse(&args[0], sot)?; + sot.add(v).context(format!("Failed to ADD({})", &args[0])) + } + "BIND" => { + let v1 = self.parse(&args[0], sot)?; + let v2 = self.parse(&args[1], sot)?; + let a = &args[2]; + sot.bind(v1, v2, a).context(format!( + "Failed to BIND({}, {}, {})", + &args[0], &args[1], &args[2] + )) + } + "PUT" => { + let v = self.parse(&args[0], sot)?; + sot.put(v, Self::parse_data(&args[1])?) + .context(format!("Failed to DATA({})", &args[0])) + } + _cmd => Err(anyhow!("Unknown command: {}", _cmd)), + } + } + + /// Parse data + fn parse_data(s: &str) -> Result> { + lazy_static! { + static ref DATA_STRIP: Regex = Regex::new("[ \t\n\r\\-]").unwrap(); + static ref DATA: Regex = Regex::new("^[0-9A-Fa-f]{2}([0-9A-Fa-f]{2})*$").unwrap(); + } + let d: &str = &DATA_STRIP.replace_all(s, ""); + if DATA.is_match(d) { + let bytes: Vec = (0..d.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&d[i..i + 2], 16).unwrap()) + .collect(); + Ok(bytes) + } else { + Err(anyhow!("Can't parse data '{}'", s)) + } + } + + /// Parses `$ν5` into `5`. + fn parse(&mut self, s: &str, sot: &mut Sot) -> Result { + let head = s.chars().next().context(format!("Empty identifier"))?; + if head == '$' { + let tail: String = s.chars().skip(1).collect::>().into_iter().collect(); + Ok(*self + .vars + .entry(tail.to_string()) + .or_insert_with(|| sot.max() + 1)) + } else { + Ok(u32::from_str(s).context(format!("Parsing of '{}' failed", s))?) + } + } +} + +impl Sot { + /// Parse string with instructions. + pub fn from_str(txt: &str) -> Result { + let mut sot = Sot::empty(); + let mut script = Script::new(txt); + script.deploy_to(&mut sot)?; + Ok(sot) + } +} + +#[cfg(test)] +use std::str; + +#[test] +fn simple_command() -> Result<()> { + let sot = Sot::from_str( + " + ADD(0); ADD($ν1); # adding two vertices + BIND(0, $ν1, foo ); + PUT($ν1 , d0-bf-d1-80-d0-b8-d0-b2-d0-b5-d1-82); + ", + )?; + assert_eq!("привет", str::from_utf8(sot.data(1)?.as_slice())?); + assert_eq!(1, sot.kid(0, "foo").unwrap()); + Ok(()) +}