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

Replace ClientMap with new Resolver #594

Merged
merged 13 commits into from
Feb 23, 2022
Merged

Replace ClientMap with new Resolver #594

merged 13 commits into from
Feb 23, 2022

Conversation

cycraig
Copy link
Contributor

@cycraig cycraig commented Jan 17, 2022

Description of change

Adds the new Resolver API discussed in #422 and #493.

The new Resolver replaces the current ClientMap and is very similar in design. The differences are:

  • Resolver does not expose publish_* methods. This is to encourage the use of the Account over the low-level API.
  • Resolver does not automatically link to both the Devnet and Mainnet networks (old behaviour of ClientMap). It has a default constructor that creates a Client for the Mainnet but this can be overridden using the ResolverBuilder.
  • Resolver contains convenience functions for resolving DID Documents associated with verifiable Credentials and Presentations.

What is not included in this PR?

Why is this different from the outcome of #422?

As mentioned in #493, it was decided after internal discussions to avoid implementing a compliant resolver API as it is more limiting than the functionality we expose through identity.rs directly. Many of its features like which encoding to accept, for example, are centred around web/API resolution rather than the strongly-typed framework we provide. Universal resolver implementations can still use identity.rs to implement such a resolver quite easily.

Notable Changes

Added

  • Add Resolver, WasmResolver.
  • Add ResolverBuilder

Removed

  • Remove ClientMap.

Open Questions

  1. Should we revert to a ClientMap-like interface where we expose helper functions for publishing across different networks?
  2. Should we expose ResolverBuilder to the Wasm bindings in order to remove set_client and remove_client from Resolver?

Links to any relevant issues

Fixes issue #493

Type of change

  • Bug fix (a non-breaking change which fixes an issue)
  • Enhancement (a non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Fix

How the change has been tested

Several VC examples have been updated to use the Resolver but only in Rust. Will be more prevalent after #599.

Change checklist

  • I have followed the contribution guidelines for this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@cycraig cycraig added Wasm Related to Wasm bindings. Becomes part of the Wasm changelog Breaking change A change to the API that requires a major release. Part of "Changed" section in changelog Rust Related to the core Rust code. Becomes part of the Rust changelog. labels Jan 17, 2022
@cycraig cycraig changed the title Add new Resolver Replace ClientMap with new Resolver Jan 17, 2022
@cycraig cycraig marked this pull request as ready for review February 22, 2022 22:01
Copy link
Contributor

@PhilippGackstatter PhilippGackstatter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

I think the DashMap can be replaced with a HashMap. The resolver should be useful enough without post-creation mutability.

identity-iota/src/tangle/resolver.rs Outdated Show resolved Hide resolved
Comment on lines 42 to 43
// TODO: is DashMap really necessary here?
// We can avoid mutation if we disallow adding/removing Clients post-creation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes a lot of sense to me to setup a resolver once at build-time and then disallow mutation afterwards. We don't have to commit to allowing mutability now, but if the need arises later, adding it is non-breaking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I guess we need the ResolverBuilder in the Wasm bindings then? It was not ported since there is no good way to avoid the builder pattern in Wasm in this case, but it would be the only option if we remove the mutators.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not take a list of ClientOrBuilders in WasmResolver::new, or even just Clients? Or is that the not-good-way you meant 😄?

const resolver = new Resolver([
      Client.fromConfig(Config.fromNetwork(Network.mainnet())),
      Client.fromConfig(Config.fromNetwork(Network.devnet())),
  ]);

Is the problem that someone could provide two clients with the same network name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know there is no way to take Vec<WasmClient> as a parameter (Vec<T> is not supported in general), and if I recall correctly we cannot cast JsValue to one of our exported structs, and since Client and ClientBuilder are not serializable (for good reason), we cannot use a duck-typed Typescript interface either.

I'll try again just to be sure though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, my bad, I was thinking along the lines of the serde solution, but of course that doesn't work with Clients. Perhaps a map would work alternatively, where the keys are network names and the values Clients. Using Reflect::own_keys we could iterate the keys and retrieve the clients. That would even prevent duplication.

Copy link
Contributor Author

@cycraig cycraig Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that returns the keys of the map/object, not the values (Clients)?

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys#using_reflect.ownkeys

Edit: not the point, I still think the map values will only give JsValue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this would actually be pretty ugly typing-wise, as we'd have to follow-up with Reflect::get which only returns a JsValue as you pointed out, and we'd have to downcast that to Client similar to how WasmStorage does that. That should be avoided if possible, though. Exposing the builder seems like the best option after all. Sorry for the half-baked ideas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally all this would be avoided if wasm-bindgen supported dyn_ref/dyn_into on exported types, but it will probably never happen: https://rustwasm.github.io/wasm-bindgen/api/wasm_bindgen/trait.JsCast.html#method.dyn_into

Wow that downcasting workaround is scary. I mean it works but there's got to be a better way than relying on the constructor name: rustwasm/wasm-bindgen#2231 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposed WasmResolverBuilder. Using it looks like this in Javascript:

const client = new Client();

const resolver = await Resolver
        .builder()
        .client(client)
        .clientConfig(Config.fromNetwork(Network.devnet()))
        .build();

}

/// Returns the [`Client`] corresponding to the given [`NetworkName`] if one exists.
pub fn get_client(&self, network_name: &NetworkName) -> Option<ClientRc> {
Copy link
Contributor

@olivereanderson olivereanderson Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the following scenario:
Crate x: depends on identity-iota without the arc feature . In Crate x there is a function

pub fn get_client() -> Option<Rc<Client>> {
  let resolver: Resolver = .. ;
  resolver.get_client()
}

Crate y: depends on identity-iota with the arc feature
Crate z: depends directly on Crate x and has a function
fn foo(client: Rc<Client>);
The following code works in crate z:

let client = crate_x::get_client().unwrap();
foo(client);

but if crate y ever becomes part of crate z's dependency tree (possibly via transitive dependencies) I think crate zwill no longer compile?

This is probably the reason why the im crate comes in two flavours im and im-rc (See https://docs.rs/im/latest/im/#thread-safety) instead of changing the pointer types via feature flags.

We might want to consider using the archery crate and make Resolver generic over the pointer type. Then the new constructor in the WasmResolver can use the Rc variant and in the Rust library we either remove new and require construction through the builder or use an Arc there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dependencies are resolved with the union of all features enabled on them. For example, if one package depends on the im package with the serde dependency enabled and another package depends on it with the rayon dependency enabled, then im will be built with both features enabled, and the serde and rayon crates will be included in the resolve graph. If no packages depend on im with those features, then those optional dependencies will be ignored, and they will not affect resolution. https://doc.rust-lang.org/cargo/reference/resolver.html#features

I guess you're right that if our crate is a dependency of two other crates, and one enables arc and the other doesn't, there will be a conflict.

The archery crate looks interesting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point, I agree that putting arc as a feature isn't ideal since it's not additive and can lead to the situation you described.

I'll try archery for now since we would want to use the same pattern to fix the Account using Arc instead of Rc in Wasm.

Copy link
Contributor Author

@cycraig cycraig Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the problem with archery I've found is that it does not support converting from Arc/Rc to their SharedPointer, nor vice-versa (at least as far as I can tell).

This is a problem because we explicitly use Rc<Client> in WasmClient rather than SharedPointer<Client, RcK> (which the Resolver uses), so it's not possible to convert the Resolver entries into WasmClient.

E.g.

#[wasm_bindgen(js_name = getClient)]
pub fn get_client(&self, network_name: String) -> Option<WasmClient> {
  let network_name: NetworkName = NetworkName::try_from(network_name).ok()?;
  let client: SharedPointer<Client, RcK> = self.0.get_client(&network_name).cloned()?;
  let rc_client: Rc<Client> = client.into(); // not possible with any safe function archery exposes.
  Some(WasmClient { client: rc_client })
}

The reverse also appears not possible.

E.g.

#[wasm_bindgen]
pub fn client(mut self, client: &WasmClient) -> WasmResolverBuilder {
  let client: Rc<Client> = client.client.clone();
  let shared_client: SharedPointer<Client, RcK> = client.into(); // not possible with any safe function archery exposes.
  self.0 = self.0.client(shared_client);
  self
}

Edit: the other problem is requiring dependents to import the archery dependency themselves to switch between ArcK and RcK but that doesn't seem so bad.

Copy link
Contributor Author

@cycraig cycraig Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One solution would be to use SharedPointer<Client, RcK> exclusively and replace Rc<Client> in the Wasm bindings, if we want to rely on archery completely?

I don't think this is very friendly to Rust developers though, since I imagine they might want something like a base Arc<Client> eventually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I have currently. It works and doesn't require any odd hacks.

We can restrict it using a sealed trait to prevent unexpected entries for T from dependents, but I don't think it's too bad as-is?

pub struct Resolver<T = Arc<Client>>
where
  T: Clone + AsRef<Client> + From<Client>,
{
  client_map: HashMap<NetworkName, T>,
}
// Developers using it in Rust will default to Resolver<Arc<Client>.
let resolver: Resolver = Resolver::new().await?; 

// Changing to Rc is a bit verbose but not bad.
let resolver: Resolver<Rc<Client>> = Resolver::<Rc<Client>>::new().await?;

Using a generic trait with an associated type of Rc or Arc for a generic T (rather than just Client) is another option but unnecessary in our case since we only require it for Client, in my opinion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's perfect 👍

Copy link
Contributor Author

@cycraig cycraig Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think you're right that a sealed trait might be better so we can change it without affecting semantic versioning? Not sure.

Copy link
Contributor

@olivereanderson olivereanderson Feb 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is nicer without the sealed trait. I can't think of any reason we would want to restrict the generic parameter T any more than what the current trait bounds do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all the feedback. I'll leave it as-is.

Copy link
Contributor

@olivereanderson olivereanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great now! Thanks for the changes.

@cycraig cycraig merged commit d29ea79 into dev Feb 23, 2022
@cycraig cycraig deleted the feat/resolver branch February 23, 2022 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking change A change to the API that requires a major release. Part of "Changed" section in changelog Rust Related to the core Rust code. Becomes part of the Rust changelog. Wasm Related to Wasm bindings. Becomes part of the Wasm changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants