From 0c84c151ebf34778e823301d861e5b453f634a47 Mon Sep 17 00:00:00 2001
From: hasezoey <hasezoey@gmail.com>
Date: Mon, 21 Aug 2023 12:42:56 +0200
Subject: [PATCH 1/5] feat: add "error.rs"

---
 Cargo.lock   |  21 +++++++
 Cargo.toml   |   1 +
 src/error.rs | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs   |   2 +
 4 files changed, 193 insertions(+)
 create mode 100644 src/error.rs

diff --git a/Cargo.lock b/Cargo.lock
index 83aea1ac..dd8c8b1e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -78,6 +78,7 @@ dependencies = [
  "proc-macro2",
  "structopt",
  "syn",
+ "thiserror",
 ]
 
 [[package]]
@@ -231,6 +232,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..2b148e50 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ 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/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<T> = std::result::Result<T, Error>;
+
+/// 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<M>(msg: M) -> Self
+        where
+            M: Into<String>,
+        {
+            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<M, P>(kind: std::io::ErrorKind, msg: M, path: P) -> Self
+    where
+        M: Into<String>,
+        P: AsRef<Path>,
+    {
+        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<M, P>(msg: M, path: P) -> Self
+    where
+        M: Into<String>,
+        P: AsRef<Path>,
+    {
+        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<T> From<T> for Error
+where
+    T: Into<ErrorEnum>,
+{
+    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 <https://github.com/rust-lang/rust/issues/86442>
+    #[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<T> {
+    /// Map a [std::io::Error] to [Error] with a [std::path::Path] attached
+    fn attach_path_err<P: AsRef<Path>>(self, path: P) -> Result<T>;
+
+    /// Map a [std::io::Error] to [Error] with a [std::path::Path] and message attached
+    fn attach_path_msg<P: AsRef<Path>, M: AsRef<str>>(self, path: P, msg: M) -> Result<T>;
+}
+
+impl<T> IOErrorToError<T> for std::result::Result<T, std::io::Error> {
+    fn attach_path_err<P: AsRef<Path>>(self, path: P) -> Result<T> {
+        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<P: AsRef<Path>, M: AsRef<str>>(self, path: P, msg: M) -> Result<T> {
+        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/lib.rs b/src/lib.rs
index f07a36c1..dd37537d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,7 +1,9 @@
 mod code;
 mod file;
 mod parser;
+pub mod error;
 
+pub use error::{Error, Result};
 use file::MarkedFile;
 use parser::ParsedTableMacro;
 pub use parser::FILE_SIGNATURE;

From 32fd4c041a91bff03e31116208213027f92b152e Mon Sep 17 00:00:00 2001
From: hasezoey <hasezoey@gmail.com>
Date: Mon, 21 Aug 2023 12:49:39 +0200
Subject: [PATCH 2/5] feat: replace most panics with results

this includes "panic!" and "expect" and some "unwrap"

feat(lib): replace panics with results
feat(parser): replace panics with results
feat(file): replace panics with results
---
 src/file.rs   | 29 ++++++++--------
 src/lib.rs    | 71 +++++++++++++++++++-------------------
 src/parser.rs | 94 ++++++++++++++++++++++++++++++++-------------------
 3 files changed, 110 insertions(+), 84 deletions(-)

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<MarkedFile> {
+        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 dd37537d..d12f22c4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,6 +3,7 @@ mod file;
 mod parser;
 pub mod error;
 
+use error::IOErrorToError;
 pub use error::{Error, Result};
 use file::MarkedFile;
 use parser::ParsedTableMacro;
@@ -143,7 +144,7 @@ impl GenerationConfig<'_> {
 pub fn generate_code(
     diesel_schema_file_contents: String,
     config: GenerationConfig,
-) -> anyhow::Result<Vec<ParsedTableMacro>> {
+) -> Result<Vec<ParsedTableMacro>> {
     parser::parse_and_generate_code(diesel_schema_file_contents, &config)
 }
 
@@ -151,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;
         }
@@ -216,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()
@@ -236,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
             }
         }
 
@@ -259,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<Vec<ParsedTableMacro>> {
+) -> Result<Vec<ParsedTableMacro>> {
     let schema_file = syn::parse_file(&schema_file_contents).unwrap();
 
     let mut tables: Vec<ParsedTableMacro> = 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<ParsedJoinMacro> {
     // println!("joinable! macro: {:#?}", macro_item);
 
     let mut table1_name: Option<Ident> = 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<ParsedTableMacro> {
     let mut table_name_ident: Option<Ident> = None;
     let mut table_primary_key_idents: Vec<Ident> = vec![];
     let mut table_columns: Vec<ParsedColumnMacro> = 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<String> {
+    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())
 }

From 863b317c87250316107a7f6559c6b314a169cdac Mon Sep 17 00:00:00 2001
From: hasezoey <hasezoey@gmail.com>
Date: Mon, 21 Aug 2023 13:01:13 +0200
Subject: [PATCH 3/5] feat(main): make use of the new results and backtrace

---
 src/bin/main.rs | 30 +++++++++++++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

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(())
 }

From 914cbca9525ee2f8432937a4cabf0b58aa869741 Mon Sep 17 00:00:00 2001
From: hasezoey <hasezoey@gmail.com>
Date: Wed, 23 Aug 2023 12:14:54 +0200
Subject: [PATCH 4/5] chore(Cargo.toml): add feature "backtrace" as a default

---
 Cargo.toml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/Cargo.toml b/Cargo.toml
index 2b148e50..798e0d04 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,9 +11,10 @@ authors = ["Haris <4259838+Wulf@users.noreply.github.com>"]
 edition = "2021"
 
 [features]
-default = ["tsync"]
+default = ["tsync", "backtrace"]
 tsync = []
 async = []
+backtrace = []
 
 [dependencies]
 structopt = "0.3"

From 22b795f737c839b6e1e6c4e74b9af9b56d70312e Mon Sep 17 00:00:00 2001
From: hasezoey <hasezoey@gmail.com>
Date: Wed, 23 Aug 2023 12:16:32 +0200
Subject: [PATCH 5/5] chore(Cargo.toml): remove dependency "anyhow"

---
 Cargo.lock | 7 -------
 Cargo.toml | 1 -
 2 files changed, 8 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index dd8c8b1e..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,7 +67,6 @@ name = "dsync"
 version = "0.0.16"
 dependencies = [
  "Inflector",
- "anyhow",
  "indoc",
  "proc-macro2",
  "structopt",
diff --git a/Cargo.toml b/Cargo.toml
index 798e0d04..68e4afb0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,7 +19,6 @@ 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" }