use std::any::Any;
use std::path::Path;
use crate::ecosystem::{
BlockedRegistryOccurrence, Dependency, ParseResult, RejectedRegistryOccurrence,
};
use crate::selection::{InvalidStabilityOccurrence, SelectionContext};
pub const MAX_DEPENDENCIES_PER_DOCUMENT: usize = 5000;
#[derive(Debug)]
pub struct DependencyBudget {
kept: usize,
total: usize,
cap: usize,
}
impl DependencyBudget {
#[must_use]
pub const fn new(cap: usize) -> Self {
Self {
kept: 0,
total: 0,
cap,
}
}
pub const fn allow(&mut self) -> bool {
self.total += 1;
if self.kept < self.cap {
self.kept += 1;
true
} else {
false
}
}
#[must_use]
pub const fn truncation(&self) -> Option<(usize, usize)> {
if self.total > self.cap {
Some((self.kept, self.total))
} else {
None
}
}
}
#[must_use]
pub fn cap_dependencies(inner: Box<dyn ParseResult>, cap: usize) -> Box<dyn ParseResult> {
let total = inner.dependencies().len();
if total <= cap {
return inner;
}
Box::new(DependencyCappedParseResult { inner, cap, total })
}
struct DependencyCappedParseResult {
inner: Box<dyn ParseResult>,
cap: usize,
total: usize,
}
impl ParseResult for DependencyCappedParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
let mut deps = self.inner.dependencies();
deps.truncate(self.cap);
deps
}
fn workspace_root(&self) -> Option<&Path> {
self.inner.workspace_root()
}
fn uri(&self) -> &url::Url {
self.inner.uri()
}
fn blocked_registries(&self) -> Vec<BlockedRegistryOccurrence> {
self.inner.blocked_registries()
}
fn rejected_registries(&self) -> Vec<RejectedRegistryOccurrence> {
self.inner.rejected_registries()
}
fn selection_context(&self) -> SelectionContext {
self.inner.selection_context()
}
fn invalid_minimum_stability(&self) -> Option<InvalidStabilityOccurrence> {
self.inner.invalid_minimum_stability()
}
fn as_any(&self) -> &dyn Any {
self.inner.as_any()
}
fn dependency_truncation(&self) -> Option<(usize, usize)> {
Some((self.cap, self.total))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::stub_parse_result_with_dependencies;
#[test]
fn cap_dependencies_truncates_when_over_the_ceiling() {
let inner = stub_parse_result_with_dependencies(12);
let capped = cap_dependencies(inner, 10);
assert_eq!(capped.dependencies().len(), 10);
assert_eq!(capped.dependency_truncation(), Some((10, 12)));
}
#[test]
fn cap_dependencies_leaves_a_document_at_the_ceiling_untouched() {
let inner = stub_parse_result_with_dependencies(10);
let capped = cap_dependencies(inner, 10);
assert_eq!(capped.dependencies().len(), 10);
assert_eq!(
capped.dependency_truncation(),
None,
"a document exactly at the ceiling must not be reported as truncated"
);
}
#[test]
fn cap_dependencies_leaves_a_document_under_the_ceiling_untouched() {
let inner = stub_parse_result_with_dependencies(3);
let capped = cap_dependencies(inner, 10);
assert_eq!(capped.dependencies().len(), 3);
assert_eq!(capped.dependency_truncation(), None);
}
#[test]
fn capped_dependencies_keep_their_original_identity() {
let inner = stub_parse_result_with_dependencies(5);
let capped = cap_dependencies(inner, 2);
let names: Vec<_> = capped
.dependencies()
.iter()
.map(|d| d.name().as_str().to_string())
.collect();
assert_eq!(names, vec!["dep-0", "dep-1"]);
}
#[test]
fn cap_dependencies_forwards_blocked_and_rejected_registries() {
use crate::net_policy::{HostClass, RegistryRejectionReason};
use crate::position::{Position, Range};
struct StubDependency {
name: crate::PackageName,
}
impl Dependency for StubDependency {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn name_range(&self) -> Range {
Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<Range> {
None
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct StubWithOccurrences {
deps: Vec<StubDependency>,
uri: url::Url,
blocked: Vec<BlockedRegistryOccurrence>,
rejected: Vec<RejectedRegistryOccurrence>,
selection_context: SelectionContext,
invalid_minimum_stability: Option<InvalidStabilityOccurrence>,
}
impl ParseResult for StubWithOccurrences {
fn dependencies(&self) -> Vec<&dyn Dependency> {
self.deps.iter().map(|d| d as &dyn Dependency).collect()
}
fn workspace_root(&self) -> Option<&Path> {
None
}
fn uri(&self) -> &url::Url {
&self.uri
}
fn blocked_registries(&self) -> Vec<BlockedRegistryOccurrence> {
self.blocked.clone()
}
fn rejected_registries(&self) -> Vec<RejectedRegistryOccurrence> {
self.rejected.clone()
}
fn selection_context(&self) -> SelectionContext {
self.selection_context
}
fn invalid_minimum_stability(&self) -> Option<InvalidStabilityOccurrence> {
self.invalid_minimum_stability.clone()
}
fn as_any(&self) -> &dyn Any {
self
}
}
let range = Range::new(Position::new(0, 0), Position::new(0, 4));
let inner: Box<dyn ParseResult> = Box::new(StubWithOccurrences {
deps: (0..12)
.map(|i| StubDependency {
name: crate::PackageName::new(format!("dep-{i}")),
})
.collect(),
uri: crate::test_util::test_uri("/project/manifest.toml"),
blocked: vec![BlockedRegistryOccurrence {
range,
class: HostClass::CloudMetadata,
raw_value: "https://169.254.169.254".to_string(),
declaration_key: "top-level".to_string(),
}],
rejected: vec![RejectedRegistryOccurrence {
range,
reason: RegistryRejectionReason::InvalidUrl,
raw_value: "not-a-valid-url".to_string(),
declaration_key: "top-level".to_string(),
}],
selection_context: SelectionContext::with_minimum_stability(
crate::selection::StabilityFloor::Beta,
),
invalid_minimum_stability: Some(InvalidStabilityOccurrence {
range,
raw: "betta".to_string(),
}),
});
let capped = cap_dependencies(inner, 10);
assert_eq!(
capped.blocked_registries().len(),
1,
"blocked_registries() must survive the cap wrapper"
);
assert_eq!(
capped.rejected_registries().len(),
1,
"rejected_registries() must survive the cap wrapper, not silently fall back to \
the trait's empty-Vec default"
);
assert_eq!(
capped.selection_context(),
SelectionContext::with_minimum_stability(crate::selection::StabilityFloor::Beta),
"selection_context() must survive the cap wrapper — a capped Composer manifest \
must not silently lose minimum-stability enforcement"
);
assert!(
capped.invalid_minimum_stability().is_some(),
"invalid_minimum_stability() must survive the cap wrapper too"
);
}
#[test]
fn budget_allows_up_to_the_cap_and_refuses_beyond_it() {
let mut budget = DependencyBudget::new(3);
assert!(budget.allow());
assert!(budget.allow());
assert!(budget.allow());
assert!(!budget.allow());
assert!(!budget.allow());
assert_eq!(budget.truncation(), Some((3, 5)));
}
#[test]
fn budget_at_exactly_the_cap_reports_no_truncation() {
let mut budget = DependencyBudget::new(3);
assert!(budget.allow());
assert!(budget.allow());
assert!(budget.allow());
assert_eq!(budget.truncation(), None);
}
#[test]
fn budget_under_the_cap_reports_no_truncation() {
let mut budget = DependencyBudget::new(10);
assert!(budget.allow());
assert_eq!(budget.truncation(), None);
}
#[test]
fn budget_of_zero_refuses_every_entry() {
let mut budget = DependencyBudget::new(0);
assert!(!budget.allow());
assert!(!budget.allow());
assert_eq!(budget.truncation(), Some((0, 2)));
}
}