online_dsl_forge/runtime/
pattern_sets.rs1use 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}