online-dsl-forge 0.3.0

A bounded parser and runtime for in-memory DSL expressions.
Documentation
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;

use regex::Regex;

use crate::parser::SourceSpan;
use crate::sema::{BodyAccess, CapabilityMeta};
use crate::value::Value;

use super::{DynamicRegistry, EvalError};

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum RuntimePatternSetKind {
  Contains,
  Regex,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RuntimePatternSetConfig {
  pub name: String,
  pub kind: RuntimePatternSetKind,
  pub patterns: Vec<String>,
}

impl RuntimePatternSetConfig {
  pub fn contains(
    name: impl Into<String>,
    patterns: impl IntoIterator<Item = impl Into<String>>,
  ) -> Self {
    Self {
      name: name.into(),
      kind: RuntimePatternSetKind::Contains,
      patterns: patterns.into_iter().map(Into::into).collect(),
    }
  }

  pub fn regex(
    name: impl Into<String>,
    patterns: impl IntoIterator<Item = impl Into<String>>,
  ) -> Self {
    Self {
      name: name.into(),
      kind: RuntimePatternSetKind::Regex,
      patterns: patterns.into_iter().map(Into::into).collect(),
    }
  }
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct RuntimePatternSetLimits {
  pub max_sets: usize,
  pub max_patterns_per_set: usize,
  pub max_pattern_bytes: usize,
}

impl Default for RuntimePatternSetLimits {
  fn default() -> Self {
    Self {
      max_sets: 256,
      max_patterns_per_set: 1024,
      max_pattern_bytes: 4096,
    }
  }
}

#[derive(Debug, Clone)]
pub struct RuntimePatternSets {
  sets: BTreeMap<String, CompiledRuntimePatternSet>,
}

impl RuntimePatternSets {
  pub fn compile(
    configs: impl IntoIterator<Item = RuntimePatternSetConfig>,
  ) -> Result<Self, RuntimePatternSetError> {
    Self::compile_with_limits(configs, RuntimePatternSetLimits::default())
  }

  pub fn compile_with_limits(
    configs: impl IntoIterator<Item = RuntimePatternSetConfig>,
    limits: RuntimePatternSetLimits,
  ) -> Result<Self, RuntimePatternSetError> {
    let mut sets = BTreeMap::new();
    for config in configs {
      if sets.len() >= limits.max_sets {
        return Err(RuntimePatternSetError::new(
          "runtime pattern set limit exceeded",
        ));
      }
      validate_config(&config, limits)?;
      if sets.contains_key(&config.name) {
        return Err(RuntimePatternSetError::new(format!(
          "duplicate runtime pattern set {}",
          config.name
        )));
      }
      let compiled = CompiledRuntimePatternSet::compile(&config)?;
      sets.insert(config.name, compiled);
    }
    Ok(Self { sets })
  }

  fn is_match(&self, name: &str, receiver: &Value, span: SourceSpan) -> Result<bool, EvalError> {
    let Some(set) = self.sets.get(name) else {
      return Err(EvalError::new(
        format!("unknown runtime pattern set {name}"),
        span,
      ));
    };
    match receiver {
      Value::String(value) => Ok(set.is_match(value)),
      Value::Array(values) => values.iter().try_fold(false, |matched, value| {
        let Value::String(value) = value else {
          return Err(EvalError::new(
            format!(
              "pattern-set methods require string array items, got {}",
              value.type_name()
            ),
            span,
          ));
        };
        Ok(matched || set.is_match(value))
      }),
      other => Err(EvalError::new(
        format!(
          "pattern-set methods require string or array receiver, got {}",
          other.type_name()
        ),
        span,
      )),
    }
  }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RuntimePatternSetError {
  message: String,
}

impl RuntimePatternSetError {
  fn new(message: impl Into<String>) -> Self {
    Self {
      message: message.into(),
    }
  }

  pub fn message(&self) -> &str {
    &self.message
  }
}

impl fmt::Display for RuntimePatternSetError {
  fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    formatter.write_str(&self.message)
  }
}

impl Error for RuntimePatternSetError {}

#[derive(Debug, Clone)]
enum CompiledRuntimePatternSet {
  Contains(Vec<String>),
  Regex(Vec<Regex>),
}

impl CompiledRuntimePatternSet {
  fn compile(config: &RuntimePatternSetConfig) -> Result<Self, RuntimePatternSetError> {
    match config.kind {
      RuntimePatternSetKind::Contains => Ok(Self::Contains(config.patterns.clone())),
      RuntimePatternSetKind::Regex => config
        .patterns
        .iter()
        .map(|pattern| {
          Regex::new(pattern).map_err(|error| {
            RuntimePatternSetError::new(format!(
              "runtime pattern set {} contains invalid regex pattern: {error}",
              config.name
            ))
          })
        })
        .collect::<Result<Vec<_>, _>>()
        .map(Self::Regex),
    }
  }

  fn is_match(&self, text: &str) -> bool {
    match self {
      Self::Contains(patterns) => patterns.iter().any(|pattern| text.contains(pattern)),
      Self::Regex(patterns) => patterns.iter().any(|pattern| pattern.is_match(text)),
    }
  }
}

pub fn register_oxirule_pattern_set_methods(
  registry: &mut DynamicRegistry,
  pattern_sets: RuntimePatternSets,
) -> &mut DynamicRegistry {
  let contains_sets = pattern_sets.clone();
  registry.register_method_capability_with_context(
    CapabilityMeta::method("containsAny", 1).with_body_access(BodyAccess::PrefixBytes),
    move |context, receiver, args| {
      evaluate_pattern_set_method(&contains_sets, context.span(), receiver, args)
    },
  );
  registry.register_method_capability_with_context(
    CapabilityMeta::method("matchesAny", 1).with_body_access(BodyAccess::PrefixBytes),
    move |context, receiver, args| {
      evaluate_pattern_set_method(&pattern_sets, context.span(), receiver, args)
    },
  );
  registry
}

pub fn oxirule_pattern_set_registry(pattern_sets: RuntimePatternSets) -> DynamicRegistry {
  let mut registry = DynamicRegistry::new();
  register_oxirule_pattern_set_methods(&mut registry, pattern_sets);
  registry
}

fn validate_config(
  config: &RuntimePatternSetConfig,
  limits: RuntimePatternSetLimits,
) -> Result<(), RuntimePatternSetError> {
  if config.name.trim().is_empty() {
    return Err(RuntimePatternSetError::new(
      "runtime pattern set name must not be empty",
    ));
  }
  if config.patterns.len() > limits.max_patterns_per_set {
    return Err(RuntimePatternSetError::new(format!(
      "runtime pattern set {} exceeds max_patterns_per_set",
      config.name
    )));
  }
  for pattern in &config.patterns {
    if pattern.len() > limits.max_pattern_bytes {
      return Err(RuntimePatternSetError::new(format!(
        "runtime pattern set {} contains an oversized pattern",
        config.name
      )));
    }
  }
  Ok(())
}

fn evaluate_pattern_set_method(
  pattern_sets: &RuntimePatternSets,
  span: SourceSpan,
  receiver: &Value,
  args: &[Value],
) -> Result<Value, EvalError> {
  let pattern_set = expect_pattern_set_name(args, span)?;
  pattern_sets
    .is_match(pattern_set, receiver, span)
    .map(Value::Bool)
}

fn expect_pattern_set_name(args: &[Value], span: SourceSpan) -> Result<&str, EvalError> {
  match &args[0] {
    Value::String(value) => Ok(value),
    other => Err(EvalError::new(
      format!(
        "pattern-set methods require string pattern-set name, got {}",
        other.type_name()
      ),
      span,
    )),
  }
}