use regex::Regex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct OverrideRule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default = "default_enabled")]
pub enabled: bool,
pub targets: Vec<String>,
pub patch: Vec<PatchOp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<String>,
#[serde(default = "default_mode")]
pub mode: OverrideMode,
#[serde(default)]
pub post_templating: bool,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum OverrideMode {
#[serde(rename = "replace")]
Replace,
#[serde(rename = "merge")]
Merge,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "op")]
pub enum PatchOp {
#[serde(rename = "add")]
Add {
path: String,
value: Value,
},
#[serde(rename = "replace")]
Replace {
path: String,
value: Value,
},
#[serde(rename = "remove")]
Remove {
path: String,
},
}
impl PatchOp {
pub fn path(&self) -> &str {
match self {
PatchOp::Add { path, .. }
| PatchOp::Replace { path, .. }
| PatchOp::Remove { path } => path,
}
}
}
#[derive(Debug, Clone)]
pub enum Target {
Operation(String),
Tag(String),
OperationRegex(Regex),
PathRegex(Regex),
Any,
}
impl Target {
fn parse(raw: &str) -> Result<Self, String> {
let regex = |pattern: &str| Regex::new(pattern).map_err(|e| e.to_string());
if raw == "*" {
Ok(Target::Any)
} else if let Some(id) = raw.strip_prefix("operation:") {
Ok(Target::Operation(id.to_string()))
} else if let Some(tag) = raw.strip_prefix("tag:") {
Ok(Target::Tag(tag.to_string()))
} else if let Some(pattern) = raw.strip_prefix("regex:") {
regex(pattern).map(Target::OperationRegex)
} else if let Some(pattern) = raw.strip_prefix("path:") {
regex(pattern).map(Target::PathRegex)
} else {
Err("expected `operation:`, `tag:`, `regex:`, `path:`, or `*`".to_string())
}
}
pub fn matches(&self, operation_id: &str, tags: &[String], path: &str) -> bool {
match self {
Target::Operation(id) => id == operation_id,
Target::Tag(tag) => tags.iter().any(|t| t == tag),
Target::OperationRegex(re) => re.is_match(operation_id),
Target::PathRegex(re) => re.is_match(path),
Target::Any => true,
}
}
}
#[derive(Debug, thiserror::Error, PartialEq)]
pub enum OverridesError {
#[error("rule {rule}: at least one target is required")]
NoTargets {
rule: usize,
},
#[error("rule {rule}: invalid target `{target}`: {reason}")]
InvalidTarget {
rule: usize,
target: String,
reason: String,
},
#[error("rule {rule}: patch path `{path}` is not a JSON pointer (use `/field/sub`, or `` for the whole body)")]
InvalidPointer {
rule: usize,
path: String,
},
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(try_from = "OverridesWire", into = "OverridesWire")]
pub struct Overrides {
rules: Vec<OverrideRule>,
targets: Vec<Vec<Target>>,
}
#[derive(Serialize, Deserialize)]
struct OverridesWire {
rules: Vec<OverrideRule>,
}
impl TryFrom<OverridesWire> for Overrides {
type Error = OverridesError;
fn try_from(wire: OverridesWire) -> Result<Self, Self::Error> {
Overrides::new(wire.rules)
}
}
impl From<Overrides> for OverridesWire {
fn from(overrides: Overrides) -> Self {
OverridesWire {
rules: overrides.rules,
}
}
}
impl Overrides {
pub fn new(rules: Vec<OverrideRule>) -> Result<Self, OverridesError> {
let mut targets = Vec::with_capacity(rules.len());
for (index, rule) in rules.iter().enumerate() {
if rule.targets.is_empty() {
return Err(OverridesError::NoTargets { rule: index });
}
let parsed = rule
.targets
.iter()
.map(|raw| {
Target::parse(raw).map_err(|reason| OverridesError::InvalidTarget {
rule: index,
target: raw.clone(),
reason,
})
})
.collect::<Result<Vec<_>, _>>()?;
if let Some(op) =
rule.patch.iter().find(|op| op.path().parse::<jsonptr::PointerBuf>().is_err())
{
return Err(OverridesError::InvalidPointer {
rule: index,
path: op.path().to_string(),
});
}
targets.push(parsed);
}
Ok(Self { rules, targets })
}
pub fn rules(&self) -> &[OverrideRule] {
&self.rules
}
pub(crate) fn compiled(&self) -> impl Iterator<Item = (&OverrideRule, &[Target])> {
self.rules.iter().zip(self.targets.iter().map(Vec::as_slice))
}
}
fn default_enabled() -> bool {
true
}
fn default_mode() -> OverrideMode {
OverrideMode::Replace
}