-
Notifications
You must be signed in to change notification settings - Fork 251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add an optional way to handle contract errors with Result
#745
Changes from 3 commits
5d6a7c8
f645024
e6425e6
bf4c468
43b65d8
f8494f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
mod code_generator; | ||
mod info_extractor; | ||
mod metadata; | ||
mod utils; | ||
pub use code_generator::*; | ||
pub use info_extractor::*; | ||
pub use metadata::metadata_visitor::MetadataVisitor; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
use syn::{GenericArgument, Path, PathArguments, Type}; | ||
|
||
/// Checks whether the given path is literally "Result". | ||
/// Note that it won't match a fully qualified name `core::result::Result` or a type alias like | ||
/// `type StringResult = Result<String, String>`. | ||
pub(crate) fn path_is_result(path: &Path) -> bool { | ||
path.leading_colon.is_none() | ||
&& path.segments.len() == 1 | ||
&& path.segments.iter().next().unwrap().ident == "Result" | ||
} | ||
|
||
/// Equivalent to `path_is_result` except that it works on `Type` values. | ||
pub(crate) fn type_is_result(ty: &Type) -> bool { | ||
match ty { | ||
Type::Path(type_path) if type_path.qself.is_none() => path_is_result(&type_path.path), | ||
_ => false, | ||
} | ||
} | ||
|
||
/// Extracts the Ok type from a `Result` type. | ||
/// | ||
/// For example, given `Result<String, u8>` type it will return `String` type. | ||
pub(crate) fn extract_ok_type(ty: &Type) -> Option<&Type> { | ||
match ty { | ||
Type::Path(type_path) if type_path.qself.is_none() && path_is_result(&type_path.path) => { | ||
// Get the first segment of the path (there should be only one, in fact: "Result"): | ||
let type_params = &type_path.path.segments.first()?.arguments; | ||
// We are interested in the first angle-bracketed param responsible for Ok type ("<String, _>"): | ||
let generic_arg = match type_params { | ||
PathArguments::AngleBracketed(params) => Some(params.args.first()?), | ||
_ => None, | ||
}?; | ||
// This argument must be a type: | ||
match generic_arg { | ||
GenericArgument::Type(ty) => Some(ty), | ||
_ => None, | ||
} | ||
} | ||
_ => None, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,4 +19,5 @@ fn compilation_tests() { | |
t.pass("compilation_tests/cond_compilation.rs"); | ||
t.compile_fail("compilation_tests/payable_view.rs"); | ||
t.pass("compilation_tests/borsh_storage_key.rs"); | ||
t.pass("compilation_tests/function_error.rs"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test is awesome, perhaps it's worth adding a test with invalid format(s) to assert it fails consistently and with a valid error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok so, I have spent half a day trying to figure out why a negative test I wrote was not failing at compile-time and finally realized that we are compiling all these files with the host target rather than wasm32-unknown-unknown, so none of the wasm-specific code we generate is tested here. I have tried making There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Oh, interesting. Yeah I wasn't aware of this. Yeah I’d say just leave it and we can create an issue and tackle it separately not to block this one. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
//! Testing FunctionError macro. | ||
|
||
use borsh::{BorshDeserialize, BorshSerialize}; | ||
use near_sdk::{near_bindgen, FunctionError}; | ||
use std::fmt; | ||
|
||
#[derive(FunctionError, BorshSerialize)] | ||
struct ErrorStruct { | ||
message: String, | ||
} | ||
|
||
impl fmt::Display for ErrorStruct { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
write!(f, "error ocurred: {}", self.message) | ||
} | ||
} | ||
|
||
#[derive(FunctionError, BorshSerialize)] | ||
enum ErrorEnum { | ||
NotFound, | ||
Banned { account_id: String }, | ||
} | ||
|
||
impl fmt::Display for ErrorEnum { | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
match self { | ||
ErrorEnum::NotFound => write!(f, "not found"), | ||
ErrorEnum::Banned { account_id } => write!(f, "account {} is banned", account_id) | ||
} | ||
} | ||
} | ||
|
||
#[near_bindgen] | ||
#[derive(BorshDeserialize, BorshSerialize, Default)] | ||
struct Contract {} | ||
|
||
#[near_bindgen] | ||
impl Contract { | ||
#[return_result] | ||
pub fn set(&self, value: String) -> Result<String, ErrorStruct> { | ||
Err(ErrorStruct { message: format!("Could not set to {}", value) }) | ||
} | ||
|
||
#[return_result] | ||
pub fn get(&self) -> Result<String, ErrorEnum> { | ||
Err(ErrorEnum::NotFound) | ||
} | ||
} | ||
|
||
fn main() {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
use std::borrow::Borrow; | ||
|
||
/// Enables contract runtime to panic with the given type. Any error type used in conjunction | ||
/// with `#[return_result]` has to implement this trait. | ||
/// | ||
/// ``` | ||
/// use near_sdk::FunctionError; | ||
/// | ||
/// enum Error { | ||
/// NotFound, | ||
/// Unexpected { message: String }, | ||
/// } | ||
/// | ||
/// impl FunctionError for Error { | ||
/// fn panic(&self) { | ||
/// match self { | ||
/// Error::NotFound => | ||
/// near_sdk::env::panic_str("not found"), | ||
/// Error::Unexpected { message } => | ||
/// near_sdk::env::panic_str(&format!("unexpected error: {}", message)) | ||
/// } | ||
/// } | ||
/// } | ||
/// ``` | ||
pub trait FunctionError { | ||
fn panic(&self); | ||
itegulov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
impl<T> FunctionError for T | ||
where | ||
T: Borrow<str>, | ||
itegulov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
fn panic(&self) { | ||
crate::env::panic_str(self.borrow()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that we're making similar calls to
expect
in a couple places, but @austinabell do we want to eventually get rid of theseexpect
calls since they should be bringing the formatting mechanism. Or is there some reason we're keeping them?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
iirc this expect doesn't embed file-specific data in the error message when used in a proc macro, but I could be remembering wrong why I didn't switch these.
Might be worth opening an issue and double-checking before next release, but wouldn't be a breaking change to come in after anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ticket: #746