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