1pub mod shape;
2
3use bigdecimal::BigDecimal;
4use chrono::{DateTime, FixedOffset, Utc};
5use derive_new::new;
6use nu_errors::ShellError;
7use nu_protocol::{
8 hir, Primitive, ShellTypeName, SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value,
9};
10use nu_source::{Span, Tag};
11use nu_value_ext::ValueExt;
12use num_bigint::BigInt;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new, Serialize)]
16pub struct Operation {
17 pub(crate) left: Value,
18 pub(crate) operator: hir::Operator,
19 pub(crate) right: Value,
20}
21
22#[derive(Serialize, Deserialize)]
23pub enum Switch {
24 Present,
25 Absent,
26}
27
28impl std::convert::TryFrom<Option<&Value>> for Switch {
29 type Error = ShellError;
30
31 fn try_from(value: Option<&Value>) -> Result<Switch, ShellError> {
32 match value {
33 None => Ok(Switch::Absent),
34 Some(value) => match &value.value {
35 UntaggedValue::Primitive(Primitive::Boolean(true)) => Ok(Switch::Present),
36 _ => Err(ShellError::type_error("Boolean", value.spanned_type_name())),
37 },
38 }
39 }
40}
41
42pub fn select_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
43 let mut out = TaggedDictBuilder::new(tag);
44
45 let descs = obj.data_descriptors();
46
47 for column_name in fields {
48 match descs.iter().find(|&d| d == column_name) {
49 None => out.insert_untagged(column_name, UntaggedValue::nothing()),
50 Some(desc) => out.insert_value(desc.clone(), obj.get_data(desc).borrow().clone()),
51 }
52 }
53
54 out.into_value()
55}
56
57pub fn reject_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
58 let mut out = TaggedDictBuilder::new(tag);
59
60 let descs = obj.data_descriptors();
61
62 for desc in descs {
63 if fields.iter().any(|field| *field == desc) {
64 continue;
65 } else {
66 out.insert_value(desc.clone(), obj.get_data(&desc).borrow().clone())
67 }
68 }
69
70 out.into_value()
71}
72
73pub enum CompareValues {
74 Ints(i64, i64),
75 Filesizes(u64, u64),
76 BigInts(BigInt, BigInt),
77 Decimals(BigDecimal, BigDecimal),
78 String(String, String),
79 Date(DateTime<FixedOffset>, DateTime<FixedOffset>),
80 DateDuration(DateTime<FixedOffset>, BigInt),
81 TimeDuration(BigInt, BigInt),
82 Booleans(bool, bool),
83}
84
85impl CompareValues {
86 pub fn compare(&self) -> std::cmp::Ordering {
87 match self {
88 CompareValues::BigInts(left, right) => left.cmp(right),
89 CompareValues::Ints(left, right) => left.cmp(right),
90 CompareValues::Filesizes(left, right) => left.cmp(right),
91 CompareValues::Decimals(left, right) => left.cmp(right),
92 CompareValues::String(left, right) => left.cmp(right),
93 CompareValues::Date(left, right) => left.cmp(right),
94 CompareValues::DateDuration(left, right) => {
95 let duration = Primitive::into_chrono_duration(
97 Primitive::Duration(right.clone()),
98 Span::unknown(),
99 )
100 .expect("Could not convert nushell Duration into chrono Duration.");
101 let right: DateTime<FixedOffset> = Utc::now()
102 .checked_sub_signed(duration)
103 .expect("Data overflow")
104 .into();
105 right.cmp(left)
106 }
107 CompareValues::Booleans(left, right) => left.cmp(right),
108 CompareValues::TimeDuration(left, right) => left.cmp(right),
109 }
110 }
111}
112
113pub fn coerce_compare(
114 left: &UntaggedValue,
115 right: &UntaggedValue,
116) -> Result<CompareValues, (&'static str, &'static str)> {
117 match (left, right) {
118 (UntaggedValue::Primitive(left), UntaggedValue::Primitive(right)) => {
119 coerce_compare_primitive(left, right)
120 }
121
122 _ => Err((left.type_name(), right.type_name())),
123 }
124}
125
126pub fn coerce_compare_primitive(
127 left: &Primitive,
128 right: &Primitive,
129) -> Result<CompareValues, (&'static str, &'static str)> {
130 use Primitive::*;
131
132 Ok(match (left, right) {
133 (Int(left), Int(right)) => CompareValues::Ints(*left, *right),
134 (Int(left), BigInt(right)) => {
135 CompareValues::BigInts(num_bigint::BigInt::from(*left), right.clone())
136 }
137 (BigInt(left), Int(right)) => {
138 CompareValues::BigInts(left.clone(), num_bigint::BigInt::from(*right))
139 }
140 (BigInt(left), BigInt(right)) => CompareValues::BigInts(left.clone(), right.clone()),
141
142 (Int(left), Decimal(right)) => {
143 CompareValues::Decimals(BigDecimal::from(*left), right.clone())
144 }
145 (BigInt(left), Decimal(right)) => {
146 CompareValues::Decimals(BigDecimal::from(left.clone()), right.clone())
147 }
148 (Decimal(left), Decimal(right)) => CompareValues::Decimals(left.clone(), right.clone()),
149 (Decimal(left), Int(right)) => {
150 CompareValues::Decimals(left.clone(), BigDecimal::from(*right))
151 }
152 (Decimal(left), BigInt(right)) => {
153 CompareValues::Decimals(left.clone(), BigDecimal::from(right.clone()))
154 }
155 (Decimal(left), Filesize(right)) => {
156 CompareValues::Decimals(left.clone(), BigDecimal::from(*right))
157 }
158 (Filesize(left), Filesize(right)) => CompareValues::Filesizes(*left, *right),
159 (Filesize(left), Decimal(right)) => {
160 CompareValues::Decimals(BigDecimal::from(*left), right.clone())
161 }
162 (Nothing, Nothing) => CompareValues::Booleans(true, true),
163 (String(left), String(right)) => CompareValues::String(left.clone(), right.clone()),
164 (Date(left), Date(right)) => CompareValues::Date(*left, *right),
165 (Date(left), Duration(right)) => CompareValues::DateDuration(*left, right.clone()),
166 (Boolean(left), Boolean(right)) => CompareValues::Booleans(*left, *right),
167 (Boolean(left), Nothing) => CompareValues::Ints(if *left { 1 } else { 0 }, -1),
168 (Nothing, Boolean(right)) => CompareValues::Ints(-1, if *right { 1 } else { 0 }),
169 (String(left), Nothing) => CompareValues::String(left.clone(), std::string::String::new()),
170 (Nothing, String(right)) => {
171 CompareValues::String(std::string::String::new(), right.clone())
172 }
173 (FilePath(left), String(right)) => {
174 CompareValues::String(left.as_path().display().to_string(), right.clone())
175 }
176 (String(left), FilePath(right)) => {
177 CompareValues::String(left.clone(), right.as_path().display().to_string())
178 }
179 (Duration(left), Duration(right)) => {
180 CompareValues::TimeDuration(left.clone(), right.clone())
181 }
182 _ => return Err((left.type_name(), right.type_name())),
183 })
184}
185#[cfg(test)]
186mod tests {
187 use nu_errors::ShellError;
188 use nu_protocol::UntaggedValue;
189 use nu_source::SpannedItem;
190 use nu_test_support::value::*;
191 use nu_value_ext::ValueExt;
192
193 use indexmap::indexmap;
194
195 #[test]
196 fn gets_matching_field_from_a_row() -> Result<(), ShellError> {
197 let row = UntaggedValue::row(indexmap! {
198 "amigos".into() => table(&[string("andres"),string("jonathan"),string("yehuda")])
199 })
200 .into_untagged_value();
201
202 assert_eq!(
203 row.get_data_by_key("amigos".spanned_unknown())
204 .ok_or_else(|| ShellError::unexpected("Failure during testing"))?,
205 table(&[string("andres"), string("jonathan"), string("yehuda")])
206 );
207
208 Ok(())
209 }
210
211 #[test]
212 fn gets_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
213 let field_path = column_path("package.version").as_column_path()?;
214
215 let (version, tag) = string("0.4.0").into_parts();
216
217 let value = UntaggedValue::row(indexmap! {
218 "package".into() =>
219 row(indexmap! {
220 "name".into() => string("nu"),
221 "version".into() => string("0.4.0")
222 })
223 });
224
225 assert_eq!(
226 *value.into_value(tag).get_data_by_column_path(
227 &field_path.item,
228 Box::new(error_callback("package.version"))
229 )?,
230 version
231 );
232
233 Ok(())
234 }
235
236 #[test]
237 fn gets_first_matching_field_from_rows_with_same_field_inside_a_table() -> Result<(), ShellError>
238 {
239 let field_path = column_path("package.authors.name").as_column_path()?;
240
241 let (_, tag) = string("Andrés N. Robalino").into_parts();
242
243 let value = UntaggedValue::row(indexmap! {
244 "package".into() => row(indexmap! {
245 "name".into() => string("nu"),
246 "version".into() => string("0.4.0"),
247 "authors".into() => table(&[
248 row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
249 row(indexmap!{"name".into() => string("Jonathan Turner")}),
250 row(indexmap!{"name".into() => string("Yehuda Katz")})
251 ])
252 })
253 });
254
255 assert_eq!(
256 value.into_value(tag).get_data_by_column_path(
257 &field_path.item,
258 Box::new(error_callback("package.authors.name"))
259 )?,
260 table(&[
261 string("Andrés N. Robalino"),
262 string("Jonathan Turner"),
263 string("Yehuda Katz")
264 ])
265 );
266
267 Ok(())
268 }
269
270 #[test]
271 fn column_path_that_contains_just_a_number_gets_a_row_from_a_table() -> Result<(), ShellError> {
272 let field_path = column_path("package.authors.0").as_column_path()?;
273
274 let (_, tag) = string("Andrés N. Robalino").into_parts();
275
276 let value = UntaggedValue::row(indexmap! {
277 "package".into() => row(indexmap! {
278 "name".into() => string("nu"),
279 "version".into() => string("0.4.0"),
280 "authors".into() => table(&[
281 row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
282 row(indexmap!{"name".into() => string("Jonathan Turner")}),
283 row(indexmap!{"name".into() => string("Yehuda Katz")})
284 ])
285 })
286 });
287
288 assert_eq!(
289 *value.into_value(tag).get_data_by_column_path(
290 &field_path.item,
291 Box::new(error_callback("package.authors.0"))
292 )?,
293 UntaggedValue::row(indexmap! {
294 "name".into() => string("Andrés N. Robalino")
295 })
296 );
297
298 Ok(())
299 }
300
301 #[test]
302 fn column_path_that_contains_just_a_number_gets_a_row_from_a_row() -> Result<(), ShellError> {
303 let field_path = column_path(r#"package.authors."0""#).as_column_path()?;
304
305 let (_, tag) = string("Andrés N. Robalino").into_parts();
306
307 let value = UntaggedValue::row(indexmap! {
308 "package".into() => row(indexmap! {
309 "name".into() => string("nu"),
310 "version".into() => string("0.4.0"),
311 "authors".into() => row(indexmap! {
312 "0".into() => row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
313 "1".into() => row(indexmap!{"name".into() => string("Jonathan Turner")}),
314 "2".into() => row(indexmap!{"name".into() => string("Yehuda Katz")}),
315 })
316 })
317 });
318
319 assert_eq!(
320 *value.into_value(tag).get_data_by_column_path(
321 &field_path.item,
322 Box::new(error_callback("package.authors.\"0\""))
323 )?,
324 UntaggedValue::row(indexmap! {
325 "name".into() => string("Andrés N. Robalino")
326 })
327 );
328
329 Ok(())
330 }
331
332 #[test]
333 fn replaces_matching_field_from_a_row() -> Result<(), ShellError> {
334 let field_path = column_path("amigos").as_column_path()?;
335
336 let sample = UntaggedValue::row(indexmap! {
337 "amigos".into() => table(&[
338 string("andres"),
339 string("jonathan"),
340 string("yehuda"),
341 ]),
342 });
343
344 let replacement = string("jonas");
345
346 let actual = sample
347 .into_untagged_value()
348 .replace_data_at_column_path(&field_path.item, replacement)
349 .ok_or_else(|| ShellError::untagged_runtime_error("Could not replace column"))?;
350
351 assert_eq!(actual, row(indexmap! {"amigos".into() => string("jonas")}));
352
353 Ok(())
354 }
355
356 #[test]
357 fn replaces_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
358 let field_path = column_path(r#"package.authors."los.3.caballeros""#).as_column_path()?;
359
360 let sample = UntaggedValue::row(indexmap! {
361 "package".into() => row(indexmap! {
362 "authors".into() => row(indexmap! {
363 "los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
364 "los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]),
365 "los.3.caballeros".into() => table(&[string("andres::yehuda::jonathan")])
366 })
367 })
368 });
369
370 let replacement = table(&[string("yehuda::jonathan::andres")]);
371 let tag = replacement.tag.clone();
372
373 let actual = sample
374 .into_value(&tag)
375 .replace_data_at_column_path(&field_path.item, replacement.clone())
376 .ok_or_else(|| {
377 ShellError::labeled_error(
378 "Could not replace column",
379 "could not replace column",
380 &tag,
381 )
382 })?;
383
384 assert_eq!(
385 actual,
386 UntaggedValue::row(indexmap! {
387 "package".into() => row(indexmap! {
388 "authors".into() => row(indexmap! {
389 "los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
390 "los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]),
391 "los.3.caballeros".into() => replacement})})})
392 .into_value(tag)
393 );
394
395 Ok(())
396 }
397 #[test]
398 fn replaces_matching_field_from_rows_inside_a_table() -> Result<(), ShellError> {
399 let field_path =
400 column_path(r#"shell_policy.releases."nu.version.arepa""#).as_column_path()?;
401
402 let sample = UntaggedValue::row(indexmap! {
403 "shell_policy".into() => row(indexmap! {
404 "releases".into() => table(&[
405 row(indexmap! {
406 "nu.version.arepa".into() => row(indexmap! {
407 "code".into() => string("0.4.0"), "tag_line".into() => string("GitHub-era")
408 })
409 }),
410 row(indexmap! {
411 "nu.version.taco".into() => row(indexmap! {
412 "code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
413 })
414 }),
415 row(indexmap! {
416 "nu.version.stable".into() => row(indexmap! {
417 "code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
418 })
419 })
420 ])
421 })
422 });
423
424 let replacement = row(indexmap! {
425 "code".into() => string("0.5.0"),
426 "tag_line".into() => string("CABALLEROS")
427 });
428 let tag = replacement.tag.clone();
429
430 let actual = sample
431 .into_value(tag.clone())
432 .replace_data_at_column_path(&field_path.item, replacement.clone())
433 .ok_or_else(|| {
434 ShellError::labeled_error(
435 "Could not replace column",
436 "could not replace column",
437 &tag,
438 )
439 })?;
440
441 assert_eq!(
442 actual,
443 UntaggedValue::row(indexmap! {
444 "shell_policy".into() => row(indexmap! {
445 "releases".into() => table(&[
446 row(indexmap! {
447 "nu.version.arepa".into() => replacement
448 }),
449 row(indexmap! {
450 "nu.version.taco".into() => row(indexmap! {
451 "code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
452 })
453 }),
454 row(indexmap! {
455 "nu.version.stable".into() => row(indexmap! {
456 "code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
457 })
458 })
459 ])
460 })
461 }).into_value(&tag)
462 );
463
464 Ok(())
465 }
466}