Skip to main content

spin_runtime_config/
lib.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::Context as _;
4use spin_common::ui::quoted_path;
5use spin_factor_key_value::KeyValueFactor;
6use spin_factor_key_value::runtime_config::spin::{self as key_value};
7use spin_factor_llm::{LlmFactor, spin as llm};
8use spin_factor_otel::OtelFactor;
9use spin_factor_outbound_http::OutboundHttpFactor;
10use spin_factor_outbound_mqtt::OutboundMqttFactor;
11use spin_factor_outbound_mysql::OutboundMysqlFactor;
12use spin_factor_outbound_networking::OutboundNetworkingFactor;
13use spin_factor_outbound_networking::runtime_config::spin::SpinRuntimeConfig as OutboundNetworkingSpinRuntimeConfig;
14use spin_factor_outbound_pg::OutboundPgFactor;
15use spin_factor_outbound_redis::OutboundRedisFactor;
16use spin_factor_sqlite::SqliteFactor;
17use spin_factor_variables::VariablesFactor;
18use spin_factor_wasi::WasiFactor;
19use spin_factors::runtime_config::toml::GetTomlValue as _;
20use spin_factors::{
21    FactorRuntimeConfigSource, RuntimeConfigSourceFinalizer, runtime_config::toml::TomlKeyTracker,
22};
23use spin_key_value_spin::{SpinKeyValueRuntimeConfig, SpinKeyValueStore};
24use spin_sqlite as sqlite;
25use spin_trigger::cli::UserProvidedPath;
26use toml::Value;
27
28pub mod variables;
29
30/// The default state directory for the trigger.
31pub const DEFAULT_STATE_DIR: &str = ".spin";
32
33/// A runtime configuration which has been resolved from a runtime config source.
34///
35/// Includes other pieces of configuration that are used to resolve the runtime configuration.
36pub struct ResolvedRuntimeConfig<T> {
37    /// The resolved runtime configuration.
38    pub runtime_config: T,
39    /// The resolver used to resolve key-value stores from runtime configuration.
40    pub key_value_resolver: key_value::RuntimeConfigResolver,
41    /// The resolver used to resolve sqlite databases from runtime configuration.
42    pub sqlite_resolver: sqlite::RuntimeConfigResolver,
43    /// The fully resolved state directory.
44    ///
45    /// `None` is used for an "unset" state directory which each factor will treat differently.
46    pub state_dir: Option<PathBuf>,
47    /// The fully resolved log directory.
48    ///
49    /// `None` is used for an "unset" log directory.
50    pub log_dir: Option<PathBuf>,
51    /// The maximum memory allocation limit.
52    pub max_instance_memory: Option<usize>,
53    /// The input TOML, for informational summaries.
54    pub toml: toml::Table,
55}
56
57impl<T> ResolvedRuntimeConfig<T> {
58    pub fn summarize(&self, runtime_config_path: Option<&Path>) {
59        let summarize_labeled_typed_tables = |key| {
60            let mut summaries = vec![];
61            if let Some(tables) = self.toml.get(key).and_then(Value::as_table) {
62                for (label, config) in tables {
63                    if let Some(ty) = config.get("type").and_then(Value::as_str) {
64                        summaries.push(format!("[{key}.{label}: {ty}]"))
65                    }
66                }
67            }
68            summaries
69        };
70
71        let mut summaries = vec![];
72        // [key_value_store.<label>: <type>]
73        summaries.extend(summarize_labeled_typed_tables("key_value_store"));
74        // [sqlite_database.<label>: <type>]
75        summaries.extend(summarize_labeled_typed_tables("sqlite_database"));
76        // [llm_compute: <type>]
77        if let Some(table) = self.toml.get("llm_compute").and_then(Value::as_table)
78            && let Some(ty) = table.get("type").and_then(Value::as_str)
79        {
80            summaries.push(format!("[llm_compute: {ty}"));
81        }
82        // [outbound_networking: max_total_connections=N]
83        if let Some(table) = self
84            .toml
85            .get("outbound_networking")
86            .and_then(Value::as_table)
87            && let Some(max) = table
88                .get("max_total_connections")
89                .and_then(Value::as_integer)
90        {
91            summaries.push(format!(
92                "[outbound_networking: max_total_connections={max}]"
93            ));
94        }
95        // [outbound_redis: max_connections=N], [outbound_pg: max_connections=N], [outbound_mysql: max_connections=N], [outbound_mqtt: max_connections=N], [outbound_http: max_connections=N]
96        for key in [
97            "outbound_redis",
98            "outbound_pg",
99            "outbound_mysql",
100            "outbound_mqtt",
101            "outbound_http",
102        ] {
103            if let Some(table) = self.toml.get(key).and_then(Value::as_table)
104                && let Some(max) = table.get("max_connections").and_then(Value::as_integer)
105            {
106                summaries.push(format!("[{key}: max_connections={max}]"));
107            }
108        }
109        // [outbound_http: max_concurrent_requests=N (deprecated)]
110        if let Some(table) = self.toml.get("outbound_http").and_then(Value::as_table)
111            && let Some(max) = table
112                .get("max_concurrent_requests")
113                .and_then(Value::as_integer)
114        {
115            summaries.push(format!(
116                "[outbound_http: max_concurrent_requests={max} (deprecated, use max_connections)]"
117            ));
118        }
119        if !summaries.is_empty() {
120            let summaries = summaries.join(", ");
121            let from_path = runtime_config_path
122                .map(|path| format!("from {}", quoted_path(path)))
123                .unwrap_or_default();
124            eprintln!("Using runtime config {summaries} {from_path}");
125        }
126    }
127}
128
129impl<T> ResolvedRuntimeConfig<T>
130where
131    T: for<'a, 'b> TryFrom<TomlRuntimeConfigSource<'a, 'b>>,
132    for<'a, 'b> <T as TryFrom<TomlRuntimeConfigSource<'a, 'b>>>::Error: Into<anyhow::Error>,
133{
134    /// Creates a new resolved runtime configuration from a runtime config source TOML file.
135    ///
136    /// `provided_state_dir` is the explicitly provided state directory, if any.
137    pub fn from_file(
138        runtime_config_path: Option<&Path>,
139        local_app_dir: Option<PathBuf>,
140        provided_state_dir: UserProvidedPath,
141        provided_log_dir: UserProvidedPath,
142    ) -> anyhow::Result<Self> {
143        let toml = match runtime_config_path {
144            Some(runtime_config_path) => {
145                let file = std::fs::read_to_string(runtime_config_path).with_context(|| {
146                    format!(
147                        "failed to read runtime config file '{}'",
148                        runtime_config_path.display()
149                    )
150                })?;
151                toml::from_str(&file).with_context(|| {
152                    format!(
153                        "failed to parse runtime config file '{}' as toml",
154                        runtime_config_path.display()
155                    )
156                })?
157            }
158            None => Default::default(),
159        };
160        let toml_resolver =
161            TomlResolver::new(&toml, local_app_dir, provided_state_dir, provided_log_dir);
162
163        Self::new(toml_resolver, runtime_config_path)
164    }
165
166    /// Creates a new resolved runtime configuration from a TOML table.
167    pub fn new(
168        toml_resolver: TomlResolver<'_>,
169        runtime_config_path: Option<&Path>,
170    ) -> anyhow::Result<Self> {
171        let runtime_config_dir = runtime_config_path
172            .and_then(Path::parent)
173            .map(ToOwned::to_owned);
174        let state_dir = toml_resolver.state_dir()?;
175        let outbound_networking = runtime_config_dir
176            .clone()
177            .map(OutboundNetworkingSpinRuntimeConfig::new);
178        let key_value_resolver = key_value_config_resolver(runtime_config_dir, state_dir.clone());
179        let sqlite_resolver = sqlite_config_resolver(state_dir.clone())
180            .context("failed to resolve sqlite runtime config")?;
181
182        let toml = toml_resolver.toml();
183        let log_dir = toml_resolver.log_dir()?;
184        let max_instance_memory = toml_resolver.max_instance_memory()?;
185
186        let source = TomlRuntimeConfigSource::new(
187            toml_resolver,
188            &key_value_resolver,
189            outbound_networking.as_ref(),
190            &sqlite_resolver,
191        );
192
193        // Note: all valid fields in the runtime config must have been referenced at
194        // this point or the finalizer will fail due to `validate_all_keys_used`
195        // not passing.
196        let runtime_config: T = source.try_into().map_err(Into::into)?;
197
198        Ok(Self {
199            runtime_config,
200            key_value_resolver,
201            sqlite_resolver,
202            state_dir,
203            log_dir,
204            max_instance_memory,
205            toml,
206        })
207    }
208
209    /// The fully resolved state directory.
210    pub fn state_dir(&self) -> Option<PathBuf> {
211        self.state_dir.clone()
212    }
213
214    /// The fully resolved state directory.
215    pub fn log_dir(&self) -> Option<PathBuf> {
216        self.log_dir.clone()
217    }
218
219    /// The maximum memory allocation limit.
220    pub fn max_instance_memory(&self) -> Option<usize> {
221        self.max_instance_memory
222    }
223}
224
225#[derive(Clone, Debug)]
226/// Resolves runtime configuration from a TOML file.
227pub struct TomlResolver<'a> {
228    table: TomlKeyTracker<'a>,
229    /// The local app directory.
230    local_app_dir: Option<PathBuf>,
231    /// Explicitly provided state directory.
232    state_dir: UserProvidedPath,
233    /// Explicitly provided log directory.
234    log_dir: UserProvidedPath,
235}
236
237impl<'a> TomlResolver<'a> {
238    /// Create a new TOML resolver.
239    pub fn new(
240        table: &'a toml::Table,
241        local_app_dir: Option<PathBuf>,
242        state_dir: UserProvidedPath,
243        log_dir: UserProvidedPath,
244    ) -> Self {
245        Self {
246            table: TomlKeyTracker::new(table),
247            local_app_dir,
248            state_dir,
249            log_dir,
250        }
251    }
252
253    /// Get the configured state_directory.
254    ///
255    /// Errors if the path cannot be converted to an absolute path.
256    pub fn state_dir(&self) -> std::io::Result<Option<PathBuf>> {
257        let mut state_dir = self.state_dir.clone();
258        // If the state_dir is not explicitly provided, check the toml.
259        if matches!(state_dir, UserProvidedPath::Default) {
260            let from_toml =
261                self.table
262                    .get("state_dir")
263                    .and_then(|v| v.as_str())
264                    .map(|toml_value| {
265                        if toml_value.is_empty() {
266                            // If the toml value is empty, treat it as unset.
267                            UserProvidedPath::Unset
268                        } else {
269                            // Otherwise, treat the toml value as a provided path.
270                            UserProvidedPath::Provided(PathBuf::from(toml_value))
271                        }
272                    });
273            // If toml value is not provided, use the original value after all.
274            state_dir = from_toml.unwrap_or(state_dir);
275        }
276
277        match (state_dir, &self.local_app_dir) {
278            (UserProvidedPath::Provided(p), _) => Ok(Some(std::path::absolute(p)?)),
279            (UserProvidedPath::Default, Some(local_app_dir)) => {
280                Ok(Some(local_app_dir.join(".spin")))
281            }
282            (UserProvidedPath::Default | UserProvidedPath::Unset, _) => Ok(None),
283        }
284    }
285
286    /// Get the configured log directory.
287    ///
288    /// Errors if the path cannot be converted to an absolute path.
289    pub fn log_dir(&self) -> std::io::Result<Option<PathBuf>> {
290        let mut log_dir = self.log_dir.clone();
291        // If the log_dir is not explicitly provided, check the toml.
292        if matches!(log_dir, UserProvidedPath::Default) {
293            let from_toml = self
294                .table
295                .get("log_dir")
296                .and_then(|v| v.as_str())
297                .map(|toml_value| {
298                    if toml_value.is_empty() {
299                        // If the toml value is empty, treat it as unset.
300                        UserProvidedPath::Unset
301                    } else {
302                        // Otherwise, treat the toml value as a provided path.
303                        UserProvidedPath::Provided(PathBuf::from(toml_value))
304                    }
305                });
306            // If toml value is not provided, use the original value after all.
307            log_dir = from_toml.unwrap_or(log_dir);
308        }
309
310        match log_dir {
311            UserProvidedPath::Provided(p) => Ok(Some(std::path::absolute(p)?)),
312            UserProvidedPath::Default => Ok(self.state_dir()?.map(|p| p.join("logs"))),
313            UserProvidedPath::Unset => Ok(None),
314        }
315    }
316
317    /// Get the configured maximum memory allocation limit.
318    pub fn max_instance_memory(&self) -> anyhow::Result<Option<usize>> {
319        self.table
320            .get("max_instance_memory")
321            .and_then(|v| v.as_integer())
322            .map(|toml_value| toml_value.try_into())
323            .transpose()
324            .map_err(Into::into)
325    }
326
327    /// Validate that all keys in the TOML file have been used.
328    pub fn validate_all_keys_used(&self) -> spin_factors::Result<()> {
329        self.table.validate_all_keys_used()
330    }
331
332    fn toml(&self) -> toml::Table {
333        self.table.as_ref().clone()
334    }
335}
336
337/// The TOML based runtime configuration source Spin CLI.
338pub struct TomlRuntimeConfigSource<'a, 'b> {
339    toml: TomlResolver<'b>,
340    key_value: &'a key_value::RuntimeConfigResolver,
341    outbound_networking: Option<&'a OutboundNetworkingSpinRuntimeConfig>,
342    sqlite: &'a sqlite::RuntimeConfigResolver,
343}
344
345impl<'a, 'b> TomlRuntimeConfigSource<'a, 'b> {
346    pub fn new(
347        toml_resolver: TomlResolver<'b>,
348        key_value: &'a key_value::RuntimeConfigResolver,
349        outbound_networking: Option<&'a OutboundNetworkingSpinRuntimeConfig>,
350        sqlite: &'a sqlite::RuntimeConfigResolver,
351    ) -> Self {
352        Self {
353            toml: toml_resolver,
354            key_value,
355            outbound_networking,
356            sqlite,
357        }
358    }
359}
360
361impl FactorRuntimeConfigSource<KeyValueFactor> for TomlRuntimeConfigSource<'_, '_> {
362    fn get_runtime_config(
363        &mut self,
364    ) -> anyhow::Result<Option<spin_factor_key_value::RuntimeConfig>> {
365        Ok(Some(self.key_value.resolve(Some(&self.toml.table))?))
366    }
367}
368
369impl FactorRuntimeConfigSource<OutboundNetworkingFactor> for TomlRuntimeConfigSource<'_, '_> {
370    fn get_runtime_config(
371        &mut self,
372    ) -> anyhow::Result<Option<<OutboundNetworkingFactor as spin_factors::Factor>::RuntimeConfig>>
373    {
374        let Some(tls) = self.outbound_networking else {
375            return Ok(None);
376        };
377        tls.config_from_table(&self.toml.table)
378    }
379}
380
381impl FactorRuntimeConfigSource<VariablesFactor> for TomlRuntimeConfigSource<'_, '_> {
382    fn get_runtime_config(
383        &mut self,
384    ) -> anyhow::Result<Option<<VariablesFactor as spin_factors::Factor>::RuntimeConfig>> {
385        Ok(Some(variables::runtime_config_from_toml(&self.toml.table)?))
386    }
387}
388
389impl FactorRuntimeConfigSource<OutboundPgFactor> for TomlRuntimeConfigSource<'_, '_> {
390    fn get_runtime_config(
391        &mut self,
392    ) -> anyhow::Result<Option<<OutboundPgFactor as spin_factors::Factor>::RuntimeConfig>> {
393        spin_factor_outbound_pg::runtime_config::spin::config_from_table(&self.toml.table)
394    }
395}
396
397impl FactorRuntimeConfigSource<OutboundMysqlFactor> for TomlRuntimeConfigSource<'_, '_> {
398    fn get_runtime_config(
399        &mut self,
400    ) -> anyhow::Result<Option<<OutboundMysqlFactor as spin_factors::Factor>::RuntimeConfig>> {
401        spin_factor_outbound_mysql::runtime_config::spin::config_from_table(&self.toml.table)
402    }
403}
404
405impl FactorRuntimeConfigSource<LlmFactor> for TomlRuntimeConfigSource<'_, '_> {
406    fn get_runtime_config(&mut self) -> anyhow::Result<Option<spin_factor_llm::RuntimeConfig>> {
407        llm::runtime_config_from_toml(&self.toml.table, self.toml.state_dir()?)
408    }
409}
410
411impl FactorRuntimeConfigSource<OutboundRedisFactor> for TomlRuntimeConfigSource<'_, '_> {
412    fn get_runtime_config(
413        &mut self,
414    ) -> anyhow::Result<Option<<OutboundRedisFactor as spin_factors::Factor>::RuntimeConfig>> {
415        spin_factor_outbound_redis::runtime_config::spin::config_from_table(&self.toml.table)
416    }
417}
418
419impl FactorRuntimeConfigSource<WasiFactor> for TomlRuntimeConfigSource<'_, '_> {
420    fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
421        Ok(None)
422    }
423}
424
425impl FactorRuntimeConfigSource<OutboundHttpFactor> for TomlRuntimeConfigSource<'_, '_> {
426    fn get_runtime_config(
427        &mut self,
428    ) -> anyhow::Result<Option<<OutboundHttpFactor as spin_factors::Factor>::RuntimeConfig>> {
429        spin_factor_outbound_http::runtime_config::spin::config_from_table(&self.toml.table)
430    }
431}
432
433impl FactorRuntimeConfigSource<OutboundMqttFactor> for TomlRuntimeConfigSource<'_, '_> {
434    fn get_runtime_config(
435        &mut self,
436    ) -> anyhow::Result<Option<<OutboundMqttFactor as spin_factors::Factor>::RuntimeConfig>> {
437        spin_factor_outbound_mqtt::runtime_config::spin::config_from_table(&self.toml.table)
438    }
439}
440
441impl FactorRuntimeConfigSource<SqliteFactor> for TomlRuntimeConfigSource<'_, '_> {
442    fn get_runtime_config(&mut self) -> anyhow::Result<Option<spin_factor_sqlite::RuntimeConfig>> {
443        Ok(Some(self.sqlite.resolve(&self.toml.table)?))
444    }
445}
446
447impl FactorRuntimeConfigSource<OtelFactor> for TomlRuntimeConfigSource<'_, '_> {
448    fn get_runtime_config(&mut self) -> anyhow::Result<Option<()>> {
449        Ok(None)
450    }
451}
452
453impl RuntimeConfigSourceFinalizer for TomlRuntimeConfigSource<'_, '_> {
454    fn finalize(&mut self) -> anyhow::Result<()> {
455        Ok(self.toml.validate_all_keys_used()?)
456    }
457}
458
459const DEFAULT_KEY_VALUE_STORE_LABEL: &str = "default";
460
461/// The key-value runtime configuration resolver.
462///
463/// Takes a base path that all local key-value stores which are configured with
464/// relative paths will be relative to. It also takes a default store base path
465/// which will be used as the directory for the default store.
466pub fn key_value_config_resolver(
467    local_store_base_path: Option<PathBuf>,
468    default_store_base_path: Option<PathBuf>,
469) -> key_value::RuntimeConfigResolver {
470    let mut key_value = key_value::RuntimeConfigResolver::new();
471
472    // Register the supported store types.
473    // Unwraps are safe because the store types are known to not overlap.
474    key_value
475        .register_store_type(spin_key_value_spin::SpinKeyValueStore::new(
476            local_store_base_path.clone(),
477        ))
478        .unwrap();
479    key_value
480        .register_store_type(spin_key_value_redis::RedisKeyValueStore::new())
481        .unwrap();
482    key_value
483        .register_store_type(spin_key_value_azure::AzureKeyValueStore::new(None))
484        .unwrap();
485    key_value
486        .register_store_type(spin_key_value_aws::AwsDynamoKeyValueStore::new())
487        .unwrap();
488
489    // Add handling of "default" store.
490    let default_store_path = default_store_base_path.map(|p| p.join(DEFAULT_SPIN_STORE_FILENAME));
491    // Unwraps are safe because the store is known to be serializable as toml.
492    key_value
493        .add_default_store::<SpinKeyValueStore>(
494            DEFAULT_KEY_VALUE_STORE_LABEL,
495            SpinKeyValueRuntimeConfig::new(default_store_path),
496        )
497        .unwrap();
498
499    key_value
500}
501
502/// The default filename for the SQLite database.
503const DEFAULT_SPIN_STORE_FILENAME: &str = "sqlite_key_value.db";
504
505/// The sqlite runtime configuration resolver.
506///
507/// Takes a path to the directory where the default database should be stored.
508/// If the path is `None`, the default database will be in-memory.
509fn sqlite_config_resolver(
510    default_database_dir: Option<PathBuf>,
511) -> anyhow::Result<sqlite::RuntimeConfigResolver> {
512    let local_database_dir =
513        std::env::current_dir().context("failed to get current working directory")?;
514    Ok(sqlite::RuntimeConfigResolver::new(
515        default_database_dir,
516        local_database_dir,
517    ))
518}
519
520#[cfg(test)]
521mod tests {
522    use std::{collections::HashMap, sync::Arc};
523
524    use spin_factors::RuntimeFactors;
525    use spin_factors_test::TestEnvironment;
526
527    use super::*;
528
529    /// Define a test factor with the given field and factor type.
530    macro_rules! define_test_factor {
531        ($field:ident : $factor:ty) => {
532            #[derive(RuntimeFactors)]
533            #[allow(unused)]
534            struct TestFactors {
535                $field: $factor,
536            }
537            impl TryFrom<TomlRuntimeConfigSource<'_, '_>> for TestFactorsRuntimeConfig {
538                type Error = anyhow::Error;
539
540                fn try_from(value: TomlRuntimeConfigSource<'_, '_>) -> Result<Self, Self::Error> {
541                    Self::from_source(value)
542                }
543            }
544            fn resolve_toml(
545                toml: toml::Table,
546                path: impl AsRef<std::path::Path>,
547            ) -> anyhow::Result<ResolvedRuntimeConfig<TestFactorsRuntimeConfig>> {
548                ResolvedRuntimeConfig::<TestFactorsRuntimeConfig>::new(
549                    toml_resolver(&toml),
550                    Some(path.as_ref()),
551                )
552            }
553        };
554    }
555
556    #[test]
557    fn sqlite_is_configured_correctly() {
558        define_test_factor!(sqlite: SqliteFactor);
559
560        impl TestFactorsRuntimeConfig {
561            /// Get the connection creators for the configured sqlite databases.
562            fn connection_creators(
563                &self,
564            ) -> &HashMap<String, Arc<dyn spin_factor_sqlite::ConnectionCreator>> {
565                &self.sqlite.as_ref().unwrap().connection_creators
566            }
567
568            /// Get the labels of the configured sqlite databases.
569            fn configured_labels(&self) -> Vec<&str> {
570                let mut configured_labels = self
571                    .connection_creators()
572                    .keys()
573                    .map(|s| s.as_str())
574                    .collect::<Vec<_>>();
575                // Sort the labels to ensure consistent ordering.
576                configured_labels.sort();
577                configured_labels
578            }
579        }
580
581        // Test that the default label is added if not provided.
582        let toml = toml::toml! {
583            [sqlite_database.foo]
584            type = "spin"
585        };
586        assert_eq!(
587            resolve_toml(toml, ".")
588                .unwrap()
589                .runtime_config
590                .configured_labels(),
591            vec!["default", "foo"]
592        );
593
594        // Test that the default label is added with an empty toml config.
595        let toml = toml::Table::new();
596        let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
597        assert_eq!(runtime_config.configured_labels(), vec!["default"]);
598    }
599
600    #[test]
601    fn key_value_is_configured_correctly() {
602        define_test_factor!(key_value: KeyValueFactor);
603        impl TestFactorsRuntimeConfig {
604            /// Get whether the store manager exists for the given label.
605            fn has_store_manager(&self, label: &str) -> bool {
606                self.key_value.as_ref().unwrap().has_store_manager(label)
607            }
608        }
609
610        // Test that the default label is added if not provided.
611        let toml = toml::toml! {
612            [key_value_store.foo]
613            type = "spin"
614        };
615        let runtime_config = resolve_toml(toml, "config.toml").unwrap().runtime_config;
616        assert!(
617            ["default", "foo"]
618                .iter()
619                .all(|label| runtime_config.has_store_manager(label))
620        );
621    }
622
623    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
624    async fn custom_spin_key_value_works_with_custom_paths() -> anyhow::Result<()> {
625        use spin_world::v2::key_value::HostStore;
626        define_test_factor!(key_value: KeyValueFactor);
627        let tmp_dir = tempfile::TempDir::with_prefix("example")?;
628        let absolute_path = tmp_dir.path().join("foo/custom.db");
629        let relative_path = tmp_dir.path().join("custom.db");
630        // Check that the dbs do not exist yet - they will exist by the end of the test
631        assert!(!absolute_path.exists());
632        assert!(!relative_path.exists());
633
634        let path_str = absolute_path.to_str().unwrap();
635        let runtime_config = toml::toml! {
636            [key_value_store.absolute]
637            type = "spin"
638            path = path_str
639
640            [key_value_store.relative]
641            type = "spin"
642            path = "custom.db"
643        };
644        let factors = TestFactors {
645            key_value: KeyValueFactor::new(),
646        };
647        let env = TestEnvironment::new(factors)
648            .extend_manifest(toml::toml! {
649                [component.test-component]
650                source = "does-not-exist.wasm"
651                key_value_stores = ["absolute", "relative"]
652            })
653            .runtime_config(
654                resolve_toml(runtime_config, tmp_dir.path().join("runtime-config.toml"))
655                    .unwrap()
656                    .runtime_config,
657            )?;
658        let mut state = env.build_instance_state().await?;
659
660        // Actually get a key since store creation is lazy
661        let store = state.key_value.open("absolute".to_owned()).await??;
662        let _ = state.key_value.get(store, "foo".to_owned()).await??;
663
664        let store = state.key_value.open("relative".to_owned()).await??;
665        let _ = state.key_value.get(store, "foo".to_owned()).await??;
666
667        // Check that the dbs have been created
668        assert!(absolute_path.exists());
669        assert!(relative_path.exists());
670        Ok(())
671    }
672
673    fn toml_resolver(toml: &toml::Table) -> TomlResolver<'_> {
674        TomlResolver::new(
675            toml,
676            None,
677            UserProvidedPath::Default,
678            UserProvidedPath::Default,
679        )
680    }
681
682    #[test]
683    fn dirs_are_resolved() {
684        define_test_factor!(sqlite: SqliteFactor);
685
686        let toml = toml::toml! {
687            state_dir = "/foo"
688            log_dir = "/bar"
689        };
690        resolve_toml(toml, "config.toml").unwrap();
691    }
692
693    #[test]
694    fn fails_to_resolve_with_unused_key() {
695        define_test_factor!(sqlite: SqliteFactor);
696
697        let toml = toml::toml! {
698            baz = "/baz"
699        };
700        // assert returns an error with value "unused runtime config key(s): local_app_dir"
701        let Err(e) = resolve_toml(toml, "config.toml") else {
702            panic!("Should not be able to resolve unknown key");
703        };
704        assert_eq!(e.to_string(), "unused runtime config key(s): baz");
705    }
706}