Skip to main content

nu_command/conversions/into/
semver_range.rs

1use crate::semver::parse;
2use crate::semver::range::SemverRangeValue;
3use nu_engine::command_prelude::*;
4use nu_protocol::shell_error::generic::GenericError;
5
6#[derive(Clone)]
7pub struct IntoSemverRange;
8
9impl Command for IntoSemverRange {
10    fn name(&self) -> &str {
11        "into semver-range"
12    }
13
14    fn signature(&self) -> Signature {
15        Signature::build("into semver-range")
16            .input_output_types(vec![
17                (Type::String, Type::Custom("semver-range".into())),
18                (
19                    Type::Custom("semver-range".into()),
20                    Type::Custom("semver-range".into()),
21                ),
22            ])
23            .switch(
24                "loose",
25                "Allow common non-strict version prefixes such as v1.2.3, v.1.2.3, v:1.2.3, v-1.2.3, or v_1.2.3 inside the range",
26                Some('l'),
27            )
28            .category(Category::Conversions)
29    }
30
31    fn description(&self) -> &str {
32        "Convert a string to a semver range."
33    }
34
35    fn search_terms(&self) -> Vec<&str> {
36        vec!["version", "requirement", "semantic"]
37    }
38
39    fn run(
40        &self,
41        engine_state: &EngineState,
42        stack: &mut Stack,
43        call: &Call,
44        input: PipelineData,
45    ) -> Result<PipelineData, ShellError> {
46        let head = call.head;
47        let loose = call.has_flag(engine_state, stack, "loose")?;
48
49        input.map(
50            move |value| into_semver_range(&value, head, loose),
51            engine_state.signals(),
52        )
53    }
54
55    fn examples(&self) -> Vec<Example<'static>> {
56        vec![
57            Example {
58                description: "Convert a string to a semver range",
59                example: "'>=1.0.0' | into semver-range",
60                result: None,
61            },
62            Example {
63                description: "Convert a complex range",
64                example: "'^1.2.3' | into semver-range",
65                result: None,
66            },
67            Example {
68                description: "Parse a range with a leading v on the version",
69                example: "'>=v1.0.0' | into semver-range --loose",
70                result: None,
71            },
72        ]
73    }
74}
75
76fn into_semver_range(input: &Value, head: Span, loose: bool) -> Value {
77    match input {
78        Value::Custom { val, .. } if val.type_name() == "semver-range" => input.clone(),
79        Value::String { val, .. } => match parse::parse_version_req(val, loose) {
80            Ok(requirement) => Value::custom(Box::new(SemverRangeValue::new(requirement)), head),
81            Err(_) => {
82                let help = if loose {
83                    "expected format: >=1.0.0, ^1.2.3, ~1.2, etc.; version numbers may use a v/V, v., v:, v-, or v_ prefix"
84                } else {
85                    "expected format: >=1.0.0, ^1.2.3, ~1.2, etc.; use --loose for prefixes like v1.2.3"
86                };
87                Value::error(
88                    ShellError::Generic(
89                        GenericError::new(
90                            format!("Cannot convert \"{val}\" to a semver range"),
91                            "the given string is not a valid semver requirement",
92                            head,
93                        )
94                        .with_help(help),
95                    ),
96                    head,
97                )
98            }
99        },
100        _ => Value::error(
101            ShellError::Generic(GenericError::new(
102                format!("Cannot convert {} to semver range", input.get_type()),
103                "expected a string or semver-range value",
104                head,
105            )),
106            head,
107        ),
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn get_custom_value(value: &Value) -> &SemverRangeValue {
116        match value {
117            Value::Custom { val, .. } => val.as_any().downcast_ref::<SemverRangeValue>().unwrap(),
118            _ => panic!("Expected Custom value"),
119        }
120    }
121
122    #[test]
123    fn test_into_semver_range_from_string() {
124        let value = Value::string(">=1.0.0", Span::test_data());
125        let result = into_semver_range(&value, Span::test_data(), false);
126
127        assert!(matches!(result, Value::Custom { .. }));
128        let range_val = get_custom_value(&result);
129        assert_eq!(range_val.requirement.to_string(), ">=1.0.0");
130    }
131
132    #[test]
133    fn test_into_semver_range_caret() {
134        let value = Value::string("^1.2.3", Span::test_data());
135        let result = into_semver_range(&value, Span::test_data(), false);
136
137        let range_val = get_custom_value(&result);
138        assert_eq!(range_val.requirement.to_string(), "^1.2.3");
139    }
140
141    #[test]
142    fn test_into_semver_range_tilde() {
143        let value = Value::string("~1.2", Span::test_data());
144        let result = into_semver_range(&value, Span::test_data(), false);
145
146        let range_val = get_custom_value(&result);
147        assert_eq!(range_val.requirement.to_string(), "~1.2");
148    }
149
150    #[test]
151    fn test_into_semver_range_complex() {
152        let value = Value::string(">=1.0.0, <2.0.0", Span::test_data());
153        let result = into_semver_range(&value, Span::test_data(), false);
154
155        let range_val = get_custom_value(&result);
156        assert_eq!(range_val.requirement.to_string(), ">=1.0.0, <2.0.0");
157    }
158
159    #[test]
160    fn test_into_semver_range_wildcard() {
161        let value = Value::string("*", Span::test_data());
162        let result = into_semver_range(&value, Span::test_data(), false);
163
164        let range_val = get_custom_value(&result);
165        assert_eq!(range_val.requirement.to_string(), "*");
166    }
167
168    #[test]
169    fn test_into_semver_range_from_semver_range() {
170        let original = SemverRangeValue::new(semver::VersionReq::parse(">=1.0.0").unwrap());
171        let value = Value::custom(Box::new(original), Span::test_data());
172        let result = into_semver_range(&value, Span::test_data(), false);
173
174        // Should return the same value
175        let range_val = get_custom_value(&result);
176        assert_eq!(range_val.requirement.to_string(), ">=1.0.0");
177    }
178
179    #[test]
180    fn test_into_semver_range_invalid() {
181        let value = Value::string("not-a-range", Span::test_data());
182        let result = into_semver_range(&value, Span::test_data(), false);
183
184        assert!(matches!(result, Value::Error { .. }));
185    }
186
187    #[test]
188    fn test_into_semver_range_unsupported_type() {
189        let value = Value::int(42, Span::test_data());
190        let result = into_semver_range(&value, Span::test_data(), false);
191
192        assert!(matches!(result, Value::Error { .. }));
193    }
194
195    #[test]
196    fn test_into_semver_range_wrong_custom_value() {
197        // Create a semver value instead of semver-range
198        use crate::semver::value::SemverValue;
199        let semver = SemverValue::new(semver::Version::parse("1.2.3").unwrap());
200        let value = Value::custom(Box::new(semver), Span::test_data());
201        let result = into_semver_range(&value, Span::test_data(), false);
202
203        assert!(matches!(result, Value::Error { .. }));
204    }
205
206    #[test]
207    fn test_into_semver_range_loose() {
208        for (input, expected) in [
209            (">=v1.0.0", ">=1.0.0"),
210            ("^v1.2.3", "^1.2.3"),
211            ("~v1.2", "~1.2"),
212            // Bare versions become caret requirements in the cargo semver crate.
213            ("v1.2.3", "^1.2.3"),
214            (">=v:1.0.0, <v.2.0.0", ">=1.0.0, <2.0.0"),
215            (">=v-1.0.0, <v_2.0.0", ">=1.0.0, <2.0.0"),
216        ] {
217            let value = Value::string(input, Span::test_data());
218            let result = into_semver_range(&value, Span::test_data(), true);
219            let range_val = get_custom_value(&result);
220            assert_eq!(range_val.requirement.to_string(), expected, "input={input}");
221        }
222
223        let value = Value::string(">=v1.0.0", Span::test_data());
224        let result = into_semver_range(&value, Span::test_data(), false);
225        assert!(matches!(result, Value::Error { .. }));
226    }
227}
228
229#[cfg(test)]
230mod test {
231    use super::*;
232
233    #[test]
234    fn test_examples() -> nu_test_support::Result {
235        nu_test_support::test().examples(IntoSemverRange)
236    }
237}