diff --git a/Cargo.lock b/Cargo.lock index 83aea1ac..7960cdb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,12 +30,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "anyhow" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" - [[package]] name = "atty" version = "0.2.14" @@ -73,11 +67,11 @@ name = "dsync" version = "0.0.16" dependencies = [ "Inflector", - "anyhow", "indoc", "proc-macro2", "structopt", "syn", + "thiserror", ] [[package]] @@ -231,6 +225,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index 1d4cd16d..68e4afb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,17 +11,18 @@ authors = ["Haris <4259838+Wulf@users.noreply.github.com>"] edition = "2021" [features] -default = ["tsync"] +default = ["tsync", "backtrace"] tsync = [] async = [] +backtrace = [] [dependencies] structopt = "0.3" syn = { version = "1", features = ["extra-traits", "full"] } -anyhow = "1" proc-macro2 = "1" indoc = "2.0.0" Inflector = { version = "0.11.4" } +thiserror = "1.0" [lib] path = "src/lib.rs" diff --git a/src/bin/main.rs b/src/bin/main.rs index ae7cd2bc..0e3c7b67 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -75,6 +75,32 @@ struct Args { } fn main() { + let res = actual_main(); + + if let Err(err) = res { + eprintln!("Error:\n{err}"); + #[cfg(feature = "backtrace")] + { + let backtrace = err.backtrace().to_string(); + + if backtrace == "disabled backtrace" { + eprintln!( + "note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace" + ); + } else { + eprintln!("{}", backtrace); + } + } + #[cfg(not(feature = "backtrace"))] + { + eprintln!("backtrace support is disabled, enable feature \"backtrace\""); + } + + std::process::exit(1); + } +} + +fn actual_main() -> dsync::Result<()> { let args: Args = Args::from_args(); let cols = args.autogenerated_columns.unwrap_or_default(); let mut default_table_options = TableOptions::default() @@ -104,5 +130,7 @@ fn main() { schema_path: args.schema_path.unwrap_or("crate::schema::".to_owned()), model_path: args.model_path.unwrap_or("crate::models::".to_owned()), }, - ); + )?; + + Ok(()) } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..1bd7a8e3 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,169 @@ +// TODO: change backtrace implementation to be by thiserror, if possible once features become stable +// error_generic_member_access https://github.com/rust-lang/rust/issues/99301 +// provide_any https://github.com/rust-lang/rust/issues/96024 + +use std::{io::Error as ioError, path::Path}; +#[cfg(feature = "backtrace")] +use std::backtrace::Backtrace; + +pub type Result = std::result::Result; + +/// Macro to not repeat having to do multiple implementations of a [ErrorInner] variant with the same string type +macro_rules! fn_string { + ($fn_name:ident, $fortype:expr) => { + #[doc = concat!("Create a new [Self] as [", stringify!($fortype), "]")] + pub fn $fn_name(msg: M) -> Self + where + M: Into, + { + return Self::new($fortype(msg.into())); + } + }; +} + +/// Error type for libytdlr, contains a backtrace, wrapper around [ErrorInner] +#[derive(Debug)] +pub struct Error { + /// The actual error + source: ErrorEnum, + #[cfg(feature = "backtrace")] + /// The backtrace for the error + backtrace: Backtrace, +} + +impl Error { + /// Construct a new [Error] instance based on [ErrorInner] + pub fn new(source: ErrorEnum) -> Self { + Self { + source, + #[cfg(feature = "backtrace")] + backtrace: Backtrace::capture(), + } + } + + #[cfg(feature = "backtrace")] + /// Get the backtrace that is stored + pub fn backtrace(&self) -> &Backtrace { + &self.backtrace + } + + fn_string!(other, ErrorEnum::Other); + fn_string!( + unsupported_schema_format, + ErrorEnum::UnsupportedSchemaFormat + ); + fn_string!(unsupported_type, ErrorEnum::UnsupportedType); + fn_string!(no_file_signature, ErrorEnum::NoFileSignature); + + /// Create a custom [ioError] with this [Error] wrapped around with a [Path] attached + pub fn custom_ioerror_path(kind: std::io::ErrorKind, msg: M, path: P) -> Self + where + M: Into, + P: AsRef, + { + return Self::new(ErrorEnum::IoError( + ioError::new(kind, msg.into()), + format_path(path.as_ref().to_string_lossy().to_string()), + )); + } + + pub fn not_a_directory(msg: M, path: P) -> Self + where + M: Into, + P: AsRef, + { + return Self::new(ErrorEnum::NotADirectory( + msg.into(), + path.as_ref().to_string_lossy().to_string(), + )); + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.source.fmt(f) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + return self.source.source(); + } +} + +// implement all From<> variants that ErrorInner also implements +impl From for Error +where + T: Into, +{ + fn from(value: T) -> Self { + Self::new(value.into()) + } +} + +/// Error type for "yt-downloader-rust", implements all Error types that could happen in this lib +#[derive(thiserror::Error, Debug)] +pub enum ErrorEnum { + /// Wrapper Variant for [`std::io::Error`] + /// Argument 1 (String) is up to the implementation to set, commonly the path + #[error("IoError: {0}; {1}")] + IoError(std::io::Error, String), + /// Variant for when a directory path was expected but did not exist yet or was not a directory + /// TODO: replace with io::ErrorKind::NotADirectory once stable + #[error("NotADirectory: {0}; Path: \"{1}\"")] + NotADirectory(String, String), + /// Variant for unsupported diesel schema formats + #[error("UnsupportedSchemaFormat: {0}")] + UnsupportedSchemaFormat(String), + /// Variant for unsupported sql types + #[error("UnsupportedType: {0}")] + UnsupportedType(String), + /// Variant for when "has_file_signature" is `false` + #[error("NoFileSignature: {0}")] + NoFileSignature(String), + + /// Variant for Other messages + #[error("Other: {0}")] + Other(String), +} + +/// Helper function to keep consistent formatting +#[inline] +fn format_path(msg: String) -> String { + format!("Path \"{}\"", msg) +} + +/// Trait to map [std::io::Error] into [Error] +pub trait IOErrorToError { + /// Map a [std::io::Error] to [Error] with a [std::path::Path] attached + fn attach_path_err>(self, path: P) -> Result; + + /// Map a [std::io::Error] to [Error] with a [std::path::Path] and message attached + fn attach_path_msg, M: AsRef>(self, path: P, msg: M) -> Result; +} + +impl IOErrorToError for std::result::Result { + fn attach_path_err>(self, path: P) -> Result { + return match self { + Ok(v) => Ok(v), + Err(e) => Err(crate::Error::new(ErrorEnum::IoError( + e, + format_path(path.as_ref().to_string_lossy().to_string()), + ))), + }; + } + + fn attach_path_msg, M: AsRef>(self, path: P, msg: M) -> Result { + match self { + Ok(v) => Ok(v), + Err(e) => Err(crate::Error::new(ErrorEnum::IoError( + e, + format!( + "{msg} {path}", + msg = msg.as_ref(), + path = format_path(path.as_ref().to_string_lossy().to_string()) + ), + ))), + } + } +} diff --git a/src/file.rs b/src/file.rs index 68498350..410b2f43 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use crate::{Result, IOErrorToError, Error}; pub struct MarkedFile { pub file_contents: String, @@ -6,18 +7,16 @@ pub struct MarkedFile { } impl MarkedFile { - pub fn new(path: PathBuf) -> MarkedFile { - MarkedFile { + pub fn new(path: PathBuf) -> Result { + Ok(MarkedFile { path: path.clone(), file_contents: if !path.exists() { - std::fs::write(&path, "") - .unwrap_or_else(|_| panic!("Could not write to '{path:#?}'")); + std::fs::write(&path, "").attach_path_err(&path)?; "".to_string() } else { - std::fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Could not read '{path:#?}'")) + std::fs::read_to_string(&path).attach_path_err(&path)? }, - } + }) } pub fn has_use_stmt(&self, use_name: &str) -> bool { @@ -91,19 +90,19 @@ impl MarkedFile { .starts_with(crate::parser::FILE_SIGNATURE) } - pub fn ensure_file_signature(&self) { + pub fn ensure_file_signature(&self) -> Result<()> { if !self.has_file_signature() { - panic!("Expected file '{path:#?}' to have file signature ('{sig}') -- you might be accidentally overwriting files that weren't generated!", path=self.path, sig=crate::parser::FILE_SIGNATURE) + return Err(Error::no_file_signature(format!("Expected file '{path:#?}' to have file signature ('{sig}') -- you might be accidentally overwriting files that weren't generated!", path=self.path, sig=crate::parser::FILE_SIGNATURE))); } + + Ok(()) } - pub fn write(&self) { - std::fs::write(&self.path, &self.file_contents) - .unwrap_or_else(|_| panic!("Could not write to file '{:#?}'", self.path)); + pub fn write(&self) -> Result<()> { + std::fs::write(&self.path, &self.file_contents).attach_path_err(&self.path) } - pub fn delete(self) { - std::fs::remove_file(&self.path) - .unwrap_or_else(|_| panic!("Could not delete redundant file '{:#?}'", self.path)); + pub fn delete(self) -> Result<()> { + std::fs::remove_file(&self.path).attach_path_err(&self.path) } } diff --git a/src/lib.rs b/src/lib.rs index f07a36c1..d12f22c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,10 @@ mod code; mod file; mod parser; +pub mod error; +use error::IOErrorToError; +pub use error::{Error, Result}; use file::MarkedFile; use parser::ParsedTableMacro; pub use parser::FILE_SIGNATURE; @@ -141,7 +144,7 @@ impl GenerationConfig<'_> { pub fn generate_code( diesel_schema_file_contents: String, config: GenerationConfig, -) -> anyhow::Result> { +) -> Result> { parser::parse_and_generate_code(diesel_schema_file_contents, &config) } @@ -149,63 +152,62 @@ pub fn generate_files( input_diesel_schema_file: PathBuf, output_models_dir: PathBuf, config: GenerationConfig, -) { +) -> Result<()> { let input = input_diesel_schema_file; let output_dir = output_models_dir; let generated = generate_code( - std::fs::read_to_string(input).expect("Could not read schema file."), + std::fs::read_to_string(&input).attach_path_err(&input)?, config, - ) - .expect("An error occurred."); + )?; if !output_dir.exists() { - std::fs::create_dir(&output_dir) - .unwrap_or_else(|_| panic!("Could not create directory '{output_dir:#?}'")); + std::fs::create_dir(&output_dir).attach_path_err(&output_dir)?; } else if !output_dir.is_dir() { - panic!("Expected output argument to be a directory or non-existent.") + return Err(Error::not_a_directory( + "Expected output argument to be a directory or non-existent.", + output_dir, + )); } // check that the mod.rs file exists - let mut mod_rs = MarkedFile::new(output_dir.join("mod.rs")); + let mut mod_rs = MarkedFile::new(output_dir.join("mod.rs"))?; // pass 1: add code for new tables for table in generated.iter() { let table_dir = output_dir.join(table.name.to_string()); if !table_dir.exists() { - std::fs::create_dir(&table_dir) - .unwrap_or_else(|_| panic!("Could not create directory '{table_dir:#?}'")); + std::fs::create_dir(&table_dir).attach_path_err(&table_dir)?; } if !table_dir.is_dir() { - panic!("Expected a directory at '{table_dir:#?}'") + return Err(Error::not_a_directory("Expected a directory", table_dir)); } - let mut table_generated_rs = MarkedFile::new(table_dir.join("generated.rs")); - let mut table_mod_rs = MarkedFile::new(table_dir.join("mod.rs")); + let mut table_generated_rs = MarkedFile::new(table_dir.join("generated.rs"))?; + let mut table_mod_rs = MarkedFile::new(table_dir.join("mod.rs"))?; - table_generated_rs.ensure_file_signature(); + table_generated_rs.ensure_file_signature()?; table_generated_rs.file_contents = table.generated_code.clone(); - table_generated_rs.write(); + table_generated_rs.write()?; table_mod_rs.ensure_mod_stmt("generated"); table_mod_rs.ensure_use_stmt("generated::*"); - table_mod_rs.write(); + table_mod_rs.write()?; mod_rs.ensure_mod_stmt(table.name.to_string().as_str()); } // pass 2: delete code for removed tables - for item in std::fs::read_dir(&output_dir) - .unwrap_or_else(|_| panic!("Could not read directory '{output_dir:#?}'")) + for item in std::fs::read_dir(&output_dir).attach_path_err(&output_dir)? { - let item = item.unwrap_or_else(|_| panic!("Could not read item in '{output_dir:#?}'")); + let item = item.attach_path_err(&output_dir)?; // check if item is a directory let file_type = item .file_type() - .unwrap_or_else(|_| panic!("Could not determine type of file '{:#?}'", item.path())); + .attach_path_msg(item.path(), "Could not determine type of file")?; if !file_type.is_dir() { continue; } @@ -214,16 +216,17 @@ pub fn generate_files( let generated_rs_path = item.path().join("generated.rs"); if !generated_rs_path.exists() || !generated_rs_path.is_file() - || !MarkedFile::new(generated_rs_path.clone()).has_file_signature() + || !MarkedFile::new(generated_rs_path.clone())?.has_file_signature() { continue; } // okay, it's generated, but we need to check if it's for a deleted table let file_name = item.file_name(); - let associated_table_name = file_name - .to_str() - .unwrap_or_else(|| panic!("Could not determine name of file '{:#?}'", item.path())); + let associated_table_name = file_name.to_str().ok_or(Error::other(format!( + "Could not determine name of file '{:#?}'", + item.path() + )))?; let found = generated.iter().find(|g| { g.name .to_string() @@ -234,22 +237,21 @@ pub fn generate_files( } // this table was deleted, let's delete the generated code - std::fs::remove_file(&generated_rs_path) - .unwrap_or_else(|_| panic!("Could not delete redundant file '{generated_rs_path:#?}'")); + std::fs::remove_file(&generated_rs_path).attach_path_err(&generated_rs_path)?; // remove the mod.rs file if there isn't anything left in there except the use stmt let table_mod_rs_path = item.path().join("mod.rs"); if table_mod_rs_path.exists() { - let mut table_mod_rs = MarkedFile::new(table_mod_rs_path); + let mut table_mod_rs = MarkedFile::new(table_mod_rs_path)?; table_mod_rs.remove_mod_stmt("generated"); table_mod_rs.remove_use_stmt("generated::*"); - table_mod_rs.write(); + table_mod_rs.write()?; if table_mod_rs.file_contents.trim().is_empty() { - table_mod_rs.delete() + table_mod_rs.delete()?; } else { - table_mod_rs.write() // write the changes we made above + table_mod_rs.write()?; // write the changes we made above } } @@ -257,17 +259,18 @@ pub fn generate_files( let is_empty = item .path() .read_dir() - .unwrap_or_else(|_| panic!("Could not read directory {:#?}", item.path())) + .attach_path_err(item.path())? .next() .is_none(); if is_empty { - std::fs::remove_dir(item.path()) - .unwrap_or_else(|_| panic!("Could not delete directory '{:#?}'", item.path())); + std::fs::remove_dir(item.path()).attach_path_err(item.path())?; } // remove the module from the main mod_rs file mod_rs.remove_mod_stmt(associated_table_name); } - mod_rs.write(); + mod_rs.write()?; + + return Ok(()); } diff --git a/src/parser.rs b/src/parser.rs index fe370f59..000dbf04 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2,7 +2,7 @@ use inflector::Inflector; use syn::Ident; use syn::Item::Macro; -use crate::{code, GenerationConfig}; +use crate::{code, GenerationConfig, Result, Error}; pub const FILE_SIGNATURE: &str = "/* This file is generated and managed by dsync */"; @@ -52,7 +52,7 @@ pub struct ParsedJoinMacro { pub fn parse_and_generate_code( schema_file_contents: String, config: &GenerationConfig, -) -> anyhow::Result> { +) -> Result> { let schema_file = syn::parse_file(&schema_file_contents).unwrap(); let mut tables: Vec = vec![]; @@ -64,13 +64,13 @@ pub fn parse_and_generate_code( .path .segments .last() - .expect("could not read identifier for macro") + .ok_or(Error::other("could not read identifier for macro"))? .ident .to_string(); match macro_identifier.as_str() { "table" => { - let parsed_table = handle_table_macro(macro_item, config); + let parsed_table = handle_table_macro(macro_item, config)?; // make sure the table isn't ignored let table_options = config.table(parsed_table.name.to_string().as_str()); @@ -79,7 +79,7 @@ pub fn parse_and_generate_code( } } "joinable" => { - let parsed_join = handle_joinable_macro(macro_item); + let parsed_join = handle_joinable_macro(macro_item)?; for table in tables.iter_mut() { if parsed_join @@ -107,7 +107,7 @@ pub fn parse_and_generate_code( Ok(tables) } -fn handle_joinable_macro(macro_item: syn::ItemMacro) -> ParsedJoinMacro { +fn handle_joinable_macro(macro_item: syn::ItemMacro) -> Result { // println!("joinable! macro: {:#?}", macro_item); let mut table1_name: Option = None; @@ -125,7 +125,9 @@ fn handle_joinable_macro(macro_item: syn::ItemMacro) -> ParsedJoinMacro { } proc_macro2::TokenTree::Group(group) => { if table1_name.is_none() || table2_name.is_none() { - panic!("Unsupported schema format! (encountered join column group too early)"); + return Err(Error::unsupported_schema_format( + "encountered join column group too early", + )); } else { table2_join_column = Some(group.stream().to_string()); } @@ -134,17 +136,20 @@ fn handle_joinable_macro(macro_item: syn::ItemMacro) -> ParsedJoinMacro { } } - ParsedJoinMacro { - table1: table1_name - .expect("Unsupported schema format! (could not determine first join table name)"), - table2: table2_name - .expect("Unsupported schema format! (could not determine second join table name)"), - table1_columns: table2_join_column - .expect("Unsupported schema format! (could not determine join column name)"), - } + Ok(ParsedJoinMacro { + table1: table1_name.ok_or(Error::unsupported_schema_format( + "could not determine first join table name", + ))?, + table2: table2_name.ok_or(Error::unsupported_schema_format( + "could not determine second join table name", + ))?, + table1_columns: table2_join_column.ok_or(Error::unsupported_schema_format( + "could not determine join column name", + ))?, + }) } -fn handle_table_macro(macro_item: syn::ItemMacro, config: &GenerationConfig) -> ParsedTableMacro { +fn handle_table_macro(macro_item: syn::ItemMacro, config: &GenerationConfig) -> Result { let mut table_name_ident: Option = None; let mut table_primary_key_idents: Vec = vec![]; let mut table_columns: Vec = vec![]; @@ -237,8 +242,19 @@ fn handle_table_macro(macro_item: syn::ItemMacro, config: &GenerationConfig) -> // add the column table_columns.push(ParsedColumnMacro { - name: column_name.expect("Unsupported schema format! (Invalid column name syntax)"), - ty: schema_type_to_rust_type(column_type.expect("Unsupported schema format! (Invalid column type syntax)").to_string(), config), + name: column_name.ok_or( + Error::unsupported_schema_format( + "Invalid column name syntax", + ), + )?, + ty: schema_type_to_rust_type( + column_type + .ok_or(Error::unsupported_schema_format( + "Invalid column type syntax", + ))? + .to_string(), + config, + )?, is_nullable: column_nullable, is_unsigned: column_unsigned, }); @@ -250,7 +266,11 @@ fn handle_table_macro(macro_item: syn::ItemMacro, config: &GenerationConfig) -> column_nullable = false; } } - _ => panic!("Unsupported schema format! (Invalid column definition token in diesel table macro)") + _ => { + return Err(Error::unsupported_schema_format( + "Invalid column definition token in diesel table macro", + )) + } } } @@ -260,24 +280,30 @@ fn handle_table_macro(macro_item: syn::ItemMacro, config: &GenerationConfig) -> || column_unsigned { // looks like a column was in the middle of being parsed, let's panic! - panic!( - "Unsupported schema format! (It seems a column was partially defined)" - ); + return Err(Error::unsupported_schema_format( + "It seems a column was partially defined", + )); } } else { - panic!("Unsupported schema format! (Invalid delimiter in diesel table macro group)") + return Err(Error::unsupported_schema_format( + "Invalid delimiter in diesel table macro group", + )); } } _ => { - panic!("Unsupported schema format! (Invalid token tree item in diesel table macro)") + return Err(Error::unsupported_schema_format( + "Invalid token tree item in diesel table macro", + )) } } } - ParsedTableMacro { + Ok(ParsedTableMacro { name: table_name_ident .clone() - .expect("Unsupported schema format! (Could not extract table name from schema file)"), + .ok_or(Error::unsupported_schema_format( + "Could not extract table name from schema file", + ))?, struct_name: table_name_ident .unwrap() .to_string() @@ -289,7 +315,7 @@ fn handle_table_macro(macro_item: syn::ItemMacro, config: &GenerationConfig) -> generated_code: format!( "{FILE_SIGNATURE}\n\nFATAL ERROR: nothing was generated; this shouldn't be possible." ), - } + }) } // A function to translate diesel schema types into rust types @@ -299,11 +325,11 @@ fn handle_table_macro(macro_item: syn::ItemMacro, config: &GenerationConfig) -> // // The docs page for sql_types is comprehensive but it hides some alias types like Int4, Float8, etc.: // https://docs.rs/diesel/latest/diesel/sql_types/index.html -fn schema_type_to_rust_type(schema_type: String, config: &GenerationConfig) -> String { - match schema_type.to_lowercase().as_str() { - "unsigned" => panic!("Unsigned types are not yet supported, please open an issue if you need this feature!"), // TODO: deal with this later - "inet" => panic!("Unsigned types are not yet supported, please open an issue if you need this feature!"), // TODO: deal with this later - "cidr" => panic!("Unsigned types are not yet supported, please open an issue if you need this feature!"), // TODO: deal with this later +fn schema_type_to_rust_type(schema_type: String, config: &GenerationConfig) -> Result { + Ok(match schema_type.to_lowercase().as_str() { + "unsigned" => return Err(Error::unsupported_type("Unsigned types are not yet supported, please open an issue if you need this feature!")), // TODO: deal with this later + "inet" => return Err(Error::unsupported_type("Unsigned types are not yet supported, please open an issue if you need this feature!")), // TODO: deal with this later + "cidr" => return Err(Error::unsupported_type("Unsigned types are not yet supported, please open an issue if you need this feature!")), // TODO: deal with this later // boolean "bool" => "bool", @@ -380,7 +406,7 @@ fn schema_type_to_rust_type(schema_type: String, config: &GenerationConfig) -> S let schema_path = &config.schema_path; // return the schema type if no type is found (this means generation is broken for this particular schema) let _type = format!("{schema_path}sql_types::{schema_type}"); - return _type; + return Ok(_type); } - }.to_string() + }.to_string()) }