use crate::schema::RustTypeInfo;
use anyhow::anyhow;
use indent_write::fmt::IndentWriter;
use serde::{Serialize, de::DeserializeOwned};
use std::{fmt, fmt::Write};
pub trait EngineSpec: Send + 'static {
fn spec_name() -> String;
type Component: Clone
+ fmt::Debug
+ DeserializeOwned
+ Serialize
+ Eq
+ Send
+ Sync;
type StepId: Clone
+ fmt::Debug
+ DeserializeOwned
+ Serialize
+ Eq
+ Send
+ Sync;
type StepMetadata: Clone
+ fmt::Debug
+ DeserializeOwned
+ Serialize
+ Eq
+ Send
+ Sync;
type ProgressMetadata: Clone
+ fmt::Debug
+ DeserializeOwned
+ Serialize
+ Eq
+ Send
+ Sync;
type CompletionMetadata: Clone
+ fmt::Debug
+ DeserializeOwned
+ Serialize
+ Eq
+ Send
+ Sync;
type SkippedMetadata: Clone
+ fmt::Debug
+ DeserializeOwned
+ Serialize
+ Eq
+ Send
+ Sync;
type Error: AsError + fmt::Debug + Send + Sync;
fn rust_type_info() -> Option<RustTypeInfo> {
None
}
}
#[cfg(feature = "schemars08")]
pub trait JsonSchemaEngineSpec:
EngineSpec<
Component: schemars::JsonSchema,
StepId: schemars::JsonSchema,
StepMetadata: schemars::JsonSchema,
ProgressMetadata: schemars::JsonSchema,
CompletionMetadata: schemars::JsonSchema,
SkippedMetadata: schemars::JsonSchema,
> + schemars::JsonSchema
{
}
#[cfg(feature = "schemars08")]
impl<S> JsonSchemaEngineSpec for S
where
S: EngineSpec + schemars::JsonSchema,
S::Component: schemars::JsonSchema,
S::StepId: schemars::JsonSchema,
S::StepMetadata: schemars::JsonSchema,
S::ProgressMetadata: schemars::JsonSchema,
S::CompletionMetadata: schemars::JsonSchema,
S::SkippedMetadata: schemars::JsonSchema,
{
}
pub struct GenericSpec(());
#[cfg(feature = "schemars08")]
impl schemars::JsonSchema for GenericSpec {
fn schema_name() -> String {
"GenericSpec".to_owned()
}
fn json_schema(
_: &mut schemars::r#gen::SchemaGenerator,
) -> schemars::schema::Schema {
schemars::schema::Schema::Bool(true)
}
}
impl EngineSpec for GenericSpec {
fn spec_name() -> String {
"GenericSpec".to_owned()
}
type Component = serde_json::Value;
type StepId = serde_json::Value;
type StepMetadata = serde_json::Value;
type ProgressMetadata = serde_json::Value;
type CompletionMetadata = serde_json::Value;
type SkippedMetadata = serde_json::Value;
type Error = SerializableError;
fn rust_type_info() -> Option<RustTypeInfo> {
Some(RustTypeInfo {
crate_name: crate::schema::CRATE_NAME,
version: crate::schema::VERSION,
path: crate::schema::GENERIC_SPEC_PATH,
})
}
}
#[derive(Clone, Debug)]
pub struct SerializableError {
message: String,
source: Option<Box<SerializableError>>,
}
impl SerializableError {
pub fn new(error: &dyn std::error::Error) -> Self {
Self {
message: format!("{}", error),
source: error.source().map(|s| Box::new(Self::new(s))),
}
}
pub fn from_message_and_causes(
message: String,
causes: Vec<String>,
) -> Self {
let mut next = None;
for cause in causes.into_iter().rev() {
let error = Self { message: cause, source: next.map(Box::new) };
next = Some(error);
}
Self { message, source: next.map(Box::new) }
}
pub fn message(&self) -> &str {
&self.message
}
pub fn sources(&self) -> SerializableErrorSources<'_> {
SerializableErrorSources { current: self.source.as_deref() }
}
}
impl fmt::Display for SerializableError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for SerializableError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_ref().map(|s| s as &(dyn std::error::Error + 'static))
}
}
#[derive(Debug)]
pub struct SerializableErrorSources<'a> {
current: Option<&'a SerializableError>,
}
impl<'a> Iterator for SerializableErrorSources<'a> {
type Item = &'a SerializableError;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current?;
self.current = current.source.as_deref();
Some(current)
}
}
mod serializable_error_serde {
use super::*;
use serde::Deserialize;
#[derive(Serialize, Deserialize)]
struct Ser {
message: String,
causes: Vec<String>,
}
impl Serialize for SerializableError {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut causes = Vec::new();
let mut cause = self.source.as_ref();
while let Some(c) = cause {
causes.push(c.message.clone());
cause = c.source.as_ref();
}
let serialized = Ser { message: self.message.clone(), causes };
serialized.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for SerializableError {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
let serialized = Ser::deserialize(deserializer)?;
Ok(SerializableError::from_message_and_causes(
serialized.message,
serialized.causes,
))
}
}
}
impl AsError for SerializableError {
fn as_error(&self) -> &(dyn std::error::Error + 'static) {
self
}
}
pub trait AsError: fmt::Debug + Send + Sync + 'static {
fn as_error(&self) -> &(dyn std::error::Error + 'static);
}
impl AsError for anyhow::Error {
fn as_error(&self) -> &(dyn std::error::Error + 'static) {
self.as_ref()
}
}
pub fn merge_anyhow_list<I>(errors: I) -> anyhow::Error
where
I: IntoIterator<Item = anyhow::Error>,
{
let mut iter = errors.into_iter().peekable();
let Some(first_error) = iter.next() else {
panic!("error_list_to_anyhow called with no errors");
};
if iter.peek().is_none() {
return first_error;
}
let mut out = String::new();
let mut nerrors = 0;
for error in std::iter::once(first_error).chain(iter) {
if nerrors > 0 {
writeln!(&mut out).unwrap();
}
nerrors += 1;
let mut current = error.as_error();
let mut writer = IndentWriter::new_skip_initial(" ", &mut out);
write!(writer, "Error: {current}").unwrap();
while let Some(cause) = current.source() {
writeln!(&mut out).unwrap();
let mut writer =
IndentWriter::new_skip_initial(" ", &mut out);
write!(writer, " - {cause}").unwrap();
current = cause;
}
}
anyhow!(out).context(format!("{nerrors} errors encountered"))
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn test_merge_anyhow_list() {
unsafe {
std::env::set_var("RUST_BACKTRACE", "0");
}
let error = anyhow!("base").context("parent").context("root");
let merged = merge_anyhow_list(vec![error]);
assert_eq!(
format!("{:?}", merged),
indoc! {"
root
Caused by:
0: parent
1: base"
},
);
let error1 =
anyhow!("base1").context("parent1\nparent1 line2").context("root1");
let error2 = anyhow!("base2").context("parent2").context("root2");
let merged = merge_anyhow_list(vec![error1, error2]);
let merged_debug = format!("{:?}", merged);
println!("merged debug: {}", merged_debug);
assert_eq!(
merged_debug,
indoc! {"
2 errors encountered
Caused by:
Error: root1
- parent1
parent1 line2
- base1
Error: root2
- parent2
- base2"
},
);
let error3 = merged.context("overall root");
let error3_debug = format!("{:?}", error3);
println!("error3 debug: {}", error3_debug);
assert_eq!(
error3_debug,
indoc! {"
overall root
Caused by:
0: 2 errors encountered
1: Error: root1
- parent1
parent1 line2
- base1
Error: root2
- parent2
- base2"
},
);
}
}