spin_factor_key_value/runtime_config/
spin.rs

1//! Runtime configuration implementation used by Spin CLI.
2
3use crate::{RuntimeConfig, StoreManager};
4use anyhow::Context as _;
5use serde::de::DeserializeOwned;
6use serde::{Deserialize, Serialize};
7use spin_factors::runtime_config::toml::GetTomlValue;
8use std::{collections::HashMap, sync::Arc};
9
10/// Defines the construction of a key value store from a serialized runtime config.
11pub trait MakeKeyValueStore: 'static + Send + Sync {
12    /// Unique type identifier for the store.
13    const RUNTIME_CONFIG_TYPE: &'static str;
14    /// Runtime configuration for the store.
15    type RuntimeConfig: DeserializeOwned;
16    /// The store manager for the store.
17    type StoreManager: StoreManager;
18
19    /// Creates a new store manager from the runtime configuration.
20    fn make_store(&self, runtime_config: Self::RuntimeConfig)
21        -> anyhow::Result<Self::StoreManager>;
22}
23
24/// A function that creates a store manager from a TOML table.
25type StoreFromToml =
26    Arc<dyn Fn(toml::Table) -> anyhow::Result<Arc<dyn StoreManager>> + Send + Sync>;
27
28/// Creates a `StoreFromToml` function from a `MakeKeyValueStore` implementation.
29fn store_from_toml_fn<T: MakeKeyValueStore>(provider_type: T) -> StoreFromToml {
30    Arc::new(move |table| {
31        let runtime_config: T::RuntimeConfig = table
32            .try_into()
33            .context("could not parse key-value runtime config")?;
34        let provider = provider_type
35            .make_store(runtime_config)
36            .context("could not make key-value store from runtime config")?;
37        Ok(Arc::new(provider))
38    })
39}
40
41/// Converts from toml based runtime configuration into a [`RuntimeConfig`].
42///
43/// The various store types (i.e., the "type" field in the toml field) are
44/// registered with the resolver using `add_store_type`. The default store for a
45/// label is registered using `add_default_store`.
46#[derive(Default, Clone)]
47pub struct RuntimeConfigResolver {
48    /// A map of store types to a function that returns the appropriate store
49    /// manager from runtime config TOML.
50    store_types: HashMap<&'static str, StoreFromToml>,
51    /// A map of default store configurations for a label.
52    defaults: HashMap<&'static str, StoreConfig>,
53}
54
55impl RuntimeConfigResolver {
56    /// Create a new RuntimeConfigResolver.
57    pub fn new() -> Self {
58        <Self as Default>::default()
59    }
60
61    /// Adds a default store configuration for a label.
62    ///
63    /// Users must ensure that the store type for `config` has been registered with
64    /// the resolver using [`Self::register_store_type`].
65    pub fn add_default_store<T>(
66        &mut self,
67        label: &'static str,
68        config: T::RuntimeConfig,
69    ) -> anyhow::Result<()>
70    where
71        T: MakeKeyValueStore,
72        T::RuntimeConfig: Serialize,
73    {
74        self.defaults.insert(
75            label,
76            StoreConfig::new(T::RUNTIME_CONFIG_TYPE.to_owned(), config)?,
77        );
78        Ok(())
79    }
80
81    /// Registers a store type to the resolver.
82    pub fn register_store_type<T: MakeKeyValueStore>(
83        &mut self,
84        store_type: T,
85    ) -> anyhow::Result<()> {
86        if self
87            .store_types
88            .insert(T::RUNTIME_CONFIG_TYPE, store_from_toml_fn(store_type))
89            .is_some()
90        {
91            anyhow::bail!(
92                "duplicate key value store type {:?}",
93                T::RUNTIME_CONFIG_TYPE
94            );
95        }
96        Ok(())
97    }
98
99    /// Resolves a toml table into a runtime config.
100    ///
101    /// The default stores are also added to the runtime config.
102    pub fn resolve(&self, table: Option<&impl GetTomlValue>) -> anyhow::Result<RuntimeConfig> {
103        let mut runtime_config = self.resolve_from_toml(table)?.unwrap_or_default();
104
105        for (&label, config) in &self.defaults {
106            if !runtime_config.store_managers.contains_key(label) {
107                let store_manager = self
108                    .store_manager_from_config(config.clone())
109                    .with_context(|| {
110                        format!("could not configure key-value store with label '{label}'")
111                    })?;
112                runtime_config.add_store_manager(label.to_owned(), store_manager);
113            }
114        }
115        Ok(runtime_config)
116    }
117
118    fn resolve_from_toml(
119        &self,
120        table: Option<&impl GetTomlValue>,
121    ) -> anyhow::Result<Option<RuntimeConfig>> {
122        let Some(table) = table.and_then(|t| t.get("key_value_store")) else {
123            return Ok(None);
124        };
125        let table: HashMap<String, StoreConfig> = table.clone().try_into()?;
126
127        let mut runtime_config = RuntimeConfig::default();
128        for (label, config) in table {
129            let store_manager = self.store_manager_from_config(config).with_context(|| {
130                format!("could not configure key-value store with label '{label}'")
131            })?;
132            runtime_config.add_store_manager(label.clone(), store_manager);
133        }
134
135        Ok(Some(runtime_config))
136    }
137
138    /// Given a [`StoreConfig`], returns a store manager.
139    ///
140    /// Errors if there is no [`MakeKeyValueStore`] registered for the store config's type
141    /// or if the store manager cannot be created from the config.
142    fn store_manager_from_config(
143        &self,
144        config: StoreConfig,
145    ) -> anyhow::Result<Arc<dyn StoreManager>> {
146        let config_type = config.type_.as_str();
147        let maker = self.store_types.get(config_type).with_context(|| {
148            format!("the store type '{config_type}' was not registered with the config resolver")
149        })?;
150        maker(config.config)
151    }
152}
153
154#[derive(Deserialize, Clone)]
155pub struct StoreConfig {
156    #[serde(rename = "type")]
157    pub type_: String,
158    #[serde(flatten)]
159    pub config: toml::Table,
160}
161
162impl StoreConfig {
163    pub fn new<T>(type_: String, config: T) -> anyhow::Result<Self>
164    where
165        T: Serialize,
166    {
167        Ok(Self {
168            type_,
169            config: toml::value::Table::try_from(config)?,
170        })
171    }
172}