Skip to content

Commit

Permalink
Add Provider API for context information on errors (and possibly ot…
Browse files Browse the repository at this point in the history
…her things)

Add tests to provider


Fix newline on Cargo.toml
  • Loading branch information
TimDiekmann committed Jan 3, 2022
1 parent 1cfc2e7 commit a6420da
Show file tree
Hide file tree
Showing 6 changed files with 440 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/engine/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/engine/lib/provider/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "provider"
version = "0.0.0"
edition = "2021"
description = "`Provider` trait and accompanying API"
publish = false
55 changes: 55 additions & 0 deletions packages/engine/lib/provider/src/internal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use super::{TypeId, TypeTag};

/// Sealed trait representing a type-erased tagged object.
///
/// # Safety
///
/// This trait must be exclusively implemented by the `TagValue` type.
pub(super) unsafe trait Tagged<'p>: 'p {
/// The `TypeId` of the `TypeTag` this value was tagged with.
fn tag_id(&self) -> TypeId;
}

/// A concrete tagged value for a given tag `I`.
///
/// This is the only type which implements the `Tagged` trait, and encodes additional information
/// about the specific `TypeTag` into the type. This allows for multiple different tags to support
/// overlapping value ranges, for example, both the `Ref<str>` and `Value<&'static str>` tags can be
/// used to tag a value of type `&'static str`.
#[repr(transparent)]
pub(super) struct TagValue<'p, I: TypeTag<'p>>(pub(super) I::Type);

unsafe impl<'p, I> Tagged<'p> for TagValue<'p, I>
where
I: TypeTag<'p>,
{
fn tag_id(&self) -> TypeId {
TypeId::of::<I>()
}
}

impl<'p> dyn Tagged<'p> {
/// Returns `true` if the dynamic type is tagged with `I`.
#[inline]
pub(super) fn is<I>(&self) -> bool
where
I: TypeTag<'p>,
{
self.tag_id() == TypeId::of::<I>()
}

/// Returns some reference to the dynamic value if it is tagged with `I`, or `None` if it isn't.
#[inline]
pub(super) fn downcast_mut<I>(&mut self) -> Option<&mut TagValue<'p, I>>
where
I: TypeTag<'p>,
{
if self.is::<I>() {
// SAFETY: Just checked whether we're pointing to a
// `TagValue<'p, I>`.
unsafe { Some(&mut *(self as *mut Self).cast::<TagValue<'p, I>>()) }
} else {
None
}
}
}
230 changes: 230 additions & 0 deletions packages/engine/lib/provider/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
//! Contains the `Provider` trait and accompanying API, which enable trait objects to provide data
//! based on typed requests, an alternate form of runtime reflection.
//!
//! `Provider` and the associated APIs support generic, type-driven access to data, and a mechanism
//! for implementers to provide such data. The key parts of the interface are the `Provider` trait
//! for objects which can provide data, and the [`request_by_type_tag`] function for data from an
//! object which implements `Provider`. Note that end users should not call requesting
//! `request_by_type_tag` directly, it is a helper function for intermediate implementers to use to
//! implement a user-facing interface.
//!
//! Typically, a data provider is a trait object of a trait which extends `Provider`. A user will
//! request data from the trait object by specifying the type or a type tag (a type tag is a type
//! used only as a type parameter to identify the type which the user wants to receive).
//!
//! ## Data flow
//!
//! * A user requests an object, which is delegated to `request_by_type_tag`
//! * `request_by_type_tag` creates a `Requisition` object and passes it to `Provider::provide`
//! * The object provider's implementation of `Provider::provide` tries providing values of
//! different types using `Requisition::provide_*`. If the type tag matches the type requested by
//! the user, it will be stored in the `Requisition` object.
//! * `request_by_type_tag` unpacks the `Requisition` object and returns any stored value to the
//! user.
//!
//! # Examples
// Taken from https://github.com/rust-lang/rfcs/pull/3192
//!
//! To provide data for example on an error type, the `Provider` API enables:
//!
//! ```rust
//! # #![feature(backtrace)]
//! use std::backtrace::Backtrace;
//!
//! use provider::{tags, Provider, Requisition, TypeTag};
//!
//! struct MyError {
//! backtrace: Backtrace,
//! suggestion: String,
//! }
//!
//! impl Provider for MyError {
//! fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) {
//! req.provide_ref(&self.backtrace)
//! .provide_ref(self.suggestion.as_str());
//! }
//! }
//!
//! trait MyErrorTrait: Provider {}
//!
//! impl MyErrorTrait for MyError {}
//!
//! impl dyn MyErrorTrait {
//! fn request_ref<T: ?Sized + 'static>(&self) -> Option<&T> {
//! provider::request_by_type_tag::<'_, tags::Ref<T>, _>(self)
//! }
//! }
//! ```
//!
//! In another module or crate, this can be requested for any `dyn MyErrorTrait`, not just
//! `MyError`:
//! ```rust
//! # #![feature(backtrace)]
//! # use std::backtrace::Backtrace;
//! # use provider::{Provider, Requisition, TypeTag, tags};
//! # struct MyError { backtrace: Backtrace, suggestion: String }
//! # impl Provider for MyError {
//! # fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) {
//! # req.provide_ref(&self.backtrace)
//! # .provide_ref(self.suggestion.as_str());
//! # }
//! # }
//! # trait MyErrorTrait: Provider {}
//! # impl MyErrorTrait for MyError {}
//! # impl dyn MyErrorTrait {
//! # fn request_ref<T: ?Sized + 'static>(&self) -> Option<&T> {
//! # provider::request_by_type_tag::<'_, tags::Ref<T>, _>(self)
//! # }
//! # }
//! fn report_error(e: &(dyn MyErrorTrait + 'static)) {
//! // Generic error handling
//! // ...
//!
//! // print backtrace
//! if let Some(backtrace) = e.request_ref::<Backtrace>() {
//! println!("{backtrace:?}")
//! }
//! # assert!(e.request_ref::<Backtrace>().is_some());
//!
//! // print suggestion text
//! if let Some(suggestions) = e.request_ref::<str>() {
//! println!("Suggestion: {suggestions}")
//! }
//! # assert_eq!(e.request_ref::<str>().unwrap(), "Do it correctly next time!");
//! }
//!
//! fn main() {
//! let error = MyError {
//! backtrace: Backtrace::capture(),
//! suggestion: "Do it correctly next time!".to_string(),
//! };
//!
//! report_error(&error);
//! }
//! ```
// Heavily inspired by https://github.com/rust-lang/project-error-handling/issues/3:
// The project-error-handling tries to improves the error trait. In order to move the trait into
// `core`, an alternative solution to backtrace provisioning had to be found. This is, where the
// provider API comes from.
//
// TODO: replace library with https://github.com/rust-lang/project-error-handling/issues/3.

pub mod tags;

mod internal;
mod requisition;

use core::any::TypeId;

use self::internal::{TagValue, Tagged};
use crate::requisition::{ConcreteRequisition, RequisitionImpl};

/// Trait implemented by a type which can dynamically provide tagged values.
pub trait Provider {
/// Object providers should implement this method to provide *all* values they are able to
/// provide using `req`.
fn provide<'p>(&'p self, req: Requisition<'p, '_>);
}

/// Request a specific value by a given tag from the `Provider`.
pub fn request_by_type_tag<'p, I, P: Provider + ?Sized>(provider: &'p P) -> Option<I::Type>
where
I: TypeTag<'p>,
{
let mut req: ConcreteRequisition<'p, I> = RequisitionImpl {
tagged: TagValue(None),
};
provider.provide(Requisition(&mut req));
req.tagged.0
}

/// This trait is implemented by specific `TypeTag` types in order to allow describing a type which
/// can be requested for a given lifetime `'p`.
///
/// A few example implementations for type-driven `TypeTag`s can be found in the [`tags`] module,
/// although crates may also implement their own tags for more complex types with internal
/// lifetimes.
pub trait TypeTag<'p>: Sized + 'static {
/// The type of values which may be tagged by this `TypeTag` for the given lifetime.
type Type: 'p;
}

/// A helper object for providing objects by type.
///
/// An object provider provides values by calling this type's provide methods.
#[allow(missing_debug_implementations)]
pub struct Requisition<'p, 'r>(&'r mut RequisitionImpl<dyn Tagged<'p> + 'p>);

#[cfg(test)]
pub(crate) mod tests {
use crate::{tags, Provider, Requisition, TypeTag};

struct CustomTagA;
impl<'p> TypeTag<'p> for CustomTagA {
type Type = usize;
}

struct CustomTagB;
impl<'p> TypeTag<'p> for CustomTagB {
type Type = usize;
}

pub(crate) struct MyError {
value: usize,
reference: usize,
custom_tag_a: usize,
custom_tag_b: usize,
option: Option<usize>,
result_ok: Result<u32, i32>,
result_err: Result<i32, u32>,
}

impl Provider for MyError {
fn provide<'p>(&'p self, mut req: Requisition<'p, '_>) {
req.provide_value(|| self.value)
.provide_ref(&self.reference)
.provide_with::<CustomTagA, _>(|| self.custom_tag_a)
.provide::<CustomTagB>(self.custom_tag_b)
.provide::<tags::OptionTag<tags::Value<usize>>>(self.option)
.provide::<tags::ResultTag<tags::Value<u32>, tags::Value<i32>>>(self.result_ok)
.provide::<tags::ResultTag<tags::Value<i32>, tags::Value<u32>>>(self.result_err);
}
}

pub(crate) const ERR: MyError = MyError {
value: 1,
reference: 2,
custom_tag_a: 3,
custom_tag_b: 4,
option: Some(5),
result_ok: Ok(6),
result_err: Err(7),
};

#[test]
fn provide_value() {
assert_eq!(
crate::request_by_type_tag::<tags::Value<usize>, _>(&ERR),
Some(1)
);
}

#[test]
fn provide_ref() {
assert_eq!(
crate::request_by_type_tag::<tags::Ref<usize>, _>(&ERR),
Some(&2)
);
}

#[test]
fn provide_with() {
assert_eq!(crate::request_by_type_tag::<CustomTagA, _>(&ERR), Some(3));
}

#[test]
fn provide() {
assert_eq!(crate::request_by_type_tag::<CustomTagB, _>(&ERR), Some(4));
}
}
58 changes: 58 additions & 0 deletions packages/engine/lib/provider/src/requisition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use crate::{tags, Requisition, TagValue, TypeTag};

impl<'p> Requisition<'p, '_> {
/// Provide a value with the given `TypeTag`.
pub fn provide<I>(&mut self, value: I::Type) -> &mut Self
where
I: TypeTag<'p>,
{
if let Some(res @ TagValue(Option::None)) =
self.0.tagged.downcast_mut::<tags::OptionTag<I>>()
{
res.0 = Some(value);
}
self
}

/// Provide a value or other type with only static lifetimes.
pub fn provide_value<T, F>(&mut self, f: F) -> &mut Self
where
T: 'static,
F: FnOnce() -> T,
{
self.provide_with::<tags::Value<T>, F>(f)
}

/// Provide a reference, note that the referee type must be bounded by `'static`, but may be
/// unsized.
pub fn provide_ref<T: ?Sized + 'static>(&mut self, value: &'p T) -> &mut Self {
self.provide::<tags::Ref<T>>(value)
}

/// Provide a value with the given `TypeTag`, using a closure to prevent unnecessary work.
pub fn provide_with<I, F>(&mut self, f: F) -> &mut Self
where
I: TypeTag<'p>,
F: FnOnce() -> I::Type,
{
if let Some(res @ TagValue(Option::None)) =
self.0.tagged.downcast_mut::<tags::OptionTag<I>>()
{
res.0 = Some(f());
}
self
}
}

/// A concrete request for a tagged value. Can be coerced to `Requisition` to be passed to provider
/// methods.
pub(super) type ConcreteRequisition<'p, I> = RequisitionImpl<TagValue<'p, tags::OptionTag<I>>>;

/// Implementation detail shared between `Requisition` and `ConcreteRequisition`.
///
/// Generally this value is used through the `Requisition` type as an `&mut Requisition<'p>` out
/// parameter, or constructed with the `ConcreteRequisition<'p, I>` type alias.
#[repr(transparent)]
pub(super) struct RequisitionImpl<T: ?Sized> {
pub(super) tagged: T,
}
Loading

0 comments on commit a6420da

Please sign in to comment.