Skip to main content

sim_lib_namespace/
specifier.rs

1//! Bounded, authority-free policy for source-module specifiers.
2
3use std::fmt;
4
5use super::ModuleIdentity;
6
7/// Maximum number of textual candidates admitted to one policy decision.
8pub const MAX_SPECIFIER_CANDIDATES: usize = 16;
9
10/// Maximum UTF-8 byte length of one candidate specifier.
11pub const MAX_SPECIFIER_BYTES: usize = 4_096;
12
13/// Data-only input to a [`ModuleSpecifierPolicy`].
14///
15/// Deliberately absent are a source root, runtime context, and authority. A
16/// policy may select or normalize text, but cannot probe storage or acquire a
17/// capability through this interface.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct SpecifierPolicyRequest {
20    importer: Option<ModuleIdentity>,
21    candidates: Vec<String>,
22}
23
24impl SpecifierPolicyRequest {
25    /// Constructs a bounded policy request, or an exact refusal when its shape
26    /// exceeds the seam's fixed limits.
27    pub fn new(
28        importer: Option<ModuleIdentity>,
29        candidates: Vec<String>,
30    ) -> std::result::Result<Self, SpecifierRefusal> {
31        if candidates.is_empty() {
32            return Err(SpecifierRefusal::new(
33                SpecifierRefusalCode::NoCandidates,
34                "module specifier policy requires at least one candidate",
35            ));
36        }
37        if candidates.len() > MAX_SPECIFIER_CANDIDATES {
38            return Err(SpecifierRefusal::new(
39                SpecifierRefusalCode::TooManyCandidates,
40                format!(
41                    "module specifier policy candidate count {} exceeds {}",
42                    candidates.len(),
43                    MAX_SPECIFIER_CANDIDATES
44                ),
45            ));
46        }
47        if let Some(candidate) = candidates
48            .iter()
49            .find(|candidate| candidate.len() > MAX_SPECIFIER_BYTES)
50        {
51            return Err(SpecifierRefusal::new(
52                SpecifierRefusalCode::CandidateTooLong,
53                format!(
54                    "module specifier policy candidate length {} exceeds {} bytes",
55                    candidate.len(),
56                    MAX_SPECIFIER_BYTES
57                ),
58            ));
59        }
60        Ok(Self {
61            importer,
62            candidates,
63        })
64    }
65
66    /// Importer identity available for language-specific textual normalization.
67    pub fn importer(&self) -> Option<&ModuleIdentity> {
68        self.importer.as_ref()
69    }
70
71    /// Bounded candidate texts, in caller-declared order.
72    pub fn candidates(&self) -> &[String] {
73        &self.candidates
74    }
75}
76
77/// One normalized request selected by a specifier policy.
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct NormalizedSpecifier(String);
80
81impl NormalizedSpecifier {
82    /// Creates a normalized textual request.
83    pub fn new(text: String) -> std::result::Result<Self, SpecifierRefusal> {
84        if text.len() > MAX_SPECIFIER_BYTES {
85            return Err(SpecifierRefusal::new(
86                SpecifierRefusalCode::CandidateTooLong,
87                format!(
88                    "normalized module specifier length {} exceeds {} bytes",
89                    text.len(),
90                    MAX_SPECIFIER_BYTES
91                ),
92            ));
93        }
94        Ok(Self(text))
95    }
96
97    /// Normalized specifier text.
98    pub fn as_str(&self) -> &str {
99        &self.0
100    }
101}
102
103/// Stable category for an exact policy refusal.
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum SpecifierRefusalCode {
106    /// No candidate was supplied.
107    NoCandidates,
108    /// More candidates were supplied than the seam admits.
109    TooManyCandidates,
110    /// A candidate or normalized result exceeded the text bound.
111    CandidateTooLong,
112    /// The installed identity policy was asked to choose among candidates.
113    IdentityRequiresOneCandidate,
114    /// A custom policy refused the candidate set.
115    PolicyRefused,
116}
117
118/// Exact, inspectable refusal returned instead of a normalized request.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct SpecifierRefusal {
121    code: SpecifierRefusalCode,
122    detail: String,
123}
124
125impl SpecifierRefusal {
126    /// Creates an exact refusal with a stable category and detail.
127    pub fn new(code: SpecifierRefusalCode, detail: impl Into<String>) -> Self {
128        Self {
129            code,
130            detail: detail.into(),
131        }
132    }
133
134    /// Stable refusal category.
135    pub fn code(&self) -> SpecifierRefusalCode {
136        self.code
137    }
138
139    /// Stable refusal detail.
140    pub fn detail(&self) -> &str {
141        &self.detail
142    }
143}
144
145impl fmt::Display for SpecifierRefusal {
146    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147        formatter.write_str(&self.detail)
148    }
149}
150
151/// Chooses exactly one bounded normalized request, or refuses exactly.
152pub trait ModuleSpecifierPolicy: Send + Sync {
153    /// Resolve bounded text without access to roots, capabilities, or a runtime context.
154    fn resolve(
155        &self,
156        request: &SpecifierPolicyRequest,
157    ) -> std::result::Result<NormalizedSpecifier, SpecifierRefusal>;
158}
159
160/// Current behavior: preserve the sole supplied specifier byte-for-byte.
161#[derive(Clone, Copy, Debug, Default)]
162pub struct IdentitySpecifierPolicy;
163
164impl ModuleSpecifierPolicy for IdentitySpecifierPolicy {
165    fn resolve(
166        &self,
167        request: &SpecifierPolicyRequest,
168    ) -> std::result::Result<NormalizedSpecifier, SpecifierRefusal> {
169        if request.candidates.len() != 1 {
170            return Err(SpecifierRefusal::new(
171                SpecifierRefusalCode::IdentityRequiresOneCandidate,
172                format!(
173                    "identity module specifier policy requires exactly one candidate, got {}",
174                    request.candidates.len()
175                ),
176            ));
177        }
178        NormalizedSpecifier::new(request.candidates[0].clone())
179    }
180}