Skip to main content

nu_command/conversions/into/
semver.rs

1use crate::semver::value::SemverValue;
2use nu_engine::command_prelude::*;
3use nu_protocol::shell_error::generic::GenericError;
4
5#[derive(Clone)]
6pub struct IntoSemver;
7
8impl Command for IntoSemver {
9    fn name(&self) -> &str {
10        "into semver"
11    }
12
13    fn signature(&self) -> Signature {
14        Signature::build("into semver")
15            .input_output_types(vec![
16                (Type::String, Type::Custom("semver".into())),
17                (Type::Custom("semver".into()), Type::Custom("semver".into())),
18                (Type::record(), Type::Custom("semver".into())),
19            ])
20            .category(Category::Conversions)
21    }
22
23    fn description(&self) -> &str {
24        "Convert a value to a semantic version."
25    }
26
27    fn search_terms(&self) -> Vec<&str> {
28        vec!["version", "convert", "semantic"]
29    }
30
31    fn run(
32        &self,
33        engine_state: &EngineState,
34        _stack: &mut Stack,
35        call: &Call,
36        input: PipelineData,
37    ) -> Result<PipelineData, ShellError> {
38        let head = call.head;
39
40        input.map(
41            move |value| into_semver(&value, head),
42            engine_state.signals(),
43        )
44    }
45
46    fn examples(&self) -> Vec<Example<'static>> {
47        vec![
48            Example {
49                description: "Convert a string to a semver value",
50                example: "'1.2.3' | into semver",
51                result: None,
52            },
53            Example {
54                description: "Convert a string with prerelease",
55                example: "'1.2.3-alpha.1+build.2' | into semver",
56                result: None,
57            },
58            Example {
59                description: "Convert a record to a semver value",
60                example: "{major: 1, minor: 2, patch: 3} | into semver",
61                result: None,
62            },
63        ]
64    }
65}
66
67fn into_semver(input: &Value, head: Span) -> Value {
68    match input {
69        Value::Custom { val, .. } if val.type_name() == "semver" => input.clone(),
70        Value::String { val, .. } => match semver::Version::parse(val) {
71            Ok(version) => Value::custom(Box::new(SemverValue::new(version)), head),
72            Err(_) => Value::error(
73                ShellError::Generic(
74                    GenericError::new(
75                        format!("Cannot convert \"{val}\" to a semver"),
76                        "the given string is not a valid semver version",
77                        head,
78                    )
79                    .with_help("expected format: major.minor.patch (e.g. 1.2.3)"),
80                ),
81                head,
82            ),
83        },
84        Value::Record { val, .. } => parse_record_to_semver(val, head),
85        _ => Value::error(
86            ShellError::Generic(GenericError::new(
87                format!("Cannot convert {} to semver", input.get_type()),
88                "expected a string, record, or semver value",
89                head,
90            )),
91            head,
92        ),
93    }
94}
95
96fn parse_record_to_semver(record: &nu_protocol::Record, head: Span) -> Value {
97    let major = record.get("major").and_then(|v| v.as_int().ok());
98    let minor = record.get("minor").and_then(|v| v.as_int().ok());
99    let patch = record.get("patch").and_then(|v| v.as_int().ok());
100
101    let major = match major {
102        Some(v) if v >= 0 => v as u64,
103        _ => {
104            return Value::error(
105                ShellError::Generic(
106                    GenericError::new(
107                        "Cannot convert record to semver",
108                        "missing or invalid 'major' field",
109                        head,
110                    )
111                    .with_help("expected a non-negative integer"),
112                ),
113                head,
114            );
115        }
116    };
117
118    let minor = match minor {
119        Some(v) if v >= 0 => v as u64,
120        _ => {
121            return Value::error(
122                ShellError::Generic(
123                    GenericError::new(
124                        "Cannot convert record to semver",
125                        "missing or invalid 'minor' field",
126                        head,
127                    )
128                    .with_help("expected a non-negative integer"),
129                ),
130                head,
131            );
132        }
133    };
134
135    let patch = match patch {
136        Some(v) if v >= 0 => v as u64,
137        _ => {
138            return Value::error(
139                ShellError::Generic(
140                    GenericError::new(
141                        "Cannot convert record to semver",
142                        "missing or invalid 'patch' field",
143                        head,
144                    )
145                    .with_help("expected a non-negative integer"),
146                ),
147                head,
148            );
149        }
150    };
151
152    let pre = record
153        .get("pre")
154        .and_then(|v| v.as_str().ok())
155        .unwrap_or("");
156
157    let build = record
158        .get("build")
159        .and_then(|v| v.as_str().ok())
160        .unwrap_or("");
161
162    let pre = match semver::Prerelease::new(pre) {
163        Ok(p) => p,
164        Err(e) => {
165            return Value::error(
166                ShellError::Generic(GenericError::new(
167                    "Cannot convert record to semver",
168                    format!("invalid prerelease: {e}"),
169                    head,
170                )),
171                head,
172            );
173        }
174    };
175
176    let build = match semver::BuildMetadata::new(build) {
177        Ok(b) => b,
178        Err(e) => {
179            return Value::error(
180                ShellError::Generic(GenericError::new(
181                    "Cannot convert record to semver",
182                    format!("invalid build metadata: {e}"),
183                    head,
184                )),
185                head,
186            );
187        }
188    };
189
190    let version = semver::Version {
191        major,
192        minor,
193        patch,
194        pre,
195        build,
196    };
197
198    Value::custom(Box::new(SemverValue::new(version)), head)
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use nu_protocol::record;
205
206    fn get_custom_value(value: &Value) -> &SemverValue {
207        match value {
208            Value::Custom { val, .. } => val.as_any().downcast_ref::<SemverValue>().unwrap(),
209            _ => panic!("Expected Custom value"),
210        }
211    }
212
213    #[test]
214    fn test_into_semver_from_string() {
215        let value = Value::string("1.2.3", Span::test_data());
216        let result = into_semver(&value, Span::test_data());
217
218        assert!(matches!(result, Value::Custom { .. }));
219        let semver_val = get_custom_value(&result);
220        assert_eq!(semver_val.version.to_string(), "1.2.3");
221    }
222
223    #[test]
224    fn test_into_semver_from_string_with_prerelease() {
225        let value = Value::string("1.2.3-alpha.1+build.2", Span::test_data());
226        let result = into_semver(&value, Span::test_data());
227
228        let semver_val = get_custom_value(&result);
229        assert_eq!(semver_val.version.to_string(), "1.2.3-alpha.1+build.2");
230    }
231
232    #[test]
233    fn test_into_semver_from_invalid_string() {
234        let value = Value::string("not-a-version", Span::test_data());
235        let result = into_semver(&value, Span::test_data());
236
237        assert!(matches!(result, Value::Error { .. }));
238    }
239
240    #[test]
241    fn test_into_semver_from_semver() {
242        let original = SemverValue::new(semver::Version::parse("1.2.3").unwrap());
243        let value = Value::custom(Box::new(original), Span::test_data());
244        let result = into_semver(&value, Span::test_data());
245
246        // Should return the same value
247        let semver_val = get_custom_value(&result);
248        assert_eq!(semver_val.version.to_string(), "1.2.3");
249    }
250
251    #[test]
252    fn test_into_semver_from_record_basic() {
253        let record = record! {
254            "major" => Value::int(1, Span::test_data()),
255            "minor" => Value::int(2, Span::test_data()),
256            "patch" => Value::int(3, Span::test_data()),
257        };
258        let value = Value::record(record, Span::test_data());
259        let result = into_semver(&value, Span::test_data());
260
261        let semver_val = get_custom_value(&result);
262        assert_eq!(semver_val.version.to_string(), "1.2.3");
263    }
264
265    #[test]
266    fn test_into_semver_from_record_with_prerelease() {
267        let record = record! {
268            "major" => Value::int(1, Span::test_data()),
269            "minor" => Value::int(2, Span::test_data()),
270            "patch" => Value::int(3, Span::test_data()),
271            "pre" => Value::string("alpha.1", Span::test_data()),
272        };
273        let value = Value::record(record, Span::test_data());
274        let result = into_semver(&value, Span::test_data());
275
276        let semver_val = get_custom_value(&result);
277        assert_eq!(semver_val.version.to_string(), "1.2.3-alpha.1");
278    }
279
280    #[test]
281    fn test_into_semver_from_record_with_build() {
282        let record = record! {
283            "major" => Value::int(1, Span::test_data()),
284            "minor" => Value::int(2, Span::test_data()),
285            "patch" => Value::int(3, Span::test_data()),
286            "build" => Value::string("build.2", Span::test_data()),
287        };
288        let value = Value::record(record, Span::test_data());
289        let result = into_semver(&value, Span::test_data());
290
291        let semver_val = get_custom_value(&result);
292        assert_eq!(semver_val.version.to_string(), "1.2.3+build.2");
293    }
294
295    #[test]
296    fn test_into_semver_from_record_with_both() {
297        let record = record! {
298            "major" => Value::int(1, Span::test_data()),
299            "minor" => Value::int(2, Span::test_data()),
300            "patch" => Value::int(3, Span::test_data()),
301            "pre" => Value::string("alpha", Span::test_data()),
302            "build" => Value::string("build", Span::test_data()),
303        };
304        let value = Value::record(record, Span::test_data());
305        let result = into_semver(&value, Span::test_data());
306
307        let semver_val = get_custom_value(&result);
308        assert_eq!(semver_val.version.to_string(), "1.2.3-alpha+build");
309    }
310
311    #[test]
312    fn test_into_semver_from_record_missing_major() {
313        let record = record! {
314            "minor" => Value::int(2, Span::test_data()),
315            "patch" => Value::int(3, Span::test_data()),
316        };
317        let value = Value::record(record, Span::test_data());
318        let result = into_semver(&value, Span::test_data());
319
320        assert!(matches!(result, Value::Error { .. }));
321    }
322
323    #[test]
324    fn test_into_semver_from_record_negative_major() {
325        let record = record! {
326            "major" => Value::int(-1, Span::test_data()),
327            "minor" => Value::int(2, Span::test_data()),
328            "patch" => Value::int(3, Span::test_data()),
329        };
330        let value = Value::record(record, Span::test_data());
331        let result = into_semver(&value, Span::test_data());
332
333        assert!(matches!(result, Value::Error { .. }));
334    }
335
336    #[test]
337    fn test_into_semver_from_unsupported_type() {
338        let value = Value::int(42, Span::test_data());
339        let result = into_semver(&value, Span::test_data());
340
341        assert!(matches!(result, Value::Error { .. }));
342    }
343}
344
345#[cfg(test)]
346mod test {
347    use super::*;
348
349    #[test]
350    fn test_examples() -> nu_test_support::Result {
351        nu_test_support::test().examples(IntoSemver)
352    }
353}