git.delta.rocks / jrsonnet / refs/commits / bbe146ee2257

difftreelog

feat refer to subfields of objects in string formatting

Yaroslav Bolyukin2023-06-14parent: #aeb8b35.patch.diff
in: master
Upstream issue: https://github.com/google/jsonnet/pull/1011

3 files changed

modifiedcrates/jrsonnet-evaluator/src/error.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/error.rs
+++ b/crates/jrsonnet-evaluator/src/error.rs
@@ -1,6 +1,6 @@
 use std::{
 	fmt::{Debug, Display},
-	path::PathBuf,
+	path::PathBuf, cmp::Ordering,
 };
 
 use jrsonnet_gcmodule::Trace;
@@ -9,9 +9,9 @@
 use jrsonnet_types::ValType;
 use thiserror::Error;
 
-use crate::{function::CallLocation, stdlib::format::FormatError, typed::TypeLocError};
+use crate::{function::CallLocation, stdlib::format::FormatError, typed::TypeLocError, ObjValue};
 
-fn format_found(list: &[IStr], what: &str) -> String {
+pub(crate) fn format_found(list: &[IStr], what: &str) -> String {
 	if list.is_empty() {
 		return String::new();
 	}
@@ -68,6 +68,26 @@
 	}
 }
 
+pub(crate) fn suggest_object_fields(v: &ObjValue, key: IStr) -> Vec<IStr> {
+	let mut heap = Vec::new();
+	for field in v.fields_ex(
+		true,
+		#[cfg(feature = "exp-preserve-order")]
+		false,
+	) {
+		let conf = strsim::jaro_winkler(field.as_str(), key.as_str());
+		if conf < 0.8 {
+			continue;
+		}
+		if field.as_str() == key.as_str() {
+			panic!("looks like string pooling failure, please write any info regarding this crash to https://github.com/CertainLach/jrsonnet/issues/113, thanks!");
+		}
+		heap.push((conf, field));
+	}
+	heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
+	heap.into_iter().map(|v| v.1).collect()
+}
+
 type FunctionSignature = Vec<(Option<IStr>, bool)>;
 
 /// Possible errors
@@ -348,6 +368,9 @@
 	($w:ident$(::$i:ident)*$(($($tt:tt)*))?) => {
 		return Err($w$(::$i)*$(($($tt)*))?.into())
 	};
+	($w:ident$(::$i:ident)*$({$($tt:tt)*})?) => {
+		return Err($w$(::$i)*$({$($tt)*})?.into())
+	};
 	($l:literal) => {
 		return Err($crate::error::ErrorKind::RuntimeError($l.into()).into())
 	};
modifiedcrates/jrsonnet-evaluator/src/evaluate/mod.rsdiffbeforeafterboth
12use crate::{12use crate::{
13 arr::ArrValue,13 arr::ArrValue,
14 destructure::evaluate_dest,14 destructure::evaluate_dest,
15 error::ErrorKind::*,15 error::{ErrorKind::*, suggest_object_fields},
16 evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},16 evaluate::operator::{evaluate_add_op, evaluate_binary_op_special, evaluate_unary_op},
17 function::{CallLocation, FuncDesc, FuncVal},17 function::{CallLocation, FuncDesc, FuncVal},
18 throw,18 throw,
466 || format!("field <{key}> access"),466 || format!("field <{key}> access"),
467 || match v.get(key.clone().into_flat()) {467 || match v.get(key.clone().into_flat()) {
468 Ok(Some(v)) => Ok(v),468 Ok(Some(v)) => Ok(v),
469 #[cfg(not(feature = "friendly-errors"))]
470 Ok(None) => throw!(NoSuchField(key.clone(), vec![])),
471 #[cfg(feature = "friendly-errors")]
472 Ok(None) => {469 Ok(None) => {
473 let mut heap = Vec::new();
474 for field in v.fields_ex(
475 true,
476 #[cfg(feature = "exp-preserve-order")]
477 false,
478 ) {
479 let conf = strsim::jaro_winkler(470 let suggestions = suggest_object_fields(&v, key.clone().into_flat());
480 &field as &str,
481 &key.clone().into_flat() as &str,
482 );
483 if conf < 0.8 {
484 continue;
485 }
486 heap.push((conf, field));
487 }
488 heap.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
489471
490 throw!(NoSuchField(472 throw!(NoSuchField(key.clone().into_flat(), suggestions))
491 key.clone().into_flat(),
492 heap.into_iter().map(|(_, v)| v).collect()
493 ))
494 }473 }
495 Err(e) => Err(e),474 Err(e) => Err(e),
modifiedcrates/jrsonnet-evaluator/src/stdlib/format.rsdiffbeforeafterboth
--- a/crates/jrsonnet-evaluator/src/stdlib/format.rs
+++ b/crates/jrsonnet-evaluator/src/stdlib/format.rs
@@ -6,7 +6,12 @@
 use jrsonnet_types::ValType;
 use thiserror::Error;
 
-use crate::{error::ErrorKind::*, throw, typed::Typed, Error, ObjValue, Result, Val};
+use crate::{
+	error::{format_found, suggest_object_fields, ErrorKind::*},
+	throw,
+	typed::Typed,
+	Error, ObjValue, Result, Val,
+};
 
 #[derive(Debug, Clone, Error, Trace)]
 pub enum FormatError {
@@ -24,6 +29,15 @@
 	MappingKeysRequired,
 	#[error("no such format field: {0}")]
 	NoSuchFormatField(IStr),
+
+	#[error("expected subfield <{0}> to be an object, got {1} instead")]
+	SubfieldDidntYieldAnObject(IStr, ValType),
+	#[error("subfield not found: <[{full}]{current}>{}", format_found(.found, "subfield"))]
+	SubfieldNotFound {
+		current: IStr,
+		full: IStr,
+		found: Box<Vec<IStr>>,
+	},
 }
 
 impl From<FormatError> for Error {
@@ -691,6 +705,37 @@
 	Ok(out)
 }
 
+fn get_dotted_field(obj: ObjValue, field: &str) -> Result<Val> {
+	let mut current = Val::Obj(obj);
+	let mut name_offset = 0;
+	for component in field.split('.') {
+		let end_offset = name_offset + component.len();
+		current = if let Val::Obj(obj) = current {
+			if let Some(value) = obj.get(component.into())? {
+				value
+			} else {
+				let current = &field[name_offset..end_offset];
+				let full = &field[..name_offset];
+				let found = Box::new(suggest_object_fields(&obj, current.into()));
+				throw!(SubfieldNotFound {
+					current: current.into(),
+					full: full.into(),
+					found,
+				})
+			}
+		} else {
+			// No underflow may happen, initially we always start with an object
+			let subfield = &field[..name_offset - 1];
+			throw!(SubfieldDidntYieldAnObject(
+				subfield.into(),
+				current.value_type()
+			));
+		};
+		name_offset = end_offset + 1;
+	}
+	Ok(current)
+}
+
 pub fn format_obj(str: &str, values: &ObjValue) -> Result<String> {
 	let codes = parse_codes(str)?;
 	let mut out = String::new();
@@ -726,7 +771,7 @@
 					if let Some(v) = values.get(f.clone())? {
 						v
 					} else {
-						throw!(NoSuchFormatField(f));
+						get_dotted_field(values.clone(), &f)?
 					}
 				};