Skip to main content

online_dsl_forge/runtime/
pattern_sets.rs

1use std::collections::BTreeMap;
2use std::error::Error;
3use std::fmt;
4
5use regex::Regex;
6
7use crate::parser::SourceSpan;
8use crate::sema::{BodyAccess, CapabilityMeta};
9use crate::value::Value;
10
11use super::{DynamicRegistry, EvalError};
12
13#[derive(Debug, Clone, Copy, Eq, PartialEq)]
14pub enum RuntimePatternSetKind {
15  Contains,
16  Regex,
17}
18
19#[derive(Debug, Clone, Eq, PartialEq)]
20pub struct RuntimePatternSetConfig {
21  pub name: String,
22  pub kind: RuntimePatternSetKind,
23  pub patterns: Vec<String>,
24}
25
26impl RuntimePatternSetConfig {
27  pub fn contains(
28    name: impl Into<String>,
29    patterns: impl IntoIterator<Item = impl Into<String>>,
30  ) -> Self {
31    Self {
32      name: name.into(),
33      kind: RuntimePatternSetKind::Contains,
34      patterns: patterns.into_iter().map(Into::into).collect(),
35    }
36  }
37
38  pub fn regex(
39    name: impl Into<String>,
40    patterns: impl IntoIterator<Item = impl Into<String>>,
41  ) -> Self {
42    Self {
43      name: name.into(),
44      kind: RuntimePatternSetKind::Regex,
45      patterns: patterns.into_iter().map(Into::into).collect(),
46    }
47  }
48}
49
50#[derive(Debug, Clone, Copy, Eq, PartialEq)]
51pub struct RuntimePatternSetLimits {
52  pub max_sets: usize,
53  pub max_patterns_per_set: usize,
54  pub max_pattern_bytes: usize,
55}
56
57impl Default for RuntimePatternSetLimits {
58  fn default() -> Self {
59    Self {
60      max_sets: 256,
61      max_patterns_per_set: 1024,
62      max_pattern_bytes: 4096,
63    }
64  }
65}
66
67#[derive(Debug, Clone)]
68pub struct RuntimePatternSets {
69  sets: BTreeMap<String, CompiledRuntimePatternSet>,
70}
71
72impl RuntimePatternSets {
73  pub fn compile(
74    configs: impl IntoIterator<Item = RuntimePatternSetConfig>,
75  ) -> Result<Self, RuntimePatternSetError> {
76    Self::compile_with_limits(configs, RuntimePatternSetLimits::default())
77  }
78
79  pub fn compile_with_limits(
80    configs: impl IntoIterator<Item = RuntimePatternSetConfig>,
81    limits: RuntimePatternSetLimits,
82  ) -> Result<Self, RuntimePatternSetError> {
83    let mut sets = BTreeMap::new();
84    for config in configs {
85      if sets.len() >= limits.max_sets {
86        return Err(RuntimePatternSetError::new(
87          "runtime pattern set limit exceeded",
88        ));
89      }
90      validate_config(&config, limits)?;
91      if sets.contains_key(&config.name) {
92        return Err(RuntimePatternSetError::new(format!(
93          "duplicate runtime pattern set {}",
94          config.name
95        )));
96      }
97      let compiled = CompiledRuntimePatternSet::compile(&config)?;
98      sets.insert(config.name, compiled);
99    }
100    Ok(Self { sets })
101  }
102
103  fn is_match(&self, name: &str, receiver: &Value, span: SourceSpan) -> Result<bool, EvalError> {
104    let Some(set) = self.sets.get(name) else {
105      return Err(EvalError::new(
106        format!("unknown runtime pattern set {name}"),
107        span,
108      ));
109    };
110    match receiver {
111      Value::String(value) => Ok(set.is_match(value)),
112      Value::Array(values) => values.iter().try_fold(false, |matched, value| {
113        let Value::String(value) = value else {
114          return Err(EvalError::new(
115            format!(
116              "pattern-set methods require string array items, got {}",
117              value.type_name()
118            ),
119            span,
120          ));
121        };
122        Ok(matched || set.is_match(value))
123      }),
124      other => Err(EvalError::new(
125        format!(
126          "pattern-set methods require string or array receiver, got {}",
127          other.type_name()
128        ),
129        span,
130      )),
131    }
132  }
133}
134
135#[derive(Debug, Clone, Eq, PartialEq)]
136pub struct RuntimePatternSetError {
137  message: String,
138}
139
140impl RuntimePatternSetError {
141  fn new(message: impl Into<String>) -> Self {
142    Self {
143      message: message.into(),
144    }
145  }
146
147  pub fn message(&self) -> &str {
148    &self.message
149  }
150}
151
152impl fmt::Display for RuntimePatternSetError {
153  fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154    formatter.write_str(&self.message)
155  }
156}
157
158impl Error for RuntimePatternSetError {}
159
160#[derive(Debug, Clone)]
161enum CompiledRuntimePatternSet {
162  Contains(Vec<String>),
163  Regex(Vec<Regex>),
164}
165
166impl CompiledRuntimePatternSet {
167  fn compile(config: &RuntimePatternSetConfig) -> Result<Self, RuntimePatternSetError> {
168    match config.kind {
169      RuntimePatternSetKind::Contains => Ok(Self::Contains(config.patterns.clone())),
170      RuntimePatternSetKind::Regex => config
171        .patterns
172        .iter()
173        .map(|pattern| {
174          Regex::new(pattern).map_err(|error| {
175            RuntimePatternSetError::new(format!(
176              "runtime pattern set {} contains invalid regex pattern: {error}",
177              config.name
178            ))
179          })
180        })
181        .collect::<Result<Vec<_>, _>>()
182        .map(Self::Regex),
183    }
184  }
185
186  fn is_match(&self, text: &str) -> bool {
187    match self {
188      Self::Contains(patterns) => patterns.iter().any(|pattern| text.contains(pattern)),
189      Self::Regex(patterns) => patterns.iter().any(|pattern| pattern.is_match(text)),
190    }
191  }
192}
193
194pub fn register_oxirule_pattern_set_methods(
195  registry: &mut DynamicRegistry,
196  pattern_sets: RuntimePatternSets,
197) -> &mut DynamicRegistry {
198  let contains_sets = pattern_sets.clone();
199  registry.register_method_capability_with_context(
200    CapabilityMeta::method("containsAny", 1).with_body_access(BodyAccess::PrefixBytes),
201    move |context, receiver, args| {
202      evaluate_pattern_set_method(&contains_sets, context.span(), receiver, args)
203    },
204  );
205  registry.register_method_capability_with_context(
206    CapabilityMeta::method("matchesAny", 1).with_body_access(BodyAccess::PrefixBytes),
207    move |context, receiver, args| {
208      evaluate_pattern_set_method(&pattern_sets, context.span(), receiver, args)
209    },
210  );
211  registry
212}
213
214pub fn oxirule_pattern_set_registry(pattern_sets: RuntimePatternSets) -> DynamicRegistry {
215  let mut registry = DynamicRegistry::new();
216  register_oxirule_pattern_set_methods(&mut registry, pattern_sets);
217  registry
218}
219
220fn validate_config(
221  config: &RuntimePatternSetConfig,
222  limits: RuntimePatternSetLimits,
223) -> Result<(), RuntimePatternSetError> {
224  if config.name.trim().is_empty() {
225    return Err(RuntimePatternSetError::new(
226      "runtime pattern set name must not be empty",
227    ));
228  }
229  if config.patterns.len() > limits.max_patterns_per_set {
230    return Err(RuntimePatternSetError::new(format!(
231      "runtime pattern set {} exceeds max_patterns_per_set",
232      config.name
233    )));
234  }
235  for pattern in &config.patterns {
236    if pattern.len() > limits.max_pattern_bytes {
237      return Err(RuntimePatternSetError::new(format!(
238        "runtime pattern set {} contains an oversized pattern",
239        config.name
240      )));
241    }
242  }
243  Ok(())
244}
245
246fn evaluate_pattern_set_method(
247  pattern_sets: &RuntimePatternSets,
248  span: SourceSpan,
249  receiver: &Value,
250  args: &[Value],
251) -> Result<Value, EvalError> {
252  let pattern_set = expect_pattern_set_name(args, span)?;
253  pattern_sets
254    .is_match(pattern_set, receiver, span)
255    .map(Value::Bool)
256}
257
258fn expect_pattern_set_name(args: &[Value], span: SourceSpan) -> Result<&str, EvalError> {
259  match &args[0] {
260    Value::String(value) => Ok(value),
261    other => Err(EvalError::new(
262      format!(
263        "pattern-set methods require string pattern-set name, got {}",
264        other.type_name()
265      ),
266      span,
267    )),
268  }
269}