├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .gitlab-ci.yml ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── logo.png ├── src └── lib.rs └── tests └── simple.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build 13 | run: cargo build --verbose 14 | - name: Run tests 15 | run: cargo test --verbose 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .DS_Store 5 | *.swp 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - build 3 | - test 4 | - deploy 5 | 6 | image: rust:latest 7 | 8 | build: 9 | stage: build 10 | tags: 11 | - docker 12 | script: 13 | - cargo build --release 14 | 15 | test: 16 | stage: test 17 | tags: 18 | - docker 19 | before_script: 20 | - rustup component add clippy 21 | script: 22 | - cargo clippy 23 | - cargo test 24 | 25 | deploy: 26 | stage: deploy 27 | tags: 28 | - docker 29 | script: 30 | - cargo login $CRATES_KEY 31 | - cargo publish 32 | when: manual 33 | only: 34 | - tags 35 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "user-error" 3 | version = "1.2.8" 4 | authors = ["Amy "] 5 | edition = "2018" 6 | repository = "https://gitlab.com/rust-crates/user-error.git" 7 | homepage = "https://gitlab.com/rust-crates/user-error" 8 | license-file = "LICENSE.txt" 9 | readme = "README.md" 10 | keywords = ["errors", "pretty-print", "struct", "xvrqt"] 11 | categories = ["data-structures", "command-line-interface"] 12 | description = "UserFacingError is an error crate that allows you to pretty print your errors and error chain for consumption by the end user. If you implement the UFE trait, the default implementation will let your print your error nicely to the TTY. There is also the UserFacingError type that most std Errors can be converted into, or that you can use directly." 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 - Amy Jie 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UserFacingError 2 | 3 | [![build-status-shield](https://img.shields.io/github/workflow/status/xvrqt/user-error/Rust)](https://github.com/xvrqt/user-error/actions) 4 | [![github-issues-open-shield](https://img.shields.io/github/issues-raw/xvrqt/user-error)](https://github.com/xvrqt/user-error/issues) 5 | [![crates-io-version-shield](https://img.shields.io/crates/v/user-error)](https://crates.io/crates/user-error) 6 | [![crates-io-downloads-shield](https://img.shields.io/crates/d/user-error)](https://crates.io/crates/user-error) 7 | [![license-shield](https://img.shields.io/github/license/xvrqt/user-error)](https://github.com/xvrqt/user-error/blob/master/LICENSE.txt) 8 | - 9 | [![discord-status-shield](https://img.shields.io/discord/524687904522371072)](https://discord.xvrqt.com) 10 | [![twitter-shield](https://img.shields.io/twitter/follow/xvrqt?label=%40xvrqt&style=social)](https://twitter.com/xvrqt) 11 | 12 | Pretty printed errors for your CLI application. 13 | 14 | This repository contains: 15 | 16 | 1. A new trait, **UFE**, that you can implement on your Error types to pretty print them 17 | 2. A new type, UserFacingError, that you can use to construct pretty CLI error messages 18 | 3. Ability to convert your error types into UserFacingErrors 19 | 20 | UserFacingError is an error type, or trait, that helps you format and print good looking error messages for users of your CLI application. These errors are intended for consumption by humans, not your program. They are divided into 3 parts: summary, reasons and help text. 21 | 22 | **Summary:** A String representing a one-line description of your error. A summary is mandatory and is printed boldly in red. 23 | 24 | **Reasons:** A vector of Strings explaining in more detail _why_ this error occured. Reasons are optional and if the terminal supports color, the bullet point ('-') will be colored yellow. Each reason will be printed on its own line. 25 | 26 | **Help Text:** A String explaining additional information, including what the user can do about the error, or where to file a bug. Help text is optional and if the terminal supports color it will be printed _dimly_. 27 | 28 | ```rust 29 | use user_error::UserFacingError; 30 | 31 | fn main() { 32 | UserFacingError::new("Failed to build project") 33 | .reason("Database could not be parsed") 34 | .reason("File \"main.db\" not found") 35 | .help("Try: touch main.db") 36 | .print() 37 | } 38 | ``` 39 | 40 | This prints: 41 | ```text 42 | Error: Failed to build project 43 | - Database could not be parsed 44 | - File "main.db" not found 45 | Try: touch main.db 46 | ``` 47 | 48 | If the user has colors enabled on their terminal, it may look something like this: 49 | ![Quickstart example of user-error library for Rust](https://xvrqt.sfo2.cdn.digitaloceanspaces.com/image-cache/user-error-output.png) 50 | 51 | ## Table of Contents 52 | 53 | - [Background](#background) 54 | - [Install](#install) 55 | - [Usage](#usage) 56 | - [UFE Trait](#ufe-trait) 57 | - [Default Implementations](#default-implementations) 58 | - [Summary](#summary) 59 | - [Reasons](#reasons) 60 | - [Helptext](#helptext) 61 | - [Trait Methods](#trait-methods) 62 | - [Print](#print) 63 | - [Print and Exit](#print-and-exit) 64 | - [Into UFE](#into-ufe) 65 | - [UserFacingError Type](#userfacingerror-type) 66 | - [Construction](#construction) 67 | - [Builder Pattern](#builder-pattern) 68 | - [From Other Errors](#from-other-error-types) 69 | - [Methods](#methods) 70 | - [Update](#update) 71 | - [Push](#push) 72 | - [Clear Reasons](#clear-reasons) 73 | - [Clear Help Text](#clear-help-text) 74 | - [Maintainers](#maintainers) 75 | - [Contributing](#contributing) 76 | - [License](#license) 77 | 78 | ## Background 79 | 80 | UserFacingError makes it easy to print errors to users of your command line applications in a sensible, pretty format. 81 | I love Rust's Result types, and using enums for matching and &str for error messages. It's great for development but less great for end users of CLI applications. For this I made a `UserFacingError` which can be used to quickly construct a pretty error message suitable to inform users what went wrong and what they can do about it. 82 | 83 | ## Install 84 | 85 | Add the following to your Cargo.toml: 86 | ```yaml 87 | [dependencies] 88 | user-error = "1.2.8" 89 | ``` 90 | 91 | ## Usage 92 | 93 | ### UFE Trait 94 | 95 | You can trivially implement the UFE trait on your custom error types, allowing you to pretty print them to stderr. The UFE trait requires your type also implements the Error trait. 96 | 97 | ```rust 98 | use user_error::{UserFacingError, UFE}; 99 | 100 | // Custom Error Type 101 | #[derive(Debug)] 102 | struct MyError { mssg: String, src: Option> } 103 | 104 | impl Display for MyError { 105 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 106 | write!(f, "{}", self.mssg.to_string()) 107 | } 108 | } 109 | 110 | impl Error for MyError { 111 | fn source(&self) -> Option<&(dyn Error + 'static)> { 112 | self.src.as_deref() 113 | } 114 | } 115 | 116 | impl UFE for MyError {} 117 | 118 | fn main() { 119 | let me = MyError { 120 | mssg: "Program Failed".into(), 121 | src: Some(Box::new(MyError { 122 | mssg: "Reason 1".into(), 123 | src: Some(Box::new(MyError { 124 | mssg: "Reason 2".into(), 125 | src: None, 126 | })), 127 | })), 128 | }; 129 | 130 | me.print(); 131 | } 132 | ``` 133 | 134 | This prints: 135 | ```text 136 | Error: Program Failed 137 | - Reason 1 138 | - Reason 2 139 | ``` 140 | 141 | #### Default Implementations 142 | There are three functions you may optionally implement: 143 | 144 | 1. `.summary() -> String` - returns a string to be used as the error summary 145 | 2. `.reasons() -> Option>` - optionally return a Vec of Strings representing the causes of the error 146 | 3. `.helptext() -> Option` - optionally return a String representing follow up advice on how to resolve the error 147 | 148 | ##### Summary 149 | 150 | By default, the error summary is the String provided by calling `.to_string()` on the error and then prefixing it with "Error: ". 151 | 152 | ##### Reasons 153 | 154 | By default the list of reasons is created by recursively calling `.source()` and prefixing each error in the chaing with a bullet point. 155 | 156 | ##### Helptext 157 | 158 | By default no helptext is added to custom types that implement UFE. You'll either have to provide your own implementation, or call `.into_ufe()` to convert your error type to a UserFacingError and use the provided `.help(&str)` function to add one. 159 | 160 | #### Trait Methods 161 | 162 | UFE provides three useful methods: 163 | 164 | 1. `.print()` - Pretty print the error 165 | 2. `.print_and_exit()` - Pretty print the error and terminate the process 166 | 3. `.into_ufe()` - consume a custom Error type and return a UserFacingError 167 | 168 | You could override these methods but then there is not much point in using this crate :p 169 | 170 | This prints: 171 | ```text 172 | Error: Program Failed! 173 | - Bad luck 174 | ``` 175 | 176 | #### Print 177 | Pretty prints the UserFacingError to stderr. 178 | 179 | ```rust 180 | use user_error::UserFacingError; 181 | 182 | fn main() { 183 | UserFacingError::new("Failed to build project") 184 | .reason("Database config could not be parsed") 185 | .reason("`db.config` not found") 186 | .help("Try: touch db.config") 187 | .print_and_exit(); 188 | } 189 | ``` 190 | 191 | This prints: 192 | ```text 193 | Error: Failed to build project 194 | - Database config could not be parsed 195 | - `db.config` not found 196 | Try: touch db.config 197 | ``` 198 | 199 | #### Print and Exit 200 | Since constructing this error is likely the last thing your program will do, you can also call `.print_and_exit()` to print the error and then terminate the process with status code 1 as a convenience. 201 | 202 | ```rust 203 | use user_error::UserFacingError; 204 | 205 | fn main() { 206 | UserFacingError::new("Failed to build project") 207 | .reason("Database config could not be parsed") 208 | .print_and_exit(); 209 | } 210 | ``` 211 | 212 | This prints: 213 | ```text 214 | Error: Failed to build project 215 | - Database config could not be parsed 216 | ``` 217 | 218 | #### Into UFE 219 | Consumes a custom Error type and returns a UserFacingError. Useful before exiting if you want to modify the summary, list of reasons or helptext before you exit the program. 220 | 221 | ```rust 222 | use user_error::{UserFacingError, UFE} 223 | 224 | fn main() { 225 | let me = MyError { ... }; 226 | me.into_ufe().help("Added helptext").print(); 227 | } 228 | ``` 229 | 230 | This prints: 231 | ```text 232 | Error: Failed to build project 233 | - Database config could not be parsed 234 | ``` 235 | 236 | ### UserFacingError Type 237 | 238 | #### Construction 239 | 240 | There are two ways to create a new UserFacingError: 241 | 242 | 1. Using a builder pattern 243 | 2. From other std Errors 244 | 245 | ##### Builder Pattern 246 | 247 | ```rust 248 | use user_error::UserFacingError; 249 | 250 | fn main() { 251 | UserFacingError::new("Failed to build project") 252 | .reason("Database could not be parsed") 253 | .reason("File \"main.db\" not found") 254 | .help("Try: touch main.db") 255 | .print() 256 | } 257 | ``` 258 | 259 | This prints: 260 | ```text 261 | Error: Failed to build project 262 | - Database could not be parsed 263 | - File "main.db" not found 264 | Try: touch main.db 265 | ``` 266 | 267 | If the user has colors enabled on their terminal, it may look something like this: 268 | ![Quickstart example of user-error library for Rust](https://xvrqt.sfo2.cdn.digitaloceanspaces.com/image-cache/user-error-output.png) 269 | 270 | ##### From Other Error Types 271 | You can also create a UserFacingError from other types that implement std::error::Error. 272 | 273 | The summary will be the result of error.to_string() and the list of reasons will be any errors in the error chain constructed by recursively calling .source() 274 | 275 | ```rust 276 | use user_error::UserFacingError; 277 | use std::io::(Error, ErrorKind); 278 | 279 | fn main() { 280 | /* Lose the type */ 281 | fn dyn_error() -> Box { 282 | let ioe = Error::new(ErrorKind::Other, "MyError"); 283 | Box::new(ioe) 284 | } 285 | 286 | /* Convert to UFE */ 287 | let ufe: UserFacingError = dyn_error().into(); 288 | } 289 | ``` 290 | #### Methods 291 | 292 | UserFacingErrors have 6 non-builder methods: 293 | 294 | 1. `.update(&str)` - Change the error summary 295 | 2. `.push(&str)` - Change the error summary, add the previous summary to the list of reasons 296 | 3. `.clear_reasons()` - Remove all reasons 297 | 4. `.clear_help()` - Remove the helptext 298 | 5. `.print()` - Pretty print the error (uses the default UFE implementation) 299 | 6. `.print_and_exit()` - Pretty print the error and terminate the process (uses the default UFE implementation) 300 | 301 | ##### Update 302 | 303 | You can call `.update(&str)` on a UserFacingError to change the error summary. 304 | 305 | ```rust 306 | use user_error::UserFacingError; 307 | 308 | fn do_thing() -> Result<(), UserFacingError> { 309 | Err(UserFacingError::new("Didn't do the thing!") 310 | .reason("Just didn't happen")) 311 | } 312 | 313 | fn main() { 314 | match do_thing() { 315 | Ok(_) => println!("Success!"), 316 | Err(E) => { 317 | e.update("Program Failed!").print() 318 | } 319 | } 320 | } 321 | ``` 322 | 323 | This prints: 324 | ```text 325 | Error: Program Failed! 326 | - Just didn't happen 327 | ``` 328 | 329 | ##### Push 330 | 331 | You can call `.push(&str)` on a UserFacingError to change the error summary and add the old error summary to the list of reasons. It adds the summary to the front of the list of reasons. 332 | 333 | ```rust 334 | use user_error::UserFacingError; 335 | 336 | fn do_thing() -> Result<(), UserFacingError> { 337 | Err(UserFacingError::new("Didn't do the thing!") 338 | .reason("Just didn't happen")) 339 | } 340 | 341 | fn main() { 342 | match do_thing() { 343 | Ok(_) => println!("Success!"), 344 | Err(E) => { 345 | e.update("Program Failed!").print() 346 | } 347 | } 348 | } 349 | ``` 350 | 351 | This prints: 352 | ```text 353 | Error: Program Failed! 354 | - Didn't do the thing! 355 | - Just didn't happen 356 | ``` 357 | 358 | ##### Clear Reasons 359 | 360 | Calling this removes all reasons from a UserFacingError. 361 | 362 | ```rust 363 | use user_error::UserFacingError; 364 | 365 | fn main() { 366 | let ufe = UserFacingError::new("Program Failed!") 367 | .reason("Program internal error message"); 368 | /* --- */ 369 | 370 | ufe.clear_reasons(); 371 | ufe.print_and_exit(); 372 | } 373 | ``` 374 | 375 | This prints: 376 | ```text 377 | Error: Program Failed! 378 | ``` 379 | 380 | ##### Clear Help Text 381 | 382 | Calling this removes the help text from a UserFacingError. 383 | 384 | ```rust 385 | use user_error::UserFacingError; 386 | 387 | fn main() { 388 | let ufe = UserFacingError::new("Program Failed!") 389 | .reason("Bad luck") 390 | .help("Try running it again?"); 391 | /* --- */ 392 | 393 | ufe.clear_help(); 394 | ufe.print_and_exit(); 395 | } 396 | ``` 397 | 398 | This prints: 399 | ```text 400 | Error: Program Failed! 401 | - Bad luck 402 | ``` 403 | 404 | ## Maintainers 405 | 406 | - Amy Jie [@xvrqt](https://twitter.com/xvrqt) 407 | 408 | ## Contributing 409 | 410 | Feel free to dive in! [Open an issue](https://github.com/xvrqt/user-error/issues/new) or submit PRs. 411 | 412 | ### Contributors 413 | 414 | - Amy Jie [@xvrqt](https://twitter.com/xvrqt) 415 | 416 | ## License 417 | 418 | [MIT](LICENSE) © Amy Jie 419 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xvrqt/user-error/55ad754c791404067a0a222aed248688875d8030/logo.png -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # User Facing Error 2 | //! A library for conveniently displaying well-formatted, and good looking 3 | //! errors to users of CLI applications. Useful for bubbling up unrecoverable 4 | //! errors to inform the user what they can do to fix them. Error messages you'd 5 | //! be proud to show your mom. 6 | #![deny( 7 | missing_docs, 8 | missing_debug_implementations, 9 | missing_copy_implementations, 10 | trivial_casts, 11 | trivial_numeric_casts, 12 | unstable_features, 13 | unsafe_code, 14 | unused_import_braces, 15 | unused_qualifications 16 | )] 17 | 18 | // Standard Library Dependencies 19 | use core::fmt::{self, Debug, Display}; 20 | use std::error::Error; 21 | 22 | /************* 23 | * CONSTANTS * 24 | *************/ 25 | 26 | // 'Error:' with a red background and white, bold, text 27 | const SUMMARY_PREFIX: &str = "\u{001b}[97;41;22mError:\u{001b}[91;49;1m "; 28 | // ' - ' bullet point in yellow and text in bold white 29 | const REASON_PREFIX: &str = "\u{001b}[93;49;1m - \u{001b}[97;49;1m"; 30 | // Muted white help text 31 | const HELPTEXT_PREFIX: &str = "\u{001b}[37;49;2m"; 32 | // ASCII Reset formatting escape code 33 | const RESET: &str = "\u{001b}[0m"; 34 | 35 | // Helper function to keep things DRY 36 | // Takes a dyn Error.source() and returns a Vec of Strings representing all the 37 | // .sources() in the error chain (if any) 38 | fn error_sources(mut source: Option<&(dyn Error + 'static)>) -> Option> { 39 | /* Check if we have any sources to derive reasons from */ 40 | if source.is_some() { 41 | /* Add all the error sources to a list of reasons for the error */ 42 | let mut reasons = Vec::new(); 43 | while let Some(error) = source { 44 | reasons.push(error.to_string()); 45 | source = error.source(); 46 | } 47 | Some(reasons) 48 | } else { 49 | None 50 | } 51 | } 52 | 53 | /********* 54 | * TRAIT * 55 | *********/ 56 | 57 | // Helper Functions 58 | 59 | /// Convenience function that converts the summary into pretty String. 60 | fn pretty_summary(summary: &str) -> String { 61 | [SUMMARY_PREFIX, summary, RESET].concat() 62 | } 63 | 64 | /// Convenience function that converts the reasons into pretty String. 65 | fn pretty_reasons(reasons: Reasons) -> Option { 66 | /* Print list of Reasons (if any) */ 67 | if let Some(reasons) = reasons { 68 | /* Vector to store the intermediate bullet point strings */ 69 | let mut reason_strings = Vec::with_capacity(reasons.len()); 70 | for reason in reasons { 71 | let bullet_point = [REASON_PREFIX, &reason].concat(); 72 | reason_strings.push(bullet_point); 73 | } 74 | /* Join the buller points with a newline, append a RESET ASCII escape code to the end */ 75 | Some([&reason_strings.join("\n"), RESET].concat()) 76 | } else { 77 | None 78 | } 79 | } 80 | 81 | /// Convenience function that converts the help text into pretty String. 82 | fn pretty_helptext(helptext: Helptext) -> Option { 83 | if let Some(helptext) = helptext { 84 | Some([HELPTEXT_PREFIX, &helptext, RESET].concat()) 85 | } else { 86 | None 87 | } 88 | } 89 | 90 | /// You can implement UFE on your error types pretty print them. The default 91 | /// implementation will print Error: followed by a list 92 | /// of reasons that are any errors returned by .source(). You should only 93 | /// override the summary, reasons and help text functions. The pretty print 94 | /// versions of these are used by print(), print_and_exit() and contain the 95 | /// formatting. If you wish to change the formatting you should update it with 96 | /// the formatting functions. 97 | pub trait UFE: Error { 98 | /************** 99 | * IMPLENT ME * 100 | **************/ 101 | 102 | /// Returns a summary of the error. This will be printed in red, prefixed 103 | /// by "Error: ", at the top of the error message. 104 | fn summary(&self) -> String { 105 | self.to_string() 106 | } 107 | 108 | /// Returns a vector of Strings that will be listed as bullet points below 109 | /// the summary. By default, lists any errors returned by .source() 110 | /// recursively. 111 | fn reasons(&self) -> Option> { 112 | /* Helper function to keep things DRY */ 113 | error_sources(self.source()) 114 | } 115 | 116 | /// Returns help text that is listed below the reasons in a muted fashion. 117 | /// Useful for additional details, or suggested next steps. 118 | fn helptext(&self) -> Option { 119 | None 120 | } 121 | 122 | /********** 123 | * USE ME * 124 | **********/ 125 | 126 | /// Prints the formatted error. 127 | /// # Example 128 | /// ``` 129 | /// use user_error::{UserFacingError, UFE}; 130 | /// UserFacingError::new("File failed to open") 131 | /// .reason("File not found") 132 | /// .help("Try: touch file.txt") 133 | /// .print(); 134 | /// ``` 135 | fn print(&self) { 136 | /* Print Summary */ 137 | eprintln!("{}", pretty_summary(&self.summary())); 138 | 139 | /* Print list of Reasons (if any) */ 140 | if let Some(reasons) = pretty_reasons(self.reasons()) { 141 | eprintln!("{}", reasons); 142 | } 143 | 144 | /* Print help text (if any) */ 145 | if let Some(helptext) = pretty_helptext(self.helptext()) { 146 | eprintln!("{}", helptext); 147 | } 148 | } 149 | 150 | /// Convenience function that pretty prints the error and exits the program. 151 | /// # Example 152 | /// ```should_panic 153 | /// use user_error::{UserFacingError, UFE}; 154 | /// UserFacingError::new("File failed to open") 155 | /// .reason("File not found") 156 | /// .help("Try: touch file.txt") 157 | /// .print_and_exit(); 158 | /// ``` 159 | fn print_and_exit(&self) { 160 | self.print(); 161 | std::process::exit(1) 162 | } 163 | 164 | /// Consumes the UFE and returns a UserFacingError. Useful if you want 165 | /// access to additional functions to edit the error message before exiting 166 | /// the program. 167 | /// # Example 168 | /// ``` 169 | /// use user_error::{UserFacingError, UFE}; 170 | /// use std::fmt::{self, Display}; 171 | /// use std::error::Error; 172 | /// 173 | /// #[derive(Debug)] 174 | /// struct MyError {} 175 | /// 176 | /// impl Display for MyError { 177 | /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 178 | /// write!(f, "MyError") 179 | /// } 180 | /// } 181 | /// 182 | /// impl Error for MyError { 183 | /// fn source(&self) -> Option<&(dyn Error + 'static)> { None } 184 | /// } 185 | /// 186 | /// impl UFE for MyError {} 187 | /// 188 | /// fn main() { 189 | /// let me = MyError {}; 190 | /// me.print(); 191 | /// me.into_ufe() 192 | /// .help("Added help text") 193 | /// .print(); 194 | /// } 195 | /// ``` 196 | fn into_ufe(&self) -> UserFacingError { 197 | UserFacingError { 198 | summary: self.summary(), 199 | reasons: self.reasons(), 200 | helptext: self.helptext(), 201 | source: None, 202 | } 203 | } 204 | } 205 | 206 | /********** 207 | * STRUCT * 208 | **********/ 209 | type Summary = String; 210 | type Reasons = Option>; 211 | type Helptext = Option; 212 | type Source = Option>; 213 | 214 | /// The eponymous struct. You can create a new one from using 215 | /// user_error::UserFacingError::new() however I recommend you use your own 216 | /// error types and have them implement UFE instead of using UserFacingError 217 | /// directly. This is more of an example type, or a way to construct a pretty 218 | /// messages without implementing your own error type. 219 | #[derive(Debug)] 220 | pub struct UserFacingError { 221 | summary: Summary, 222 | reasons: Reasons, 223 | helptext: Helptext, 224 | source: Source, 225 | } 226 | 227 | /****************** 228 | * IMPLEMENTATION * 229 | ******************/ 230 | 231 | // Implement Display so our struct also implements std::error::Error 232 | impl Display for UserFacingError { 233 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 234 | let summary = pretty_summary(&self.summary()); 235 | let reasons = pretty_reasons(self.reasons()); 236 | let helptext = pretty_helptext(self.helptext()); 237 | 238 | // Love this - thanks Rust! 239 | match (summary, reasons, helptext) { 240 | (summary, None, None) => writeln!(f, "{}", summary), 241 | (summary, Some(reasons), None) => writeln!(f, "{}\n{}", summary, reasons), 242 | (summary, None, Some(helptext)) => writeln!(f, "{}\n{}", summary, helptext), 243 | (summary, Some(reasons), Some(helptext)) => { 244 | writeln!(f, "{}\n{}\n{}", summary, reasons, helptext) 245 | } 246 | } 247 | } 248 | } 249 | 250 | // Implement std::error::Error 251 | impl Error for UserFacingError { 252 | fn source(&self) -> Option<&(dyn Error + 'static)> { 253 | match self.source { 254 | Some(_) => self.source.as_deref(), 255 | None => None, 256 | } 257 | } 258 | } 259 | 260 | // Implement our own trait for our example struct 261 | // Cloning is not super efficient but this should be the last thing a program 262 | // does, and it will only do it once so... ¯\_(ツ)_/¯ 263 | impl UFE for UserFacingError { 264 | fn summary(&self) -> Summary { 265 | self.summary.clone() 266 | } 267 | fn reasons(&self) -> Reasons { 268 | self.reasons.clone() 269 | } 270 | fn helptext(&self) -> Helptext { 271 | self.helptext.clone() 272 | } 273 | } 274 | 275 | // Helper function to keep things DRY 276 | fn get_ufe_struct_members(error: &(dyn Error)) -> (Summary, Reasons) { 277 | /* Error Display format is the summary */ 278 | let summary = error.to_string(); 279 | /* Form the reasons from the error source chain */ 280 | let reasons = error_sources(error.source()); 281 | (summary, reasons) 282 | } 283 | 284 | /// Allows you to create UserFacingErrors From std::io::Error for convenience 285 | /// You should really just implement UFE for your error type, but if you wanted 286 | /// to convert before quitting so you could add help text of something you can 287 | /// use this. 288 | impl From for UserFacingError { 289 | fn from(error: std::io::Error) -> UserFacingError { 290 | let (summary, reasons) = get_ufe_struct_members(&error); 291 | 292 | UserFacingError { 293 | summary, 294 | reasons, 295 | helptext: None, 296 | source: Some(Box::new(error)), 297 | } 298 | } 299 | } 300 | 301 | /// Allows you to create UserFacingErrors From std Errors. 302 | /// You should really just implement UFE for your error type, but if you wanted 303 | /// to convert before quitting so you could add help text of something you can 304 | /// use this. 305 | impl From> for UserFacingError { 306 | fn from(error: Box<(dyn Error)>) -> UserFacingError { 307 | let (summary, reasons) = get_ufe_struct_members(error.as_ref()); 308 | 309 | UserFacingError { 310 | summary, 311 | reasons, 312 | helptext: None, 313 | source: Some(error), 314 | } 315 | } 316 | } 317 | 318 | /// Allows you to create UserFacingErrors From std Errors. 319 | /// You should really just implement UFE for your error type, but if you wanted 320 | /// to convert before quitting so you could add help text of something you can 321 | /// use this. 322 | impl From<&(dyn Error)> for UserFacingError { 323 | fn from(error: &(dyn Error)) -> UserFacingError { 324 | let (summary, reasons) = get_ufe_struct_members(error); 325 | 326 | UserFacingError { 327 | summary, 328 | reasons, 329 | helptext: None, 330 | source: None, 331 | } 332 | } 333 | } 334 | 335 | /// Allows you to create UserFacingErrors From std Errors wrapped in a Result 336 | /// You should really just implement UFE for your error type, but if you wanted 337 | /// to convert before quitting so you could add help text of something you can 338 | /// use this. 339 | impl From>> for UserFacingError { 340 | fn from(error: Result>) -> UserFacingError { 341 | /* Panics if you try to convert an Ok() Result to a UserFacingError */ 342 | let error = error.unwrap_err(); 343 | let (summary, reasons) = get_ufe_struct_members(error.as_ref()); 344 | 345 | UserFacingError { 346 | summary, 347 | reasons, 348 | helptext: None, 349 | source: Some(error), 350 | } 351 | } 352 | } 353 | 354 | impl UserFacingError { 355 | /// This is how users create a new User Facing Error. The value passed to 356 | /// new() will be used as an error summary. Error summaries are displayed 357 | /// first, prefixed by 'Error: '. 358 | /// # Example 359 | /// ``` 360 | /// # use user_error::UserFacingError; 361 | /// let err = UserFacingError::new("File failed to open"); 362 | /// ``` 363 | pub fn new>(summary: S) -> UserFacingError { 364 | UserFacingError { 365 | summary: summary.into(), 366 | reasons: None, 367 | helptext: None, 368 | source: None, 369 | } 370 | } 371 | 372 | /// Replace the error summary. 373 | /// # Example 374 | /// ``` 375 | /// # use user_error::UserFacingError; 376 | /// let mut err = UserFacingError::new("File failed to open"); 377 | /// err.update("Failed Task"); 378 | /// ``` 379 | pub fn update>(&mut self, summary: S) { 380 | self.summary = summary.into(); 381 | } 382 | 383 | /// Replace the error summary and add the previous error summary to the 384 | /// list of reasons 385 | /// # Example 386 | /// ``` 387 | /// # use user_error::UserFacingError; 388 | /// let mut err = UserFacingError::new("File failed to open"); 389 | /// err.push("Failed Task"); 390 | /// ``` 391 | pub fn push>(&mut self, new_summary: S) { 392 | // Add the old summary to the list of reasons 393 | let old_summary = self.summary(); 394 | match self.reasons.as_mut() { 395 | Some(reasons) => reasons.insert(0, old_summary), 396 | None => self.reasons = Some(vec![old_summary]), 397 | } 398 | 399 | // Update the summary 400 | self.summary = new_summary.into(); 401 | } 402 | 403 | /// Add a reason to the UserFacingError. Reasons are displayed in a 404 | /// bulleted list below the summary, in the reverse order they were added. 405 | /// # Example 406 | /// ``` 407 | /// # use user_error::UserFacingError; 408 | /// let err = UserFacingError::new("File failed to open") 409 | /// .reason("File not found") 410 | /// .reason("Directory cannot be entered"); 411 | /// ``` 412 | pub fn reason>(mut self, reason: S) -> UserFacingError { 413 | self.reasons = match self.reasons { 414 | Some(mut reasons) => { 415 | reasons.push(reason.into()); 416 | Some(reasons) 417 | } 418 | None => Some(vec![reason.into()]), 419 | }; 420 | self 421 | } 422 | 423 | // Return ref to previous? 424 | 425 | /// Clears all reasons from a UserFacingError. 426 | /// # Example 427 | /// ``` 428 | /// # use user_error::UserFacingError; 429 | /// let mut err = UserFacingError::new("File failed to open") 430 | /// .reason("File not found") 431 | /// .reason("Directory cannot be entered"); 432 | /// err.clear_reasons(); 433 | /// ``` 434 | pub fn clear_reasons(&mut self) { 435 | self.reasons = None; 436 | } 437 | 438 | /// Add help text to the error. Help text is displayed last, in a muted 439 | /// fashion. 440 | /// # Example 441 | /// ``` 442 | /// # use user_error::UserFacingError; 443 | /// let err = UserFacingError::new("File failed to open") 444 | /// .reason("File not found") 445 | /// .help("Check if the file exists."); 446 | /// ``` 447 | pub fn help>(mut self, helptext: S) -> UserFacingError { 448 | self.helptext = Some(helptext.into()); 449 | self 450 | } 451 | 452 | /// Clears all the help text from a UserFacingError. 453 | /// # Example 454 | /// ``` 455 | /// # use user_error::UserFacingError; 456 | /// let mut err = UserFacingError::new("File failed to open") 457 | /// .reason("File not found") 458 | /// .reason("Directory cannot be entered") 459 | /// .help("Check if the file exists."); 460 | /// err.clear_helptext(); 461 | /// ``` 462 | pub fn clear_helptext(&mut self) { 463 | self.helptext = None; 464 | } 465 | } 466 | 467 | #[cfg(test)] 468 | mod tests { 469 | use super::*; 470 | // Statics to keep the testing DRY/cleaner 471 | static S: &'static str = "Test Error"; 472 | static R: &'static str = "Reason 1"; 473 | static H: &'static str = "Try Again"; 474 | 475 | #[test] 476 | fn new_test() { 477 | eprintln!("{}", UserFacingError::new("Test Error")); 478 | } 479 | 480 | #[test] 481 | fn summary_test() { 482 | let e = UserFacingError::new(S); 483 | let expected = [SUMMARY_PREFIX, S, RESET, "\n"].concat(); 484 | assert_eq!(e.to_string(), String::from(expected)); 485 | eprintln!("{}", e); 486 | } 487 | 488 | #[test] 489 | fn helptext_test() { 490 | let e = UserFacingError::new(S).help(H); 491 | let expected = format!( 492 | "{}{}{}\n{}{}{}\n", 493 | SUMMARY_PREFIX, S, RESET, HELPTEXT_PREFIX, H, RESET 494 | ); 495 | assert_eq!(e.to_string(), expected); 496 | eprintln!("{}", e); 497 | } 498 | 499 | #[test] 500 | fn reason_test() { 501 | let e = UserFacingError::new(S).reason(R).reason(R); 502 | 503 | /* Create Reasons String */ 504 | let reasons = vec![String::from(R), String::from(R)]; 505 | let mut reason_strings = Vec::with_capacity(reasons.len()); 506 | for reason in reasons { 507 | let bullet_point = [REASON_PREFIX, &reason].concat(); 508 | reason_strings.push(bullet_point); 509 | } 510 | // Join the bullet points with a newline, append a RESET ASCII escape 511 | // code to the end. 512 | let reasons = [&reason_strings.join("\n"), RESET].concat(); 513 | 514 | let expected = format!("{}{}{}\n{}\n", SUMMARY_PREFIX, S, RESET, reasons); 515 | assert_eq!(e.to_string(), expected); 516 | eprintln!("{}", e); 517 | } 518 | 519 | #[test] 520 | fn push_test() { 521 | let mut e = UserFacingError::new(S).reason("R1"); 522 | e.push("R2"); 523 | 524 | /* Create Reasons String */ 525 | let reasons = vec![String::from(S), String::from("R1")]; 526 | let mut reason_strings = Vec::with_capacity(reasons.len()); 527 | for reason in reasons { 528 | let bullet_point = [REASON_PREFIX, &reason].concat(); 529 | reason_strings.push(bullet_point); 530 | } 531 | // Join the bullet points with a newline, append a RESET ASCII escape 532 | // code to the end 533 | let reasons = [&reason_strings.join("\n"), RESET].concat(); 534 | 535 | let expected = format!("{}{}{}\n{}\n", SUMMARY_PREFIX, "R2", RESET, reasons); 536 | assert_eq!(e.to_string(), expected); 537 | eprintln!("{}", e); 538 | } 539 | 540 | #[test] 541 | fn push_test_empty() { 542 | let mut e = UserFacingError::new(S); 543 | e.push("S2"); 544 | 545 | // Create Reasons String 546 | let reasons = vec![String::from(S)]; 547 | let mut reason_strings = Vec::with_capacity(reasons.len()); 548 | for reason in reasons { 549 | let bullet_point = [REASON_PREFIX, &reason].concat(); 550 | reason_strings.push(bullet_point); 551 | } 552 | // Join the bullet points with a newline, append a RESET ASCII escape 553 | // code to the end 554 | let reasons = [&reason_strings.join("\n"), RESET].concat(); 555 | 556 | let expected = format!("{}{}{}\n{}\n", SUMMARY_PREFIX, "S2", RESET, reasons); 557 | assert_eq!(e.to_string(), expected); 558 | eprintln!("{}", e); 559 | } 560 | 561 | #[test] 562 | fn reason_and_helptext_test() { 563 | let e = UserFacingError::new(S).reason(R).reason(R).help(H); 564 | 565 | // Create Reasons String 566 | let reasons = vec![String::from(R), String::from(R)]; 567 | let mut reason_strings = Vec::with_capacity(reasons.len()); 568 | for reason in reasons { 569 | let bullet_point = [REASON_PREFIX, &reason].concat(); 570 | reason_strings.push(bullet_point); 571 | } 572 | 573 | // Join the bullet points with a newline, append a RESET ASCII escape 574 | // code to the end 575 | let reasons = [&reason_strings.join("\n"), RESET].concat(); 576 | 577 | let expected = format!( 578 | "{}{}{}\n{}\n{}{}{}\n", 579 | SUMMARY_PREFIX, S, RESET, reasons, HELPTEXT_PREFIX, H, RESET 580 | ); 581 | assert_eq!(e.to_string(), expected); 582 | eprintln!("{}", e); 583 | } 584 | 585 | #[test] 586 | fn from_error_test() { 587 | let error_text = "Error"; 588 | let ioe = std::io::Error::new(std::io::ErrorKind::Other, error_text); 589 | 590 | // Lose the type 591 | fn de(ioe: std::io::Error) -> Box { 592 | Box::new(ioe) 593 | } 594 | // Convert to UFE 595 | let ufe: UserFacingError = de(ioe).into(); 596 | 597 | let expected = [SUMMARY_PREFIX, error_text, RESET, "\n"].concat(); 598 | assert_eq!(ufe.to_string(), expected); 599 | } 600 | 601 | #[test] 602 | fn from_error_source_test() { 603 | let ufe: UserFacingError = get_super_error().into(); 604 | let expected = [ 605 | SUMMARY_PREFIX, 606 | "SuperError", 607 | RESET, 608 | "\n", 609 | REASON_PREFIX, 610 | "Sidekick", 611 | RESET, 612 | "\n", 613 | ] 614 | .concat(); 615 | 616 | assert_eq!(ufe.to_string(), expected); 617 | } 618 | 619 | // Used for to test that source is working correctly 620 | #[derive(Debug)] 621 | struct SuperError { 622 | side: SuperErrorSideKick, 623 | } 624 | 625 | impl Display for SuperError { 626 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 627 | write!(f, "SuperError") 628 | } 629 | } 630 | 631 | impl Error for SuperError { 632 | fn source(&self) -> Option<&(dyn Error + 'static)> { 633 | Some(&self.side) 634 | } 635 | } 636 | 637 | #[derive(Debug)] 638 | struct SuperErrorSideKick; 639 | 640 | impl Display for SuperErrorSideKick { 641 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 642 | write!(f, "Sidekick") 643 | } 644 | } 645 | 646 | impl Error for SuperErrorSideKick { 647 | fn source(&self) -> Option<&(dyn Error + 'static)> { 648 | None 649 | } 650 | } 651 | 652 | fn get_super_error() -> Result<(), Box> { 653 | Err(Box::new(SuperError { 654 | side: SuperErrorSideKick, 655 | })) 656 | } 657 | 658 | // Custom Error Type 659 | #[derive(Debug)] 660 | struct MyError { 661 | mssg: String, 662 | src: Option>, 663 | } 664 | 665 | impl Display for MyError { 666 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 667 | write!(f, "{}", self.mssg.to_string()) 668 | } 669 | } 670 | 671 | impl Error for MyError { 672 | fn source(&self) -> Option<&(dyn Error + 'static)> { 673 | self.src.as_deref() 674 | } 675 | } 676 | 677 | impl UFE for MyError {} 678 | 679 | #[test] 680 | fn custom_error_implements_ufe() { 681 | let me = MyError { 682 | mssg: "Program Failed".into(), 683 | src: Some(Box::new(MyError { 684 | mssg: "Reason 1".into(), 685 | src: Some(Box::new(MyError { 686 | mssg: "Reason 2".into(), 687 | src: None, 688 | })), 689 | })), 690 | }; 691 | me.print(); 692 | me.into_ufe().help("Helptext Added").print(); 693 | } 694 | } 695 | -------------------------------------------------------------------------------- /tests/simple.rs: -------------------------------------------------------------------------------- 1 | /* Duh */ 2 | use std::fmt::{self, Display}; 3 | use user_error::{UserFacingError, UFE}; 4 | 5 | /* Standard Library */ 6 | use std::error::Error; 7 | 8 | #[test] 9 | fn simple_constructor_test() { 10 | let _ufe = UserFacingError::new("Too gay to live"); 11 | } 12 | 13 | #[test] 14 | fn complex_builder_test() { 15 | let _ufe = UserFacingError::new("Too cool for cats") 16 | .reason("Neato shades") 17 | .reason("Fashionable jacket") 18 | .help("There is no help coming"); 19 | } 20 | 21 | #[test] 22 | fn to_error_coercion_test() { 23 | fn returns_err() -> Result<(), Box> { 24 | Err(Box::new(UserFacingError::new("Error"))) 25 | } 26 | 27 | match returns_err() { 28 | Ok(_) => panic!(), 29 | Err(e) => eprintln!("{}", e), 30 | } 31 | } 32 | 33 | // Dummy Error type to ensure that we can implement UFE on it 34 | #[derive(Debug)] 35 | struct MyError { 36 | sub: MySubError, 37 | } 38 | 39 | impl Display for MyError { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | write!(f, "MyError") 42 | } 43 | } 44 | 45 | impl Error for MyError { 46 | fn source(&self) -> Option<&(dyn Error + 'static)> { 47 | Some(&self.sub) 48 | } 49 | } 50 | 51 | // Dummy sub error to represent the error chain 52 | #[derive(Debug)] 53 | struct MySubError { 54 | sub: MySubSubError, 55 | } 56 | 57 | impl Display for MySubError { 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 59 | write!(f, "MySubError") 60 | } 61 | } 62 | 63 | impl Error for MySubError { 64 | fn source(&self) -> Option<&(dyn Error + 'static)> { 65 | Some(&self.sub) 66 | } 67 | } 68 | 69 | // Dummy sub-sub error to represent the error chain 70 | #[derive(Debug)] 71 | struct MySubSubError; 72 | 73 | impl Display for MySubSubError { 74 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 75 | write!(f, "MySubSubError") 76 | } 77 | } 78 | 79 | impl Error for MySubSubError { 80 | fn source(&self) -> Option<&(dyn Error + 'static)> { 81 | None 82 | } 83 | } 84 | 85 | impl UFE for MyError {} 86 | 87 | #[test] 88 | fn custom_error_implements_ufe() { 89 | let me = MyError { 90 | sub: MySubError { 91 | sub: MySubSubError {}, 92 | }, 93 | }; 94 | me.summary(); 95 | me.reasons(); 96 | me.helptext(); 97 | me.print(); 98 | } 99 | --------------------------------------------------------------------------------