Skip to main content

nu_command/conversions/into/
record.rs

1use crate::semver::value::SemverValue;
2use chrono::{DateTime, Datelike, FixedOffset, Timelike};
3use nu_engine::command_prelude::*;
4use nu_protocol::{DurationMaxUnit, format_duration_as_timeperiod};
5
6#[derive(Clone)]
7pub struct IntoRecord;
8
9impl Command for IntoRecord {
10    fn name(&self) -> &str {
11        "into record"
12    }
13
14    fn signature(&self) -> Signature {
15        Signature::build("into record")
16            .input_output_types(vec![
17                (Type::Date, Type::record()),
18                (Type::Duration, Type::record()),
19                (Type::List(Box::new(Type::Any)), Type::record()),
20                (Type::record(), Type::record()),
21            ])
22            .category(Category::Conversions)
23    }
24
25    fn description(&self) -> &str {
26        "Convert value to a record."
27    }
28
29    fn search_terms(&self) -> Vec<&str> {
30        vec!["convert"]
31    }
32
33    fn run(
34        &self,
35        _engine_state: &EngineState,
36        _stack: &mut Stack,
37        call: &Call,
38        input: PipelineData,
39    ) -> Result<PipelineData, ShellError> {
40        into_record(call, input)
41    }
42
43    fn examples(&self) -> Vec<Example<'_>> {
44        vec![
45            Example {
46                description: "Convert from one row table to record.",
47                example: "[[value]; [false]] | into record",
48                result: Some(Value::test_record(record! {
49                    "value" => Value::test_bool(false),
50                })),
51            },
52            Example {
53                description: "Convert from list of records to record.",
54                example: "[{foo: bar} {baz: quux}] | into record",
55                result: Some(Value::test_record(record! {
56                    "foo" => Value::test_string("bar"),
57                    "baz" => Value::test_string("quux"),
58                })),
59            },
60            Example {
61                description: "Convert from list of pairs into record.",
62                example: "[[foo bar] [baz quux]] | into record",
63                result: Some(Value::test_record(record! {
64                    "foo" => Value::test_string("bar"),
65                    "baz" => Value::test_string("quux"),
66                })),
67            },
68            Example {
69                description: "convert duration to record (weeks max).",
70                example: "(-500day - 4hr - 5sec) | into record",
71                result: Some(Value::test_record(record! {
72                    "week" =>   Value::test_int(71),
73                    "day" =>    Value::test_int(3),
74                    "hour" =>   Value::test_int(4),
75                    "second" => Value::test_int(5),
76                    "sign" =>   Value::test_string("-"),
77                })),
78            },
79            Example {
80                description: "convert record to record.",
81                example: "{a: 1, b: 2} | into record",
82                result: Some(Value::test_record(record! {
83                    "a" =>  Value::test_int(1),
84                    "b" =>  Value::test_int(2),
85                })),
86            },
87            Example {
88                description: "convert date to record.",
89                example: "2020-04-12T22:10:57+02:00 | into record",
90                result: Some(Value::test_record(record! {
91                    "year" =>     Value::test_int(2020),
92                    "month" =>    Value::test_int(4),
93                    "day" =>      Value::test_int(12),
94                    "hour" =>     Value::test_int(22),
95                    "minute" =>   Value::test_int(10),
96                    "second" =>   Value::test_int(57),
97                    "millisecond" => Value::test_int(0),
98                    "microsecond" => Value::test_int(0),
99                    "nanosecond" => Value::test_int(0),
100                    "timezone" => Value::test_string("+02:00"),
101                })),
102            },
103            Example {
104                description: "convert date components to table columns.",
105                example: "2020-04-12T22:10:57+02:00 | into record | transpose | transpose -r",
106                result: None,
107            },
108        ]
109    }
110}
111
112fn into_record(call: &Call, input: PipelineData) -> Result<PipelineData, ShellError> {
113    let span = input.span().unwrap_or(call.head);
114    match input {
115        PipelineData::Value(Value::Date { val, .. }, _) => {
116            Ok(parse_date_into_record(val, span).into_pipeline_data())
117        }
118        PipelineData::Value(Value::Duration { val, .. }, _) => {
119            Ok(parse_duration_into_record(val, span).into_pipeline_data())
120        }
121        PipelineData::Value(Value::Custom { val, .. }, _) => {
122            if let Some(semver) = val.as_any().downcast_ref::<SemverValue>() {
123                Ok(parse_semver_into_record(semver, span).into_pipeline_data())
124            } else {
125                Err(ShellError::TypeMismatch {
126                    err_message: format!("Can't convert {} to record", val.type_name()),
127                    span,
128                })
129            }
130        }
131        PipelineData::Value(Value::List { .. }, _) | PipelineData::ListStream(..) => {
132            let mut input = input;
133            let mut record = Record::new();
134            let metadata = input.take_metadata();
135
136            enum ExpectedType {
137                Record,
138                Pair,
139            }
140            let mut expected_type = None;
141
142            for item in input.into_iter() {
143                let span = item.span();
144                match item {
145                    Value::Record { val, .. }
146                        if matches!(expected_type, None | Some(ExpectedType::Record)) =>
147                    {
148                        // Don't use .extend() unless that gets changed to check for duplicate keys
149                        for (key, val) in val.into_owned() {
150                            record.insert(key, val);
151                        }
152                        expected_type = Some(ExpectedType::Record);
153                    }
154                    Value::List { vals, .. }
155                        if matches!(expected_type, None | Some(ExpectedType::Pair)) =>
156                    {
157                        if vals.len() == 2 {
158                            let mut vals = vals.into_owned();
159                            let (val, key) = vals.pop().zip(vals.pop()).expect("length is < 2");
160                            record.insert(key.coerce_into_string()?, val);
161                        } else {
162                            return Err(ShellError::IncorrectValue {
163                                msg: format!(
164                                    "expected inner list with two elements, but found {} element(s)",
165                                    vals.len()
166                                ),
167                                val_span: span,
168                                call_span: call.head,
169                            });
170                        }
171                        expected_type = Some(ExpectedType::Pair);
172                    }
173                    Value::Nothing { .. } => {}
174                    Value::Error { error, .. } => return Err(*error),
175                    _ => {
176                        return Err(ShellError::TypeMismatch {
177                            err_message: format!(
178                                "expected {}, found {} (while building record from list)",
179                                match expected_type {
180                                    Some(ExpectedType::Record) => "record",
181                                    Some(ExpectedType::Pair) => "list with two elements",
182                                    None => "record or list with two elements",
183                                },
184                                item.get_type(),
185                            ),
186                            span,
187                        });
188                    }
189                }
190            }
191            Ok(Value::record(record, span).into_pipeline_data_with_metadata(metadata))
192        }
193        PipelineData::Value(Value::Record { .. }, _) => Ok(input),
194        PipelineData::Value(Value::Error { error, .. }, _) => Err(*error),
195        other => Err(ShellError::TypeMismatch {
196            err_message: format!("Can't convert {} to record", other.get_type()),
197            span,
198        }),
199    }
200}
201
202fn parse_date_into_record(date: DateTime<FixedOffset>, span: Span) -> Value {
203    Value::record(
204        record! {
205            "year" => Value::int(date.year() as i64, span),
206            "month" => Value::int(date.month() as i64, span),
207            "day" => Value::int(date.day() as i64, span),
208            "hour" => Value::int(date.hour() as i64, span),
209            "minute" => Value::int(date.minute() as i64, span),
210            "second" => Value::int(date.second() as i64, span),
211            "millisecond" => Value::int(date.timestamp_subsec_millis() as i64, span),
212            "microsecond" => Value::int((date.nanosecond() / 1_000 % 1_000) as i64, span),
213            "nanosecond" => Value::int((date.nanosecond() % 1_000) as i64, span),
214            "timezone" => Value::string(date.offset().to_string(), span),
215        },
216        span,
217    )
218}
219
220fn parse_duration_into_record(duration: i64, span: Span) -> Value {
221    let (sign, periods) = format_duration_as_timeperiod(duration, DurationMaxUnit::default());
222
223    let mut record = Record::new();
224    for p in periods {
225        let num_with_unit = p.to_text().to_string();
226        let split = num_with_unit.split(' ').collect::<Vec<&str>>();
227        record.push(
228            match split[1] {
229                "ns" => "nanosecond",
230                "µs" => "microsecond",
231                "ms" => "millisecond",
232                "sec" => "second",
233                "min" => "minute",
234                "hr" => "hour",
235                "day" => "day",
236                "wk" => "week",
237                _ => "unknown",
238            },
239            Value::int(split[0].parse().unwrap_or(0), span),
240        );
241    }
242
243    record.push(
244        "sign",
245        Value::string(if sign == -1 { "-" } else { "+" }, span),
246    );
247
248    Value::record(record, span)
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::semver::value::SemverValue;
255
256    fn create_semver_value(version: &str) -> Value {
257        let semver = SemverValue::new(semver::Version::parse(version).unwrap());
258        Value::custom(Box::new(semver), Span::test_data())
259    }
260
261    #[test]
262    fn test_parse_semver_into_record_basic() {
263        let semver_val = SemverValue::new(semver::Version::parse("1.2.3").unwrap());
264        let result = parse_semver_into_record(&semver_val, Span::test_data());
265
266        match result {
267            Value::Record { val, .. } => {
268                assert_eq!(val.get("major").unwrap().as_int().unwrap(), 1);
269                assert_eq!(val.get("minor").unwrap().as_int().unwrap(), 2);
270                assert_eq!(val.get("patch").unwrap().as_int().unwrap(), 3);
271                assert_eq!(val.get("pre").unwrap().as_str().unwrap(), "");
272                assert_eq!(val.get("build").unwrap().as_str().unwrap(), "");
273
274                let pre_identifiers = val.get("pre_identifiers").unwrap().as_list().unwrap();
275                assert_eq!(pre_identifiers.len(), 0);
276
277                let build_identifiers = val.get("build_identifiers").unwrap().as_list().unwrap();
278                assert_eq!(build_identifiers.len(), 0);
279            }
280            _ => panic!("Expected Record value"),
281        }
282    }
283
284    #[test]
285    fn test_parse_semver_into_record_with_prerelease() {
286        let semver_val = SemverValue::new(semver::Version::parse("1.2.3-alpha.1").unwrap());
287        let result = parse_semver_into_record(&semver_val, Span::test_data());
288
289        match result {
290            Value::Record { val, .. } => {
291                assert_eq!(val.get("pre").unwrap().as_str().unwrap(), "alpha.1");
292
293                let pre_identifiers = val.get("pre_identifiers").unwrap().as_list().unwrap();
294                assert_eq!(pre_identifiers.len(), 2);
295                assert_eq!(pre_identifiers[0].as_str().unwrap(), "alpha");
296                assert_eq!(pre_identifiers[1].as_int().unwrap(), 1);
297            }
298            _ => panic!("Expected Record value"),
299        }
300    }
301
302    #[test]
303    fn test_parse_semver_into_record_with_build() {
304        let semver_val = SemverValue::new(semver::Version::parse("1.2.3+build.2").unwrap());
305        let result = parse_semver_into_record(&semver_val, Span::test_data());
306
307        match result {
308            Value::Record { val, .. } => {
309                assert_eq!(val.get("build").unwrap().as_str().unwrap(), "build.2");
310
311                let build_identifiers = val.get("build_identifiers").unwrap().as_list().unwrap();
312                assert_eq!(build_identifiers.len(), 2);
313                assert_eq!(build_identifiers[0].as_str().unwrap(), "build");
314                assert_eq!(build_identifiers[1].as_int().unwrap(), 2);
315            }
316            _ => panic!("Expected Record value"),
317        }
318    }
319
320    #[test]
321    fn test_parse_semver_into_record_with_both() {
322        let semver_val = SemverValue::new(semver::Version::parse("1.2.3-alpha.1+build.2").unwrap());
323        let result = parse_semver_into_record(&semver_val, Span::test_data());
324
325        match result {
326            Value::Record { val, .. } => {
327                assert_eq!(val.get("major").unwrap().as_int().unwrap(), 1);
328                assert_eq!(val.get("minor").unwrap().as_int().unwrap(), 2);
329                assert_eq!(val.get("patch").unwrap().as_int().unwrap(), 3);
330                assert_eq!(val.get("pre").unwrap().as_str().unwrap(), "alpha.1");
331                assert_eq!(val.get("build").unwrap().as_str().unwrap(), "build.2");
332
333                let pre_identifiers = val.get("pre_identifiers").unwrap().as_list().unwrap();
334                assert_eq!(pre_identifiers.len(), 2);
335
336                let build_identifiers = val.get("build_identifiers").unwrap().as_list().unwrap();
337                assert_eq!(build_identifiers.len(), 2);
338            }
339            _ => panic!("Expected Record value"),
340        }
341    }
342
343    #[test]
344    fn test_into_record_with_semver() {
345        let semver_val = create_semver_value("1.2.3");
346        let semver_ref = match &semver_val {
347            Value::Custom { val, .. } => val.as_any().downcast_ref::<SemverValue>().unwrap(),
348            _ => panic!("Expected Custom value"),
349        };
350        let result = parse_semver_into_record(semver_ref, Span::test_data());
351
352        match result {
353            Value::Record { val, .. } => {
354                assert_eq!(val.get("major").unwrap().as_int().unwrap(), 1);
355                assert_eq!(val.get("minor").unwrap().as_int().unwrap(), 2);
356                assert_eq!(val.get("patch").unwrap().as_int().unwrap(), 3);
357                assert_eq!(val.get("prefix").unwrap().as_str().unwrap(), "");
358            }
359            _ => panic!("Expected Record value"),
360        }
361    }
362
363    #[test]
364    fn test_parse_semver_into_record_with_prefix() {
365        let semver_val = SemverValue::parse("v1.2.3", true).unwrap();
366        let result = parse_semver_into_record(&semver_val, Span::test_data());
367
368        match result {
369            Value::Record { val, .. } => {
370                assert_eq!(val.get("major").unwrap().as_int().unwrap(), 1);
371                assert_eq!(val.get("minor").unwrap().as_int().unwrap(), 2);
372                assert_eq!(val.get("patch").unwrap().as_int().unwrap(), 3);
373                assert_eq!(val.get("prefix").unwrap().as_str().unwrap(), "v");
374            }
375            _ => panic!("Expected Record value"),
376        }
377    }
378}
379
380fn parse_semver_into_record(semver: &SemverValue, span: Span) -> Value {
381    let version = &semver.version;
382
383    let pre_identifiers: Vec<Value> = if version.pre.is_empty() {
384        Vec::new()
385    } else {
386        version
387            .pre
388            .split('.')
389            .map(|id| {
390                if let Ok(num) = id.parse::<i64>() {
391                    Value::int(num, span)
392                } else {
393                    Value::string(id.to_string(), span)
394                }
395            })
396            .collect()
397    };
398
399    let build_identifiers: Vec<Value> = if version.build.is_empty() {
400        Vec::new()
401    } else {
402        version
403            .build
404            .split('.')
405            .map(|id| {
406                if let Ok(num) = id.parse::<i64>() {
407                    Value::int(num, span)
408                } else {
409                    Value::string(id.to_string(), span)
410                }
411            })
412            .collect()
413    };
414
415    Value::record(
416        record! {
417            "major" => Value::int(version.major as i64, span),
418            "minor" => Value::int(version.minor as i64, span),
419            "patch" => Value::int(version.patch as i64, span),
420            "pre" => Value::string(version.pre.to_string(), span),
421            "build" => Value::string(version.build.to_string(), span),
422            "prefix" => Value::string(semver.prefix.clone(), span),
423            "pre_identifiers" => Value::list(pre_identifiers, span),
424            "build_identifiers" => Value::list(build_identifiers, span),
425        },
426        span,
427    )
428}
429
430#[cfg(test)]
431mod test {
432    use super::*;
433
434    #[test]
435    fn test_examples() -> nu_test_support::Result {
436        nu_test_support::test().examples(IntoRecord)
437    }
438}