Unverified Commit 1e71992d authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Add docs and more color to configbag (#2469)

* Add docs and more color to configbag

* Add delegating add_layer to configbag

* Fix lints / docs

* namespace links
parent 87a62ae3
Loading
Loading
Loading
Loading
+141 −21
Original line number Diff line number Diff line
@@ -3,23 +3,32 @@
 * SPDX-License-Identifier: Apache-2.0
 */

// This code is functionally equivalent to `Extensions` in the `http` crate. Examples
// have been updated to be more relevant for smithy use, the interface has been made public,
// and the doc comments have been updated to reflect how the config bag is used in smithy-rs.
// Additionally, optimizations around the HTTP use case have been removed in favor or simpler code.

//! Layered Configuration Bag Structure
//!
//! [`config_bag::ConfigBag`] and [`config_bag::FrozenConfigBag`] are the two representations of a layered configuration structure
//! with the following properties:
//! 1. A new layer of configuration may be applied onto an existing configuration structure without modifying it or taking ownership.
//! 2. No lifetime shenanigans to deal with
use aws_smithy_http::property_bag::PropertyBag;
use std::any::type_name;
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::Arc;

/// Layered Configuration Structure
///
/// [`ConfigBag`] is the "unlocked" form of the bag. Only the top layer of the bag may be unlocked.
#[must_use]
pub struct ConfigBag {
    head: Layer,
    tail: Option<FrozenConfigBag>,
}

/// Layered Configuration Structure
///
/// [`FrozenConfigBag`] is the "locked" form of the bag.
#[derive(Clone)]
#[must_use]
pub struct FrozenConfigBag(Arc<ConfigBag>);

impl Deref for FrozenConfigBag {
@@ -30,6 +39,17 @@ impl Deref for FrozenConfigBag {
    }
}

pub trait Persist {
    fn layer_name(&self) -> &'static str;
    fn persist(&self, layer: &mut ConfigBag);
}

pub trait Load: Sized {
    fn load(bag: &ConfigBag) -> Option<Self>;
}

pub trait ConfigLayer: Persist + Load {}

enum Value<T> {
    Set(T),
    ExplicitlyUnset,
@@ -40,13 +60,40 @@ struct Layer {
    props: PropertyBag,
}

fn no_op(_: &mut ConfigBag) {}

impl FrozenConfigBag {
    /// Attempts to convert this bag directly into a [`ConfigBag`] if no other references exist
    ///
    /// This allows modifying the top layer of the bag. [`Self::add_layer`] may be
    /// used to add a new layer to the bag.
    pub fn try_modify(self) -> Option<ConfigBag> {
        Arc::try_unwrap(self.0).ok()
    }

    #[must_use]
    pub fn with_open(&self, name: &'static str, next: impl Fn(&mut ConfigBag)) -> ConfigBag {
    /// Add a new layer to the config bag
    ///
    /// This is equivalent to calling [`Self::with_fn`] with a no-op function
    ///
    /// # Examples
    /// ```
    /// use aws_smithy_runtime_api::config_bag::ConfigBag;
    /// fn add_more_config(bag: &mut ConfigBag) { /* ... */ }
    /// let bag = ConfigBag::base().with_fn("first layer", |_| { /* add a property */ });
    /// let mut bag = bag.add_layer("second layer");
    /// add_more_config(&mut bag);
    /// let bag = bag.freeze();
    /// ```
    pub fn add_layer(&self, name: &'static str) -> ConfigBag {
        self.with_fn(name, no_op)
    }

    pub fn with(&self, layer: impl Persist) -> ConfigBag {
        self.with_fn(layer.layer_name(), |bag| layer.persist(bag))
    }

    /// Add more items to the config bag
    pub fn with_fn(&self, name: &'static str, next: impl Fn(&mut ConfigBag)) -> ConfigBag {
        let new_layer = Layer {
            name,
            props: PropertyBag::new(),
@@ -58,10 +105,6 @@ impl FrozenConfigBag {
        next(&mut bag);
        bag
    }

    pub fn with(&self, name: &'static str, next: impl Fn(&mut ConfigBag)) -> Self {
        self.with_open(name, next).close()
    }
}

impl ConfigBag {
@@ -75,6 +118,7 @@ impl ConfigBag {
        }
    }

    /// Retrieve the value of type `T` from the bag if exists
    pub fn get<T: Send + Sync + Debug + 'static>(&self) -> Option<&T> {
        let mut source = vec![];
        let out = self.sourced_get(&mut source);
@@ -82,29 +126,59 @@ impl ConfigBag {
        out
    }

    /// Insert `value` into the bag
    pub fn put<T: Send + Sync + Debug + 'static>(&mut self, value: T) -> &mut Self {
        self.head.props.insert(Value::Set(value));
        self
    }

    /// Remove `T` from this bag
    pub fn unset<T: Send + Sync + 'static>(&mut self) -> &mut Self {
        self.head.props.insert(Value::<T>::ExplicitlyUnset);
        self
    }

    pub fn close(self) -> FrozenConfigBag {
    /// Freeze this layer by wrapping it in an `Arc`
    ///
    /// This prevents further items from being added to this layer, but additional layers can be
    /// added to the bag.
    pub fn freeze(self) -> FrozenConfigBag {
        self.into()
    }

    #[must_use]
    pub fn with(self, name: &'static str, next: impl Fn(&mut ConfigBag)) -> FrozenConfigBag {
        self.close().with_open(name, next).close()
    /// Add another layer to this configuration bag
    ///
    /// Hint: If you want to re-use this layer, call `freeze` first.
    /// ```
    /// use aws_smithy_runtime_api::config_bag::ConfigBag;
    /// let bag = ConfigBag::base();
    /// let first_layer = bag.with_fn("a", |b: &mut ConfigBag| { b.put("a"); }).freeze();
    /// let second_layer = first_layer.with_fn("other", |b: &mut ConfigBag| { b.put(1i32); });
    /// // The number is only in the second layer
    /// assert_eq!(first_layer.get::<i32>(), None);
    /// assert_eq!(second_layer.get::<i32>(), Some(&1));
    ///
    /// // The string is in both layers
    /// assert_eq!(first_layer.get::<&'static str>(), Some(&"a"));
    /// assert_eq!(second_layer.get::<&'static str>(), Some(&"a"));
    /// ```
    pub fn with_fn(self, name: &'static str, next: impl Fn(&mut ConfigBag)) -> ConfigBag {
        self.freeze().with_fn(name, next)
    }

    pub fn with(self, layer: impl Persist) -> ConfigBag {
        self.freeze().with(layer)
    }

    pub fn add_layer(self, name: &'static str) -> ConfigBag {
        self.freeze().add_layer(name)
    }

    pub fn sourced_get<T: Send + Sync + Debug + 'static>(
        &self,
        source_trail: &mut Vec<SourceInfo>,
    ) -> Option<&T> {
        // todo: optimize so we don't need to compute the source if it's unused
        let bag = &self.head;
        let inner_item = self
            .tail
@@ -142,6 +216,7 @@ pub enum SourceInfo {
#[cfg(test)]
mod test {
    use super::ConfigBag;
    use crate::config_bag::{Load, Persist};

    #[test]
    fn layered_property_bag() {
@@ -160,9 +235,10 @@ mod test {
        #[derive(Debug)]
        struct Prop3;

        let mut base_bag = ConfigBag::base().with("a", layer_a).with_open("b", layer_b);
        let mut base_bag = ConfigBag::base()
            .with_fn("a", layer_a)
            .with_fn("b", layer_b);
        base_bag.put(Prop3);
        let base_bag = base_bag.close();
        assert!(base_bag.get::<Prop1>().is_some());

        #[derive(Debug)]
@@ -173,7 +249,8 @@ mod test {
            bag.unset::<Prop3>();
        };

        let final_bag = base_bag.with("c", layer_c);
        let base_bag = base_bag.freeze();
        let final_bag = base_bag.with_fn("c", layer_c);

        assert!(final_bag.get::<Prop4>().is_some());
        assert!(base_bag.get::<Prop4>().is_none());
@@ -188,7 +265,7 @@ mod test {
        let bag = ConfigBag::base();
        #[derive(Debug)]
        struct Region(&'static str);
        let bag = bag.with("service config", |layer| {
        let bag = bag.with_fn("service config", |layer: &mut ConfigBag| {
            layer.put(Region("asdf"));
        });

@@ -196,14 +273,57 @@ mod test {

        #[derive(Debug)]
        struct SigningName(&'static str);
        let operation_config = bag.with("operation", |layer| {
        let bag = bag.freeze();
        let operation_config = bag.with_fn("operation", |layer: &mut ConfigBag| {
            layer.put(SigningName("s3"));
        });

        assert!(bag.get::<SigningName>().is_none());
        assert_eq!(operation_config.get::<SigningName>().unwrap().0, "s3");

        let mut open_bag = operation_config.with_open("my_custom_info", |_bag| {});
        let mut open_bag = operation_config.with_fn("my_custom_info", |_bag: &mut ConfigBag| {});
        open_bag.put("foo");
    }

    #[test]
    fn persist_trait() {
        #[derive(Debug, Eq, PartialEq, Clone)]
        struct MyConfig {
            a: bool,
            b: String,
        }

        #[derive(Debug)]
        struct A(bool);
        #[derive(Debug)]
        struct B(String);

        impl Persist for MyConfig {
            fn layer_name(&self) -> &'static str {
                "my_config"
            }

            fn persist(&self, layer: &mut ConfigBag) {
                layer.put(A(self.a));
                layer.put(B(self.b.clone()));
            }
        }
        impl Load for MyConfig {
            fn load(bag: &ConfigBag) -> Option<Self> {
                Some(MyConfig {
                    a: bag.get::<A>().unwrap().0,
                    b: bag.get::<B>().unwrap().0.clone(),
                })
            }
        }

        let conf = MyConfig {
            a: true,
            b: "hello!".to_string(),
        };

        let bag = ConfigBag::base().with(conf.clone());

        assert_eq!(MyConfig::load(&bag), Some(conf));
    }
}