diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 2dcce131..6eef9bc1 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -44,6 +44,7 @@ where } /// Any [`Action`] that can be boxed. +// https://www.reddit.com/r/rust/comments/droxdg/why_arent_traits_impld_for_boxdyn_trait/ impl Action for Box { fn act(&self, input: &str) -> String { self.as_ref().act(input) diff --git a/src/actions/replace/mod.rs b/src/actions/replace/mod.rs index 07f154bb..16efbd8b 100644 --- a/src/actions/replace/mod.rs +++ b/src/actions/replace/mod.rs @@ -12,7 +12,7 @@ use super::Action; /// use srgn::scoping::{view::ScopedViewBuilder, regex::Regex}; /// /// let scoper = Regex::new(RegexPattern::new(r"[^a-zA-Z0-9]+").unwrap()); -/// let mut view = ScopedViewBuilder::new("hyphenated-variable-name").explode_from_scoper( +/// let mut view = ScopedViewBuilder::new("hyphenated-variable-name").explode( /// &scoper /// ).build(); /// @@ -31,7 +31,7 @@ use super::Action; /// // A Unicode character class category. See also /// // https://github.com/rust-lang/regex/blob/061ee815ef2c44101dba7b0b124600fcb03c1912/UNICODE.md#rl12-properties /// let scoper = Regex::new(RegexPattern::new(r"\p{Emoji}").unwrap()); -/// let mut view = ScopedViewBuilder::new("Party! 😁 💃 🎉 🥳 So much fun! ╰(°▽°)╯").explode_from_scoper( +/// let mut view = ScopedViewBuilder::new("Party! 😁 💃 🎉 🥳 So much fun! ╰(°▽°)╯").explode( /// &scoper /// ).build(); /// diff --git a/src/main.rs b/src/main.rs index 8b6a9f7c..8b27f1ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,8 @@ use srgn::actions::Titlecase; use srgn::actions::Upper; #[cfg(feature = "symbols")] use srgn::actions::{Symbols, SymbolsInversion}; +use srgn::scoping::literal::LiteralError; +use srgn::scoping::regex::RegexError; use srgn::{ actions::Action, scoping::{ @@ -19,7 +21,7 @@ use srgn::{ }, literal::Literal, regex::Regex, - view::{ScopedViewBuilder, ScoperBuildError}, + view::ScopedViewBuilder, Scoper, }, }; @@ -67,7 +69,7 @@ fn main() -> Result<(), String> { debug!("Building view."); let mut builder = ScopedViewBuilder::new(&buf); for scoper in scopers { - builder = builder.explode(|s| scoper.scope(s)); + builder = builder.explode(&scoper); } let mut view = builder.build(); debug!("Done building view: {view:?}"); @@ -110,6 +112,25 @@ fn main() -> Result<(), String> { Ok(()) } +#[derive(Debug)] +pub enum ScoperBuildError { + EmptyScope, + RegexError(RegexError), + LiteralError(LiteralError), +} + +impl From for ScoperBuildError { + fn from(e: LiteralError) -> Self { + Self::LiteralError(e) + } +} + +impl From for ScoperBuildError { + fn from(e: RegexError) -> Self { + Self::RegexError(e) + } +} + fn assemble_scopers(args: &cli::Cli) -> Result>, ScoperBuildError> { let mut scopers: Vec> = Vec::new(); diff --git a/src/scoping/literal.rs b/src/scoping/literal.rs index 4aa81625..05eae6e2 100644 --- a/src/scoping/literal.rs +++ b/src/scoping/literal.rs @@ -88,7 +88,7 @@ mod tests { ) { let builder = crate::scoping::view::ScopedViewBuilder::new(input); let literal = Literal::try_from(literal.to_owned()).unwrap(); - let actual = builder.explode_from_scoper(&literal).build(); + let actual = builder.explode(&literal).build(); assert_eq!(actual, expected); } diff --git a/src/scoping/mod.rs b/src/scoping/mod.rs index 11f45f04..afce643f 100644 --- a/src/scoping/mod.rs +++ b/src/scoping/mod.rs @@ -1,15 +1,28 @@ //! Items for defining the scope actions are applied within. -use self::scope::ROScopes; +use crate::scoping::scope::ROScopes; +#[cfg(doc)] +use crate::scoping::{scope::Scope, view::ScopedView}; use std::fmt; +/// Create views using programming language grammar-aware types. pub mod langs; +/// Create views using string literals. pub mod literal; +/// Create views using regular expressions. pub mod regex; +/// [`Scope`] and its various wrappers. pub mod scope; +/// [`ScopedView`] and its related types. pub mod view; +/// An item capable of scoping down a given input into individual scopes. pub trait Scoper { + /// Scope the given `input`. + /// + /// After application, the returned scopes are a collection of either in-scope or + /// out-of-scope parts of the input. Assembling them back together should yield the + /// original input. fn scope<'viewee>(&self, input: &'viewee str) -> ROScopes<'viewee>; } @@ -18,3 +31,19 @@ impl fmt::Debug for dyn Scoper { f.debug_struct("Scoper").finish() } } + +impl Scoper for T +where + T: Fn(&str) -> ROScopes, +{ + fn scope<'viewee>(&self, input: &'viewee str) -> ROScopes<'viewee> { + self(input) + } +} + +// https://www.reddit.com/r/rust/comments/droxdg/why_arent_traits_impld_for_boxdyn_trait/ +impl Scoper for Box { + fn scope<'viewee>(&self, input: &'viewee str) -> ROScopes<'viewee> { + self.as_ref().scope(input) + } +} diff --git a/src/scoping/regex.rs b/src/scoping/regex.rs index f54306a8..9522c751 100644 --- a/src/scoping/regex.rs +++ b/src/scoping/regex.rs @@ -210,7 +210,7 @@ mod tests { ) { let builder = crate::scoping::view::ScopedViewBuilder::new(input); let regex = Regex::new(RegexPattern::new(pattern).unwrap()); - let actual = builder.explode_from_scoper(®ex).build(); + let actual = builder.explode(®ex).build(); assert_eq!(actual, expected); } diff --git a/src/scoping/view.rs b/src/scoping/view.rs index b231f9e1..7e97eaf9 100644 --- a/src/scoping/view.rs +++ b/src/scoping/view.rs @@ -1,111 +1,20 @@ use crate::actions::{self, Action}; -use crate::scoping::literal::LiteralError; -use crate::scoping::regex::RegexError; -use crate::scoping::scope::{ROScope, ROScopes, RWScope, RWScopes, Scope}; +use crate::scoping::scope::{ + ROScope, ROScopes, RWScope, RWScopes, + Scope::{In, Out}, +}; use crate::scoping::Scoper; use log::{debug, trace}; use std::borrow::Cow; use std::fmt; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ScopedViewBuilder<'viewee> { - scopes: ROScopes<'viewee>, -} - -impl<'viewee> ScopedViewBuilder<'viewee> { - #[must_use] - pub fn new(input: &'viewee str) -> Self { - Self { - scopes: ROScopes(vec![ROScope(Scope::In(input))]), - } - } - - #[must_use] - pub fn build(self) -> ScopedView<'viewee> { - ScopedView { - scopes: RWScopes( - self.scopes - .0 - .into_iter() - .map(std::convert::Into::into) - .collect(), - ), - } - } -} - -#[derive(Debug)] -pub enum ScoperBuildError { - EmptyScope, - RegexError(RegexError), - LiteralError(LiteralError), -} - -impl From for ScoperBuildError { - fn from(e: LiteralError) -> Self { - Self::LiteralError(e) - } -} - -impl From for ScoperBuildError { - fn from(e: RegexError) -> Self { - Self::RegexError(e) - } -} - -impl<'viewee> IntoIterator for ScopedViewBuilder<'viewee> { - type Item = ROScope<'viewee>; - - type IntoIter = std::vec::IntoIter>; - - fn into_iter(self) -> Self::IntoIter { - self.scopes.0.into_iter() - } -} - -impl<'viewee> ScopedViewBuilder<'viewee> { - #[must_use] - pub fn explode_from_scoper(self, scoper: &impl Scoper) -> Self { - self.explode(|s| scoper.scope(s)) - } - - #[must_use] - pub fn explode(mut self, exploder: F) -> Self - where - F: Fn(&'viewee str) -> ROScopes<'viewee>, - { - trace!("Exploding scopes: {:?}", self.scopes); - let mut new = Vec::with_capacity(self.scopes.0.len()); - for scope in self.scopes.0.drain(..) { - trace!("Exploding scope: {:?}", scope); - - if scope.is_empty() { - trace!("Skipping empty scope"); - continue; - } - - match scope { - ROScope(Scope::In(s)) => { - let mut new_scopes = exploder(s); - new_scopes.0.retain(|s| !s.is_empty()); - new.extend(new_scopes.0); - } - // Be explicit about the `Out(_)` case, so changing the enum is a - // compile error - ROScope(Scope::Out("")) => {} - out @ ROScope(Scope::Out(_)) => new.push(out), - } - - trace!("Exploded scope, new scopes are: {:?}", new); - } - trace!("Done exploding scopes."); - - ScopedViewBuilder { - scopes: ROScopes(new), - } - } -} - +/// A view of some input, sorted into parts, which are either [`In`] or [`Out`] of scope +/// for processing. +/// +/// The view is **writable**. It can be manipulated by [mapping][`Self::map`] +/// [`Action`]s over it. +/// +/// The main avenue for constructing a view is [`Self::builder`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ScopedView<'viewee> { scopes: RWScopes<'viewee>, @@ -113,33 +22,38 @@ pub struct ScopedView<'viewee> { /// Core implementations. impl<'viewee> ScopedView<'viewee> { + /// Create a new view from the given scopes. #[must_use] pub fn new(scopes: RWScopes<'viewee>) -> Self { Self { scopes } } + /// Return a builder for a view of the given input. + /// /// For API discoverability. #[must_use] pub fn builder(input: &'viewee str) -> ScopedViewBuilder<'viewee> { ScopedViewBuilder::new(input) } - /// Apply an action to all in-scope occurrences. + /// Apply an `action` to all [`In`] scope items contained in this view. + /// + /// They are **replaced** with whatever the action returns for the particular scope. /// /// See implementors of [`Action`] for available types. pub fn map(&mut self, action: &impl Action) -> &mut Self { for scope in &mut self.scopes.0 { match scope { - RWScope(Scope::In(s)) => { + RWScope(In(s)) => { let res = action.act(s); debug!( "Replacing '{}' with '{}'", s.escape_debug(), res.escape_debug() ); - *scope = RWScope(Scope::In(Cow::Owned(res))); + *scope = RWScope(In(Cow::Owned(res))); } - RWScope(Scope::Out(s)) => { + RWScope(Out(s)) => { debug!("Appending '{}'", s.escape_debug()); } } @@ -148,15 +62,14 @@ impl<'viewee> ScopedView<'viewee> { self } - /// Squeeze all consecutive [`Scope::In`] scopes into a single occurrence (the first - /// one). + /// Squeeze all consecutive [`In`] scopes into a single occurrence (the first one). pub fn squeeze(&mut self) -> &mut Self { debug!("Squeezing view by collapsing all consecutive in-scope occurrences."); let mut prev_was_in = false; self.scopes.0.retain(|scope| { - let keep = !(prev_was_in && matches!(scope, RWScope(Scope::In(_)))); - prev_was_in = matches!(scope, RWScope(Scope::In(_))); + let keep = !(prev_was_in && matches!(scope, RWScope(In(_)))); + prev_was_in = matches!(scope, RWScope(In(_))); trace!("keep: {}, scope: {:?}", keep, scope); keep }); @@ -166,12 +79,12 @@ impl<'viewee> ScopedView<'viewee> { self } - /// Check whether anything is in scope. + /// Check whether anything is [`In`] scope for this view. #[must_use] pub fn has_any_in_scope(&self) -> bool { self.scopes.0.iter().any(|s| match s { - RWScope(Scope::In(_)) => true, - RWScope(Scope::Out(_)) => false, + RWScope(In(_)) => true, + RWScope(Out(_)) => false, }) } } @@ -248,6 +161,101 @@ impl fmt::Display for ScopedView<'_> { } } +/// A builder for [`ScopedView`]. Chain [`Self::explode`] to build up the view, then +/// finally call [`Self::build`]. +/// +/// Note: while building, the view is **read-only**: no manipulation of the contents is +/// possible yet. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ScopedViewBuilder<'viewee> { + scopes: ROScopes<'viewee>, +} + +/// Core implementations. +impl<'viewee> ScopedViewBuilder<'viewee> { + /// Create a new builder from the given input. + /// + /// Initially, the entire `input` is [`In`] scope. + #[must_use] + pub fn new(input: &'viewee str) -> Self { + Self { + scopes: ROScopes(vec![ROScope(In(input))]), + } + } + + /// Build the view. + /// + /// This makes the view writable. + #[must_use] + pub fn build(self) -> ScopedView<'viewee> { + ScopedView { + scopes: RWScopes( + self.scopes + .0 + .into_iter() + .map(std::convert::Into::into) + .collect(), + ), + } + } + + /// Using a `scoper`, iterate over all scopes currently contained in this view under + /// construction, apply the scoper to all [`In`] scopes, and **replace** each with + /// whatever the scoper returned for the particular scope. These are *multiple* + /// entries (hence 'exploding' this view: after application, it will likely be + /// longer). + /// + /// Note this necessarily means a view can only be *narrowed*. What was previously + /// [`In`] scope can be: + /// + /// - either still fully [`In`] scope, + /// - or partially [`In`] scope, partially [`Out`] of scope + /// + /// after application. Anything [`Out`] out of scope can never be brought back. + #[must_use] + pub fn explode(mut self, scoper: &impl Scoper) -> Self { + trace!("Exploding scopes: {:?}", self.scopes); + let mut new = Vec::with_capacity(self.scopes.0.len()); + for scope in self.scopes.0.drain(..) { + trace!("Exploding scope: {:?}", scope); + + if scope.is_empty() { + trace!("Skipping empty scope"); + continue; + } + + match scope { + ROScope(In(s)) => { + let mut new_scopes = scoper.scope(s); + new_scopes.0.retain(|s| !s.is_empty()); + new.extend(new_scopes.0); + } + // Be explicit about the `Out(_)` case, so changing the enum is a + // compile error + ROScope(Out("")) => {} + out @ ROScope(Out(_)) => new.push(out), + } + + trace!("Exploded scope, new scopes are: {:?}", new); + } + trace!("Done exploding scopes."); + + ScopedViewBuilder { + scopes: ROScopes(new), + } + } +} + +impl<'viewee> IntoIterator for ScopedViewBuilder<'viewee> { + type Item = ROScope<'viewee>; + + type IntoIter = std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.scopes.0.into_iter() + } +} + #[cfg(test)] mod tests { use crate::scoping::view::ScopedViewBuilder; @@ -362,7 +370,7 @@ mod tests { )] fn test_squeeze(#[case] input: &str, #[case] pattern: RegexPattern, #[case] expected: &str) { let builder = ScopedViewBuilder::new(input) - .explode_from_scoper(&crate::scoping::regex::Regex::new(pattern.clone())); + .explode(&crate::scoping::regex::Regex::new(pattern.clone())); let mut view = builder.build(); view.squeeze(); diff --git a/tests/langs/python/mod.rs b/tests/langs/python/mod.rs index 6ca4c80f..f67ad5b8 100644 --- a/tests/langs/python/mod.rs +++ b/tests/langs/python/mod.rs @@ -23,7 +23,7 @@ fn test_python(#[case] file: &str, #[case] query: PythonQuery) { let (input, output) = get_input_output("python", file); let builder = ScopedViewBuilder::new(&input); - let mut view = builder.explode_from_scoper(&lang).build(); + let mut view = builder.explode(&lang).build(); view.delete(); assert_eq!(view.to_string(), output); diff --git a/tests/properties/squeeze.rs b/tests/properties/squeeze.rs index 7c13a6a0..6ef029b7 100644 --- a/tests/properties/squeeze.rs +++ b/tests/properties/squeeze.rs @@ -14,7 +14,7 @@ proptest! { // https://www.unicode.org/reports/tr44/tr44-24.html#General_Category_Values input in r"\p{Any}*AA\p{Any}*" ) { - let mut view = ScopedViewBuilder::new(&input).explode_from_scoper( + let mut view = ScopedViewBuilder::new(&input).explode( &Regex::new(RegexPattern::new("A").unwrap()) ).build();