From 452648e36cc9931ca6cb25c056547b6a8e6e44da Mon Sep 17 00:00:00 2001 From: Mehul Mathur Date: Mon, 26 Aug 2024 23:52:08 +0530 Subject: [PATCH] refactor: move cli error to core (#2708) Co-authored-by: Tushar Mathur --- src/cli/mod.rs | 2 - src/cli/runtime/file.rs | 7 +- src/cli/server/http_1.rs | 6 +- src/cli/server/http_2.rs | 4 +- src/cli/server/http_server.rs | 4 +- src/cli/tc/check.rs | 4 +- src/cli/telemetry.rs | 6 +- src/{cli/error.rs => core/errata.rs} | 113 ++++++++++++++------------- src/core/grpc/request.rs | 2 +- src/core/http/response.rs | 6 +- src/core/ir/error.rs | 69 +++++++++++----- src/core/ir/eval.rs | 7 +- src/core/ir/eval_http.rs | 2 +- src/core/mod.rs | 2 + src/main.rs | 6 +- 15 files changed, 131 insertions(+), 109 deletions(-) rename src/{cli/error.rs => core/errata.rs} (78%) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 59798bc6b5..eebb64373a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,5 +1,4 @@ pub mod command; -mod error; mod fmt; pub mod generator; #[cfg(feature = "js")] @@ -11,5 +10,4 @@ pub mod server; mod tc; pub mod telemetry; pub(crate) mod update_checker; -pub use error::CLIError; pub use tc::run::run; diff --git a/src/cli/runtime/file.rs b/src/cli/runtime/file.rs index 3d9be7ed77..72a55160c6 100644 --- a/src/cli/runtime/file.rs +++ b/src/cli/runtime/file.rs @@ -1,7 +1,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use crate::cli::CLIError; -use crate::core::FileIO; +use crate::core::{Errata, FileIO}; #[derive(Clone)] pub struct NativeFileIO {} @@ -29,7 +28,7 @@ async fn write<'a>(path: &'a str, content: &'a [u8]) -> anyhow::Result<()> { impl FileIO for NativeFileIO { async fn write<'a>(&'a self, path: &'a str, content: &'a [u8]) -> anyhow::Result<()> { write(path, content).await.map_err(|err| { - CLIError::new(format!("Failed to write file: {}", path).as_str()) + Errata::new(format!("Failed to write file: {}", path).as_str()) .description(err.to_string()) })?; tracing::info!("File write: {} ... ok", path); @@ -38,7 +37,7 @@ impl FileIO for NativeFileIO { async fn read<'a>(&'a self, path: &'a str) -> anyhow::Result { let content = read(path).await.map_err(|err| { - CLIError::new(format!("Failed to read file: {}", path).as_str()) + Errata::new(format!("Failed to read file: {}", path).as_str()) .description(err.to_string()) })?; tracing::info!("File read: {} ... ok", path); diff --git a/src/cli/server/http_1.rs b/src/cli/server/http_1.rs index 22d7d97c96..76360e860a 100644 --- a/src/cli/server/http_1.rs +++ b/src/cli/server/http_1.rs @@ -4,9 +4,9 @@ use hyper::service::{make_service_fn, service_fn}; use tokio::sync::oneshot; use super::server_config::ServerConfig; -use crate::cli::CLIError; use crate::core::async_graphql_hyper::{GraphQLBatchRequest, GraphQLRequest}; use crate::core::http::handle_request; +use crate::core::Errata; pub async fn start_http_1( sc: Arc, @@ -31,7 +31,7 @@ pub async fn start_http_1( } }); let builder = hyper::Server::try_bind(&addr) - .map_err(CLIError::from)? + .map_err(Errata::from)? .http1_pipeline_flush(sc.app_ctx.blueprint.server.pipeline_flush); super::log_launch(sc.as_ref()); @@ -48,7 +48,7 @@ pub async fn start_http_1( builder.serve(make_svc_single_req).await }; - let result = server.map_err(CLIError::from); + let result = server.map_err(Errata::from); Ok(result?) } diff --git a/src/cli/server/http_2.rs b/src/cli/server/http_2.rs index 30ee21b5d1..1895789603 100644 --- a/src/cli/server/http_2.rs +++ b/src/cli/server/http_2.rs @@ -9,9 +9,9 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use tokio::sync::oneshot; use super::server_config::ServerConfig; -use crate::cli::CLIError; use crate::core::async_graphql_hyper::{GraphQLBatchRequest, GraphQLRequest}; use crate::core::http::handle_request; +use crate::core::Errata; pub async fn start_http_2( sc: Arc, @@ -60,7 +60,7 @@ pub async fn start_http_2( builder.serve(make_svc_single_req).await }; - let result = server.map_err(CLIError::from); + let result = server.map_err(Errata::from); Ok(result?) } diff --git a/src/cli/server/http_server.rs b/src/cli/server/http_server.rs index 62928c492f..3661c9f5f7 100644 --- a/src/cli/server/http_server.rs +++ b/src/cli/server/http_server.rs @@ -8,9 +8,9 @@ use super::http_1::start_http_1; use super::http_2::start_http_2; use super::server_config::ServerConfig; use crate::cli::telemetry::init_opentelemetry; -use crate::cli::CLIError; use crate::core::blueprint::{Blueprint, Http}; use crate::core::config::ConfigModule; +use crate::core::Errata; pub struct Server { config_module: ConfigModule, @@ -32,7 +32,7 @@ impl Server { /// Starts the server in the current Runtime pub async fn start(self) -> Result<()> { - let blueprint = Blueprint::try_from(&self.config_module).map_err(CLIError::from)?; + let blueprint = Blueprint::try_from(&self.config_module).map_err(Errata::from)?; let endpoints = self.config_module.extensions().endpoint_set.clone(); let server_config = Arc::new(ServerConfig::new(blueprint.clone(), endpoints).await?); diff --git a/src/cli/tc/check.rs b/src/cli/tc/check.rs index 9e41cb7a9d..6816836092 100644 --- a/src/cli/tc/check.rs +++ b/src/cli/tc/check.rs @@ -2,11 +2,11 @@ use anyhow::Result; use super::helpers::{display_schema, log_endpoint_set}; use crate::cli::fmt::Fmt; -use crate::cli::CLIError; use crate::core::blueprint::Blueprint; use crate::core::config::reader::ConfigReader; use crate::core::config::Source; use crate::core::runtime::TargetRuntime; +use crate::core::Errata; pub(super) struct CheckParams { pub(super) file_paths: Vec, @@ -24,7 +24,7 @@ pub(super) async fn check_command(params: CheckParams, config_reader: &ConfigRea if let Some(format) = format { Fmt::display(format.encode(&config_module)?); } - let blueprint = Blueprint::try_from(&config_module).map_err(CLIError::from); + let blueprint = Blueprint::try_from(&config_module).map_err(Errata::from); match blueprint { Ok(blueprint) => { diff --git a/src/cli/telemetry.rs b/src/cli/telemetry.rs index 46a64cba6c..50ea364d41 100644 --- a/src/cli/telemetry.rs +++ b/src/cli/telemetry.rs @@ -24,12 +24,12 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::{Layer, Registry}; use super::metrics::init_metrics; -use crate::cli::CLIError; use crate::core::blueprint::telemetry::{OtlpExporter, Telemetry, TelemetryExporter}; use crate::core::runtime::TargetRuntime; use crate::core::tracing::{ default_tracing, default_tracing_tailcall, get_log_level, tailcall_filter_target, }; +use crate::core::Errata; static RESOURCE: Lazy = Lazy::new(|| { Resource::default().merge(&Resource::new(vec![ @@ -206,8 +206,8 @@ pub fn init_opentelemetry(config: Telemetry, runtime: &TargetRuntime) -> anyhow: | global::Error::Log(LogError::Other(_)), ) { tracing::subscriber::with_default(default_tracing_tailcall(), || { - let cli = crate::cli::CLIError::new("Open Telemetry Error") - .caused_by(vec![CLIError::new(error.to_string().as_str())]) + let cli = crate::core::Errata::new("Open Telemetry Error") + .caused_by(vec![Errata::new(error.to_string().as_str())]) .trace(vec!["schema".to_string(), "@telemetry".to_string()]); tracing::error!("{}", cli.color(true)); }); diff --git a/src/cli/error.rs b/src/core/errata.rs similarity index 78% rename from src/cli/error.rs rename to src/core/errata.rs index 50e06e5301..7cc493ef3b 100644 --- a/src/cli/error.rs +++ b/src/core/errata.rs @@ -2,12 +2,15 @@ use std::fmt::{Debug, Display}; use colored::Colorize; use derive_setters::Setters; -use thiserror::Error; +use crate::core::error::Error as CoreError; use crate::core::valid::ValidationError; -#[derive(Debug, Error, Setters, PartialEq, Clone)] -pub struct CLIError { +/// The moral equivalent of a serde_json::Value but for errors. +/// It's a data structure like Value that can hold any error in an untyped +/// manner. +#[derive(Debug, thiserror::Error, Setters, PartialEq, Clone)] +pub struct Errata { is_root: bool, #[setters(skip)] color: bool, @@ -17,12 +20,12 @@ pub struct CLIError { trace: Vec, #[setters(skip)] - caused_by: Vec, + caused_by: Vec, } -impl CLIError { +impl Errata { pub fn new(message: &str) -> Self { - CLIError { + Errata { is_root: true, color: false, message: message.to_string(), @@ -32,7 +35,7 @@ impl CLIError { } } - pub fn caused_by(mut self, error: Vec) -> Self { + pub fn caused_by(mut self, error: Vec) -> Self { self.caused_by = error; for error in self.caused_by.iter_mut() { @@ -82,7 +85,7 @@ fn bullet(str: &str) -> String { chars.into_iter().collect::() } -impl Display for CLIError { +impl Display for Errata { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let default_padding = 2; @@ -132,46 +135,37 @@ impl Display for CLIError { } } -impl From for CLIError { +impl From for Errata { fn from(error: hyper::Error) -> Self { - // TODO: add type-safety to CLIError conversion - let cli_error = CLIError::new("Server Failed"); + // TODO: add type-safety to Errata conversion + let cli_error = Errata::new("Server Failed"); let message = error.to_string(); if message.to_lowercase().contains("os error 48") { cli_error .description("The port is already in use".to_string()) - .caused_by(vec![CLIError::new(message.as_str())]) + .caused_by(vec![Errata::new(message.as_str())]) } else { cli_error.description(message) } } } -impl From for CLIError { - fn from(error: rustls::Error) -> Self { - let cli_error = CLIError::new("Failed to create TLS Acceptor"); - let message = error.to_string(); - - cli_error.description(message) - } -} - -impl From for CLIError { +impl From for Errata { fn from(error: anyhow::Error) -> Self { - // Convert other errors to CLIError - let cli_error = match error.downcast::() { + // Convert other errors to Errata + let cli_error = match error.downcast::() { Ok(cli_error) => cli_error, Err(error) => { - // Convert other errors to CLIError + // Convert other errors to Errata let cli_error = match error.downcast::>() { - Ok(validation_error) => CLIError::from(validation_error), + Ok(validation_error) => Errata::from(validation_error), Err(error) => { let sources = error .source() - .map(|error| vec![CLIError::new(error.to_string().as_str())]) + .map(|error| vec![Errata::new(error.to_string().as_str())]) .unwrap_or_default(); - CLIError::new(&error.to_string()).caused_by(sources) + Errata::new(&error.to_string()).caused_by(sources) } }; cli_error @@ -181,24 +175,32 @@ impl From for CLIError { } } -impl From for CLIError { +impl From for Errata { fn from(error: std::io::Error) -> Self { - let cli_error = CLIError::new("IO Error"); + let cli_error = Errata::new("IO Error"); let message = error.to_string(); cli_error.description(message) } } -impl<'a> From> for CLIError { +impl From for Errata { + fn from(error: CoreError) -> Self { + let cli_error = Errata::new("Core Error"); + let message = error.to_string(); + + cli_error.description(message) + } +} + +impl<'a> From> for Errata { fn from(error: ValidationError<&'a str>) -> Self { - CLIError::new("Invalid Configuration").caused_by( + Errata::new("Invalid Configuration").caused_by( error .as_vec() .iter() .map(|cause| { - let mut err = - CLIError::new(cause.message).trace(Vec::from(cause.trace.clone())); + let mut err = Errata::new(cause.message).trace(Vec::from(cause.trace.clone())); if let Some(description) = cause.description { err = err.description(description.to_owned()); } @@ -209,29 +211,28 @@ impl<'a> From> for CLIError { } } -impl From> for CLIError { +impl From> for Errata { fn from(error: ValidationError) -> Self { - CLIError::new("Invalid Configuration").caused_by( + Errata::new("Invalid Configuration").caused_by( error .as_vec() .iter() .map(|cause| { - CLIError::new(cause.message.as_str()).trace(Vec::from(cause.trace.clone())) + Errata::new(cause.message.as_str()).trace(Vec::from(cause.trace.clone())) }) .collect(), ) } } -impl From> for CLIError { +impl From> for Errata { fn from(value: Box) -> Self { - CLIError::new(value.to_string().as_str()) + Errata::new(value.to_string().as_str()) } } #[cfg(test)] mod tests { - use pretty_assertions::assert_eq; use stripmargin::StripMargin; @@ -275,14 +276,14 @@ mod tests { #[test] fn test_title() { - let error = CLIError::new("Server could not be started"); + let error = Errata::new("Server could not be started"); let expected = r"Server could not be started".strip_margin(); assert_eq!(error.to_string(), expected); } #[test] fn test_title_description() { - let error = CLIError::new("Server could not be started") + let error = Errata::new("Server could not be started") .description("The port is already in use".to_string()); let expected = r"|Server could not be started: The port is already in use".strip_margin(); @@ -291,7 +292,7 @@ mod tests { #[test] fn test_title_description_trace() { - let error = CLIError::new("Server could not be started") + let error = Errata::new("Server could not be started") .description("The port is already in use".to_string()) .trace(vec!["@server".into(), "port".into()]); @@ -304,7 +305,7 @@ mod tests { #[test] fn test_title_trace_caused_by() { - let error = CLIError::new("Configuration Error").caused_by(vec![CLIError::new( + let error = Errata::new("Configuration Error").caused_by(vec![Errata::new( "Base URL needs to be specified", ) .trace(vec![ @@ -324,20 +325,20 @@ mod tests { #[test] fn test_title_trace_multiple_caused_by() { - let error = CLIError::new("Configuration Error").caused_by(vec![ - CLIError::new("Base URL needs to be specified").trace(vec![ + let error = Errata::new("Configuration Error").caused_by(vec![ + Errata::new("Base URL needs to be specified").trace(vec![ "User".into(), "posts".into(), "@http".into(), "baseURL".into(), ]), - CLIError::new("Base URL needs to be specified").trace(vec![ + Errata::new("Base URL needs to be specified").trace(vec![ "Post".into(), "users".into(), "@http".into(), "baseURL".into(), ]), - CLIError::new("Base URL needs to be specified") + Errata::new("Base URL needs to be specified") .description("Set `baseURL` in @http or @server directives".into()) .trace(vec![ "Query".into(), @@ -345,7 +346,7 @@ mod tests { "@http".into(), "baseURL".into(), ]), - CLIError::new("Base URL needs to be specified").trace(vec![ + Errata::new("Base URL needs to be specified").trace(vec![ "Query".into(), "posts".into(), "@http".into(), @@ -370,7 +371,7 @@ mod tests { .description("Set `baseURL` in @http or @server directives") .trace(vec!["Query", "users", "@http", "baseURL"]); let valid = ValidationError::from(cause); - let error = CLIError::from(valid); + let error = Errata::from(valid); let expected = r"|Invalid Configuration |Caused by: | • Base URL needs to be specified: Set `baseURL` in @http or @server directives [at Query.users.@http.baseURL]" @@ -381,12 +382,12 @@ mod tests { #[test] fn test_cli_error_identity() { - let cli_error = CLIError::new("Server could not be started") + let cli_error = Errata::new("Server could not be started") .description("The port is already in use".to_string()) .trace(vec!["@server".into(), "port".into()]); let anyhow_error: anyhow::Error = cli_error.clone().into(); - let actual = CLIError::from(anyhow_error); + let actual = Errata::from(anyhow_error); let expected = cli_error; assert_eq!(actual, expected); @@ -399,8 +400,8 @@ mod tests { ); let anyhow_error: anyhow::Error = validation_error.clone().into(); - let actual = CLIError::from(anyhow_error); - let expected = CLIError::from(validation_error); + let actual = Errata::from(anyhow_error); + let expected = Errata::from(validation_error); assert_eq!(actual, expected); } @@ -409,8 +410,8 @@ mod tests { fn test_generic_error() { let anyhow_error = anyhow::anyhow!("Some error msg"); - let actual: CLIError = CLIError::from(anyhow_error); - let expected = CLIError::new("Some error msg"); + let actual: Errata = Errata::from(anyhow_error); + let expected = Errata::new("Some error msg"); assert_eq!(actual, expected); } diff --git a/src/core/grpc/request.rs b/src/core/grpc/request.rs index c7e28b53e3..7bea4ccd68 100644 --- a/src/core/grpc/request.rs +++ b/src/core/grpc/request.rs @@ -160,7 +160,7 @@ mod tests { if let Err(err) = result { match err.downcast_ref::() { - Some(Error::GRPCError { + Some(Error::GRPC { grpc_code, grpc_description, grpc_status_message, diff --git a/src/core/http/response.rs b/src/core/http/response.rs index 2bb28e2b93..710ab0ac1a 100644 --- a/src/core/http/response.rs +++ b/src/core/http/response.rs @@ -102,9 +102,7 @@ impl Response { pub fn to_grpc_error(&self, operation: &ProtobufOperation) -> anyhow::Error { let grpc_status = match Status::from_header_map(&self.headers) { Some(status) => status, - None => { - return Error::IOException("Error while parsing upstream headers".to_owned()).into() - } + None => return Error::IO("Error while parsing upstream headers".to_owned()).into(), }; let mut obj: IndexMap = IndexMap::new(); @@ -136,7 +134,7 @@ impl Response { } obj.insert(Name::new("details"), ConstValue::List(status_details)); - let error = Error::GRPCError { + let error = Error::GRPC { grpc_code: grpc_status.code() as i32, grpc_description: grpc_status.code().description().to_owned(), grpc_status_message: grpc_status.message().to_owned(), diff --git a/src/core/ir/error.rs b/src/core/ir/error.rs index e08523bb71..2bcd513f83 100644 --- a/src/core/ir/error.rs +++ b/src/core/ir/error.rs @@ -1,48 +1,75 @@ +use std::fmt::Display; use std::sync::Arc; use async_graphql::{ErrorExtensions, Value as ConstValue}; use derive_more::From; use thiserror::Error; -use crate::core::{auth, cache, worker}; +use crate::core::{auth, cache, worker, Errata}; #[derive(From, Debug, Error, Clone)] pub enum Error { - #[error("IOException: {0}")] - IOException(String), + IO(String), - #[error("gRPC Error: status: {grpc_code}, description: `{grpc_description}`, message: `{grpc_status_message}`")] - GRPCError { + GRPC { grpc_code: i32, grpc_description: String, grpc_status_message: String, grpc_status_details: ConstValue, }, - #[error("APIValidationError: {0:?}")] - APIValidationError(Vec), + APIValidation(Vec), - #[error("ExprEvalError: {0}")] #[from(ignore)] - ExprEvalError(String), + ExprEval(String), - #[error("DeserializeError: {0}")] #[from(ignore)] - DeserializeError(String), + Deserialize(String), - #[error("Authentication Failure: {0}")] - AuthError(auth::error::Error), + Auth(auth::error::Error), - #[error("Worker Error: {0}")] - WorkerError(worker::Error), + Worker(worker::Error), - #[error("Cache Error: {0}")] - CacheError(cache::Error), + Cache(cache::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Errata::from(self.to_owned()).fmt(f) + } +} + +impl From for Errata { + fn from(value: Error) -> Self { + match value { + Error::IO(message) => Errata::new("IOException").description(message), + Error::GRPC { + grpc_code, + grpc_description, + grpc_status_message, + grpc_status_details: _, + } => Errata::new("gRPC Error") + .description(format!("status: {grpc_code}, description: `{grpc_description}`, message: `{grpc_status_message}`")), + Error::APIValidation(errors) => Errata::new("API Validation Error") + .caused_by(errors.iter().map(|e| Errata::new(e)).collect::>()), + Error::Deserialize(message) => { + Errata::new("Deserialization Error").description(message) + } + Error::ExprEval(message) => { + Errata::new("Expression Evaluation Error").description(message) + } + Error::Auth(err) => { + Errata::new("Authentication Failure").description(err.to_string()) + } + Error::Worker(err) => Errata::new("Worker Error").description(err.to_string()), + Error::Cache(err) => Errata::new("Cache Error").description(err.to_string()), + } + } } impl ErrorExtensions for Error { fn extend(&self) -> async_graphql::Error { async_graphql::Error::new(format!("{}", self)).extend_with(|_err, e| { - if let Error::GRPCError { + if let Error::GRPC { grpc_code, grpc_description, grpc_status_message, @@ -60,7 +87,7 @@ impl ErrorExtensions for Error { impl<'a> From> for Error { fn from(value: crate::core::valid::ValidationError<&'a str>) -> Self { - Error::APIValidationError( + Error::APIValidation( value .as_vec() .iter() @@ -74,7 +101,7 @@ impl From> for Error { fn from(error: Arc) -> Self { match error.downcast_ref::() { Some(err) => err.clone(), - None => Error::IOException(error.to_string()), + None => Error::IO(error.to_string()), } } } @@ -86,7 +113,7 @@ impl From for Error { fn from(value: anyhow::Error) -> Self { match value.downcast::() { Ok(err) => err, - Err(err) => Error::IOException(err.to_string()), + Err(err) => Error::IO(err.to_string()), } } } diff --git a/src/core/ir/eval.rs b/src/core/ir/eval.rs index 6bbf8871bd..c0d8c2356a 100644 --- a/src/core/ir/eval.rs +++ b/src/core/ir/eval.rs @@ -72,13 +72,10 @@ impl IR { if let Some(value) = map.get(&key) { Ok(ConstValue::String(value.to_owned())) } else { - Err(Error::ExprEvalError(format!( - "Can't find mapped key: {}.", - key - ))) + Err(Error::ExprEval(format!("Can't find mapped key: {}.", key))) } } else { - Err(Error::ExprEvalError( + Err(Error::ExprEval( "Mapped key must be string value.".to_owned(), )) } diff --git a/src/core/ir/eval_http.rs b/src/core/ir/eval_http.rs index bc99ef72a9..446eb9009a 100644 --- a/src/core/ir/eval_http.rs +++ b/src/core/ir/eval_http.rs @@ -236,7 +236,7 @@ pub fn parse_graphql_response( field_name: &str, ) -> Result { let res: async_graphql::Response = - from_value(res.body).map_err(|err| Error::DeserializeError(err.to_string()))?; + from_value(res.body).map_err(|err| Error::Deserialize(err.to_string()))?; for error in res.errors { ctx.add_error(error); diff --git a/src/core/mod.rs b/src/core/mod.rs index 5ca24f55e9..a885e5ab4f 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -12,6 +12,7 @@ pub mod data_loader; pub mod directive; pub mod document; pub mod endpoint; +mod errata; pub mod error; pub mod generator; pub mod graphql; @@ -47,6 +48,7 @@ use std::hash::Hash; use std::num::NonZeroU64; use async_graphql_value::ConstValue; +pub use errata::Errata; pub use error::{Error, Result}; use http::Response; use ir::model::IoId; diff --git a/src/main.rs b/src/main.rs index 3e912d28ad..b2dc98e001 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,8 +3,8 @@ use std::cell::Cell; -use tailcall::cli::CLIError; use tailcall::core::tracing::default_tracing_tailcall; +use tailcall::core::Errata; use tracing::subscriber::DefaultGuard; thread_local! { @@ -42,8 +42,8 @@ fn main() -> anyhow::Result<()> { match result { Ok(_) => {} Err(error) => { - // Ensure all errors are converted to CLIErrors before being printed. - let cli_error: CLIError = error.into(); + // Ensure all errors are converted to Errata before being printed. + let cli_error: Errata = error.into(); tracing::error!("{}", cli_error.color(true)); std::process::exit(exitcode::CONFIG); }