#data-access #any #no-alloc #provide

no-std supply

Provider API for arbitrary number of lifetimes

3 releases (breaking)

new 0.3.0 Jan 7, 2025
0.2.0 Sep 22, 2024
0.1.0 Sep 20, 2024

#811 in Rust patterns

MIT/Apache

38KB
398 lines

Supply

Provider API for arbitrary number of lifetimes.

Version docs.rs Version Crates.io License Crates.io

supply implements an API similar to that proposed in RFC 3192. It was proposed to provide Error and API to expose extra context information for errors. While the original RFC was a general API, the current implementation in core/std is specifically for Error. The original proof of concept implementation can be found at https://github.com/nrc/provide-any.

supply is a reimagining of the provide API to be more flexable and general purpose. The motivation section of RFC 3192 states the problem this API is trying to solve very well.

However, in practice some kind of partial abstraction is required, where objects are treated abstractly but can be queried for data only present in a subset of all types which implement the trait interface. In this case there are only bad options: speculatively downcasting to concrete types (inefficient, boilerplatey, and fragile due to breaking abstraction) or adding numerous methods to the trait which might be functionally implemented, typically returning an Option where None means not applicable for the concrete type (boilerplatey, confusing, and leads to poor abstractions).

https://rust-lang.github.io/rfcs/3192-dyno.html#motivation

A similar pattern that is very useful but rarely seen in the wild is that proposed by gdbstub. That of so called Inlineable Dyn Extension Traits (IDETs). You can think of the provide API as an abstract form of this pattern.

The core idea of the provide pattern is to use an output parameter to receive a type erased value.

You may ask why just returning a type erased value is an issue, and its a good question. The answer is that returning ownership directly requires something like a Box. However, we don't always have access to a box or don't want the extra allocation. But wait what if we just returned a &dyn Any? This doesn't need a box. However, this prevents returning owned values. We are limited to things that can be borrowed from the source.

Instead we take another approach. We construct a "hole". A place a value should go. We then give a borrow of this hole to a value for it to "fill".

This design has some major advantages. For one it solves the returning ownership issue. Instead the provider transfers ownership into the existing hole we gave it. Because as the requester we know the type of data we want we can use some stack space to store the hole and eventual value. Additionally, this design allows a requester to change the behavior of the hole. For example we can have many holes that all need to be filled by the provider in one operation.

Another aspect that supply expands on over the RFC is the use of arbitrary length lifetime lists. We won't get into those here. See the ty-tag crate for more information about those.

Connecting this theoretical design to supply's implementation we get the following. The hole is represented by something implementing the [Want] trait. This trait has methods for providing it a value to store. The provider then implements [Provider] and when it's methods are called provides any values it can to the passed in want.

A requester uses a provider by first constructing an empty [Want] implementer. Then, the requester calls .provide() on the [Provider] to give the want a value. Then the requester can remove the value from the want and use it however it needs to. Using [ProviderExt] we can use a simple method call of the form provider.request::<Request>() to request values from a provider.

It is recommended to use the prelude which has the common API elements.

use supply::prelude::*;

Most users should start with implementing the [Provider] trait on one or more types they want to expose extra information from. Then the .request::<T>() method can be used to request a specific type of information from the provider. This is the core API flow of supply. Below is an example with a couple of the more advanced features for demonstration.

Examples

use supply::prelude::*;

struct Person<'a> {
    name: &'a str,
    age: u8,
}

// Implementing Provider allows requesting data from a Person value.
impl<'r, 'a> Provider<'r> for Person<'a> {
    type Lifetimes = l!['a];

    fn provide(&'r self, want: &mut dyn Want<Self::Lifetimes>) {
        // Provide the name and age fields.
        want.provide_value(self.name)
            .provide_value(self.age);
    }
}

// Make an example person to request data from.
let name = String::from("bob");

let provided_name;
{
    let person = Person {
        name: &name,
        age: 42,
    };

    // Convert to a trait object to show Provider is object safe.
    let provider: &dyn ProviderDyn<l!['_]> = &person;

    // Request the person's name.
    provided_name = provider.request::<&str>();
    assert_eq!(provided_name, Some("bob"));

    // Request the person's age.
    let provided_age = provider.request::<u8>();
    assert_eq!(provided_age, Some(42));

    // Request something Person doesn't provide.
    // We just don't get a value from the request.
    let provided_something = provider.request::<f32>();
    assert_eq!(provided_something, None);
};

// Because the name was tagged as &'a str we can still access it here.
assert_eq!(provided_name, Some("bob"));

no_std Support

This crate is always #![no_std], it can be used anywhere Rust can.

Minimum Supported Rust Version

Requires Rust 1.83.0.

This crate follows the "Latest stable Rust" policy. The listed MSRV won't be changed unless needed. However, updating the MSRV anywhere up to the latest stable at time of release is allowed.

Contributing

Contributions in any form (issues, pull requests, etc.) to this project must adhere to Rust's Code of Conduct.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in supply by you shall be licensed as below, without any additional terms or conditions.

License

This project is licensed under either of

at your option.

Dependencies

~55KB