user_panic/
lib.rs

1//! Custom Panic Messages According to the error.
2//!
3//! Handles panics by calling a custom function using
4//! [`std::panic::set_hook`](https://doc.rust-lang.org/std/panic/fn.set_hook.html)
5//! and a Yaml File to generate the custom structs.
6//!
7//! This allows for seperate error messages for seperate error and also allows the user to run some simple fixes (if possible).
8//!
9//! ### Output Example
10//!
11//! Example of an API error's panic output
12//!
13//! ```txt
14//! The Program Crashed
15//!
16//! Error: There was an error during the API request
17//! It seems like an error that can be fixed by you!
18//! Please follow the following instructions to try and fix the Error
19//!
20//!     1: Try to check your Internet Connection.
21//!
22//! 	2: Check if your API request quota has been exhausted.
23//! 		1.  Instructions on how
24//! 		2.  to check
25//! 		3.  API quota
26//!
27//! If the error still persists
28//! Contact the Developer at xyz@wkl.com
29//! ```
30//! ### Code Example
31//! To replicate the above output you need to first create a yaml file as follows.
32//! ```txt
33//! API:
34//!   message: There was an error during the API request
35//!   fix instructions:
36//!       - Try to check your Internet Connection.
37//!       - Check if your API request quota has been exhausted.
38//!       - - Instructions on how
39//!         - to check
40//!         - API quota
41//! ```
42//! then you need to create the [build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html) make sure userpanic is present in both dependencies and build dependencies in cargo.toml file
43//! ```toml
44//! [dependencies]
45//! user-panic = "0.1.0"
46//!
47//! [build-dependencies]
48//! user-panic = "0.1.0"
49//! ```
50//! and make build.rs file as follows
51//! ```
52//! fn main() {
53//!    println!("cargo:rerun-if-changed=errors.yaml");
54//!    println!("cargo:rerun-if-changed=build.rs");
55//!    userpanic::panic_setup!("errors.yaml"); // Enter the yaml file path here
56//! }
57//! ```
58//! This will create `panic_strucs.rs` file in src directory
59//! This file can be then imported and used with panic_any to display the custom panics
60//! ```
61//! mod panic_structs;
62//!
63//! use std::panic::panic_any;
64//! use crate::panic_structs::API;
65//!
66//! fn main(){
67//!     // This sets the custom hook for panic messages
68//!     userpanic::set_hooks(Some("If the error still persists\nContact the developer at xyz@wkl.com"));
69//!     // If None is passed then No developer info/message is shown.
70//!
71//!     panic_any(API);
72//! }
73//! ```
74
75use log::{debug, info};
76use std::fmt;
77use std::io::Write;
78use std::panic;
79use std::panic::PanicInfo;
80use yaml_rust::{Yaml, YamlLoader};
81
82type StrList = [&'static [&'static str]];
83type Panicfn = Box<dyn Fn(&PanicInfo) + Sync + Send>;
84
85#[derive(Debug, Clone)]
86/// This Struct is auto generated from the yaml file
87pub struct UserPanic {
88    /// It describes the error
89    ///
90    /// If left empty then the program panics silently without giving any output
91    pub error_msg: &'static str,
92    /// It contains the instructions to fix the error
93    pub fix_instructions: Option<&'static StrList>,
94}
95impl fmt::Display for UserPanic {
96    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97        if self.error_msg == "" {
98            return write!(f, "");
99        }
100        // Need something better than "The Program Crashed" :(
101        let mut s = String::from("The Program Crashed\n\n");
102        if self.fix_instructions.is_none() {
103            s += &format!("Error: {}", self.error_msg);
104            s += "\nIt seems like an error that can't be fixed by you!\nPlease submit a Bug report to Developer\n";
105        } else {
106            s += &format!("Error: {}", self.error_msg);
107            s += "\nIt seems like an error that can be fixed by you!\nPlease follow the following instructions to try and fix the Error\n";
108            let insts = self.fix_instructions.as_ref().unwrap();
109            let mut i = 1;
110            for inst in *insts {
111                s += &format!("\n\t{}: {}\n", i, inst[0]);
112                let inst = &inst[1..];
113                if inst.len() > 1 {
114                    let mut j = 1;
115                    for ii in inst {
116                        s += &format!("\t\t{}. {}\n", j, ii);
117                        j += 1;
118                    }
119                }
120                i += 1;
121            }
122        }
123        write!(f, "{}", s)
124    }
125}
126/// This function is used to set custom panic function
127/// Use this to use the custom hooks and set up the developer message
128pub fn set_hooks(developer: Option<&'static str>) {
129    let org: Panicfn = panic::take_hook();
130    if let Some(dev) = developer {
131        // Used if The developer provides custom info
132        panic::set_hook(Box::new(move |pan_inf| {
133            panic_func(pan_inf, &org);
134            eprintln!("{}", dev);
135        }))
136    } else {
137        // Used if Developer doesn't want info to be shown.
138        panic::set_hook(Box::new(move |pan_inf| {
139            panic_func(pan_inf, &org);
140        }));
141    }
142}
143// The panic function
144fn panic_func(panic_info: &PanicInfo, original: &Panicfn) {
145    match panic_info.payload().downcast_ref::<UserPanic>() {
146        Some(err) => {
147            if err.error_msg != "" {
148                eprintln!("{}", err);
149            }
150        }
151        // Default to original panic routine if downcast_ref fails
152        None => original(panic_info),
153    }
154}
155// Returns the auto generated rust code
156fn read_from_yml(yaml: String) -> String {
157    debug!("Started Reading the yaml string");
158    let mut file = "use user_panic::UserPanic;\n".to_string();
159    let yaml = YamlLoader::load_from_str(&yaml).unwrap();
160    let structs = &yaml[0];
161    if let Yaml::Hash(hash) = structs {
162        info!("Found Hash");
163        // for test case keys -> foo bar
164        for (key, val) in hash {
165            let st_name = key.as_str().unwrap();
166            debug!("parsing key {}", st_name);
167            file += &format!(
168                "pub const {}:UserPanic = UserPanic {{{}}};",
169                st_name,
170                get_err_msg(val)
171            );
172        }
173    }
174    file
175}
176// Helper function for read_from_yml
177// Idk why I named it this it doesn't make sense
178fn get_err_msg(hash: &Yaml) -> String {
179    let print_arr = |arr: &Vec<Yaml>| -> String {
180        let mut s = String::new();
181        let _ = arr
182            .iter()
183            .map(|a| {
184                s += &format!(",\"{}\"", a.as_str().unwrap());
185            })
186            .collect::<Vec<_>>();
187        s
188    };
189    let mut s = String::new();
190    debug!("found hash {:#?}", hash);
191    let err_ms = hash["message"].as_str().unwrap();
192    debug!("Collecting  err message: {}", err_ms);
193    debug!("{:?}", &hash["fix instructions"]);
194    if let Yaml::Array(arr) = &hash["fix instructions"] {
195        debug!("Found fix instructions");
196        s += &format!("error_msg:\"{}\",fix_instructions:Some(&[", err_ms);
197        let items = arr.len();
198        debug!("Number of instuctions {}", items);
199        let mut i = 0;
200        while i < items {
201            if i + 1 < items {
202                match &arr[i + 1] {
203                    Yaml::String(_) => {
204                        s += &format!("&[\"{}\"],", arr[i].as_str().unwrap());
205                        i += 1;
206                    }
207                    Yaml::Array(ar) => {
208                        s += &format!("&[\"{}\"{}],", arr[i].as_str().unwrap(), print_arr(ar));
209                        i += 2;
210                    }
211                    _ => {}
212                }
213            } else {
214                match &arr[i] {
215                    Yaml::String(ss) => {
216                        s += &format!("&[\"{}\"],", ss);
217                        i += 1;
218                    }
219                    Yaml::Array(ar) => {
220                        s += &format!("&[\"{}\"{}],", arr[i].as_str().unwrap(), print_arr(ar));
221                        i += 2;
222                    }
223                    _ => {}
224                }
225            }
226        }
227        s += "]),";
228    } else {
229        s += &format!("error_msg:\"{}\",fix_instructions: None,", err_ms);
230    }
231    s
232}
233
234#[macro_export]
235/// Macro to be used in build script
236/// Only yaml file path or both yaml and output rust file can be provided
237macro_rules! panic_setup {
238    ($file_path:expr) => {
239        user_panic::panic_setup_function($file_path, "src/panic_structs.rs");
240    };
241    ($file_path:expr,$file_out:expr) => {
242        user_panic::panic_setup_function($file_path, $file_out);
243    };
244}
245/// Not intended to be used directly and to be called by panic_setup! macro
246/// The main build script function
247pub fn panic_setup_function(path_from: &str, path_to: &str) {
248    let file_str = std::fs::read_to_string(path_from).expect("Failed to read yaml file");
249    let s = read_from_yml(file_str);
250    let mut fp = std::fs::File::create(path_to).expect("failed to create output file");
251    write!(&mut fp, "{}", s).expect("failed to write to file");
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    #[test]
258    #[should_panic]
259    fn it_works() {
260        const ERROR: UserPanic = UserPanic {
261            error_msg: "This is an error",
262            fix_instructions: Some(&[
263                &["Only one"],
264                &["one", "two", "tem"],
265                &["bem", "lem", "jem"],
266            ]),
267        };
268
269        set_hooks(None);
270        std::panic::panic_any(ERROR);
271    }
272
273    #[test]
274    fn print_s() {
275        //        env_logger::init();
276        let s = "
277foo:
278    message: this is the main error
279    fix instructions:
280        - first
281        - - in first
282          - in first second
283        - second
284        - - second first
285          - second second
286        - third
287bar:
288    message: This is un fixable error
289";
290        let s = read_from_yml(s.to_string());
291        assert_eq!("use user_panic::UserPanic;\npub const foo:UserPanic = UserPanic {error_msg:\"this is the main error\",fix_instructions:Some(&[&[\"first\",\"in first\",\"in first second\"],&[\"second\",\"second first\",\"second second\"],&[\"third\"],]),};pub const bar:UserPanic = UserPanic {error_msg:\"This is un fixable error\",fix_instructions: None,};", s);
292    }
293
294    #[test]
295    fn output_string_fixable() {
296        const ERR: UserPanic = UserPanic {
297            error_msg: "Error msg",
298            fix_instructions: Some(&[&["One"], &["two", "two-one", "two-two"], &["Three"]]),
299        };
300        let s = format!("{}", ERR);
301        let manual = "The Program Crashed\n\nError: Error msg\nIt seems like an error that can be fixed by you!\nPlease follow the following instructions to try and fix the Error\n\n\t1: One\n\n\t2: two\n\t\t1. two-one\n\t\t2. two-two\n\n\t3: Three\n";
302        assert_eq!(s, manual);
303    }
304
305    #[test]
306    fn output_string_unfixable() {
307        const ERR: UserPanic = UserPanic {
308            error_msg: "Unfixable Error",
309            fix_instructions: None,
310        };
311        let s = format!("{}", ERR);
312        let manual = "The Program Crashed\n\nError: Unfixable Error\nIt seems like an error that can't be fixed by you!\nPlease submit a Bug report to Developer\n";
313        assert_eq!(s, manual);
314    }
315}