Skip to content

Commit

Permalink
feat: teach vscode/fiddle to explain when we drop information (#897)
Browse files Browse the repository at this point in the history
when there's an explanation to be shown, the information icon is
clickable and toggles showing the explanation:


![image](https://github.com/user-attachments/assets/50338f22-f18f-4364-9160-a25f5e9e7ee9)

if there's no explanation, the information icon tooltip just shows
"parsing succeeded" and clicking it does nothing


![image](https://github.com/user-attachments/assets/b0a7d1bc-e957-4db3-842b-17c4c045c007)
  • Loading branch information
sxlijin authored Sep 21, 2024
1 parent c779c05 commit 93e2b9b
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 162 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -222,41 +222,20 @@ impl TypeCoercer for Class {
}
log::trace!("----");

let unparsed_required_fields = required_values
let unparsed_or_missing = required_values
.iter()
.filter_map(|(k, v)| match v {
Some(Ok(_)) => None,
Some(Err(e)) => Some((k.clone(), e.to_string())),
None => None,
})
.collect::<Vec<_>>();
let missing_required_fields = required_values
.iter()
.filter_map(|(k, v)| match v {
Some(Ok(_)) => None,
Some(Err(e)) => None,
None => Some(k.clone()),
Some(Err(e)) => Some((k.clone(), Some(e.clone()))),
None => Some((k.clone(), None)),
})
.collect::<Vec<_>>();

if !missing_required_fields.is_empty() || !unparsed_required_fields.is_empty() {
if !unparsed_or_missing.is_empty() {
if completed_cls.is_empty() {
return Err(ctx.error_missing_required_field(
&unparsed_required_fields,
&missing_required_fields,
value,
));
return Err(ctx.error_missing_required_field(unparsed_or_missing, value));
}
} else {
let merged_errors = required_values
.iter()
.filter_map(|(_k, v)| v.clone())
.filter_map(|v| match v {
Ok(_) => None,
Err(e) => Some(e.to_string()),
})
.collect::<Vec<_>>();

let valid_fields = required_values
.iter()
.filter_map(|(k, v)| match v.to_owned() {
Expand Down
131 changes: 55 additions & 76 deletions engine/baml-lib/jsonish/src/deserializer/coercer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ impl ParsingContext<'_> {
})
),
scope: self.scope.clone(),
causes: vec![],
}
}

Expand All @@ -70,119 +71,81 @@ impl ParsingContext<'_> {
summary: &str,
error: impl IntoIterator<Item = &'a ParsingError>,
) -> ParsingError {
let reasons = error
.into_iter()
.map(|e| {
// Strip all shared prefixes (assume the same unless different length)
let remaining =
e.scope
.iter()
.skip(self.scope.len())
.fold("".to_string(), |acc, f| {
if acc.is_empty() {
return f.clone();
}
return format!("{}.{}", acc, f);
});

if remaining.is_empty() {
return e.reason.clone();
} else {
// Prefix each new lines in e.reason with " "
return format!("{}: {}", remaining, e.reason.replace("\n", "\n "));
}
})
.collect::<Vec<_>>();

ParsingError {
reason: format!("{}:\n{}", summary, reasons.join("\n").replace("\n", "\n ")),
reason: format!("{}", summary),
scope: self.scope.clone(),
causes: error.into_iter().map(|e| e.clone()).collect(),
}
}

pub(crate) fn error_unexpected_empty_array(&self, target: &FieldType) -> ParsingError {
ParsingError {
reason: format!("Expected {}, got empty array", target.to_string()),
scope: self.scope.clone(),
causes: vec![],
}
}

pub(crate) fn error_unexpected_null(&self, target: &FieldType) -> ParsingError {
ParsingError {
reason: format!("Expected {}, got null", target),
scope: self.scope.clone(),
causes: vec![],
}
}

pub(crate) fn error_image_not_supported(&self) -> ParsingError {
ParsingError {
reason: "Image type is not supported here".to_string(),
scope: self.scope.clone(),
causes: vec![],
}
}

pub(crate) fn error_audio_not_supported(&self) -> ParsingError {
ParsingError {
reason: "Audio type is not supported here".to_string(),
scope: self.scope.clone(),
causes: vec![],
}
}

pub(crate) fn error_map_must_have_string_key(&self, key_type: &FieldType) -> ParsingError {
ParsingError {
reason: format!("Maps may only have strings for keys, but got {}", key_type),
scope: self.scope.clone(),
causes: vec![],
}
}

pub(crate) fn error_missing_required_field<T: AsRef<str>>(
pub(crate) fn error_missing_required_field(
&self,
unparsed_fields: &[(T, T)],
missing_fields: &[T],
unparsed_or_missing: Vec<(String, Option<ParsingError>)>,
item: Option<&crate::jsonish::Value>,
) -> ParsingError {
let fields = missing_fields
.iter()
.map(|c| c.as_ref())
.collect::<Vec<_>>()
.join(", ");
let missing_error = match missing_fields.len() {
0 => None,
1 => Some(format!("Missing required field: {}", fields)),
_ => Some(format!("Missing required fields: {}", fields)),
};

let unparsed = unparsed_fields
.iter()
.map(|(k, v)| format!("{}: {}", k.as_ref(), v.as_ref().replace("\n", "\n ")))
.collect::<Vec<_>>()
.join("\n");
let unparsed_error = match unparsed_fields.len() {
0 => None,
1 => Some(format!(
"Unparsed field: {}\n {}",
unparsed_fields[0].0.as_ref(),
unparsed_fields[0].1.as_ref().replace("\n", "\n ")
)),
_ => Some(format!(
"Unparsed fields:\n{}\n {}",
unparsed_fields
.iter()
.map(|(k, _)| k.as_ref())
.collect::<Vec<_>>()
.join(", "),
unparsed.replace("\n", "\n ")
)),
};
let (missing, unparsed): (Vec<_>, Vec<_>) =
unparsed_or_missing.iter().partition(|(_, f)| f.is_none());

ParsingError {
reason: match (missing_error, unparsed_error) {
(Some(m), Some(u)) => format!("{}\n{}", m, u),
(Some(m), None) => m,
(None, Some(u)) => u,
(None, None) => "Unexpected error".to_string(),
},
reason: format!(
"Failed while parsing required fields: missing={}, unparsed={}",
missing.len(),
unparsed.len()
),
scope: self.scope.clone(),
causes: unparsed_or_missing
.into_iter()
.map(|(k, f)| match f {
// Failed while parsing required field
Some(e) => e,
// Missing required field
None => ParsingError {
scope: self.scope.clone(),
reason: format!("Missing required field: {}", k),
causes: vec![],
},
})
.collect(),
}
}

Expand All @@ -192,36 +155,52 @@ impl ParsingContext<'_> {
got: &T,
) -> ParsingError {
ParsingError {
reason: format!("Expected {}, got {}.\n{:#?}", target, got, got),
reason: format!(
"Expected {}, got {:?}.",
match target {
FieldType::Enum(_) => format!("{} enum value", target),
FieldType::Class(_) => format!("{}", target),
_ => format!("{target}"),
},
got
),
scope: self.scope.clone(),
causes: vec![],
}
}

pub(crate) fn error_internal<T: std::fmt::Display>(&self, error: T) -> ParsingError {
ParsingError {
reason: format!("Internal error: {}", error),
scope: self.scope.clone(),
causes: vec![],
}
}
}

#[derive(Debug, Clone)]
pub struct ParsingError {
reason: String,
scope: Vec<String>,
pub scope: Vec<String>,
pub reason: String,
pub causes: Vec<ParsingError>,
}

impl std::fmt::Display for ParsingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.scope.is_empty() {
return write!(f, "Error parsing '<root>': {}", self.reason);
}
write!(
f,
"Error parsing '{}': {}",
self.scope.join("."),
"{}: {}",
if self.scope.is_empty() {
"<root>".to_string()
} else {
self.scope.join(".")
},
self.reason
)
)?;
for cause in &self.causes {
write!(f, "\n - {}", format!("{}", cause).replace("\n", "\n "))?;
}
Ok(())
}
}

Expand Down
46 changes: 46 additions & 0 deletions engine/baml-lib/jsonish/src/deserializer/deserialize_flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,52 @@ pub struct DeserializerConditions {
pub(super) flags: Vec<Flag>,
}

impl DeserializerConditions {
pub fn explanation(&self) -> Vec<ParsingError> {
self.flags
.iter()
.filter_map(|c| match c {
Flag::ObjectFromMarkdown(_) => None,
Flag::ObjectFromFixedJson(_) => None,
Flag::ArrayItemParseError(_idx, e) => {
// TODO: should idx be recorded?
Some(e.clone())
}
Flag::ObjectToString(_) => None,
Flag::ObjectToPrimitive(_) => None,
Flag::ObjectToMap(_) => None,
Flag::ExtraKey(_, _) => None,
Flag::StrippedNonAlphaNumeric(_) => None,
Flag::SubstringMatch(_) => None,
Flag::SingleToArray => None,
Flag::MapKeyParseError(_idx, e) => {
// Some(format!("Error parsing key {} in map: {}", idx, e))
Some(e.clone())
}
Flag::MapValueParseError(_key, e) => {
// Some(format!( "Error parsing value for key '{}' in map: {}", key, e))
Some(e.clone())
}
Flag::JsonToString(_) => None,
Flag::ImpliedKey(_) => None,
Flag::InferedObject(_) => None,
Flag::FirstMatch(_idx, _) => None,
Flag::EnumOneFromMany(_matches) => None,
Flag::DefaultFromNoValue => None,
Flag::DefaultButHadValue(_) => None,
Flag::OptionalDefaultFromNoValue => None,
Flag::StringToBool(_) => None,
Flag::StringToNull(_) => None,
Flag::StringToChar(_) => None,
Flag::FloatToInt(_) => None,
Flag::NoFields(_) => None,
Flag::UnionMatch(_idx, _) => None,
Flag::DefaultButHadUnparseableValue(e) => Some(e.clone()),
})
.collect::<Vec<_>>()
}
}

impl std::fmt::Debug for DeserializerConditions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
Expand Down
2 changes: 1 addition & 1 deletion engine/baml-lib/jsonish/src/deserializer/score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl WithScore for BamlValueWithFlags {
s.score() + 10 * kv.iter().map(|(_, v)| v.score()).sum::<i32>()
}
BamlValueWithFlags::Null(s) => s.score(),
BamlValueWithFlags::Image(s) => s.score(),
BamlValueWithFlags::Media(s) => s.score(),
}
}
}
Expand Down
Loading

0 comments on commit 93e2b9b

Please sign in to comment.