Skip to content
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

Merged
merged 10 commits into from
May 11, 2021

Conversation

austinabell
Copy link
Contributor

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).

Copy link
Contributor

@matklad matklad left a 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)?;
Copy link
Contributor

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 Show resolved Hide resolved
}

impl FromStr for Base58CryptoHash {
type Err = Box<dyn std::error::Error>;
Copy link
Contributor

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

Suggested change
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

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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> {
        ...
    }
}

Copy link
Contributor Author

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

Copy link
Contributor

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%.

@austinabell
Copy link
Contributor Author

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.

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;
Copy link
Contributor

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

Copy link
Contributor Author

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).

Copy link
Contributor

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)
}

Copy link
Contributor

@evgenykuzyakov evgenykuzyakov May 6, 2021

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

@austinabell
Copy link
Contributor Author

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

@austinabell austinabell changed the title feat: implement FromStr for Base58CryptoHash and cleanup feat(json_types): implement FromStr for Base58CryptoHash and cleanup May 10, 2021
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));
Copy link
Contributor

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]
Copy link
Contributor

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

Copy link
Contributor Author

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

Copy link
Contributor

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.

@mikedotexe mikedotexe merged commit cc73d55 into near:master May 11, 2021
#[derive(Debug)]
pub enum ParseCryptoHashError {
InvalidLength(usize),
Base58(B58Error),
Copy link
Contributor

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),
}

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

Copy link
Contributor

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 .

Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants