-
Notifications
You must be signed in to change notification settings - Fork 241
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(json_types): implement FromStr for Base58CryptoHash and cleanup #398
Conversation
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.
Hm, this got me thinking...
Do we perhaps want to introduce pub struct ParseCrypoHashError
error, while we are adding this API anyway? There's one problem with Box<dyn Error>
-- it's not trivially compatible with anyhow
and such. That is, you can't ?
one type-erased error into the other.
let s = <String as serde::Deserialize>::deserialize(deserializer)?; | ||
s.try_into() | ||
.map_err(|err: Box<dyn std::error::Error>| serde::de::Error::custom(err.to_string())) | ||
let s: Cow<'de, str> = Deserialize::deserialize(deserializer)?; |
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.
👍 nice optimization!
near-sdk/src/json_types/hash.rs
Outdated
} | ||
|
||
impl FromStr for Base58CryptoHash { | ||
type Err = Box<dyn std::error::Error>; |
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.
If this were a general-purpose probject, I'd write
type Err = Box<dyn std::error::Error>; | |
type Err = Box<dyn std::error::Error + Send + Sync>; |
but I guess for us it doesn't really make sense
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 didn't want to open up to a breaking change, but I've created an alias to make this error type easier to switch in the future
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.
This particular impl is new, so, for this impl, we can change the error type, as long as it is convertable to original one via ?
. In particular, changing this (but only this) type to ParseCryptoHashError
would work.
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.
which ParseCryptoHashError
are you referring to? The box std::error with send/sync or are you referring to something specific/ generating a new error type?
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.
Sorry for being confusing here. I mean we can do the following, which should is backwards compatible, but, at the same time, is forwards compatible with a nice API of a hypothetical ideal sdk.
impl TryFrom<&str> for Base58CryptoHash {
type Error = Box<dyn std::error::Error>;
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let res = value.parse()?; // `?` converts ParseCryptoHashError to Box<dyn std::error::Error>
Ok(res)
}
}
#[non_exhaustive]
pub struct ParseCryptoHashError;
impl std::error::Error for ParseCryptoHashError { ... }
impl std::str::FromStr for Base58CryptoHash {
// Ok to use new type here, as this is a new impl, so we don't care about backcompat.
type Err = ParseCryptoHashError;
fn from_str(value: &str) -> Result<Self, ParseCryptoHashError> {
...
}
}
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.
Yeah, the only reason I'm hesitant to add this is that it's unclear what the exact Error will be in the future. Using a concrete type like this enforces we keep that type (and trait bounds) in this ideal future API.
I'm happy to make the change and manually implement std::error if someone gives me the go-ahead with a thumbs up on this comment
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.
is that it's unclear what the exact Error will be in the future
I think it's probably good to keep errors for well-isolated parts of code specific. IE, even we have something like type NearError = ...
in the future, it would still make sense to use ParseCryptoHashError
here.
I don't think that'll affect the size of the compiled contract (the type is ZST, the implementation is trivial and inlinable, etc), though my confidence level here is not 100%.
I'm glad you brought it up. I was going to open an issue around changing this as the standard, as this error type is used everywhere. I was going to ask if this would be an acceptable change but was not in a rush because I didn't know if the base type was used to minimize compilation size. |
@@ -1,6 +1,10 @@ | |||
use crate::CryptoHash; | |||
use borsh::{BorshDeserialize, BorshSerialize}; | |||
use std::convert::{TryFrom, TryInto}; | |||
use serde::{de, ser, Deserialize}; | |||
use std::borrow::Cow; |
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.
We should try to rebuild some contract that uses Base58CryptoHash
with the contract builder, to compare the compilation size with using Cow
. I don't expect anything dramatic, but just to be careful
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.
if you give me a workflow or point me in the direction of one that does, I can do that. I'd be curious to see if it actually increases the compiled code (and to what scale).
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.
Not sure what's the existing workflow, but I'd suggest adding a smoke test for this.
Something stupid like:
#[test]
fn test_we_are_smol() {
std::process::Command::new("cargo")
.current_dir("examples/locakable-fungible-token")
.env("RUSTFLAGS", "--link-args=-s")
.args(&["build", "--release", "--target", "wasm"])
.status().unwrap()
let wasm = std::fs::read("examples/locakable-fungible-token/target/wasm").unwrap();
assert!(100k <= wasm && wasm <= 300k)
}
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.
You can use Base58CryptoHash in examples/test-contract
and compile it before the change and after the change using contract builder https://github.com/near/near-sdk-rs/tree/master/contact-builder and compare binary sizes
before this and #391 are pulled in, should probably come to consensus or I just implement a concrete error type from the new parse method |
let mut crypto_hash: CryptoHash = CryptoHash::default(); | ||
let size = bs58::decode(value).into(&mut crypto_hash)?; | ||
if size != std::mem::size_of::<CryptoHash>() { | ||
return Err("Invalid length of the crypto hash (32)".into()); | ||
return Err(ParseCryptoHashError::InvalidLength(size)); |
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.
🙏🏼
} | ||
Ok(Self(crypto_hash)) | ||
} | ||
} | ||
|
||
#[non_exhaustive] |
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.
Interesting, I'm making a mental note of this as it may be a macro we'd think about adding when we use Enums for contract upgradability. Like:
https://nomicon.io/ChainSpec/Upgradability.html#versioned-data-structures
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.
Yeah it's super handy when handling releases as it opens a lot of changes to not break semver, but can be harder to track the additions on new releases externally
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.
pedantry police: #[non_exhaustive]
is an attribute. Some attributes are macros (like derives or #[near_bindgen]
), but this one isn't, it's just some meta info for the compiler.
#[derive(Debug)] | ||
pub enum ParseCryptoHashError { | ||
InvalidLength(usize), | ||
Base58(B58Error), |
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.
Note that this makes B58Error
a public API, so if we chose to switch to a different library to implement this (or even do a major upgrade of Base58), then we'd have to do a major semver bump. I'd personally would try to be concervative with our errors and a) get maximum room for future extensibility b) without exposing internal implementation details. So, something like this:
pub struct ParseCryptoHashError { kind: ParseCryptoHashErrorKind }
enum ParseCryptoHashErrorKind {
InvalidLength(usize),
Base58(B58Error),
}
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.
How about switching the B58 Error to be a String
or something? The functionality that would be nice to keep is being able to match the error type, and this can't happen if the kind is private.
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.
Or I can just switch to keep the variants private in for this (and the other usage of b58 errors), let me know if you think that's better. I did overlook exposing the b58 error type
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.
Do people need to match over the error though? My gut feeling is that people generally need only a Display out of this error.
For error classification of foundational libraries, the ErrorKind pattern, as used by std::io::Error is the best -- you keep the error opaque, but there's a .kind()
fildless enum which allows for matching .
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.
yeah probably best to be safe for now, I'll keep the types private for now, and can just expose later if/when solidified
a similar change to #391 and also cleanup for readability and deserialize type for potential performance increase (
Cow
can avoid an allocation depending on the underlying deserialize buffer).