diff --git a/server/src/main.rs b/server/src/main.rs index 35840e89..fd84b5ec 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, bail}; +use anyhow::bail; use clap::{Parser, Subcommand}; use flate2::read::MultiGzDecoder; use oxhttp::model::{Body, HeaderName, HeaderValue, Request, Response, Status}; @@ -189,30 +189,31 @@ impl GraphOrDatasetFormat { } fn from_extension(name: &str) -> anyhow::Result { - match (GraphFormat::from_extension(name), DatasetFormat::from_extension(name)) { - (Some(g), Some(d)) => Err(anyhow!("The file extension '{}' can be resolved to both '{}' and '{}', not sure what to pick", name, g.file_extension(), d.file_extension())), - (Some(g), None) => Ok(GraphOrDatasetFormat::Graph(g)), - (None, Some(d)) => Ok(GraphOrDatasetFormat::Dataset(d)), + Ok( match (GraphFormat::from_extension(name), DatasetFormat::from_extension(name)) { + (Some(g), Some(d)) => bail!("The file extension '{name}' can be resolved to both '{}' and '{}', not sure what to pick", g.file_extension(), d.file_extension()), + (Some(g), None) => GraphOrDatasetFormat::Graph(g), + (None, Some(d)) => GraphOrDatasetFormat::Dataset(d), (None, None) => - Err(anyhow!("The file extension '{}' is unknown", name)) - } + bail!("The file extension '{name}' is unknown") + }) } fn from_media_type(name: &str) -> anyhow::Result { - match ( - GraphFormat::from_media_type(name), - DatasetFormat::from_media_type(name), - ) { - (Some(g), Some(d)) => Err(anyhow!( - "The media type '{}' can be resolved to both '{}' and '{}', not sure what to pick", - name, + Ok( + match ( + GraphFormat::from_media_type(name), + DatasetFormat::from_media_type(name), + ) { + (Some(g), Some(d)) => bail!( + "The media type '{name}' can be resolved to both '{}' and '{}', not sure what to pick", g.file_extension(), d.file_extension() - )), - (Some(g), None) => Ok(GraphOrDatasetFormat::Graph(g)), - (None, Some(d)) => Ok(GraphOrDatasetFormat::Dataset(d)), - (None, None) => Err(anyhow!("The media type '{}' is unknown", name)), - } + ), + (Some(g), None) => GraphOrDatasetFormat::Graph(g), + (None, Some(d)) => GraphOrDatasetFormat::Dataset(d), + (None, None) => bail!("The media type '{name}' is unknown"), + }, + ) } } diff --git a/testsuite/src/files.rs b/testsuite/src/files.rs index 17b1c57f..4d8b4809 100644 --- a/testsuite/src/files.rs +++ b/testsuite/src/files.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use oxigraph::io::{DatasetFormat, DatasetParser, GraphFormat, GraphParser}; use oxigraph::model::{Dataset, Graph, GraphNameRef}; use oxigraph::store::Store; @@ -9,29 +9,29 @@ use std::path::PathBuf; pub fn read_file(url: &str) -> Result { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push(if url.starts_with("http://w3c.github.io/rdf-tests/") { - Ok(url.replace("http://w3c.github.io/rdf-tests/", "rdf-tests/")) + url.replace("http://w3c.github.io/rdf-tests/", "rdf-tests/") } else if url.starts_with("http://www.w3.org/2013/RDFXMLTests/") { - Ok(url.replace("http://www.w3.org/2013/RDFXMLTests/", "rdf-tests/rdf-xml/")) + url.replace("http://www.w3.org/2013/RDFXMLTests/", "rdf-tests/rdf-xml/") } else if url.starts_with("http://www.w3.org/2001/sw/DataAccess/tests/data-r2/") { - Ok(url.replace( + url.replace( "http://www.w3.org/2001/sw/DataAccess/tests/", "rdf-tests/sparql11/", - )) + ) } else if url.starts_with("http://www.w3.org/2009/sparql/docs/tests/data-sparql11/") { - Ok(url.replace( + url.replace( "http://www.w3.org/2009/sparql/docs/tests/", "rdf-tests/sparql11/", - )) + ) } else if url.starts_with("https://w3c.github.io/rdf-star/") { - Ok(url.replace("https://w3c.github.io/", "")) + url.replace("https://w3c.github.io/", "") } else if url.starts_with("https://github.com/oxigraph/oxigraph/tests/") { - Ok(url.replace( + url.replace( "https://github.com/oxigraph/oxigraph/tests/", "oxigraph-tests/", - )) + ) } else { - Err(anyhow!("Not supported url for file: {}", url)) - }?); + bail!("Not supported url for file: {url}") + }); Ok(BufReader::new(File::open(&path)?)) } @@ -72,7 +72,7 @@ pub fn load_to_store<'a>( } else if url.ends_with(".trig") { store.load_dataset(read_file(url)?, DatasetFormat::TriG, Some(url))? } else { - return Err(anyhow!("Serialization type not found for {}", url)); + bail!("Serialization type not found for {url}"); } Ok(()) } @@ -81,7 +81,7 @@ pub fn load_to_graph(url: &str, graph: &mut Graph) -> Result<()> { let format = url .rsplit_once('.') .and_then(|(_, extension)| GraphFormat::from_extension(extension)) - .ok_or_else(|| anyhow!("Serialization type not found for {}", url))?; + .ok_or_else(|| anyhow!("Serialization type not found for {url}"))?; let parser = GraphParser::from_format(format).with_base_iri(url)?; for t in parser.read_triples(read_file(url)?)? { graph.insert(&t?); @@ -107,16 +107,15 @@ pub fn load_to_dataset<'a>( for t in parser.read_triples(read_file(url)?)? { dataset.insert(&t?.in_graph(to_graph_name)); } - Ok(()) } else if let Some(format) = extension.and_then(DatasetFormat::from_extension) { let parser = DatasetParser::from_format(format).with_base_iri(url)?; for q in parser.read_quads(read_file(url)?)? { dataset.insert(&q?); } - Ok(()) } else { - Err(anyhow!("Serialization type not found for {}", url)) + bail!("Serialization type not found for {url}") } + Ok(()) } pub fn load_dataset(url: &str) -> Result { diff --git a/testsuite/src/manifest.rs b/testsuite/src/manifest.rs index be3ee5a8..4a15db8f 100644 --- a/testsuite/src/manifest.rs +++ b/testsuite/src/manifest.rs @@ -89,7 +89,7 @@ impl TestManifest { let test_node = match test_node { Term::NamedNode(n) => n, _ => { - return Some(Err(anyhow!("Invalid test identifier. Got {}", test_node))); + return Some(Err(anyhow!("Invalid test identifier. Got {test_node}"))); } }; @@ -107,8 +107,7 @@ impl TestManifest { Some(TermRef::NamedNode(c)) => c.into_owned(), _ => { return Some(Err(anyhow!( - "The test {} named {} has no rdf:type", - test_node, + "The test {test_node} named {} has no rdf:type", name.as_deref().unwrap_or("") ))); } @@ -202,7 +201,7 @@ impl TestManifest { } Some(_) => return Some(Err(anyhow!("invalid action"))), None => { - return Some(Err(anyhow!("action not found for test {}", test_node))); + return Some(Err(anyhow!("action not found for test {test_node}"))); } }; let (result, result_graph_data) = match self @@ -276,8 +275,7 @@ impl TestManifest { .collect::>(); if manifests.len() != 1 { return Some(Err(anyhow!( - "The file {} should contain a single manifest", - url + "The file {url} should contain a single manifest" ))); } for manifest in manifests { @@ -307,7 +305,7 @@ impl TestManifest { .extend(RdfListIterator::iter(&self.graph, list.into())); } Some(term) => { - return Some(Err(anyhow!("Invalid tests list. Got term {}", term))); + return Some(Err(anyhow!("Invalid tests list. Got term {term}"))); } None => (), } diff --git a/testsuite/src/parser_evaluator.rs b/testsuite/src/parser_evaluator.rs index 70cffd89..50ac25b5 100644 --- a/testsuite/src/parser_evaluator.rs +++ b/testsuite/src/parser_evaluator.rs @@ -2,7 +2,7 @@ use crate::evaluator::TestEvaluator; use crate::files::load_dataset; use crate::manifest::Test; use crate::report::dataset_diff; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; pub fn register_parser_tests(evaluator: &mut TestEvaluator) { evaluator.register( @@ -67,20 +67,18 @@ fn evaluate_positive_syntax_test(test: &Test) -> Result<()> { let action = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; - match load_dataset(action) { - Ok(_) => Ok(()), - Err(e) => Err(anyhow!(format!("Parse error: {e}"))), - } + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + load_dataset(action).map_err(|e| anyhow!("Parse error: {e}"))?; + Ok(()) } fn evaluate_negative_syntax_test(test: &Test) -> Result<()> { let action = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; + .ok_or_else(|| anyhow!("No action found for test {test}"))?; match load_dataset(action) { - Ok(_) => Err(anyhow!("File parsed with an error even if it should not",)), + Ok(_) => bail!("File parsed with an error even if it should not"), Err(_) => Ok(()), } } @@ -89,29 +87,23 @@ fn evaluate_eval_test(test: &Test) -> Result<()> { let action = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; - match load_dataset(action) { - Ok(mut actual_graph) => { - actual_graph.canonicalize(); - if let Some(result) = &test.result { - match load_dataset(result) { - Ok(mut expected_graph) => { - expected_graph.canonicalize(); - if expected_graph == actual_graph { - Ok(()) - } else { - Err(anyhow!( - "The two files are not isomorphic. Diff:\n{}", - dataset_diff(&expected_graph, &actual_graph) - )) - } - } - Err(e) => Err(anyhow!("Parse error on file {}: {}", action, e)), - } - } else { - Err(anyhow!("No tests result found")) - } + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + let mut actual_graph = + load_dataset(action).map_err(|e| anyhow!("Parse error on file {action}: {e}"))?; + actual_graph.canonicalize(); + if let Some(result) = &test.result { + let mut expected_graph = + load_dataset(result).map_err(|e| anyhow!("Parse error on file {action}: {e}"))?; + expected_graph.canonicalize(); + if expected_graph == actual_graph { + Ok(()) + } else { + bail!( + "The two files are not isomorphic. Diff:\n{}", + dataset_diff(&expected_graph, &actual_graph) + ) } - Err(e) => Err(anyhow!("Parse error on file {}: {}", action, e)), + } else { + bail!("No tests result found") } } diff --git a/testsuite/src/sparql_evaluator.rs b/testsuite/src/sparql_evaluator.rs index e217201b..e38b5ebc 100644 --- a/testsuite/src/sparql_evaluator.rs +++ b/testsuite/src/sparql_evaluator.rs @@ -3,7 +3,7 @@ use crate::files::*; use crate::manifest::*; use crate::report::dataset_diff; use crate::vocab::*; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use oxigraph::model::vocab::*; use oxigraph::model::*; use oxigraph::sparql::*; @@ -73,32 +73,23 @@ fn evaluate_positive_syntax_test(test: &Test) -> Result<()> { let query_file = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; - match Query::parse(&read_file_to_string(query_file)?, Some(query_file)) { - Err(error) => Err(anyhow!("Not able to parse {} with error: {}", test, error)), - Ok(query) => match Query::parse(&query.to_string(), None) { - Ok(_) => Ok(()), - Err(error) => Err(anyhow!( - "Failure to deserialize \"{}\" of {} with error: {}", - query.to_string(), - test, - error - )), - }, - } + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + let query = Query::parse(&read_file_to_string(query_file)?, Some(query_file)) + .map_err(|e| anyhow!("Not able to parse {test} with error: {e}"))?; + Query::parse(&query.to_string(), None) + .map_err(|e| anyhow!("Failure to deserialize \"{query}\" of {test} with error: {e}"))?; + Ok(()) } fn evaluate_negative_syntax_test(test: &Test) -> Result<()> { let query_file = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; + .ok_or_else(|| anyhow!("No action found for test {test}"))?; match Query::parse(&read_file_to_string(query_file)?, Some(query_file)) { - Ok(result) => Err(anyhow!( - "Oxigraph parses even if it should not {}. The output tree is: {}", - test, - result - )), + Ok(result) => { + bail!("Oxigraph parses even if it should not {test}. The output tree is: {result}") + } Err(_) => Ok(()), } } @@ -109,7 +100,7 @@ fn evaluate_positive_json_result_syntax_test(test: &Test) -> Result<()> { fn evaluate_negative_json_result_syntax_test(test: &Test) -> Result<()> { if result_syntax_check(test, QueryResultsFormat::Json).is_ok() { - Err(anyhow!("Oxigraph parses even if it should not {}.", test)) + bail!("Oxigraph parses even if it should not {test}.") } else { Ok(()) } @@ -121,7 +112,7 @@ fn evaluate_positive_xml_result_syntax_test(test: &Test) -> Result<()> { fn evaluate_negative_xml_result_syntax_test(test: &Test) -> Result<()> { if result_syntax_check(test, QueryResultsFormat::Xml).is_ok() { - Err(anyhow!("Oxigraph parses even if it should not {}.", test)) + bail!("Oxigraph parses even if it should not {test}.") } else { Ok(()) } @@ -129,7 +120,7 @@ fn evaluate_negative_xml_result_syntax_test(test: &Test) -> Result<()> { fn evaluate_negative_tsv_result_syntax_test(test: &Test) -> Result<()> { if result_syntax_check(test, QueryResultsFormat::Tsv).is_ok() { - Err(anyhow!("Oxigraph parses even if it should not {}.", test)) + bail!("Oxigraph parses even if it should not {test}.") } else { Ok(()) } @@ -139,7 +130,7 @@ fn result_syntax_check(test: &Test, format: QueryResultsFormat) -> Result<()> { let results_file = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; + .ok_or_else(|| anyhow!("No action found for test {test}"))?; match QueryResults::read(Cursor::new(read_file_to_string(results_file)?), format)? { QueryResults::Solutions(solutions) => { for s in solutions { @@ -167,125 +158,83 @@ fn evaluate_evaluation_test(test: &Test) -> Result<()> { let query_file = test .query .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; + .ok_or_else(|| anyhow!("No action found for test {test}"))?; let options = QueryOptions::default() .with_service_handler(StaticServiceHandler::new(&test.service_data)?); - match Query::parse(&read_file_to_string(query_file)?, Some(query_file)) { - Err(error) => Err(anyhow!( - "Failure to parse query of {} with error: {}", - test, - error - )), - Ok(query) => { - // We check parsing roundtrip - if let Err(error) = Query::parse(&query.to_string(), None) { - return Err(anyhow!( - "Failure to deserialize \"{}\" of {} with error: {}", - query.to_string(), - test, - error - )); + let query = Query::parse(&read_file_to_string(query_file)?, Some(query_file)) + .map_err(|e| anyhow!("Failure to parse query of {test} with error: {e}"))?; + + // We check parsing roundtrip + Query::parse(&query.to_string(), None) + .map_err(|e| anyhow!("Failure to deserialize \"{query}\" of {test} with error: {e}"))?; + + // FROM and FROM NAMED support. We make sure the data is in the store + if !query.dataset().is_default_dataset() { + for graph_name in query.dataset().default_graph_graphs().unwrap_or(&[]) { + if let GraphName::NamedNode(graph_name) = graph_name { + load_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; + } else { + bail!("Invalid FROM in query {query} for test {test}"); } - - // FROM and FROM NAMED support. We make sure the data is in the store - if !query.dataset().is_default_dataset() { - for graph_name in query.dataset().default_graph_graphs().unwrap_or(&[]) { - if let GraphName::NamedNode(graph_name) = graph_name { - load_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; - } else { - return Err(anyhow!("Invalid FROM in query {} for test {}", query, test)); - } - } - for graph_name in query.dataset().available_named_graphs().unwrap_or(&[]) { - if let NamedOrBlankNode::NamedNode(graph_name) = graph_name { - load_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; - } else { - return Err(anyhow!( - "Invalid FROM NAMED in query {} for test {}", - query, - test - )); - } - } + } + for graph_name in query.dataset().available_named_graphs().unwrap_or(&[]) { + if let NamedOrBlankNode::NamedNode(graph_name) = graph_name { + load_to_store(graph_name.as_str(), &store, graph_name.as_ref())?; + } else { + bail!("Invalid FROM NAMED in query {query} for test {test}"); } + } + } - for with_query_optimizer in [true, false] { - let mut options = options.clone(); - if !with_query_optimizer { - options = options.without_optimizations(); - } - match store.query_opt(query.clone(), options) { - Err(error) => { - return Err(anyhow!( - "Failure to execute query of {} with error: {}", - test, - error - )) - } - Ok(actual_results) => { - let expected_results = load_sparql_query_result( - test.result.as_ref().unwrap(), - ) - .map_err(|e| { - anyhow!("Error constructing expected graph for {}: {}", test, e) - })?; - let with_order = if let StaticQueryResults::Solutions { ordered, .. } = - &expected_results - { - *ordered - } else { - false - }; - let actual_results = - StaticQueryResults::from_query_results(actual_results, with_order)?; - - if !are_query_results_isomorphic(&expected_results, &actual_results) { - return Err(anyhow!("Failure on {}.\nExpected file:\n{}\nOutput file:\n{}\nParsed query:\n{}\nData:\n{}\n", - test, - expected_results, - actual_results, - Query::parse(&read_file_to_string(query_file)?, Some(query_file)).unwrap(), - store - )); - } - } - } - } - Ok(()) + let expected_results = load_sparql_query_result(test.result.as_ref().unwrap()) + .map_err(|e| anyhow!("Error constructing expected graph for {test}: {e}"))?; + let with_order = if let StaticQueryResults::Solutions { ordered, .. } = &expected_results { + *ordered + } else { + false + }; + + for with_query_optimizer in [true, false] { + let mut options = options.clone(); + if !with_query_optimizer { + options = options.without_optimizations(); + } + let actual_results = store + .query_opt(query.clone(), options) + .map_err(|e| anyhow!("Failure to execute query of {test} with error: {e}"))?; + let actual_results = StaticQueryResults::from_query_results(actual_results, with_order)?; + + if !are_query_results_isomorphic(&expected_results, &actual_results) { + bail!( + "Failure on {test}.\nExpected file:\n{expected_results}\nOutput file:\n{actual_results}\nParsed query:\n{}\nData:\n{store}\n", + Query::parse(&read_file_to_string(query_file)?, Some(query_file)).unwrap() + ); } } + Ok(()) } fn evaluate_positive_update_syntax_test(test: &Test) -> Result<()> { let update_file = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; - match Update::parse(&read_file_to_string(update_file)?, Some(update_file)) { - Err(error) => Err(anyhow!("Not able to parse {} with error: {}", test, error)), - Ok(update) => match Update::parse(&update.to_string(), None) { - Ok(_) => Ok(()), - Err(error) => Err(anyhow!( - "Failure to deserialize \"{}\" of {} with error: {}", - update.to_string(), - test, - error - )), - }, - } + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + let update = Update::parse(&read_file_to_string(update_file)?, Some(update_file)) + .map_err(|e| anyhow!("Not able to parse {test} with error: {e}"))?; + Update::parse(&update.to_string(), None) + .map_err(|e| anyhow!("Failure to deserialize \"{update}\" of {test} with error: {e}"))?; + Ok(()) } fn evaluate_negative_update_syntax_test(test: &Test) -> Result<()> { let update_file = test .action .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; + .ok_or_else(|| anyhow!("No action found for test {test}"))?; match Update::parse(&read_file_to_string(update_file)?, Some(update_file)) { - Ok(result) => Err(anyhow!( - "Oxigraph parses even if it should not {}. The output tree is: {}", - test, - result - )), + Ok(result) => { + bail!("Oxigraph parses even if it should not {test}. The output tree is: {result}") + } Err(_) => Ok(()), } } @@ -310,50 +259,29 @@ fn evaluate_update_evaluation_test(test: &Test) -> Result<()> { let update_file = test .update .as_deref() - .ok_or_else(|| anyhow!("No action found for test {}", test))?; - match Update::parse(&read_file_to_string(update_file)?, Some(update_file)) { - Err(error) => Err(anyhow!( - "Failure to parse update of {} with error: {}", - test, - error - )), - Ok(update) => { - // We check parsing roundtrip - if let Err(error) = Update::parse(&update.to_string(), None) { - return Err(anyhow!( - "Failure to deserialize \"{}\" of {} with error: {}", - update.to_string(), - test, - error - )); - } - - match store.update(update) { - Err(error) => Err(anyhow!( - "Failure to execute update of {} with error: {}", - test, - error - )), - Ok(()) => { - let mut store_dataset: Dataset = store.iter().collect::>()?; - store_dataset.canonicalize(); - let mut result_store_dataset: Dataset = - result_store.iter().collect::>()?; - result_store_dataset.canonicalize(); - if store_dataset == result_store_dataset { - Ok(()) - } else { - Err(anyhow!( - "Failure on {}.\nDiff:\n{}\nParsed update:\n{}\n", - test, - dataset_diff(&result_store_dataset, &store_dataset), - Update::parse(&read_file_to_string(update_file)?, Some(update_file)) - .unwrap(), - )) - } - } - } - } + .ok_or_else(|| anyhow!("No action found for test {test}"))?; + let update = Update::parse(&read_file_to_string(update_file)?, Some(update_file)) + .map_err(|e| anyhow!("Failure to parse update of {test} with error: {e}"))?; + + // We check parsing roundtrip + Update::parse(&update.to_string(), None) + .map_err(|e| anyhow!("Failure to deserialize \"{update}\" of {test} with error: {e}"))?; + + store + .update(update) + .map_err(|e| anyhow!("Failure to execute update of {test} with error: {e}"))?; + let mut store_dataset: Dataset = store.iter().collect::>()?; + store_dataset.canonicalize(); + let mut result_store_dataset: Dataset = result_store.iter().collect::>()?; + result_store_dataset.canonicalize(); + if store_dataset == result_store_dataset { + Ok(()) + } else { + bail!( + "Failure on {test}.\nDiff:\n{}\nParsed update:\n{}\n", + dataset_diff(&result_store_dataset, &store_dataset), + Update::parse(&read_file_to_string(update_file)?, Some(update_file)).unwrap(), + ) } } @@ -624,7 +552,7 @@ impl StaticQueryResults { if let TermRef::Literal(l) = object { Ok(Variable::new_unchecked(l.value())) } else { - Err(anyhow!("Invalid rs:resultVariable: {}", object)) + bail!("Invalid rs:resultVariable: {object}") } }) .collect::>>()?; @@ -650,10 +578,10 @@ impl StaticQueryResults { value.into_owned(), )) } else { - Err(anyhow!("Invalid rs:binding: {}", binding)) + bail!("Invalid rs:binding: {binding}") } } else { - Err(anyhow!("Invalid rs:binding: {}", object)) + bail!("Invalid rs:binding: {object}") } }) .collect::>>()?; @@ -664,13 +592,13 @@ impl StaticQueryResults { if let TermRef::Literal(l) = object { Ok(u64::from_str(l.value())?) } else { - Err(anyhow!("Invalid rs:index: {}", object)) + bail!("Invalid rs:index: {object}") } }) .transpose()?; Ok((bindings, index)) } else { - Err(anyhow!("Invalid rs:solution: {}", object)) + bail!("Invalid rs:solution: {object}") } }) .collect::>>()?;