Skip to main content

rez_next_package/requirement/
types.rs

1//! Core requirement types and their methods.
2
3use rez_next_version::Version;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::str::FromStr;
7
8/// A package requirement specification
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct Requirement {
11    /// Package name
12    pub name: String,
13
14    /// Version constraint
15    pub version_constraint: Option<VersionConstraint>,
16
17    /// Whether this is a weak requirement (optional)
18    pub weak: bool,
19
20    /// Whether matching package versions must be excluded from the solve.
21    #[serde(default)]
22    pub conflict: bool,
23
24    /// Platform-specific conditions
25    pub platform_conditions: Vec<PlatformCondition>,
26
27    /// Environment variable conditions
28    pub env_conditions: Vec<EnvCondition>,
29
30    /// Conditional expressions (for complex logic)
31    pub conditional_expression: Option<String>,
32
33    /// Namespace (for scoped packages)
34    pub namespace: Option<String>,
35}
36
37/// Platform-specific condition
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub struct PlatformCondition {
40    /// Platform name (e.g., "windows", "linux", "darwin")
41    pub platform: String,
42    /// Architecture (e.g., "x86_64", "aarch64")
43    pub arch: Option<String>,
44    /// Whether this condition should be negated
45    pub negate: bool,
46}
47
48/// Environment variable condition
49#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
50pub struct EnvCondition {
51    /// Environment variable name
52    pub var_name: String,
53    /// Expected value (None means just check existence)
54    pub expected_value: Option<String>,
55    /// Whether this condition should be negated
56    pub negate: bool,
57}
58
59/// Version constraint types
60#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
61pub enum VersionConstraint {
62    /// Exact version match (==)
63    Exact(Version),
64    /// Greater than (>)
65    GreaterThan(Version),
66    /// Greater than or equal (>=)
67    GreaterThanOrEqual(Version),
68    /// Less than (<)
69    LessThan(Version),
70    /// Less than or equal (<=)
71    LessThanOrEqual(Version),
72    /// Compatible version (~=)
73    Compatible(Version),
74    /// Range constraint (>=min, <max)
75    Range(Version, Version),
76    /// Multiple constraints (AND logic)
77    Multiple(Vec<VersionConstraint>),
78    /// Alternative constraints (OR logic)
79    Alternative(Vec<VersionConstraint>),
80    /// Exclude specific versions
81    Exclude(Vec<Version>),
82    /// Wildcard pattern (e.g., "1.2.*")
83    Wildcard(String),
84    /// Prefix match: version starts with the given prefix tokens.
85    /// Used for rez "point release" syntax: pkg-3.11 matches 3.11, 3.11.0, 3.11.5, etc.
86    Prefix(Version),
87    /// Any version
88    Any,
89}
90
91impl Requirement {
92    /// Create a new requirement
93    pub fn new(name: String) -> Self {
94        Self {
95            name,
96            version_constraint: None,
97            weak: false,
98            conflict: false,
99            platform_conditions: Vec::new(),
100            env_conditions: Vec::new(),
101            conditional_expression: None,
102            namespace: None,
103        }
104    }
105
106    /// Create a requirement with version constraint
107    pub fn with_version(name: String, constraint: VersionConstraint) -> Self {
108        Self {
109            name,
110            version_constraint: Some(constraint),
111            weak: false,
112            conflict: false,
113            platform_conditions: Vec::new(),
114            env_conditions: Vec::new(),
115            conditional_expression: None,
116            namespace: None,
117        }
118    }
119
120    /// Create a weak requirement
121    pub fn weak(name: String) -> Self {
122        Self {
123            name,
124            version_constraint: None,
125            weak: true,
126            conflict: false,
127            platform_conditions: Vec::new(),
128            env_conditions: Vec::new(),
129            conditional_expression: None,
130            namespace: None,
131        }
132    }
133
134    /// Check if a version satisfies this requirement
135    pub fn is_satisfied_by(&self, version: &Version) -> bool {
136        match &self.version_constraint {
137            None => true,
138            Some(constraint) => constraint.is_satisfied_by(version),
139        }
140    }
141
142    /// Check if platform conditions are satisfied
143    pub fn is_platform_satisfied(&self, platform: &str, arch: Option<&str>) -> bool {
144        if self.platform_conditions.is_empty() {
145            return true;
146        }
147
148        for condition in &self.platform_conditions {
149            let platform_match = condition.platform == platform;
150            let arch_match = condition
151                .arch
152                .as_ref()
153                .is_none_or(|a| arch.is_some_and(|arch| arch == a));
154
155            let condition_satisfied = platform_match && arch_match;
156
157            if condition.negate {
158                if condition_satisfied {
159                    return false;
160                }
161            } else if condition_satisfied {
162                return true;
163            }
164        }
165
166        self.platform_conditions.iter().all(|c| c.negate)
167    }
168
169    /// Check if environment conditions are satisfied
170    pub fn is_env_satisfied(&self, env_vars: &HashMap<String, String>) -> bool {
171        if self.env_conditions.is_empty() {
172            return true;
173        }
174
175        for condition in &self.env_conditions {
176            let var_exists = env_vars.contains_key(&condition.var_name);
177            let value_match = if let Some(expected) = &condition.expected_value {
178                env_vars.get(&condition.var_name) == Some(expected)
179            } else {
180                var_exists
181            };
182
183            if condition.negate {
184                if value_match {
185                    return false;
186                }
187            } else if !value_match {
188                return false;
189            }
190        }
191
192        true
193    }
194
195    /// Get the package name
196    pub fn package_name(&self) -> &str {
197        &self.name
198    }
199
200    /// Get the full qualified name (including namespace if present)
201    pub fn qualified_name(&self) -> String {
202        if let Some(ref namespace) = self.namespace {
203            format!("{}::{}", namespace, self.name)
204        } else {
205            self.name.clone()
206        }
207    }
208
209    /// Add a platform condition
210    pub fn add_platform_condition(&mut self, platform: String, arch: Option<String>, negate: bool) {
211        self.platform_conditions.push(PlatformCondition {
212            platform,
213            arch,
214            negate,
215        });
216    }
217
218    /// Add an environment condition
219    pub fn add_env_condition(
220        &mut self,
221        var_name: String,
222        expected_value: Option<String>,
223        negate: bool,
224    ) {
225        self.env_conditions.push(EnvCondition {
226            var_name,
227            expected_value,
228            negate,
229        });
230    }
231}
232
233impl VersionConstraint {
234    /// Check if a version satisfies this constraint.
235    ///
236    /// Rez semantics: when comparing against a constraint version with fewer tokens,
237    /// the comparison is done at the depth of the constraint.
238    /// e.g., `>=3` on `3.11.0` → compare first token: `3 >= 3` ✓
239    ///        `<4` on `3.11.0`  → compare first token: `3 < 4` ✓
240    pub fn is_satisfied_by(&self, version: &Version) -> bool {
241        match self {
242            VersionConstraint::Exact(v) => {
243                Self::cmp_at_depth(version, v) == std::cmp::Ordering::Equal
244            }
245            VersionConstraint::GreaterThan(v) => {
246                Self::cmp_at_depth(version, v) == std::cmp::Ordering::Greater
247            }
248            VersionConstraint::GreaterThanOrEqual(v) => {
249                let ord = Self::cmp_at_depth(version, v);
250                ord == std::cmp::Ordering::Greater || ord == std::cmp::Ordering::Equal
251            }
252            VersionConstraint::LessThan(v) => {
253                Self::cmp_at_depth(version, v) == std::cmp::Ordering::Less
254            }
255            VersionConstraint::LessThanOrEqual(v) => {
256                let ord = Self::cmp_at_depth(version, v);
257                ord == std::cmp::Ordering::Less || ord == std::cmp::Ordering::Equal
258            }
259            VersionConstraint::Compatible(v) => {
260                // Compatible version (~=) uses a "locked prefix + floor" rule.
261                // Rule:
262                //   ~=X.Y   → prefix=["X"] (locked), minor >= Y
263                //   ~=X.Y.Z → prefix=["X","Y"] (locked), patch >= Z
264                let version_parts: Vec<&str> = version.as_str().split('.').collect();
265                let constraint_parts: Vec<&str> = v.as_str().split('.').collect();
266
267                if constraint_parts.is_empty() {
268                    return true;
269                }
270                if version_parts.len() < constraint_parts.len() {
271                    return false;
272                }
273
274                let last_idx = constraint_parts.len() - 1;
275                for i in 0..last_idx {
276                    if version_parts[i] != constraint_parts[i] {
277                        return false;
278                    }
279                }
280
281                let v_last = version_parts[last_idx];
282                let c_last = constraint_parts[last_idx];
283                if let (Ok(vn), Ok(cn)) = (v_last.parse::<u64>(), c_last.parse::<u64>()) {
284                    vn >= cn
285                } else {
286                    v_last >= c_last
287                }
288            }
289            VersionConstraint::Range(min, max) => version >= min && version < max,
290            VersionConstraint::Multiple(constraints) => {
291                constraints.iter().all(|c| c.is_satisfied_by(version))
292            }
293            VersionConstraint::Alternative(constraints) => {
294                constraints.iter().any(|c| c.is_satisfied_by(version))
295            }
296            VersionConstraint::Exclude(versions) => !versions.iter().any(|v| version == v),
297            VersionConstraint::Wildcard(pattern) => self.matches_wildcard(version, pattern),
298            VersionConstraint::Prefix(prefix) => version.has_prefix(prefix),
299            VersionConstraint::Any => true,
300        }
301    }
302
303    /// Check if version matches wildcard pattern
304    fn matches_wildcard(&self, version: &Version, pattern: &str) -> bool {
305        let version_str = version.as_str();
306        let pattern_parts: Vec<&str> = pattern.split('.').collect();
307        let version_parts: Vec<&str> = version_str.split('.').collect();
308
309        for (i, pattern_part) in pattern_parts.iter().enumerate() {
310            if *pattern_part == "*" {
311                return true;
312            }
313            if i >= version_parts.len() {
314                return false;
315            }
316            if *pattern_part != version_parts[i] {
317                return false;
318            }
319        }
320
321        pattern_parts.len() == version_parts.len()
322    }
323
324    /// Compare `version` against `constraint_ver` at the depth of `constraint_ver`.
325    ///
326    /// Rez semantics: constraints with fewer tokens are compared only at the token depth
327    /// of the constraint.
328    pub fn cmp_at_depth(version: &Version, constraint_ver: &Version) -> std::cmp::Ordering {
329        let ver_str = version.as_str();
330        let con_str = constraint_ver.as_str();
331
332        let ver_parts: Vec<&str> = ver_str.split('.').collect();
333        let con_parts: Vec<&str> = con_str.split('.').collect();
334
335        let depth = con_parts.len();
336
337        for (v_tok, c_tok) in ver_parts.iter().zip(con_parts.iter()).take(depth) {
338            let v_tok = *v_tok;
339            let c_tok = *c_tok;
340
341            if let (Ok(vn), Ok(cn)) = (v_tok.parse::<u64>(), c_tok.parse::<u64>()) {
342                match vn.cmp(&cn) {
343                    std::cmp::Ordering::Equal => continue,
344                    ord => return ord,
345                }
346            } else {
347                match v_tok.cmp(c_tok) {
348                    std::cmp::Ordering::Equal => continue,
349                    ord => return ord,
350                }
351            }
352        }
353
354        std::cmp::Ordering::Equal
355    }
356
357    /// Combine two constraints with AND logic
358    pub fn and(self, other: VersionConstraint) -> VersionConstraint {
359        match (self, other) {
360            (VersionConstraint::Multiple(mut constraints), other) => {
361                constraints.push(other);
362                VersionConstraint::Multiple(constraints)
363            }
364            (self_constraint, VersionConstraint::Multiple(mut constraints)) => {
365                constraints.insert(0, self_constraint);
366                VersionConstraint::Multiple(constraints)
367            }
368            (self_constraint, other) => VersionConstraint::Multiple(vec![self_constraint, other]),
369        }
370    }
371
372    /// Combine two constraints with OR logic
373    pub fn or(self, other: VersionConstraint) -> VersionConstraint {
374        match (self, other) {
375            (VersionConstraint::Alternative(mut constraints), other) => {
376                constraints.push(other);
377                VersionConstraint::Alternative(constraints)
378            }
379            (self_constraint, VersionConstraint::Alternative(mut constraints)) => {
380                constraints.insert(0, self_constraint);
381                VersionConstraint::Alternative(constraints)
382            }
383            (self_constraint, other) => {
384                VersionConstraint::Alternative(vec![self_constraint, other])
385            }
386        }
387    }
388}
389
390impl FromStr for Requirement {
391    type Err = String;
392
393    fn from_str(s: &str) -> Result<Self, Self::Err> {
394        super::parser::RequirementParser::new().parse(s)
395    }
396}