1use std::{
4 collections::HashMap,
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use serde::Deserialize;
10use spin_factor_sqlite::ConnectionCreator;
11use spin_factors::{
12 anyhow::{self, Context as _},
13 runtime_config::toml::GetTomlValue,
14};
15use spin_sqlite_inproc::InProcDatabaseLocation;
16use spin_sqlite_libsql::LazyLibSqlConnection;
17
18#[derive(Clone, Debug)]
23pub struct RuntimeConfigResolver {
24 default_database_dir: Option<PathBuf>,
25 local_database_dir: PathBuf,
26}
27
28impl RuntimeConfigResolver {
29 pub fn new(default_database_dir: Option<PathBuf>, local_database_dir: PathBuf) -> Self {
39 Self {
40 default_database_dir,
41 local_database_dir,
42 }
43 }
44
45 pub fn resolve(
56 &self,
57 table: &impl GetTomlValue,
58 ) -> anyhow::Result<spin_factor_sqlite::runtime_config::RuntimeConfig> {
59 let mut runtime_config = self.resolve_from_toml(table)?.unwrap_or_default();
60 if !runtime_config.connection_creators.contains_key("default") {
62 runtime_config
63 .connection_creators
64 .insert("default".to_owned(), self.default());
65 }
66
67 Ok(runtime_config)
68 }
69
70 fn resolve_from_toml(
72 &self,
73 table: &impl GetTomlValue,
74 ) -> anyhow::Result<Option<spin_factor_sqlite::runtime_config::RuntimeConfig>> {
75 let Some(table) = table.get("sqlite_database") else {
76 return Ok(None);
77 };
78 let config: std::collections::HashMap<String, TomlRuntimeConfig> =
79 table.clone().try_into()?;
80 let connection_creators = config
81 .into_iter()
82 .map(|(k, v)| Ok((k, self.get_connection_creator(v)?)))
83 .collect::<anyhow::Result<HashMap<_, _>>>()?;
84
85 Ok(Some(spin_factor_sqlite::runtime_config::RuntimeConfig {
86 connection_creators,
87 }))
88 }
89
90 pub fn get_connection_creator(
92 &self,
93 config: TomlRuntimeConfig,
94 ) -> anyhow::Result<Arc<dyn ConnectionCreator>> {
95 let database_kind = config.type_.as_str();
96 match database_kind {
97 "spin" => {
98 let config: InProcDatabase = config.config.try_into()?;
99 Ok(Arc::new(
100 config.connection_creator(&self.local_database_dir)?,
101 ))
102 }
103 "libsql" => {
104 let config: LibSqlDatabase = config.config.try_into()?;
105 Ok(Arc::new(config.connection_creator()?))
106 }
107 _ => anyhow::bail!("Unknown database kind: {database_kind}"),
108 }
109 }
110}
111
112#[derive(Deserialize)]
113pub struct TomlRuntimeConfig {
114 #[serde(rename = "type")]
115 pub type_: String,
116 #[serde(flatten)]
117 pub config: toml::Table,
118}
119
120impl RuntimeConfigResolver {
121 pub fn default(&self) -> Arc<dyn ConnectionCreator> {
123 let path = self
124 .default_database_dir
125 .as_deref()
126 .map(|p| p.join(DEFAULT_SQLITE_DB_FILENAME));
127 let factory = move || {
128 let location = InProcDatabaseLocation::from_path(path.clone())?;
129 let connection = spin_sqlite_inproc::InProcConnection::new(location)?;
130 Ok(Box::new(connection) as _)
131 };
132 Arc::new(factory)
133 }
134}
135
136const DEFAULT_SQLITE_DB_FILENAME: &str = "sqlite_db.db";
137
138#[derive(Clone, Debug, Deserialize)]
140#[serde(deny_unknown_fields)]
141pub struct InProcDatabase {
142 pub path: Option<PathBuf>,
143}
144
145impl InProcDatabase {
146 fn connection_creator(self, base_dir: &Path) -> anyhow::Result<impl ConnectionCreator> {
150 let path = self
151 .path
152 .as_ref()
153 .map(|p| resolve_relative_path(p, base_dir));
154 let location = InProcDatabaseLocation::from_path(path)?;
155 let factory = move || {
156 let connection = spin_sqlite_inproc::InProcConnection::new(location.clone())?;
157 Ok(Box::new(connection) as _)
158 };
159 Ok(factory)
160 }
161}
162
163fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
167 if path.is_absolute() {
168 return path.to_owned();
169 }
170 base_dir.join(path)
171}
172
173#[derive(Clone, Debug, Deserialize)]
177#[serde(deny_unknown_fields)]
178pub struct LibSqlDatabase {
179 url: String,
180 token: String,
181}
182
183impl LibSqlDatabase {
184 fn connection_creator(self) -> anyhow::Result<impl ConnectionCreator> {
186 let url = check_url(&self.url)
187 .with_context(|| {
188 format!(
189 "unexpected libSQL URL '{}' in runtime config file ",
190 self.url
191 )
192 })?
193 .to_owned();
194 let factory = move || {
195 let connection = LazyLibSqlConnection::new(url.clone(), self.token.clone());
196 Ok(Box::new(connection) as _)
197 };
198 Ok(factory)
199 }
200}
201
202fn check_url(url: &str) -> anyhow::Result<&str> {
204 if url.starts_with("https://") || url.starts_with("http://") {
205 Ok(url)
206 } else {
207 Err(anyhow::anyhow!(
208 "URL does not start with 'https://' or 'http://'. Spin currently only supports talking to libSQL databases over HTTP(S)"
209 ))
210 }
211}