Skip to content

Commit f99f60e

Browse files
authored
Various enhancements to ion jq (#199)
* Fix indexing * More consistent error reporting style * Fix truthiness * Support String/Symbol concat for interpolation
1 parent 795d1ed commit f99f60e

File tree

1 file changed

+72
-37
lines changed
  • src/bin/ion/commands

1 file changed

+72
-37
lines changed

src/bin/ion/commands/jq.rs

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,14 @@ fn jaq_error(e: impl Into<Element>) -> ValR<JaqElement> {
177177
Err(jaq_err(e))
178178
}
179179

180-
fn jaq_unary_error(v1: Value, reason: &str) -> ValR<JaqElement> {
181-
let type1 = v1.ion_type();
182-
jaq_error(format!("{type1} ({v1}) {reason}"))
180+
fn jaq_unary_error(a: Value, reason: &str) -> ValR<JaqElement> {
181+
let alpha = a.ion_type();
182+
jaq_error(format!("{alpha} ({a}) {reason}"))
183183
}
184184

185-
fn jaq_binary_error(v1: Value, v2: Value, reason: &str) -> ValR<JaqElement> {
186-
let type1 = v1.ion_type();
187-
let type2 = v2.ion_type();
188-
jaq_error(format!("{type1} ({v1}) and {type2} ({v2}) {reason}"))
185+
fn jaq_binary_error(a: Value, b: Value, reason: &str) -> ValR<JaqElement> {
186+
let (alpha, beta) = (a.ion_type(), b.ion_type());
187+
jaq_error(format!("{alpha} ({a}) and {beta} ({b}) {reason}"))
189188
}
190189

191190
// Convenience method to return a bare `JaqError`, not wrapped in a Result::Err.
@@ -241,6 +240,11 @@ impl Add for JaqElement {
241240
fn add(self, _rhs: Self) -> Self::Output {
242241
let (lhv, rhv) = (self.into_value(), _rhs.into_value());
243242

243+
fn unknown_symbol_err(a: Value, b: Value) -> ValR<JaqElement> {
244+
let (alpha, beta) = (a.ion_type(), b.ion_type());
245+
jaq_error(format!("{alpha} ({a}) and {beta} ({b}) cannot be added"))
246+
}
247+
244248
use ion_math::{DecimalMath, ToFloat};
245249
use Value::*;
246250

@@ -255,14 +259,25 @@ impl Add for JaqElement {
255259
// Sequences and strings concatenate
256260
(List(a), List(b)) => ion_rs::List::from_iter(a.into_iter().chain(b)).into(),
257261
(SExp(a), SExp(b)) => ion_rs::SExp::from_iter(a.into_iter().chain(b)).into(),
258-
//TODO: Does it make sense to concatenate a String and a Symbol? What type results?
262+
263+
// We don't work with unknown symbols
264+
(Symbol(a), b) if a.text().is_none() => return unknown_symbol_err(Symbol(a), b),
265+
(a, Symbol(b)) if b.text().is_none() => return unknown_symbol_err(a, Symbol(b)),
266+
267+
// Like text types add to the same type
259268
(String(a), String(b)) => format!("{}{}", a.text(), b.text()).into(),
260-
(Symbol(a), Symbol(b)) => match (a.text(), b.text()) {
261-
(Some(ta), Some(tb)) => format!("{}{}", ta, tb),
262-
//TODO: Handle symbols with unknown text?
263-
_ => return jaq_binary_error(Symbol(a), Symbol(b), "cannot be added"),
269+
(Symbol(a), Symbol(b)) => {
270+
Symbol(format!("{}{}", a.text().unwrap(), b.text().unwrap()).into()).into()
271+
}
272+
273+
// Any combination of String/Symbol gets to be a String
274+
// We have to account for these cases to allow string interpolation
275+
(String(a), Symbol(b)) => {
276+
String(format!("{}{}", a.text(), b.text().unwrap()).into()).into()
277+
}
278+
(Symbol(a), String(b)) => {
279+
String(format!("{}{}", a.text().unwrap(), b.text()).into()).into()
264280
}
265-
.into(),
266281

267282
// Structs merge
268283
//TODO: Recursively remove duplicate fields, see doc comment for rules
@@ -278,7 +293,7 @@ impl Add for JaqElement {
278293
(a @ Int(_) | a @ Decimal(_), Float(b)) => (a.to_f64().unwrap() + b).into(),
279294
(Float(a), b @ Int(_) | b @ Decimal(_)) => (a + b.to_f64().unwrap()).into(),
280295

281-
(a, b) => return jaq_binary_error(a, b, "cannot be added"),
296+
(a, b) => return unknown_symbol_err(a, b),
282297
};
283298

284299
Ok(JaqElement::from(elt))
@@ -519,29 +534,43 @@ impl jaq_core::ValT for JaqElement {
519534
fn index(self, index: &Self) -> ValR<Self> {
520535
use ion_rs::Value::*;
521536

522-
match (self.value(), index.value()) {
523-
(List(seq) | SExp(seq), Int(i)) => {
524-
let index = i
525-
.expect_usize()
526-
.map_err(|_| jaq_err("index must be usize"))?;
527-
let element = seq
528-
.get(index)
529-
.ok_or_else(|| jaq_err("index out of bounds"))?;
530-
Ok(JaqElement::from(element.to_owned()))
537+
trait OrOwnedNull {
538+
fn or_owned_null(self) -> Element;
539+
}
540+
541+
impl OrOwnedNull for Option<&Element> {
542+
fn or_owned_null(self) -> Element {
543+
self.map_or_else(|| Null(IonType::Null).into(), Element::to_owned)
531544
}
532-
(Struct(strukt), String(name)) => strukt
533-
.get(name)
534-
.ok_or_else(|| jaq_err(format!("field name '{name}' not found")))
535-
.map(Element::to_owned)
536-
.map(JaqElement::from),
537-
(Struct(strukt), Symbol(name)) => strukt
538-
.get(name)
539-
.ok_or_else(|| jaq_err(format!("field name '{name}' not found")))
540-
.map(Element::to_owned)
541-
.map(JaqElement::from),
542-
(Struct(_), Int(i)) => jaq_error(format!("cannot index struct with number ({i})")),
543-
_ => jaq_error(format!("cannot index into {self:?}")),
544545
}
546+
547+
/// Handles the case where we want to index into a Sequence with a potentially-negative
548+
/// value. Negative numbers index from the back of the sequence.
549+
/// Returns an owned Null Element if the index is out of bounds.
550+
fn index_i128(seq: &Sequence, index: Option<i128>) -> Element {
551+
let opt = match index {
552+
Some(i @ ..0) => (seq.len() as i128 + i).to_usize(),
553+
Some(i) => i.to_usize(),
554+
None => None,
555+
};
556+
557+
opt.and_then(|u| seq.get(u)).or_owned_null()
558+
}
559+
560+
let elt: Element = match (self.value(), index.value()) {
561+
(List(seq) | SExp(seq), Int(i)) => index_i128(seq, i.as_i128()),
562+
(List(seq) | SExp(seq), Float(f)) => index_i128(seq, Some(*f as i128)),
563+
(List(seq) | SExp(seq), Decimal(d)) => index_i128(seq, d.into_big_decimal().to_i128()),
564+
(Struct(strukt), String(name)) => strukt.get(name).or_owned_null(),
565+
(Struct(strukt), Symbol(name)) => strukt.get(name).or_owned_null(),
566+
567+
(a, b) => {
568+
let (alpha, beta) = (a.ion_type(), b.ion_type());
569+
return jaq_error(format!("cannot index {} with {}", alpha, beta));
570+
}
571+
};
572+
573+
Ok(JaqElement::from(elt))
545574
}
546575

547576
// Behavior for slicing containers.
@@ -578,9 +607,15 @@ impl jaq_core::ValT for JaqElement {
578607
todo!()
579608
}
580609

581-
// If we want "truthiness" for containers (e.g. empty list -> false), define that here
610+
/// From https://jqlang.org/manual/#if-then-else-end
611+
///
612+
/// > `if A then B else C end` will act the same as `B` if `A` produces a value other than
613+
/// > `false` or `null`, but act the same as `C` otherwise.
582614
fn as_bool(&self) -> bool {
583-
self.0.as_bool().unwrap_or(false)
615+
match self.0.value() {
616+
Value::Null(_) | Value::Bool(false) => false,
617+
_ => true,
618+
}
584619
}
585620

586621
// If the element is a text value, return its text.

0 commit comments

Comments
 (0)