use std::fmt;
use std::path::Path;
use std::sync::{PoisonError, RwLock};
use serde::Deserialize;
use serde_saphyr::Budget;
use serde_saphyr::budget::BudgetBreach;
use super::{Input, InputError};
pub const REPO_FILE: &str = ".ctl/input.yaml";
macro_rules! limits {
($( $(#[$doc:meta])* $field:ident: $ty:ty $(=> $wrap:ident)?, )*) => {
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Limits {
$( $(#[$doc])* pub $field: Option<$ty>, )*
}
impl Limits {
pub const NONE: Self = Self { $( $field: None, )* };
#[must_use]
pub fn or(self, lower: &Self) -> Self {
Self { $( $field: self.$field.or(lower.$field), )* }
}
#[must_use]
pub fn budget(&self) -> Budget {
let mut budget = Budget::default();
$( if let Some(value) = self.$field {
$( let value = $wrap(value); )?
budget.$field = value;
} )*
budget
}
fn loosened(&self) -> Vec<Loosened> {
let default = Budget::default();
let mut loosened = Vec::new();
$( if let Some(value) = self.$field {
let wrapped = value;
$( let wrapped = $wrap(wrapped); )?
if wrapped.looser_than(&default.$field) {
loosened.push(Loosened {
field: stringify!($field),
value: value.to_string(),
default: default.$field.shown(),
});
}
} )*
loosened
}
}
};
}
limits! {
max_reader_input_bytes: usize => Some,
max_buffered_comment_events: usize,
simple_key_max_lookahead: usize,
flow_nesting_limit: usize,
max_events: usize,
max_aliases: usize,
max_anchors: usize,
max_recorded_anchor_events: usize,
max_recorded_anchor_bytes: usize,
max_property_expansion_depth: usize,
max_total_property_interpolation_work: usize,
max_depth: usize,
max_inclusion_depth: u32,
max_documents: usize,
max_nodes: usize,
max_total_scalar_bytes: usize,
max_total_comment_bytes: usize,
max_merge_keys: usize,
alias_anchor_min_aliases: usize,
alias_anchor_ratio_multiplier: usize,
}
static PROCESS: RwLock<Limits> = RwLock::new(Limits::NONE);
static REPOSITORY: RwLock<Limits> = RwLock::new(Limits::NONE);
pub fn set_default_limits(limits: Limits) {
*PROCESS.write().unwrap_or_else(PoisonError::into_inner) = limits;
}
pub fn use_repo_limits(root: &Path) -> Result<Vec<Loosened>, InputError> {
let path = root.join(REPO_FILE);
let limits = match std::fs::read_to_string(&path) {
Ok(text) => {
let process = PROCESS
.read()
.unwrap_or_else(PoisonError::into_inner)
.budget();
Input::new(path.display().to_string(), text)
.parse_within::<RepoFile>(&process, Raise::InProcess)?
.limits
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Limits::NONE,
Err(error) => {
return Err(InputError::whole(
path.display().to_string(),
error.to_string(),
));
}
};
let loosened = limits.loosened();
*REPOSITORY.write().unwrap_or_else(PoisonError::into_inner) = limits;
Ok(loosened)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Loosened {
pub field: &'static str,
pub value: String,
pub default: String,
}
impl fmt::Display for Loosened {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{REPO_FILE} raises {} to {}, above the default {}",
self.field, self.value, self.default
)
}
}
trait Bound {
fn looser_than(&self, default: &Self) -> bool;
fn shown(&self) -> String;
}
impl Bound for usize {
fn looser_than(&self, default: &Self) -> bool {
self > default
}
fn shown(&self) -> String {
self.to_string()
}
}
impl Bound for u32 {
fn looser_than(&self, default: &Self) -> bool {
self > default
}
fn shown(&self) -> String {
self.to_string()
}
}
impl Bound for Option<usize> {
fn looser_than(&self, default: &Self) -> bool {
match (self, default) {
(None, Some(_)) => true,
(Some(value), Some(default)) => value > default,
(_, None) => false,
}
}
fn shown(&self) -> String {
self.map_or_else(|| "none".to_owned(), |value| value.to_string())
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RepoFile {
#[serde(default)]
limits: Limits,
}
pub(super) fn effective(call: &Limits) -> Limits {
let repository = REPOSITORY.read().unwrap_or_else(PoisonError::into_inner);
let process = PROCESS.read().unwrap_or_else(PoisonError::into_inner);
call.clone().or(&repository).or(&process)
}
#[derive(Clone, Copy)]
pub(super) enum Raise {
InRepository,
InProcess,
}
pub(super) struct Exceeded {
field: &'static str,
limit: String,
raise: Raise,
}
impl Exceeded {
pub(super) fn input_bytes(limit: usize, raise: Raise) -> Self {
Self {
field: "max_reader_input_bytes",
limit: limit.to_string(),
raise,
}
}
#[expect(
clippy::wildcard_enum_match_arm,
reason = "BudgetBreach is non_exhaustive: a variant added later keeps serde-saphyr's wording"
)]
pub(super) fn breach(breach: &BudgetBreach, budget: &Budget, raise: Raise) -> Option<Self> {
let (field, limit) = match breach {
BudgetBreach::Events { .. } => ("max_events", budget.max_events.to_string()),
BudgetBreach::Aliases { .. } => ("max_aliases", budget.max_aliases.to_string()),
BudgetBreach::Anchors { .. } => ("max_anchors", budget.max_anchors.to_string()),
BudgetBreach::Depth { .. } => ("max_depth", budget.max_depth.to_string()),
BudgetBreach::InclusionDepth { .. } => (
"max_inclusion_depth",
budget.max_inclusion_depth.to_string(),
),
BudgetBreach::Documents { .. } => ("max_documents", budget.max_documents.to_string()),
BudgetBreach::Nodes { .. } => ("max_nodes", budget.max_nodes.to_string()),
BudgetBreach::ScalarBytes { .. } => (
"max_total_scalar_bytes",
budget.max_total_scalar_bytes.to_string(),
),
BudgetBreach::CommentBytes { .. } => (
"max_total_comment_bytes",
budget.max_total_comment_bytes.to_string(),
),
BudgetBreach::MergeKeys { .. } => ("max_merge_keys", budget.max_merge_keys.to_string()),
BudgetBreach::AliasAnchorRatio { .. } => (
"alias_anchor_ratio_multiplier",
budget.alias_anchor_ratio_multiplier.to_string(),
),
BudgetBreach::InputBytes { .. } => (
"max_reader_input_bytes",
budget
.max_reader_input_bytes
.map_or_else(|| "none".to_owned(), |limit| limit.to_string()),
),
BudgetBreach::RecordedAnchorEvents { .. } => (
"max_recorded_anchor_events",
budget.max_recorded_anchor_events.to_string(),
),
BudgetBreach::RecordedAnchorBytes { .. } => (
"max_recorded_anchor_bytes",
budget.max_recorded_anchor_bytes.to_string(),
),
BudgetBreach::PropertyExpansionDepth { .. } => (
"max_property_expansion_depth",
budget.max_property_expansion_depth.to_string(),
),
BudgetBreach::PropertyInterpolationWork { .. } => (
"max_total_property_interpolation_work",
budget.max_total_property_interpolation_work.to_string(),
),
_ => return None,
};
Some(Self {
field,
limit,
raise,
})
}
}
impl fmt::Display for Exceeded {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "exceeds {} ({}); ", self.field, self.limit)?;
match self.raise {
Raise::InRepository => {
write!(formatter, "raise limits.{} in {REPO_FILE}", self.field)
}
Raise::InProcess => write!(
formatter,
"raise {} in the tool's process default, which reads {REPO_FILE}",
self.field
),
}
}
}