shinyframework_configuration 0.1.2

Shiny Configuration
Documentation
use std::collections::{HashMap, HashSet};
use thiserror::Error;

use crate::Configuration;
use crate::configuration_provider::ConfigurationProvider;
use crate::value::Value;

pub struct ConfigurationBuilder {
    value: Value,
    providers: HashMap<String, Value>,
}

impl ConfigurationBuilder {
    pub(crate) fn new(provider: impl ConfigurationProvider) -> Self {
        ConfigurationBuilder {
            value: provider.provide(),
            providers: HashMap::new(),
        }
    }

    pub fn set_provider(&mut self, key: impl ToString, provider: impl ConfigurationProvider) {
        self.providers.insert(key.to_string(), provider.provide());
    }

    pub fn with_provider(mut self, key: impl ToString, provider: impl ConfigurationProvider) -> Self {
        self.set_provider(key, provider);
        self
    }

    pub fn build(self) -> Result<Configuration, BuildConfigurationError> {
        let mut hashset: HashSet<String> = HashSet::new();

        let resolve_ref_ctx = ResolveRefCtx {
            key: KeyInfo::None,
            hashset: &mut hashset,
        };

        Ok(Configuration::new(resolver_refs(
            self.value,
            &self.providers,
            resolve_ref_ctx,
        )?))
    }
}

#[derive(Debug, Error)]
pub enum BuildConfigurationError {
    #[error("Circular reference: {}", .refs.join(" -> "))]
    CircularReference { refs: Vec<String> },

    #[error("Invalid reference [reference = `{0}`]")]
    InvalidReference(String),

    #[error("Reference does not exists [reference = `{0}`]")]
    ReferenceDoesNotExists(String),

    #[error("Provider does not exists [provider = `{0}`]")]
    ProviderDoesNotExists(String),
}

fn resolver_refs(
    value: Value,
    providers: &HashMap<String, Value>,
    mut ctx: ResolveRefCtx,
) -> Result<Value, BuildConfigurationError> {
    Ok(match value {
        Value::String(string) => {
            if let Some((provider_key, key)) = is_ref(&string)? {
                match providers.get(&provider_key) {
                    None => return Err(BuildConfigurationError::ProviderDoesNotExists(provider_key)),
                    Some(provider) => match provider.get(&key) {
                        None => return Err(BuildConfigurationError::ReferenceDoesNotExists(key)),
                        Some(value) => resolver_refs(
                            value.clone(),
                            providers,
                            ctx.enter_ref(&provider_key, &key)?,
                        )?,
                    },
                }
            } else {
                Value::String(string)
            }
        }
        Value::Bool(bool) => Value::Bool(bool),
        Value::Number(number) => Value::Number(number),
        Value::None => Value::None,
        Value::Map(map) => Value::Map(
            map.into_iter()
                .map(|(k, v)| {
                    let value = resolver_refs(v, providers, ctx.enter_key(&k))?;
                    Ok((k, value))
                })
                .collect::<Result<HashMap<_, _>, BuildConfigurationError>>()?,
        ),
        Value::Array(array) => Value::Array(
            array
                .into_iter()
                .enumerate()
                .map(|(i, v)| {
                    resolver_refs(v, providers, ctx.enter_key(&i.to_string()))
                })
                .collect::<Result<Vec<_>, BuildConfigurationError>>()?,
        ),
    })
}

fn is_ref(str: &str) -> Result<Option<(String, String)>, BuildConfigurationError> {
    let Some(inner) = str.strip_prefix("{{") else { return Ok(None); };
    let Some(inner) = inner.strip_suffix("}}") else { return Ok(None); };

    let Some((provider, key)) = inner.split_once('.') else {
        return Err(BuildConfigurationError::ReferenceDoesNotExists(inner.to_string()));
    };

    Ok(Some((provider.trim().to_string(), key.trim().to_string())))
}

struct ResolveRefCtx<'a> {
    key: KeyInfo<'a>,
    hashset: &'a mut HashSet<String>,
}

enum KeyInfo<'a> {
    None,
    Key {
        prev: &'a KeyInfo<'a>,
        key: &'a str,
    },
    Ref {
        prev: &'a KeyInfo<'a>,

        // key of reference: d.b in d.b: "{{ a.b.c }}"
        source_key: String,

        // provider key: a in d.b: "{{ a.b.c }}"
        provider_key: &'a str,

        // reference key: b.c in d.b: "{{ a.b.c }}"
        reference_key: &'a str,
    },
}

impl<'a> ResolveRefCtx<'a> {
    pub fn enter_key<'b>(&'b mut self, key: &'b str) -> ResolveRefCtx<'b> {
        ResolveRefCtx {
            key: KeyInfo::Key {
                prev: &self.key,
                key,
            },
            hashset: self.hashset,
        }
    }

    pub fn enter_ref<'b>(
        &'b mut self,
        provider_key: &'b str,
        key: &'b str,
    ) -> Result<ResolveRefCtx<'b>, BuildConfigurationError> {
        let mut current_key: Vec<&str> = Vec::new();

        let mut current = &self.key;

        loop {
            match current {
                KeyInfo::None => break,
                KeyInfo::Key {
                    prev: prev_ref,
                    key,
                } => {
                    current_key.push(key);
                    current = prev_ref;
                }
                KeyInfo::Ref {
                    provider_key, reference_key: key, ..
                } => {
                    current_key.push(provider_key);
                    current_key.push(".");
                    current_key.push(key);
                    break;
                }
            }
        }

        current_key.reverse();

        let current_key = current_key.join(".");

        if !self.hashset.insert(current_key.clone()) {
            let refs = self.collect_keys();
            return Err(BuildConfigurationError::CircularReference { refs });
        };

        Ok(ResolveRefCtx {
            key: KeyInfo::Ref {
                prev: &self.key,
                source_key: current_key,
                provider_key,
                reference_key: key,
            },
            hashset: self.hashset,
        })
    }

    fn collect_keys(&self) -> Vec<String> {
        let mut out = Vec::new();
        let mut current: Vec<&str> = Vec::new();

        let mut key_ref = &self.key;

        loop {
            match key_ref {
                KeyInfo::None => {
                    current.reverse();
                    out.push(current.join("."));

                    out.reverse();
                    return out;
                }
                KeyInfo::Key {
                    key,
                    prev: prev_ref,
                } => {
                    current.push(key);
                    key_ref = prev_ref;
                }
                KeyInfo::Ref {
                    prev: prev_ref,
                    reference_key: key,
                    provider_key,
                    ..
                } => {
                    current.push(key);
                    current.push(provider_key);
                    current.reverse();

                    out.push(current.join("."));
                    current.clear();

                    key_ref = prev_ref;
                }
            }
        }
    }
}

impl<'a> Drop for ResolveRefCtx<'a> {
    fn drop(&mut self) {
        if let KeyInfo::Ref { source_key: ref_key, .. } = &self.key {
            self.hashset.remove(ref_key);
        }
    }
}