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