use std::{
path::{Path, PathBuf},
str::FromStr,
};
use either::Either;
use crate::{PostgresExtensionsCollection, PostgresVersion, SqlDialect};
#[derive(Debug, Clone, PartialEq)]
pub struct EngineVersion(Either<PostgresVersion, String>);
impl<'de> serde::Deserialize<'de> for EngineVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s)
.map_err(|e| serde::de::Error::custom(format!("invalid value for EngineVersion {e}")))
}
}
impl FromStr for EngineVersion {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(pg_ver) = PostgresVersion::from_str(s) {
Ok(Self(Either::Left(pg_ver)))
} else {
Ok(Self(Either::Right(s.to_string())))
}
}
}
impl std::fmt::Display for EngineVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.0 {
Either::Left(v) => write!(f, "{v}"),
Either::Right(v) => write!(f, "{v}"),
}
}
}
impl EngineVersion {
pub fn is_wellknown(&self) -> bool {
self.0.is_left()
}
pub fn definition_base_path<P: AsRef<Path>>(&self, home: P) -> PathBuf {
if self.is_wellknown() {
home.as_ref()
.join("postgres")
.join(Path::new(&self.to_string()))
} else {
home.as_ref()
.join("postgres")
.join("custom")
.join(self.to_string())
}
}
}
#[derive(serde::Deserialize, Debug)]
pub enum WellKnownDbEngine {
PostgreSQL,
}
impl FromStr for WellKnownDbEngine {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "PostgreSQL" {
Ok(Self::PostgreSQL)
} else {
Err(s.to_string())
}
}
}
#[derive(Debug)]
pub struct DatabaseEngine(Either<WellKnownDbEngine, String>);
impl<'de> serde::Deserialize<'de> for DatabaseEngine {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(match s.parse::<WellKnownDbEngine>() {
Ok(wk) => DatabaseEngine(Either::Left(wk)),
Err(_) => DatabaseEngine(Either::Right(s)),
})
}
}
impl DatabaseEngine {
#[must_use]
pub fn is_wellknown(&self) -> bool {
self.0.is_left()
}
#[must_use]
pub fn is_custom(&self) -> bool {
self.0.is_right()
}
#[must_use]
pub fn as_wellknown(&self) -> Option<&WellKnownDbEngine> {
match &self.0 {
Either::Left(l) => Some(l),
Either::Right(_) => None,
}
}
#[must_use]
pub fn as_custom(&self) -> Option<&String> {
match &self.0 {
Either::Right(r) => Some(r),
Either::Left(_) => None,
}
}
}
#[derive(Debug, serde::Deserialize)]
pub struct SqlFunMetadata {
engine: DatabaseEngine,
dialect: SqlDialect,
#[serde(rename = "version")]
engine_version: Option<EngineVersion>,
extensions: Option<PostgresExtensionsCollection>,
search_path: Option<Vec<String>>,
cte_catalog: Option<PathBuf>,
connector: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum MetadataError {
#[error("sql-fun metadata can not load from {0}: {1}")]
MetadataLoadFromFile(PathBuf, String),
#[error("metadata io error {0}")]
Io(#[from] std::io::Error),
#[error("metadata format error {0}")]
Serde(#[from] toml::de::Error),
#[error("CTE catalog not exists in {0}")]
CteCatalogNotExist(PathBuf),
}
impl MetadataError {
pub fn metadata_load_from_file<T, P: AsRef<Path>>(path: P, message: &str) -> Result<T, Self> {
Err(Self::MetadataLoadFromFile(
path.as_ref().to_path_buf(),
String::from(message),
))
}
pub fn cte_catalog_not_exist<T, P: AsRef<Path>>(path: P) -> Result<T, Self> {
Err(Self::CteCatalogNotExist(path.as_ref().to_path_buf()))
}
}
impl SqlFunMetadata {
pub fn load_from<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, MetadataError> {
let file_text = std::fs::read_to_string(&path)?;
Self::from_str(&file_text, path)
}
pub(crate) fn from_str<P: AsRef<Path> + std::fmt::Debug>(
source: &str,
path_hint: P,
) -> Result<Self, MetadataError> {
use MetadataError as Error;
let cargo_metadata: toml::Value = toml::from_str(source)?;
let Some(package) = cargo_metadata.get("package") else {
Error::metadata_load_from_file(
&path_hint,
&format!("package not defined in {path_hint:?}"),
)?
};
let Some(metadata) = package.get("metadata") else {
Error::metadata_load_from_file(
&path_hint,
&format!("package.metadata not defined in {path_hint:?}"),
)?
};
let Some(database) = metadata.get("database") else {
Error::metadata_load_from_file(
&path_hint,
&format!("package.metadata.database not defined in {path_hint:?}"),
)?
};
Self::from_value(database)
}
pub(crate) fn from_value(database: &toml::Value) -> Result<Self, MetadataError> {
let db_matadata: Self = database.clone().try_into()?;
Ok(db_matadata)
}
pub fn cte_catalog(&self) -> Result<Option<&PathBuf>, MetadataError> {
let Some(cte_catalog) = &self.cte_catalog else {
return Ok(None);
};
if cte_catalog.exists() {
Ok(Some(cte_catalog))
} else {
MetadataError::cte_catalog_not_exist(cte_catalog)?
}
}
#[must_use]
pub fn sql_dialect(&self) -> SqlDialect {
self.dialect
}
#[must_use]
pub fn engine(&self) -> &DatabaseEngine {
&self.engine
}
#[must_use]
pub fn engine_version(&self) -> &Option<EngineVersion> {
&self.engine_version
}
#[must_use]
pub fn search_path(&self) -> &Option<Vec<String>> {
&self.search_path
}
#[must_use]
pub fn postgres_extensions(&self) -> &Option<PostgresExtensionsCollection> {
&self.extensions
}
#[must_use]
pub fn connector(&self) -> &Option<String> {
&self.connector
}
}