Rust API: Low and high-level

Earlier this year I worked on bindings/wrappers for the Safe APIs in Node.js. I noticed I was struggling to use it ‘ergonomically’. Now that Safe is getting to a point where looking at the high-level APIs becomes relevant, I want to write down some of my reflections.

Currently we have a low-level API (client, safe_network::client) and high-level API (app, sn_api). The high-level API at the moment mostly functions as a way to support the CLI and does this by providing a function for each CLI command.

  • Perhaps the high-level API is focused too narrowly on supporting the CLI, which is stateless.
    • As a result, it seems most function calls stand on their own. For example, to put a value in a new register on the network, the register has to be created and written to, but with two different async calls that can not be combined into a single ‘action’.
    • The low-level API does provide a way where there is separation of concerns. One can create a register, get the operations to upload and change it on the network, and execute those operations at once.
  • Most functions require a flag for specifying a ‘dry run’. This doesn’t seem ergonomic, and would be better captured with separate functionality allowing to calculate addresses and costs.
  • The high-level API can’t be used with the low-level API in tandem. This means the high-level API stands very much on its own. Which is enough for the CLI, but probably not for apps wanting to go deeper.

The high-level API can definitely be changed, but perhaps it needs quite an overhaul to make it more ergonomic to use for app developers. It at least needs to conform to the API guideluines in quite some places as a way to make it more friendly to be used.

A possibility that I also thought about is merging the two. I think the low-level API is actually very usable, is not that much different in terms of complexity and lines of code when using it. Merging will at least mean less maintenance to keep them in sync.
But, I understand the need for a high-level API that makes things more easy for developers. Though, Rust can be used in such a way to make use of sensible defaults, which still allows for a lot of control by the end developer.
Also, the high-level API provides Safe URLs and other helpful utilities and protocols, which is good to separate from the low-level part. Perhaps this needs to be put in a separate crate.

The end goal is to have a single API that allows the app developer to do everything the network is capable of, in an ergonomic way that reflects the powers of Safe. I’d love to get some input from the community and MaidSafe about this.

2 Likes

I was thinking we could set that in the Safe instance and remove the bool arg from all APIs, e.g. you do:

let safe = Safe::default();
safe.set_dry_run(true);
let xorurl = safe.store_public_bytes(my_bytes, None).await?;

To add it to the combo, we also need to consider how we can support signing each message/request offline, we have some support in sn_client API already, but sn_api still doesn’t allow us to sign the message offline, e.g. I was imagining somethig like the following could make it easy if possible:

safe.set_offline_signing(true);
let (xorurl, _, _, ops) = safe.files_container_create(....).await?;

for op in ops {
    let signed_op = sign_with_trezor(op).await?;
    safe.send_signed_message(signed_op).await?;
}
2 Likes

I agree, we have it, perhaps it’s time to move SafeUrl impl back to it: https://github.com/maidsafe/sn_url. It was earlier discussed in Getting a safe:// url for a file

This is where the builder pattern might be suitable. This allows for all kinds of sensible defaults to be put in the builder, and allowing the developer to only tweak what is necessary, instead of passing values themselves.

Regardless, I think there is a need for calculating the potential costs up front. This can be used for a UX flow where a user is asked to confirm an action that leads to PUTs with the calculated costs.