Skip to content

Commit

Permalink
Improve Wasm error handling (#344)
Browse files Browse the repository at this point in the history
* Add WasmError

Enables idiomatic Javascript error handling with js_sys::Error which
includes error enum variant names.

* Add WasmError test
  • Loading branch information
cycraig authored Aug 6, 2021
1 parent fdcf8ee commit 923d7c8
Show file tree
Hide file tree
Showing 41 changed files with 298 additions and 181 deletions.
1 change: 1 addition & 0 deletions bindings/wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ console_error_panic_hook = { version = "0.1" }
futures = { version = "0.3" }
js-sys = { version = "0.3" }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", default-features = false }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
wasm-bindgen-futures = { version = "0.4", default-features = false }

Expand Down
24 changes: 12 additions & 12 deletions bindings/wasm/src/credential/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use identity::credential::CredentialBuilder;
use identity::credential::Subject;
use wasm_bindgen::prelude::*;

use crate::utils::err;
use crate::error::wasm_error;
use crate::wasm_document::WasmDocument;

#[wasm_bindgen(inspectable)]
Expand All @@ -22,7 +22,7 @@ pub struct VerifiableCredential(pub(crate) Credential);
impl VerifiableCredential {
#[wasm_bindgen]
pub fn extend(value: &JsValue) -> Result<VerifiableCredential, JsValue> {
let mut base: Object = value.into_serde().map_err(err)?;
let mut base: Object = value.into_serde().map_err(wasm_error)?;

if !base.contains_key("credentialSubject") {
return Err("Missing property: `credentialSubject`".into());
Expand All @@ -35,23 +35,23 @@ impl VerifiableCredential {
if !base.contains_key("@context") {
base.insert(
"@context".into(),
Credential::<()>::base_context().serde_into().map_err(err)?,
Credential::<()>::base_context().serde_into().map_err(wasm_error)?,
);
}

let mut types: Vec<String> = match base.remove("type") {
Some(value) => value.serde_into().map(OneOrMany::into_vec).map_err(err)?,
Some(value) => value.serde_into().map(OneOrMany::into_vec).map_err(wasm_error)?,
None => Vec::new(),
};

types.insert(0, Credential::<()>::base_type().into());
base.insert("type".into(), types.serde_into().map_err(err)?);
base.insert("type".into(), types.serde_into().map_err(wasm_error)?);

if !base.contains_key("issuanceDate") {
base.insert("issuanceDate".into(), Timestamp::now_utc().to_string().into());
}

base.serde_into().map_err(err).map(Self)
base.serde_into().map_err(wasm_error).map(Self)
}

#[wasm_bindgen]
Expand All @@ -61,8 +61,8 @@ impl VerifiableCredential {
credential_type: Option<String>,
credential_id: Option<String>,
) -> Result<VerifiableCredential, JsValue> {
let subjects: OneOrMany<Subject> = subject_data.into_serde().map_err(err)?;
let issuer_url: Url = Url::parse(issuer_doc.0.id().as_str()).map_err(err)?;
let subjects: OneOrMany<Subject> = subject_data.into_serde().map_err(wasm_error)?;
let issuer_url: Url = Url::parse(issuer_doc.0.id().as_str()).map_err(wasm_error)?;
let mut builder: CredentialBuilder = CredentialBuilder::default().issuer(issuer_url);

for subject in subjects.into_vec() {
Expand All @@ -74,21 +74,21 @@ impl VerifiableCredential {
}

if let Some(credential_id) = credential_id {
builder = builder.id(Url::parse(credential_id).map_err(err)?);
builder = builder.id(Url::parse(credential_id).map_err(wasm_error)?);
}

builder.build().map(Self).map_err(err)
builder.build().map(Self).map_err(wasm_error)
}

/// Serializes a `VerifiableCredential` object as a JSON object.
#[wasm_bindgen(js_name = toJSON)]
pub fn to_json(&self) -> Result<JsValue, JsValue> {
JsValue::from_serde(&self.0).map_err(err)
JsValue::from_serde(&self.0).map_err(wasm_error)
}

/// Deserializes a `VerifiableCredential` object from a JSON object.
#[wasm_bindgen(js_name = fromJSON)]
pub fn from_json(json: &JsValue) -> Result<VerifiableCredential, JsValue> {
json.into_serde().map_err(err).map(Self)
json.into_serde().map_err(wasm_error).map(Self)
}
}
14 changes: 7 additions & 7 deletions bindings/wasm/src/credential/presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use identity::credential::Presentation;
use identity::credential::PresentationBuilder;
use wasm_bindgen::prelude::*;

use crate::utils::err;
use crate::error::wasm_error;
use crate::wasm_document::WasmDocument;

#[wasm_bindgen(inspectable)]
Expand All @@ -24,8 +24,8 @@ impl VerifiablePresentation {
presentation_type: Option<String>,
presentation_id: Option<String>,
) -> Result<VerifiablePresentation, JsValue> {
let credentials: OneOrMany<Credential> = credential_data.into_serde().map_err(err)?;
let holder_url: Url = Url::parse(holder_doc.0.id().as_str()).map_err(err)?;
let credentials: OneOrMany<Credential> = credential_data.into_serde().map_err(wasm_error)?;
let holder_url: Url = Url::parse(holder_doc.0.id().as_str()).map_err(wasm_error)?;

let mut builder: PresentationBuilder = PresentationBuilder::default().holder(holder_url);

Expand All @@ -38,21 +38,21 @@ impl VerifiablePresentation {
}

if let Some(presentation_id) = presentation_id {
builder = builder.id(Url::parse(presentation_id).map_err(err)?);
builder = builder.id(Url::parse(presentation_id).map_err(wasm_error)?);
}

builder.build().map_err(err).map(Self)
builder.build().map_err(wasm_error).map(Self)
}

/// Serializes a `VerifiablePresentation` object as a JSON object.
#[wasm_bindgen(js_name = toJSON)]
pub fn to_json(&self) -> Result<JsValue, JsValue> {
JsValue::from_serde(&self.0).map_err(err)
JsValue::from_serde(&self.0).map_err(wasm_error)
}

/// Deserializes a `VerifiablePresentation` object from a JSON object.
#[wasm_bindgen(js_name = fromJSON)]
pub fn from_json(json: &JsValue) -> Result<VerifiablePresentation, JsValue> {
json.into_serde().map_err(err).map(Self)
json.into_serde().map_err(wasm_error).map(Self)
}
}
10 changes: 5 additions & 5 deletions bindings/wasm/src/crypto/key_collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use wasm_bindgen::prelude::*;
use crate::crypto::Digest;
use crate::crypto::KeyPair;
use crate::crypto::KeyType;
use crate::utils::err;
use crate::error::wasm_error;

#[derive(Deserialize, Serialize)]
struct JsonData {
Expand All @@ -41,7 +41,7 @@ impl KeyCollection {
/// Creates a new `KeyCollection` with the specified key type.
#[wasm_bindgen(constructor)]
pub fn new(type_: KeyType, count: usize) -> Result<KeyCollection, JsValue> {
KeyCollection_::new(type_.into(), count).map_err(err).map(Self)
KeyCollection_::new(type_.into(), count).map_err(wasm_error).map(Self)
}

/// Returns the number of keys in the collection.
Expand Down Expand Up @@ -123,13 +123,13 @@ impl KeyCollection {
type_: self.0.type_().into(),
};

JsValue::from_serde(&data).map_err(err)
JsValue::from_serde(&data).map_err(wasm_error)
}

/// Deserializes a `KeyCollection` object from a JSON object.
#[wasm_bindgen(js_name = fromJSON)]
pub fn from_json(json: &JsValue) -> Result<KeyCollection, JsValue> {
let data: JsonData = json.into_serde().map_err(err)?;
let data: JsonData = json.into_serde().map_err(wasm_error)?;

let iter: _ = data.keys.iter().flat_map(|data| {
let pk: PublicKey = decode_b58(&data.public).ok()?.into();
Expand All @@ -139,7 +139,7 @@ impl KeyCollection {
});

KeyCollection_::from_iterator(data.type_.into(), iter)
.map_err(err)
.map_err(wasm_error)
.map(Self)
}
}
12 changes: 6 additions & 6 deletions bindings/wasm/src/crypto/key_pair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use identity::crypto::SecretKey;
use wasm_bindgen::prelude::*;

use crate::crypto::KeyType;
use crate::utils::err;
use crate::error::wasm_error;

#[derive(Deserialize, Serialize)]
struct JsonData {
Expand All @@ -31,14 +31,14 @@ impl KeyPair {
/// Generates a new `KeyPair` object.
#[wasm_bindgen(constructor)]
pub fn new(type_: KeyType) -> Result<KeyPair, JsValue> {
KeyPair_::new(type_.into()).map_err(err).map(Self)
KeyPair_::new(type_.into()).map_err(wasm_error).map(Self)
}

/// Parses a `KeyPair` object from base58-encoded public/secret keys.
#[wasm_bindgen(js_name = fromBase58)]
pub fn from_base58(type_: KeyType, public_key: &str, secret_key: &str) -> Result<KeyPair, JsValue> {
let public: PublicKey = decode_b58(public_key).map_err(err)?.into();
let secret: SecretKey = decode_b58(secret_key).map_err(err)?.into();
let public: PublicKey = decode_b58(public_key).map_err(wasm_error)?.into();
let secret: SecretKey = decode_b58(secret_key).map_err(wasm_error)?.into();

Ok(Self((type_.into(), public, secret).into()))
}
Expand All @@ -64,13 +64,13 @@ impl KeyPair {
secret: self.secret(),
};

JsValue::from_serde(&data).map_err(err)
JsValue::from_serde(&data).map_err(wasm_error)
}

/// Deserializes a `KeyPair` object from a JSON object.
#[wasm_bindgen(js_name = fromJSON)]
pub fn from_json(json: &JsValue) -> Result<KeyPair, JsValue> {
let data: JsonData = json.into_serde().map_err(err)?;
let data: JsonData = json.into_serde().map_err(wasm_error)?;

Self::from_base58(data.type_, &data.public, &data.secret)
}
Expand Down
91 changes: 91 additions & 0 deletions bindings/wasm/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2020-2021 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use std::borrow::Cow;

use wasm_bindgen::JsValue;

/// Convert an error into an idiomatic [js_sys::Error].
pub fn wasm_error<'a, T>(error: T) -> JsValue
where
T: Into<WasmError<'a>>,
{
let wasm_err: WasmError = error.into();
JsValue::from(wasm_err)
}

/// Convenience struct to convert internal errors to [js_sys::Error]. Uses [std::borrow::Cow]
/// internally to avoid unnecessary clones.
///
/// This is a workaround for orphan rules so we can implement [core::convert::From] on errors from
/// dependencies.
#[derive(Debug, Clone)]
pub struct WasmError<'a> {
pub name: Cow<'a, str>,
pub message: Cow<'a, str>,
}

impl<'a> WasmError<'a> {
pub fn new(name: Cow<'a, str>, message: Cow<'a, str>) -> Self {
Self { name, message }
}
}

/// Convert [WasmError] into [js_sys::Error] for idiomatic error handling.
impl From<WasmError<'_>> for js_sys::Error {
fn from(error: WasmError<'_>) -> Self {
let js_error = js_sys::Error::new(&error.message);
js_error.set_name(&error.name);
js_error
}
}

/// Convert [WasmError] into [wasm_bindgen::JsValue].
impl From<WasmError<'_>> for JsValue {
fn from(error: WasmError<'_>) -> Self {
JsValue::from(js_sys::Error::from(error))
}
}

/// Implement WasmError for each type individually rather than a trait due to Rust's orphan rules.
/// Each type must implement `Into<&'static str> + Display`. The `Into<&'static str>` trait can be
/// derived using `strum::IntoStaticStr`.
#[macro_export]
macro_rules! impl_wasm_error_from {
( $($t:ty),* ) => {
$(impl From<$t> for WasmError<'_> {
fn from(error: $t) -> Self {
Self {
message: Cow::Owned(error.to_string()),
name: Cow::Borrowed(error.into()),
}
}
})*
}
}

impl_wasm_error_from!(
identity::comm::Error,
identity::core::Error,
identity::credential::Error,
identity::did::Error,
identity::iota::Error
);

impl From<serde_json::Error> for WasmError<'_> {
fn from(error: serde_json::Error) -> Self {
Self {
name: Cow::Borrowed("serde_json::Error"), // the exact error code is embedded in the message
message: Cow::Owned(error.to_string()),
}
}
}

impl From<identity::iota::BeeMessageError> for WasmError<'_> {
fn from(error: identity::iota::BeeMessageError) -> Self {
Self {
name: Cow::Borrowed("bee_message::Error"),
message: Cow::Owned(error.to_string()),
}
}
}
4 changes: 3 additions & 1 deletion bindings/wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use wasm_bindgen::prelude::*;

#[macro_use]
mod macros;
mod utils;

#[macro_use]
pub mod error;

pub mod credential;
pub mod crypto;
Expand Down
10 changes: 5 additions & 5 deletions bindings/wasm/src/message/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use identity::comm;
use wasm_bindgen::prelude::*;

use crate::utils::err;
use crate::error::wasm_error;

#[wasm_bindgen(inspectable)]
#[derive(Clone, Debug, PartialEq)]
Expand All @@ -14,12 +14,12 @@ pub struct AuthenticationRequest(pub(crate) comm::AuthenticationRequest);
impl AuthenticationRequest {
#[wasm_bindgen(js_name = toJSON)]
pub fn to_json(&self) -> Result<JsValue, JsValue> {
JsValue::from_serde(&self.0).map_err(err)
JsValue::from_serde(&self.0).map_err(wasm_error)
}

#[wasm_bindgen(js_name = fromJSON)]
pub fn from_json(value: &JsValue) -> Result<AuthenticationRequest, JsValue> {
value.into_serde().map_err(err).map(Self)
value.into_serde().map_err(wasm_error).map(Self)
}
}

Expand All @@ -31,11 +31,11 @@ pub struct AuthenticationResponse(pub(crate) comm::AuthenticationResponse);
impl AuthenticationResponse {
#[wasm_bindgen(js_name = toJSON)]
pub fn to_json(&self) -> Result<JsValue, JsValue> {
JsValue::from_serde(&self.0).map_err(err)
JsValue::from_serde(&self.0).map_err(wasm_error)
}

#[wasm_bindgen(js_name = fromJSON)]
pub fn from_json(value: &JsValue) -> Result<AuthenticationResponse, JsValue> {
value.into_serde().map_err(err).map(Self)
value.into_serde().map_err(wasm_error).map(Self)
}
}
Loading

0 comments on commit 923d7c8

Please sign in to comment.