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 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}