use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
};
use serde::Deserialize;
use spin_factor_sqlite::ConnectionCreator;
use spin_factors::{
anyhow::{self, Context as _},
runtime_config::toml::GetTomlValue,
};
use spin_sqlite_inproc::InProcDatabaseLocation;
use spin_sqlite_libsql::LazyLibSqlConnection;
#[derive(Clone, Debug)]
pub struct RuntimeConfigResolver {
default_database_dir: Option<PathBuf>,
local_database_dir: PathBuf,
}
impl RuntimeConfigResolver {
pub fn new(default_database_dir: Option<PathBuf>, local_database_dir: PathBuf) -> Self {
Self {
default_database_dir,
local_database_dir,
}
}
pub fn resolve(
&self,
table: &impl GetTomlValue,
) -> anyhow::Result<spin_factor_sqlite::runtime_config::RuntimeConfig> {
let mut runtime_config = self.resolve_from_toml(table)?.unwrap_or_default();
if !runtime_config.connection_creators.contains_key("default") {
runtime_config
.connection_creators
.insert("default".to_owned(), self.default());
}
Ok(runtime_config)
}
fn resolve_from_toml(
&self,
table: &impl GetTomlValue,
) -> anyhow::Result<Option<spin_factor_sqlite::runtime_config::RuntimeConfig>> {
let Some(table) = table.get("sqlite_database") else {
return Ok(None);
};
let config: std::collections::HashMap<String, TomlRuntimeConfig> =
table.clone().try_into()?;
let connection_creators = config
.into_iter()
.map(|(k, v)| Ok((k, self.get_connection_creator(v)?)))
.collect::<anyhow::Result<HashMap<_, _>>>()?;
Ok(Some(spin_factor_sqlite::runtime_config::RuntimeConfig {
connection_creators,
}))
}
pub fn get_connection_creator(
&self,
config: TomlRuntimeConfig,
) -> anyhow::Result<Arc<dyn ConnectionCreator>> {
let database_kind = config.type_.as_str();
match database_kind {
"spin" => {
let config: InProcDatabase = config.config.try_into()?;
Ok(Arc::new(
config.connection_creator(&self.local_database_dir)?,
))
}
"libsql" => {
let config: LibSqlDatabase = config.config.try_into()?;
Ok(Arc::new(config.connection_creator()?))
}
_ => anyhow::bail!("Unknown database kind: {database_kind}"),
}
}
}
#[derive(Deserialize)]
pub struct TomlRuntimeConfig {
#[serde(rename = "type")]
pub type_: String,
#[serde(flatten)]
pub config: toml::Table,
}
impl RuntimeConfigResolver {
pub fn default(&self) -> Arc<dyn ConnectionCreator> {
let path = self
.default_database_dir
.as_deref()
.map(|p| p.join(DEFAULT_SQLITE_DB_FILENAME));
let factory = move || {
let location = InProcDatabaseLocation::from_path(path.clone())?;
let connection = spin_sqlite_inproc::InProcConnection::new(location)?;
Ok(Box::new(connection) as _)
};
Arc::new(factory)
}
}
const DEFAULT_SQLITE_DB_FILENAME: &str = "sqlite_db.db";
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InProcDatabase {
pub path: Option<PathBuf>,
}
impl InProcDatabase {
fn connection_creator(self, base_dir: &Path) -> anyhow::Result<impl ConnectionCreator> {
let path = self
.path
.as_ref()
.map(|p| resolve_relative_path(p, base_dir));
let location = InProcDatabaseLocation::from_path(path)?;
let factory = move || {
let connection = spin_sqlite_inproc::InProcConnection::new(location.clone())?;
Ok(Box::new(connection) as _)
};
Ok(factory)
}
}
fn resolve_relative_path(path: &Path, base_dir: &Path) -> PathBuf {
if path.is_absolute() {
return path.to_owned();
}
base_dir.join(path)
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LibSqlDatabase {
url: String,
token: String,
}
impl LibSqlDatabase {
fn connection_creator(self) -> anyhow::Result<impl ConnectionCreator> {
let url = check_url(&self.url)
.with_context(|| {
format!(
"unexpected libSQL URL '{}' in runtime config file ",
self.url
)
})?
.to_owned();
let factory = move || {
let connection = LazyLibSqlConnection::new(url.clone(), self.token.clone());
Ok(Box::new(connection) as _)
};
Ok(factory)
}
}
fn check_url(url: &str) -> anyhow::Result<&str> {
if url.starts_with("https://") || url.starts_with("http://") {
Ok(url)
} else {
Err(anyhow::anyhow!(
"URL does not start with 'https://' or 'http://'. Spin currently only supports talking to libSQL databases over HTTP(S)"
))
}
}