I've recently started learning Rust, and I'm currently trying to create a small API; I've made my own struct for an API Response and enum for an Error:
// API Response Structure
pub struct ApiResponse {
pub body: JsonValue,
pub status: Status,
}
// Enum for my "custom errors"
#[derive(Debug, Snafu, Clone, Serialize)]
pub enum Errors {
#[snafu(display("Unable to retrieve the specified entry."))]
NotFound,
#[snafu(display("Internal Server Error, unable to process the data."))]
ISE,
}
Then I've implemented the Responder<'_> trait for my API Response struct:
impl<'r> Responder<'r> for ApiResponse {
fn respond_to(self, req: &Request) -> Result<Response<'r>, Status> {
Response::build_from(self.body.respond_to(req).unwrap())
.status(self.status)
.header(ContentType::JSON)
.ok()
}
}
However, when I try to use the mentioned, it would seem as I cannot have my function return a Result<JsonValue, Errors>. I'm not quite sure about this issue, nor am I that experienced with Rust, so any documentation/pointers will be greatly appreciated.
Here's the function which returns the Response type.
#[put("/users/<user_id>", format = "json", data = "<new_user>")]
pub fn update_user(
conn: DbConnection,
user_id: i32,
new_user: Json<NewUser>,
) -> Result<JsonValue, ApiResponse> {
match users::table.find(user_id).load::<User>(&*conn) {
Ok(result) => {
if result.len() < 1 {
let response = ApiResponse {
body: json!({
"message": Errors::NotFound
}),
status: Status::NotFound
};
return Err(response)
}
},
Err(e) => {
let response = ApiResponse {
body: json!({
"message": Errors::ISE
}),
status: Status::InternalServerError
};
return Err(response);
},
}
match diesel::update(users::table.filter(id.eq(user_id)))
.set((
user_name.eq(new_user.user_name.to_string()),
age.eq(new_user.age),
gender.eq(new_user.gender.to_string()),
))
.get_result::<User>(&*conn)
{
Ok(result) => Ok(json!(result)),
Err(_) => {
let res = ApiResponse {
body: json!({
"message": Errors::ISE
}),
status: Status::InternalServerError
};
return Err(res);
},
}
}
Side-note: Please keep in mind that I'm still a beginner with Rust, and my error handling / code in general is not the best.
Edit: I forgot to include the error stack:
error[E0277]: the trait bound `std::result::Result<rocket_contrib::json::JsonValue, api_cont::ApiResponse>: rocket::response::Responder<'_>` is not satisfied
--> src/routes.rs:72:6
|
72 | ) -> Result<JsonValue, ApiResponse> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `rocket::response::Responder<'_>` is not implemented for `std::result::Result<rocket_contrib::json::JsonValue, api_cont::ApiResponse>`
|
::: /home/kenneth/.cargo/registry/src/github.com-1ecc6299db9ec823/rocket-0.4.4/src/handler.rs:202:20
|
202 | pub fn from<T: Responder<'r>>(req: &Request, responder: T) -> Outcome<'r> {
| ------------- required by this bound in `rocket::handler::<impl rocket::Outcome<rocket::Response<'r>, rocket::http::Status, rocket::Data>>::from`
|
= help: the following implementations were found:
<std::result::Result<R, E> as rocket::response::Responder<'r>>
<std::result::Result<R, E> as rocket::response::Responder<'r>>
I've run into this error as well.
The following is mentioned in the API Docs:
A type implementing Responder should implement the Debug trait when possible. This is because the Responder implementation for Result requires its Err type to implement Debug. Therefore, a type implementing Debug can more easily be composed.
That means you must implement Debug, as you use ApiResponse as an Err type.
#[derive(Debug)]
pub struct ApiResponse {
pub body: JsonValue,
pub status: Status,
}
Related
#[derive(BorshSerialize, BorshDeserialize)]
pub struct NotesDs {
pub own: Vec<String>,
pub shared: UnorderedMap<AccountId,Vec<String>>,
}
impl NotesDs{
pub fn new() -> Self {
assert!(env::state_read::<Self>().is_none(), "Already initialized");
Self {
own: Vec:: new(),
shared: UnorderedMap::new(b"w".to_vec()),
}
}
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Note {
pub note_list : UnorderedMap<AccountId,NotesDs>,
}
impl Default for Note {
fn default() -> Self {
// Check incase the contract is not initialized
env::panic(b"The contract is not initialized.")
}
}
#[near_bindgen]
impl Note {
/// Init attribute used for instantiation.
#[init]
pub fn new() -> Self {
assert!(env::state_read::<Self>().is_none(), "Already initialized");
Self {
note_list: UnorderedMap::new(b"h".to_vec()),
}
}
pub fn add_notes2(&mut self, status: String){
if self.note_list.get(&env::predecessor_account_id()).is_none() {
let mut temp = NotesDs:: new();
let mut vec = Vec:: new();
let mut vec2 = Vec:: new();
vec.push(status.clone());
temp.own = vec;
temp.shared = vec2;
self.note_list.insert(&env::predecessor_account_id(), &temp);
}
else {
let mut temp1 = self.note_list.get(&env::predecessor_account_id()).unwrap();
let mut vec1 = temp1.own;
vec1.push(status.clone());
temp1.own = vec1;
self.note_list.insert(&env::predecessor_account_id(), &temp1);
}
}
}
I am getting the following error
Failure [share.meghaha.testnet]: Error: {"index":0,"kind":{"ExecutionError":"Smart contract panicked: panicked at 'Cannot deserialize the contract state.: Custom { kind: InvalidInput, error: \"Unexpected length of input\" }', /home/meghaa105/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-3.1.0/src/environment/env.rs:786:46"}}
ServerTransactionError: {"index":0,"kind":{"ExecutionError":"Smart contract panicked: panicked at 'Cannot deserialize the contract state.: Custom { kind: InvalidInput, error: \"Unexpected length of input\" }', /home/meghaa105/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-3.1.0/src/environment/env.rs:786:46"}}
at Object.parseResultError (/usr/lib/node_modules/near-cli/node_modules/near-api-js/lib/utils/rpc_errors.js:31:29)
at Account.signAndSendTransactionV2 (/usr/lib/node_modules/near-cli/node_modules/near-api-js/lib/account.js:160:36)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async scheduleFunctionCall (/usr/lib/node_modules/near-cli/commands/call.js:57:38)
at async Object.handler (/usr/lib/node_modules/near-cli/utils/exit-on-error.js:52:9) {
type: 'FunctionCallError',
context: undefined,
index: 0,
kind: {
ExecutionError: `Smart contract panicked: panicked at 'Cannot deserialize the contract state.: Custom { kind: InvalidInput, error: "Unexpected length of input" }', /home/meghaa105/.cargo/registry/src/github.com-1ecc6299db9ec823/near-sdk-3.1.0/src/environment/env.rs:786:46`
},
transaction_outcome: {
block_hash: 'EesG3NjqXdbYZqEYE22nC12AYpU3gkC9uaC7rSjToGSA',
id: '89g7HhiXgZFZRLntMzFCPk82TQ5m8diwW2nh6jVnEgKz',
outcome: {
executor_id: 'share.meghaha.testnet',
gas_burnt: 2428050684172,
logs: [],
metadata: [Object],
receipt_ids: [Array],
status: [Object],
tokens_burnt: '242805068417200000000'
},
proof: [ [Object], [Object] ]
}
}
This error comes for the query
near calladd_notes2 '{"status" : "Trying out writing a smart contract" }'
I have even tried deleting and creating a new account with same or different names. I even tried redeploying the smart contract. Further, I also have added the serialize and deserialize dependencies. I don't know what is going wrong.
Edit: The original answer (which is marked correct) said that the standard Rust Vec can't be used in a NEAR contract. It can along with all the Rust types in https://docs.rs/borsh/0.2.9/borsh/ser/trait.BorshSerialize.html. The NEAR collections https://docs.rs/near-sdk/2.0.1/near_sdk/collections/index.html are recommended for bigger collections as they are more storage efficient, but have few features and are less familiar than Rust built ins.
Something else must have fixed the issue. Usually "can't deserialize the contract state" in NEAR happens when new contract code is deployed on an existing contract and is not compatible with the data that has been previously stored by the contract.
Original Answer
The following code may help to resolve the error. NEAR has it's own datatypes that persist the state of the contract.
near_sdk::collections::Vector is used in place of Vec.
The code below replaces Vec with the persistent NEAR Vector:
/// New imports...
use near_sdk::collections::{ UnorderedMap, Vector };
use near_sdk::{ BorshStorageKey };
#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKeyNotes {
MapKey,
OwnKey,
SharedKey
}
#[derive(BorshSerialize, BorshDeserialize)]
pub struct NotesDs {
pub own: Vector<String>,
pub shared: UnorderedMap<AccountId, Vector<String>>,
}
impl NotesDs{
pub fn new() -> Self {
assert!(env::state_read::<Self>().is_none(), "Already initialized");
let mut notesDs = Self {
own: Vector::new(StorageKeyNotes::OwnKey),
shared: UnorderedMap::<AccountId, Vector<String>>::new(StorageKeyNotes::MapKey)
};
notesDs
}
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Note {
pub note_list : UnorderedMap<AccountId,NotesDs>,
}
impl Default for Note {
fn default() -> Self {
// Check incase the contract is not initialized
env::panic(b"The contract is not initialized.")
}
}
#[near_bindgen]
impl Note {
/// Init attribute used for instantiation.
#[init]
pub fn new() -> Self {
assert!(env::state_read::<Self>().is_none(), "Already initialized");
Self {
note_list: UnorderedMap::new(b"h".to_vec()),
}
}
pub fn add_notes2(&mut self, status: String){
if self.note_list.get(&env::predecessor_account_id()).is_none() {
let mut temp = NotesDs:: new();
let mut vec = Vector::new(StorageKeyNotes::OwnKey);
let mut vec2 = Vector::new(StorageKeyNotes::SharedKey);
vec.push(&status.clone());
temp.own = vec;
temp.shared.insert(&String::from("Max Power"), &vec2);
self.note_list.insert(&env::predecessor_account_id(), &temp);
}
else {
let mut temp1 = self.note_list.get(&env::predecessor_account_id()).unwrap();
let mut vec1 = temp1.own;
vec1.push(&status.clone());
temp1.own = vec1;
self.note_list.insert(&env::predecessor_account_id(), &temp1);
}
}
}
The code hasn't been tested on chain but could help with solving the error.
For anybody searching for the similar error:
Smart contract panicked: panicked at 'Cannot deserialize the contract
state.: Custom { kind: InvalidData, error: "Not all bytes read" }
It usually means that a new contract has been deployed that does not work with the data that a previous contract stored.
See Not All Bytes Read Common Solutions
I´m writing a smart contract with rust in near protocol. I have the following struct for the main contract:
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
pub total_transactions: u64,
pub owner_id: AccountId,
pub transactions_per_account: LookupMap<AccountId, UnorderedSet<TransactionId>>,
pub transaction_by_id: LookupMap<TransactionId, Transaction>,
pub transaction_metadata_by_id: UnorderedMap<TransactionId, TransactionMetadata>,
}
I have already written a function to initialize the contract as the following:
#[derive(BorshSerialize)]
pub enum StorageKey {
TransactionsPerAccount,
TransactionById,
TransactionMetadataById,
}
#[near_bindgen]
impl Contract {
#[init]
pub fn new(owner_id: AccountId) -> Self {
let this = Self {
total_transactions: 0,
owner_id,
transactions_per_account: LookupMap::new(StorageKey::TransactionsPerAccount.try_to_vec().unwrap()),
transaction_by_id: LookupMap::new(StorageKey::TransactionById.try_to_vec().unwrap()),
transaction_metadata_by_id: UnorderedMap::new(
StorageKey::TransactionMetadataById.try_to_vec().unwrap(),
),
};
//return the Contract object
this
}
}
But there the UnorderedSet that is inside the LookupMap transactions_per_account is not initialize because it is supposed that there will be an UnorderedSet that corresponds to each user, so it should be created when a user log in for first time and creates an account:
I tried to write that function as the following:
#[payable]
pub fn create_transaction( //implement storage management for transactions
&mut self,
seller_id: AccountId,
buyer_id: AccountId,
price: Price,
nft_id: TokenId,
nft_contract_id: AccountId,
) -> Transaction {
let sender = env::predecessor_account_id();
let transaction = Transaction {
transaction_id: self.total_transactions,
creator_id: sender.clone(),
seller_id: seller_id.clone(),
buyer_id: buyer_id.clone(),
price: price,
nft_id: nft_id.clone(),
nft_contract_id: nft_contract_id.clone(),
amount_in_escrow: false,
token_in_escrow: false,
transaction_status: TransactionStatus::Pending,
};
//let account_as_key = transaction.creator_id.try_to_vec().unwrap();
// update number of transactions
self.total_transactions += 1;
let unordered_set = self.transactions_per_account.get(&transaction.creator_id);
match unordered_set {
Some(mut set) => {
set.insert(&transaction.transaction_id);
},
None => {
let mut set = UnorderedSet::new(&*transaction.creator_id.try_to_vec().unwrap());
set.insert(&transaction.transaction_id);
self.transactions_per_account.insert(&transaction.creator_id, &set);
},
}
self.transaction_by_id.insert(&transaction.transaction_id, &transaction);
transaction
}
Cargo check let it pass with no error but when trying to compile I got the following error:
error[E0277]: the trait bound `UnorderedSet<u64>: Serialize` is not satisfied
--> src/lib.rs:133:1
|
133 | #[near_bindgen]
| ^^^^^^^^^^^^^^^ the trait `Serialize` is not implemented for `UnorderedSet<u64>`
|
note: required by a bound in `to_vec`
--> /home/walquer/.cargo/registry/src/github.com-1ecc6299db9ec823/serde_json-1.0.78/src/ser.rs:2193:17
|
2193 | T: ?Sized + Serialize,
| ^^^^^^^^^ required by this bound in `to_vec`
= note: this error originates in the attribute macro `near_bindgen` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
I believe the error is caused by a erroneous initialization of the UnorderedSet collection that was defined inside the struct that do have the serialize trait.
Can someone tell me what im missing?
can anyone help get my head around the FromRequest trait.
As I understand, we can use it to share data with the resolvers.
I'm trying to inject the http headers so I can authenticate users before some actions in the resolvers.
See my code below.
Looks like the from_request isn't even called.
I'm using rocket 0.5.rc1
Thank you for your help
use juniper::{graphql_object, EmptySubscription, RootNode};
use mongodb::Client;
use mongodb::Database;
use rocket::{catch, catchers};
use rocket::{response::content, Rocket, State};
type Schema = RootNode<'static, Query, Mutations, EmptySubscription<AppContext>>;
use rocket::{
http::Status,
request::{FromRequest, Outcome},
Request,
};
struct Mutations;
struct Query;
#[derive(Debug)]
pub struct AppContext {
pub mongodb_pool: Database,
pub user: String,
}
impl juniper::Context for AppContext {}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AppContext {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<AppContext, Self::Error> {
let client = Client::with_uri_str("mongodb://admin:admin#localhost:28017").await;
let db = client.unwrap().database("override");
Outcome::Success(AppContext { mongodb_pool: db, user: "from request".to_string() }, )
}
}
#[graphql_object(context = AppContext)]
impl Query {
fn api_version(db: &AppContext) -> &str {
println!("{:?}", db.user);
"1.0"
}
}
#[graphql_object(context = AppContext)]
impl Mutations {
fn api_version(db: &AppContext) -> String {
"1.0".to_string()
}
}
#[rocket::get("/")]
fn graphiql() -> content::Html<String> {
juniper_rocket::graphiql_source("/graphql", None)
}
#[rocket::get("/graphql?<request>")]
async fn get_graphql_handler(
context: &State<AppContext>,
request: juniper_rocket::GraphQLRequest,
schema: &State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute(&*schema, &*context).await
}
#[rocket::post("/graphql", data = "<request>")]
async fn post_graphql_handler(
context: &State<AppContext>,
request: juniper_rocket::GraphQLRequest,
schema: &State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute(&*schema, &*context).await
}
#[tokio::main]
async fn main() {
let client = Client::with_uri_str("mongodb://admin:admin#localhost:28017").await;
let db = client.unwrap().database("app_db");
Rocket::build()
.manage(AppContext{ mongodb_pool: db, user: "from build".to_string() })
.manage(Schema::new(
Query,
Mutations,
EmptySubscription::<AppContext>::new(),
))
.mount(
"/",
rocket::routes![graphiql, get_graphql_handler, post_graphql_handler],
)
.launch()
.await
.expect("server failed to launch");
}
I'm trying to build an HTTP Server using Actix web framework in Rust.
I'm used to segregate Business Model and Business Error from HttpResponse.
For doing that, I've my service, CredentialService, that exposes a method that returns a result Result<String, CredentialServiceError>.
My WebServer exposes an API POST /login that accepts username and password and returns a JWT.
The enum `CredentialServiceError' is the following
use derive_more::{Display, Error};
#[derive(Debug, Display, Error)]
pub enum CredentialServiceError {
NoCredentialFound,
ErrorOnGeneratingJWT,
}
My handler is something like:
async fn login(request_body: web::Json<LoginRequest>, credential_service: web::Data<CredentialService>) -> Result<HttpResponse> {
let request_body: LoginRequest = request_body.0;
let jwt = credential_service.login(request_body.username, request_body.password);
let response: Result<LoginResponse, CredentialServiceError> = jwt.map(|jwt| {
LoginResponse { jwt }
});
response.into()
}
I receive this error:
the trait bound `std::result::Result<actix_web::HttpResponse, actix_web::Error>: std::convert::From<std::result::Result<model::LoginResponse, credential_service::CredentialServiceError>>` is not satisfied
the trait `std::convert::From<std::result::Result<model::LoginResponse, credential_service::CredentialServiceError>>` is not implemented for `std::result::Result<actix_web::HttpResponse, actix_web::Error>`
help: the following implementations were found:
<std::result::Result<(), idna::uts46::Errors> as std::convert::From<idna::uts46::Errors>>
<std::result::Result<(), ring::error::Unspecified> as std::convert::From<ring::bssl::Result>>
<std::result::Result<miniz_oxide::MZStatus, miniz_oxide::MZError> as std::convert::From<&miniz_oxide::StreamResult>>
<std::result::Result<miniz_oxide::MZStatus, miniz_oxide::MZError> as std::convert::From<&miniz_oxide::StreamResult>>
and 2 others
note: required because of the requirements on the impl of `std::convert::Into<std::result::Result<actix_web::HttpResponse, actix_web::Error>>` for `std::result::Result<model::LoginResponse, credential_service::CredentialServiceError>`rustc(E0277)
I've also tried to implement
impl error::ResponseError for CredentialServiceError { ... }
impl Into<HttpResponse> for LoginResponse { ... }
The the error doesn't change.
So, how can I convert Result<String, CredentialServiceError> into Result<HttpResponse> ?
So the problem you are facing is, that you want to convert your result into the Result that actix_web functions expect. I hope I understood that correctly.
I don't see an other way than to map your Result to the expected one. There are different ways you could accomplish that. If you would want to keep the implementation details in your LoginResponse struct you could use the Into trait.
impl Into<HttpResponse> for LoginResponse {
fn into(self) -> HttpResponse {
HttpResponse::Ok().body(self.jwt)
}
}
If you don't really care you can use the map functions provided by Result.
fn map_credential_error(error: CredentialServiceError) -> actix_web::error::Error {
match error {
CredentialServiceError::NoCredentialFound => {
actix_web::error::ErrorUnauthorized("Not authorized")
}
CredentialServiceError::ErrorOnGeneratingJWT => {
actix_web::error::ErrorInternalServerError("Something went wrong generating your jwt")
}
}
}
So in the end your login function would look something like this.
async fn login(
request_body: web::Json<LoginRequest>,
credential_service: web::Data<CredentialService>,
) -> Result<HttpResponse> {
let request_body: LoginRequest = request_body.0;
let jwt = credential_service.login(request_body.username, request_body.password);
let response: Result<HttpResponse, CredentialServiceError> =
jwt.map(|jwt| LoginResponse { jwt }.into());
response.map_err(map_credential_error)
}
Hope I could help and that I understood your question.
In addition to #Julius-Kreutz's answer you may consider impl ResponseError trait Source from Chanda, Abhishek. For example if your CredentialServiceError is an enum like:
#[derive(Debug, Display)]
pub enum CredentialServiceError {
#[display(fmt = "Internal Server Error")]
InternalServerError,
#[display(fmt = "BadRequest: {}", _0)]
BadRequest(String),
#[display(fmt = "JWKSFetchError")]
JWKSFetchError,
}
Then yuo can do something like:
impl ResponseError for CredentialServiceError {
fn error_response(&self) -> HttpResponse {
match self {
CredentialServiceError::InternalServerError => {
HttpResponse::InternalServerError().json("Internal Server Error, Please try later")
}
CredentialServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
ServiceError::JWKSFetchError => {
HttpResponse::InternalServerError().json("Could not fetch JWKS")
}
}
}
}
To enable the .into()
Introduction
I'm learning rust and have been trying to find the right signature for using multiple Results in a single function and then returning either correct value, or exit the program with a message.
So far I have 2 different methods and I'm trying to combine them.
Context
This is what I'm trying to achieve:
fn blur(image: DynamicImage, amount: &str) -> DynamicImage {
let amount = parse_between_or_error_out("blur", amount, 0.0, 10.0);
image.brighten(amount)
}
This is what I have working now, but would like to refactor.
fn blur(image: DynamicImage, amount: &str) -> DynamicImage {
match parse::<f32>(amount) {
Ok(amount) => {
verify_that_value_is_between("blur", amount, 0.0, 10.0);
image.blur(amount)
}
_ => {
println!("Error");
process::exit(1)
}
}
}
Combining these methods
Now here's the two working methods that I'm trying to combine, to achieve this.
fn parse<T: FromStr>(value: &str) -> Result<T, <T as FromStr>::Err> {
value.parse::<T>()
}
fn verify_that_value_is_between<T: PartialOrd + std::fmt::Display>(
name: &str,
amount: T,
minimum: T,
maximum: T,
) {
if amount > maximum || amount < minimum {
println!(
"Error: Expected {} amount to be between {} and {}",
name, minimum, maximum
);
process::exit(1)
};
println!("- Using {} of {:.1}/{}", name, amount, maximum);
}
Here's what I tried
I have tried the following. I realise I'm likely doing a range of things wrong. This is because I'm still learning Rust, and I'd like any feedback that helps me learn how to improve.
fn parse_between_or_error_out<T: PartialOrd + FromStr + std::fmt::Display>(
name: &str,
amount: &str,
minimum: T,
maximum: T,
) -> Result<T, <T as FromStr>::Err> {
fn error_and_exit() {
println!(
"Error: Expected {} amount to be between {} and {}",
name, minimum, maximum
);
process::exit(1);
}
match amount.parse::<T>() {
Ok(amount) => {
if amount > maximum || amount < minimum {
error_and_exit();
};
println!("- Using {} of {:.1}/{}", name, amount, maximum);
amount
}
_ => {
error_and_exit();
}
}
}
Currently this looks quite messy, probably I'm using too many or the wrong types and the error needs to be in two places (hence the inlined function, which I know is not good practice).
Full reproducible example.
The question
How to best combine logic that is using a Result and another condition (or Result), exit with a message or give T as a result?
Comments on any of the mistakes are making are very welcome too.
You can use a crate such as anyhow to bubble your events up and handle them as needed.
Alternatively, you can write your own trait and implement it on Result.
trait PrintAndExit<T> {
fn or_print_and_exit(&self) -> T;
}
Then use it by calling the method on any type that implements it:
fn try_get_value() -> Result<bool, MyError> {
MyError { msg: "Something went wrong".to_string() }
}
let some_result: Result<bool, MyError> = try_get_value();
let value: bool = some_result.or_print_and_exit();
// Exits with message: "Error: Something went wrong"
Implementing this trait on Result could be done with:
struct MyError {
msg: String,
}
impl<T> PrintAndExit<T> for Result<T, MyError> {
fn or_print_and_exit(&self) -> T {
match self {
Ok(val) => val,
Err(e) => {
println!("Error: {}", e.msg);
std::process::exit(1);
},
}
}
}
Here are a few DRY tricks.
tl;dr:
Convert other Errors into your unified error type(s) with impl From<ExxError> for MyError;
In any function that may result in an Error, use ? as much as you can. Return Result<???, MyError> (*). ? will utilize the implicit conversion.
(*) Only if MyError is an appropriate type for the function. Always create or use the most appropriate error types. (Kinda obvious, but people often treat error types as a second-class code, pun intended)
Recommendations are in the comments.
use std::error::Error;
use std::str::FromStr;
// Debug and Display are required by "impl Error" below.
#[derive(Debug)]
enum ProcessingError {
NumberFormat{ message: String },
NumberRange{ message: String },
ProcessingError{ message: String },
}
// Display will be used when the error is printed.
// No need to litter the business logic with error
// formatting code.
impl Display for ProcessingError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ProcessingError::NumberFormat { message } =>
write!(f, "Number format error: {}", message),
ProcessingError::NumberRange { message } =>
write!(f, "Number range error: {}", message),
ProcessingError::ProcessingError { message } =>
write!(f, "Image processing error: {}", message),
}
}
}
impl Error for ProcessingError {}
// FromStr::Err will be implicitly converted into ProcessingError,
// when ProcessingError is needed. I guess this is what
// anyhow::Error does under the hood.
// Implement From<X> for ProcessingError for every X error type
// that your functions like process_image() may encounter.
impl From<FromStr::Err> for ProcessingError {
fn from(e: FromStr::Err) -> ProcessingError {
ProcessingError::NumberFormat { message: format!("{}", e) }
}
}
pub fn try_parse<T: FromStr>(value: &str) -> Result<T, ProcessingError> {
// Note ?. It will implicitly return
// Err(ProcessingError created from FromStr::Err)
Ok (
value.parse::<T>()?
)
}
// Now, we can have each function only report/handle errors that
// are relevant to it. ? magically eliminates meaningless code like
// match x { ..., Err(e) => Err(e) }.
pub fn parse_between<T>(value: &str, min_amount: T, max_amount: T)
-> Result<T, ProcessingError>
where
T: FromStr + PartialOrd + std::fmt::Display,
{
let amount = try_parse::<T>(value)?;
if amount > max_amount || amount < min_amount {
Err(ProcessingError::NumberRange {
message: format!(
"Expected value to be between {} and {} but received {}",
min_amount,
max_amount,
amount)
})
} else {
Ok(amount)
}
}
main.rs
use image::{DynamicImage};
use std::fmt::{Debug, Formatter, Display};
fn blur(image: DynamicImage, value: &str)
-> Result<DynamicImage, ProcessingError>
{
let min_amount = 0.0;
let max_amount = 10.0;
// Again, note ? in the end.
let amount = parse_between(value, min_amount, max_amount)?;
image.blur(amount)
}
// All processing extracted into a function, whose Error
// then can be handled by main().
fn process_image(image: DynamicImage, value: &str)
-> Result<DynamicImage, ProcessingError>
{
println!("applying blur {:.1}/{:.1}...", amount, max_amount);
image = blur(image, value);
// save image ...
image
}
fn main() {
let mut image = DynamicImage::new(...);
image = match process_image(image, "1") {
Ok(image) => image,
// No need to reuse print-and-exit functionality. I doubt
// you want to reuse it a lot.
// If you do, and then change your mind, you will have to
// root it out of all corners of your code. Better return a
// Result and let the caller decide what to do with errors.
// Here's a single point to process errors and exit() or do
// something else.
Err(e) => {
println!("Error processing image: {:?}", e);
std::process::exit(1);
}
}
}
Sharing my results
I'll share my results/answer as well for other people who are new to Rust. This answer is based on that of #Acidic9's answer.
The types seem to be fine
anyhow looks to be the de facto standard in Rust.
I should have used a trait and implement that trait for the Error type.
I believe the below example is close to what it might look like in the wild.
// main.rs
use image::{DynamicImage};
use app::{parse_between, PrintAndExit};
fn main() {
// mut image = ...
image = blur(image, "1")
// save image
}
fn blur(image: DynamicImage, value: &str) -> DynamicImage {
let min_amount = 0.0;
let max_amount = 10.0;
match parse_between(value, min_amount, max_amount).context("Input error") {
Ok(amount) => {
println!("applying blur {:.1}/{:.1}...", amount, max_amount);
image.blur(amount)
}
Err(error) => error.print_and_exit(),
}
}
And the implementation inside the apps library, using anyhow.
// lib.rs
use anyhow::{anyhow, Error, Result};
use std::str::FromStr;
pub trait Exit {
fn print_and_exit(self) -> !;
}
impl Exit for Error {
fn print_and_exit(self) -> ! {
eprintln!("{:#}", self);
std::process::exit(1);
}
}
pub fn try_parse<T: FromStr>(value: &str) -> Result<T, Error> {
match value.parse::<T>() {
Ok(value) => Ok(value),
Err(_) => Err(anyhow!("\"{}\" is not a valid value.", value)),
}
}
pub fn parse_between<T>(value: &str, min_amount: T, max_amount: T) -> Result<T, Error>
where
T: FromStr + PartialOrd + std::fmt::Display,
{
match try_parse::<T>(value) {
Ok(amount) => {
if amount > max_amount || amount < min_amount {
return Err(anyhow!(
"Expected value to be between {} and {} but received {}",
min_amount,
max_amount,
amount
));
};
Ok(amount)
}
Err(error) => Err(error),
}
}
Hopefully seeing this full implementation will help someone out there.
Source code.