From 8d56f30ed0058b643f85461e30d35c5c5d669688 Mon Sep 17 00:00:00 2001 From: Jonathan Giroux Date: Fri, 23 Jun 2023 13:17:55 +0200 Subject: [PATCH 1/2] Handle errors in `main` with `color-eyre` --- Cargo.lock | 185 +++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 70 +++++++--------- src/wrapped_context.rs | 54 ++++++------ 4 files changed, 245 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a5c5f7..de93965 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.20" @@ -111,6 +126,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -235,6 +265,33 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "color-eyre" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1885697ee8a177096d42f158922251a41973117f6d8a234cee94b9509157b7" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -345,6 +402,16 @@ dependencies = [ "libc", ] +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -458,6 +525,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + [[package]] name = "globset" version = "0.4.10" @@ -555,6 +628,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -677,6 +756,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -692,12 +780,27 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "owo-colors" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" + [[package]] name = "parse-zoneinfo" version = "0.3.0" @@ -796,6 +899,12 @@ dependencies = [ "uncased", ] +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -910,6 +1019,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1007,6 +1122,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -1116,6 +1240,7 @@ version = "0.2.4" dependencies = [ "assert_cmd", "clap", + "color-eyre", "env_logger", "fluent-templates", "log", @@ -1214,6 +1339,60 @@ dependencies = [ "winnow", ] +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + [[package]] name = "type-map" version = "0.4.0" @@ -1370,6 +1549,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index d7aec51..a8fb73f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ fluent = ["fluent-templates"] [dependencies] clap = { version = "4", features = ["derive", "env", "unicode", "cargo"] } +color-eyre = "0.5" env_logger = "0.10" fluent-templates = { version = "0.8", optional = true, default-features = false, features = ["tera"]} log = "0.4" diff --git a/src/main.rs b/src/main.rs index b407b58..4d61b32 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,17 @@ +#![deny(clippy::expect_used)] +#![deny(clippy::unwrap_used)] + mod opts; mod template; mod wrapped_context; use crate::template::Template; use clap::{crate_name, crate_version, Parser}; +use color_eyre::eyre::{Context as EyreContext, ContextCompat, Result}; use env_logger::Env; use log::{debug, info, trace}; use opts::*; -use std::{fs::canonicalize, fs::File, io::Write, string::String}; +use std::{fs::canonicalize, fs::File, io::Write}; use tera::{Context, Tera}; #[cfg(feature = "fluent")] @@ -16,66 +20,49 @@ use fluent_templates::{ArcLoader, FluentLoader, LanguageIdentifier}; #[cfg(feature = "fluent")] use std::env; -fn main() -> Result<(), String> { +fn main() -> Result<()> { + color_eyre::install()?; + env_logger::Builder::from_env(Env::default().default_filter_or("none")).init(); info!("Running {} v{}", crate_name!(), crate_version!()); - let opts: Opts = Opts::parse(); + let opts = Opts::parse(); debug!("opts:\n{:#?}", opts); - let template = Template::load(&opts.template).expect("Failed reading the template"); + let template = Template::load(&opts.template).context("failed to read the template")?; trace!("template:\n{}", template); let autoescape = opts.autoescape; - let output = opts.out.to_owned(); + let output = opts.out.clone(); let mut include = opts.include; - let mut path = canonicalize(&opts.template).unwrap(); + let mut path = canonicalize(&opts.template).context("failed to get absolute path to `template`")?; - if opts.include_path.is_some() { + if let Some(include_path) = &opts.include_path { include = true; - path = canonicalize(opts.include_path.as_ref().unwrap()).unwrap(); + path = canonicalize(include_path).context("failed to get absolute path to `include`")?; } #[cfg(feature = "fluent")] - let locale: LanguageIdentifier = match opts.locale.to_owned() { - Some(locale) => locale.parse(), - None => "und".parse(), - } - .unwrap(); + let locale: LanguageIdentifier = opts.locale.as_deref().unwrap_or("und").parse().context("failed to parse locale")?; #[cfg(feature = "fluent")] - let locales_path = match opts.locales_path.to_owned() { - Some(path) => path, - None => "./locales".into(), - }; + let locales_path = opts.locales_path.clone().unwrap_or_else(|| "./locales".into()); let mut wrapped_context = wrapped_context::WrappedContext::new(opts); - wrapped_context.create_context(); + wrapped_context.create_context().context("failed to create context")?; let context: &Context = wrapped_context.context(); trace!("context:\n{:#?}", context); - let mut tera: Tera; - - if include { - let mut dir = path.to_str().unwrap(); + let mut tera = if include { + let dir = if path.is_file() { path.parent().context("failed to get parent directory")? } else { &path }; - if path.is_file() { - dir = path.parent().unwrap().to_str().unwrap(); - } - - let glob = dir.to_owned() + "/**/*"; + let glob = format!("{}/**/*", dir.to_str().context("invalid UTF8 string")?); - tera = match Tera::new(&glob) { - Ok(t) => t, - Err(e) => { - println!("Parsing error(s): {e}"); - ::std::process::exit(1); - } - }; + Tera::new(&glob)? } else { - tera = Tera::default(); - } + Tera::default() + }; if !autoescape { tera.autoescape_on(vec![]) @@ -87,18 +74,19 @@ fn main() -> Result<(), String> { ArcLoader::builder(&locales_path, locale.clone()).customize(|bundle| bundle.set_use_isolating(false)); if let Ok(locale_loader) = builder.build() { let ftls = FluentLoader::new(locale_loader).with_default_lang(locale); - tera.register_function("fluent", ftls) + tera.register_function("fluent", ftls); } }; - let rendered = tera.render_str(&template, context).unwrap(); + let rendered = tera.render_str(&template, context).context("failed to render")?; if let Some(out_file) = output { debug!("Saving to {}", out_file.display()); - let mut file = File::create(out_file).expect("Failed opening output file"); - file.write_all(rendered.as_bytes()).map_err(|e| e.to_string()) + let mut file = File::create(out_file).context("failed to open output file")?; + file.write_all(rendered.as_bytes()).context("failed to write to output file")?; } else { println!("{rendered}"); - Ok(()) } + + Ok(()) } diff --git a/src/wrapped_context.rs b/src/wrapped_context.rs index 0dc7b67..9360edf 100644 --- a/src/wrapped_context.rs +++ b/src/wrapped_context.rs @@ -1,4 +1,5 @@ use crate::opts::Opts; +use color_eyre::eyre::{bail, Context as EyreContext, ContextCompat, Result}; use log::{debug, info, trace, warn}; use serde_json::{self, json}; use std::{ @@ -35,35 +36,41 @@ impl WrappedContext { &self.context } - pub fn append_json(&mut self, str: &str) { + pub fn append_json(&mut self, str: &str) -> Result<()> { debug!("Appending json"); - let json = str.parse::().expect("JSON parsing"); - let object = json.as_object().expect("JSON as object"); + let json = str.parse::().context("failed to parse JSON")?; + let object = json.as_object().context("JSON value must be an object")?; for (k, v) in object.iter() { self.handle_collision("json", k, v); } + + Ok(()) } - pub fn append_toml(&mut self, str: &str) { + pub fn append_toml(&mut self, str: &str) -> Result<()> { debug!("Appending toml"); - let value = str.parse::().expect("TOML Parsing"); - let table = value.as_table().expect("TOML as table"); + let value = str.parse::().context("failed to parse TOML")?; + let table = value.as_table().context("TOML value must be a table")?; for (k, v) in table.iter() { self.handle_collision("toml", k, v); } + + Ok(()) } - pub fn append_yaml(&mut self, str: &str) { + pub fn append_yaml(&mut self, str: &str) -> Result<()> { debug!("Appending yaml"); - let value: serde_yaml::Value = serde_yaml::from_str(str).expect("YAML parsing"); - let mapping = value.as_mapping().expect("YAML as mapping"); + let value: serde_yaml::Value = serde_yaml::from_str(str).context("failed to parse YAML")?; + let mapping = value.as_mapping().context("YAML value must be a mapping")?; for (k, v) in mapping.iter() { - let k = k.as_str().unwrap(); + let k = k.as_str().context("YAML mapping's key must be a string")?; self.handle_collision("yaml", k, v); } + + Ok(()) } fn handle_collision(&mut self, from: &str, k: K, v: V) @@ -134,7 +141,7 @@ impl WrappedContext { None } - pub fn create_context(&mut self) { + pub fn create_context(&mut self) -> Result<()> { if (self.opts.env || self.opts.env_only) && self.opts.env_first { info!("Appending env to context first, env-key: {:?}", self.opts.env_key); self.append_env(); @@ -144,35 +151,34 @@ impl WrappedContext { let stdin = io::stdin(); let mut stdin = stdin.lock(); let mut buf: Vec = Vec::with_capacity(BUFFER_SIZE); - let res = stdin.read_to_end(&mut buf).map_err(|e| e.to_string()); - res.expect("Failed reading stdin"); - let input = String::from_utf8(buf.to_vec()).unwrap(); + stdin.read_to_end(&mut buf).context("failed to read stdin")?; + let input = String::from_utf8(buf.to_vec()).context("invalid UTF8 string")?; match Self::get_type(&input) { Some(SupportedType::Json) if !input.is_empty() => self.append_json(&input), Some(SupportedType::Toml) if !input.is_empty() => self.append_toml(&input), Some(SupportedType::Yaml) if !input.is_empty() => self.append_yaml(&input), - _ => {} + _ => Ok(()), } - } else if self.opts.context.is_some() { - // here we know that we have a Path since --stdin is not passed - let context_file = self.opts.context.as_ref().unwrap(); - let input = fs::read_to_string(context_file).unwrap(); + .context("failed to append stdin to context")?; + } else if let Some(context_file) = &self.opts.context { + let input = fs::read_to_string(context_file).context("failed to read context file")?; match context_file.extension() { Some(ext) if ext == "json" => self.append_json(&input), Some(ext) if ext == "toml" => self.append_toml(&input), Some(ext) if ext == "yaml" || ext == "yml" => self.append_yaml(&input), - ext => { - panic!("Extension not supported: {ext:?}") - } - }; - }; + ext => bail!("extension not supported: {ext:?}"), + } + .context("failed to append file to context")?; + } if (self.opts.env || self.opts.env_only) && !self.opts.env_first { info!("Appending env to context, env-key: {:?}", self.opts.env_key); self.append_env(); } + + Ok(()) } } From 429fa34407cc848a6eb03dad0a0c6d2fbd591447 Mon Sep 17 00:00:00 2001 From: Jonathan Giroux Date: Fri, 23 Jun 2023 13:48:52 +0200 Subject: [PATCH 2/2] Add test for `throw` --- data/throw/throw.tera | 1 + data/throw/throw.toml | 0 tests/test.rs | 7 +++++++ 3 files changed, 8 insertions(+) create mode 100644 data/throw/throw.tera create mode 100644 data/throw/throw.toml diff --git a/data/throw/throw.tera b/data/throw/throw.tera new file mode 100644 index 0000000..b8bb9bd --- /dev/null +++ b/data/throw/throw.tera @@ -0,0 +1 @@ +{{ throw(message="some failure") }} diff --git a/data/throw/throw.toml b/data/throw/throw.toml new file mode 100644 index 0000000..e69de29 diff --git a/tests/test.rs b/tests/test.rs index 0099b84..a35ebb0 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -250,5 +250,12 @@ mod cli_tests { let assert = cmd.arg("-t").arg("data/cargo-toml/cargo-toml.tera").arg("Cargo.toml").assert(); assert.success().code(0); } + + #[test] + fn it_handles_error() { + let mut cmd = Command::cargo_bin("tera").unwrap(); + let assert = cmd.arg("-t").arg("data/throw/throw.tera").arg("data/throw/throw.toml").assert(); + assert.failure().code(1); + } } }