1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::cargo)]
#![warn(missing_copy_implementations)]
#![warn(missing_debug_implementations)]
#![warn(trivial_casts, trivial_numeric_casts)]
#![warn(unused_qualifications)]
#![warn(variant_size_differences)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![allow(clippy::multiple_crate_versions)]
#![allow(missing_docs)]
#![allow(clippy::module_name_repetitions)]

use crate::actions::Action;
use crate::scoping::ScopedViewBuilder;
use log::debug;
#[cfg(doc)]
use scoping::Scope::In;
use scoping::{ScopedView, ScopedViewBuildStep};
use std::{error::Error, fmt};

/// Main components around [`Action`]s and their [processing][Action::substitute].
pub mod actions;
pub mod scoping;
pub mod text;

/// Pattern signalling global scope, aka matching entire inputs.
pub const GLOBAL_SCOPE: &str = r".*";

/// The type of regular expression used throughout the crate. Abstracts away the
/// underlying implementation.
pub use fancy_regex::Regex as RegexPattern;

/// An error as returned by [`apply`].
#[derive(Debug, Clone)]
pub enum ApplicationError<'viewee> {
    /// After scoping, the resulting [`ScopedView`] was found to contain nothing [`In`]
    /// scope. No action was applied.
    ViewWithoutAnyInScope {
        /// The original input application was tried on.
        input: &'viewee str,
        /// The built view over the input.
        view: ScopedView<'viewee>,
    },
}

impl fmt::Display for ApplicationError<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ApplicationError::ViewWithoutAnyInScope { input, view } => {
                // Use debug representation for more debuggable feedback.
                write!(f, "View has nothing in scope: {view:?} (input: {input})")
            }
        }
    }
}

impl Error for ApplicationError<'_> {}

/// Apply the list of [actions][Action] to a source, writing results to the given
/// destination.
///
/// The actions will be applied in the order given. The source is expected to be
/// UTF-8-encoded text, and will be read [line-by-line][BufRead::read_line]. Each
/// processed line will be written to the destination immediately.
///
/// # Example: Using a single action (German)
///
/// See also [`crate::actions::German`].
///
///
/// ```
/// use srgn::{apply, scoping::{ScopedViewBuildStep, regex::Regex}, actions::{Action, German}};
///
/// let actions: &[Box<dyn Action>] = &[Box::new(German::default())];
/// let scopers: &[Box<dyn ScopedViewBuildStep>] = &[Box::new(Regex::default())];
///
/// let mut input = "Gruess Gott!\n";
///
/// let result = apply(input, &scopers, &actions).unwrap();
/// assert_eq!(result, "Grüß Gott!\n");
/// ```
///
/// # Errors
///
/// Refer to [`ApplicationError`].
pub fn apply<'viewee>(
    input: &'viewee str,
    scopers: &[Box<dyn ScopedViewBuildStep>],
    actions: &[Box<dyn Action>],
) -> Result<String, ApplicationError<'viewee>> {
    let mut builder = ScopedViewBuilder::new(input);
    for scoper in scopers {
        builder = builder.explode(|s| scoper.scope(s));
    }

    let mut view = builder.build();

    if !view.has_any_in_scope() {
        return Err(ApplicationError::ViewWithoutAnyInScope { input, view });
    }

    for action in actions {
        debug!("Applying action {:?}", action);
        action.map(&mut view);
    }

    Ok(view.to_string())
}