use serde::Deserialize;
use std::{fs, path::Path};
mod default_respond;
mod guard;
pub mod prefix;
pub mod rule;
use crate::{
error::{RoutingError, RoutingResult},
parsed_request::ParsedRequest,
strategy::Strategy,
util::http::normalize_url_path,
};
use default_respond::DefaultRespond;
use guard::Guard;
use prefix::Prefix;
use rule::{Rule, respond::Respond};
#[derive(Clone, Deserialize, Debug)]
pub struct RuleSet {
pub prefix: Option<Prefix>,
pub default: Option<DefaultRespond>,
pub guard: Option<Guard>,
pub rules: Vec<Rule>,
#[serde(skip)]
pub file_path: String,
}
impl RuleSet {
pub fn new(
rule_set_file_path: &str,
current_dir_to_config_dir_relative_path: &str,
rule_set_idx: usize,
) -> RoutingResult<Self> {
let path = Path::new(rule_set_file_path);
let toml_string = fs::read_to_string(rule_set_file_path).map_err(|e| {
RoutingError::RuleSetRead {
path: path.to_path_buf(),
source: e,
}
})?;
let mut ret: Self = toml::from_str(&toml_string).map_err(|e| RoutingError::RuleSetParse {
path: path.to_path_buf(),
canonical: path.canonicalize().ok(),
source: e,
})?;
let mut prefix = ret.prefix.clone().unwrap_or_default();
prefix.url_path_prefix = prefix
.url_path_prefix
.as_deref()
.map(|p| normalize_url_path(p, None));
let respond_dir_prefix = prefix.respond_dir_prefix.as_deref().unwrap_or(".");
let respond_dir_prefix =
Path::new(current_dir_to_config_dir_relative_path).join(respond_dir_prefix);
let respond_dir_prefix = respond_dir_prefix.to_str().ok_or_else(|| {
RoutingError::RuleSetRead {
path: path.to_path_buf(),
source: std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"respond_dir path contains non-UTF-8 bytes: {}",
respond_dir_prefix.to_string_lossy()
),
),
}
})?;
prefix.respond_dir_prefix = Some(respond_dir_prefix.to_owned());
ret.prefix = Some(prefix);
ret.rules = ret
.rules
.iter()
.enumerate()
.map(|(rule_idx, rule)| rule.compute_derived_fields(&ret, rule_idx, rule_set_idx))
.collect();
ret.file_path = rule_set_file_path.to_owned();
Ok(ret)
}
pub fn find_matched(
&self,
parsed_request: &ParsedRequest,
strategy: Option<&Strategy>,
rule_set_idx: usize,
) -> Option<Respond> {
let _ = match self.prefix.as_ref() {
Some(prefix) if prefix.url_path_prefix.is_some() => {
if !parsed_request
.url_path
.starts_with(prefix.url_path_prefix.as_ref().unwrap())
{
return None;
}
}
_ => (),
};
let strategy = strategy.unwrap_or(&Strategy::FirstMatch);
match strategy {
Strategy::FirstMatch => {
for (rule_idx, rule) in self.rules.iter().enumerate() {
if rule.when.is_match(parsed_request, rule_idx, rule_set_idx) {
return Some(rule.respond.clone());
}
}
None
}
Strategy::UniformRandom { seed } => {
let matches: Vec<&Rule> = self
.rules
.iter()
.enumerate()
.filter(|(idx, r)| r.when.is_match(parsed_request, *idx, rule_set_idx))
.map(|(_, r)| r)
.collect();
if matches.is_empty() {
return None;
}
let mut rng = crate::strategy::make_rng(*seed);
let idx = rng.next_index(matches.len());
Some(matches[idx].respond.clone())
}
Strategy::WeightedRandom { seed } => {
let candidates: Vec<(&Rule, u32)> = self
.rules
.iter()
.enumerate()
.filter(|(idx, r)| r.when.is_match(parsed_request, *idx, rule_set_idx))
.map(|(_, r)| (r, r.weight.unwrap_or(1)))
.filter(|(_, w)| *w > 0)
.collect();
if candidates.is_empty() {
return None;
}
let total: u32 = candidates.iter().map(|(_, w)| w).sum();
let mut rng = crate::strategy::make_rng(*seed);
let pick = (rng.next() % total as u64) as u32;
let mut acc = 0u32;
for (rule, weight) in &candidates {
acc += weight;
if pick < acc {
return Some(rule.respond.clone());
}
}
candidates.last().map(|(r, _)| r.respond.clone())
}
Strategy::Priority { tiebreaker } => {
let matches: Vec<(&Rule, i32)> = self
.rules
.iter()
.enumerate()
.filter(|(idx, r)| r.when.is_match(parsed_request, *idx, rule_set_idx))
.map(|(_, r)| (r, r.priority.unwrap_or(0)))
.collect();
if matches.is_empty() {
return None;
}
let max_priority = matches.iter().map(|(_, p)| *p).max().unwrap();
let top: Vec<&Rule> = matches
.into_iter()
.filter(|(_, p)| *p == max_priority)
.map(|(r, _)| r)
.collect();
match tiebreaker {
crate::strategy::PriorityTiebreaker::FirstMatch => {
top.into_iter().next().map(|r| r.respond.clone())
}
crate::strategy::PriorityTiebreaker::UniformRandom => {
let mut rng = crate::strategy::make_rng(None);
let idx = rng.next_index(top.len());
Some(top[idx].respond.clone())
}
}
}
}
}
pub fn validate(&self) -> bool {
true
}
pub fn dir_prefix(&self) -> String {
if let Some(dir_prefix) = self.prefix.clone().unwrap_or_default().respond_dir_prefix {
dir_prefix
} else {
String::new()
}
}
}
impl std::fmt::Display for RuleSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(x) = self.prefix.as_ref() {
let _ = write!(f, "{}", x);
}
if let Some(x) = self.guard.as_ref() {
let _ = write!(f, "{}", x);
}
if let Some(x) = self.default.as_ref() {
let _ = write!(f, "{}", x);
}
for rule in self.rules.iter() {
let _ = write!(f, "{}", rule);
}
Ok(())
}
}