Skip to main content

sql_fun_core/
metadata.rs

1use std::{
2    path::{Path, PathBuf},
3    str::FromStr,
4};
5
6use either::Either;
7
8use crate::{PostgresExtensionsCollection, PostgresVersion, SqlDialect};
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct EngineVersion(Either<PostgresVersion, String>);
12
13impl<'de> serde::Deserialize<'de> for EngineVersion {
14    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
15    where
16        D: serde::Deserializer<'de>,
17    {
18        let s = String::deserialize(deserializer)?;
19        Self::from_str(&s)
20            .map_err(|e| serde::de::Error::custom(format!("invalid value for EngineVersion {e}")))
21    }
22}
23
24impl FromStr for EngineVersion {
25    type Err = String;
26
27    fn from_str(s: &str) -> Result<Self, Self::Err> {
28        if let Ok(pg_ver) = PostgresVersion::from_str(s) {
29            Ok(Self(Either::Left(pg_ver)))
30        } else {
31            // TODO validate custom version string
32            Ok(Self(Either::Right(s.to_string())))
33        }
34    }
35}
36
37impl std::fmt::Display for EngineVersion {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match &self.0 {
40            Either::Left(v) => write!(f, "{v}"),
41            Either::Right(v) => write!(f, "{v}"),
42        }
43    }
44}
45
46impl EngineVersion {
47    pub fn is_wellknown(&self) -> bool {
48        self.0.is_left()
49    }
50
51    pub fn definition_base_path<P: AsRef<Path>>(&self, home: P) -> PathBuf {
52        if self.is_wellknown() {
53            home.as_ref()
54                .join("postgres")
55                .join(Path::new(&self.to_string()))
56        } else {
57            home.as_ref()
58                .join("postgres")
59                .join("custom")
60                .join(self.to_string())
61        }
62    }
63}
64
65/// Well known database engine
66#[derive(serde::Deserialize, Debug)]
67pub enum WellKnownDbEngine {
68    /// `PostgreSQL` Engine
69    PostgreSQL,
70}
71
72impl FromStr for WellKnownDbEngine {
73    type Err = String;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        if s == "PostgreSQL" {
77            Ok(Self::PostgreSQL)
78        } else {
79            Err(s.to_string())
80        }
81    }
82}
83
84/// Database engine name
85#[derive(Debug)]
86pub struct DatabaseEngine(Either<WellKnownDbEngine, String>);
87
88impl<'de> serde::Deserialize<'de> for DatabaseEngine {
89    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90    where
91        D: serde::Deserializer<'de>,
92    {
93        let s = String::deserialize(deserializer)?;
94        Ok(match s.parse::<WellKnownDbEngine>() {
95            Ok(wk) => DatabaseEngine(Either::Left(wk)),
96            Err(_) => DatabaseEngine(Either::Right(s)),
97        })
98    }
99}
100
101impl DatabaseEngine {
102    #[must_use]
103    pub fn is_wellknown(&self) -> bool {
104        self.0.is_left()
105    }
106
107    #[must_use]
108    pub fn is_custom(&self) -> bool {
109        self.0.is_right()
110    }
111
112    #[must_use]
113    pub fn as_wellknown(&self) -> Option<&WellKnownDbEngine> {
114        match &self.0 {
115            Either::Left(l) => Some(l),
116            Either::Right(_) => None,
117        }
118    }
119
120    #[must_use]
121    pub fn as_custom(&self) -> Option<&String> {
122        match &self.0 {
123            Either::Right(r) => Some(r),
124            Either::Left(_) => None,
125        }
126    }
127}
128
129/// `sql-fun` metadata structures
130///
131#[derive(Debug, serde::Deserialize)]
132pub struct SqlFunMetadata {
133    /// Database Engine
134    engine: DatabaseEngine,
135    /// SQL dialect
136    dialect: SqlDialect,
137    /// Database engine version
138    #[serde(rename = "version")]
139    engine_version: Option<EngineVersion>,
140    /// Database extensions
141    extensions: Option<PostgresExtensionsCollection>,
142    /// Schema search path
143    search_path: Option<Vec<String>>,
144    /// CTE catalog directory path
145    cte_catalog: Option<PathBuf>,
146    /// Database connection dirver/midle-ware
147    connector: Option<String>,
148}
149
150/// metadata related error
151#[derive(Debug, thiserror::Error)]
152pub enum MetadataError {
153    /// container structure failure
154    #[error("sql-fun metadata can not load from {0}: {1}")]
155    MetadataLoadFromFile(PathBuf, String),
156
157    /// file io error
158    #[error("metadata io error {0}")]
159    Io(#[from] std::io::Error),
160
161    /// metadata deserialization error
162    #[error("metadata format error {0}")]
163    Serde(#[from] toml::de::Error),
164
165    /// CTE Catalog not existing
166    #[error("CTE catalog not exists in {0}")]
167    CteCatalogNotExist(PathBuf),
168}
169
170impl MetadataError {
171    /// error in load metadata from file
172    ///
173    /// # Errors
174    ///
175    /// Always returns [`MetadataError::MetadataLoadFromFile`].
176    pub fn metadata_load_from_file<T, P: AsRef<Path>>(path: P, message: &str) -> Result<T, Self> {
177        Err(Self::MetadataLoadFromFile(
178            path.as_ref().to_path_buf(),
179            String::from(message),
180        ))
181    }
182
183    /// CTE catalog not exist
184    ///
185    /// # Errors
186    ///
187    /// Always returns [`MetadataError::CteCatalogNotExist`].
188    pub fn cte_catalog_not_exist<T, P: AsRef<Path>>(path: P) -> Result<T, Self> {
189        Err(Self::CteCatalogNotExist(path.as_ref().to_path_buf()))
190    }
191}
192
193impl SqlFunMetadata {
194    /// load form TOML.
195    ///
196    /// - source toml requires Cargo.toml like structure.
197    /// - this function retrieves package.metadata.database and deserialize metadata
198    ///
199    /// # Errors
200    ///
201    /// Returns [`MetadataError`] if the file cannot be read or parsed.
202    pub fn load_from<P: AsRef<Path> + std::fmt::Debug>(path: P) -> Result<Self, MetadataError> {
203        let file_text = std::fs::read_to_string(&path)?;
204
205        Self::from_str(&file_text, path)
206    }
207
208    /// load from `Cargo.toml` like text
209    pub(crate) fn from_str<P: AsRef<Path> + std::fmt::Debug>(
210        source: &str,
211        path_hint: P,
212    ) -> Result<Self, MetadataError> {
213        use MetadataError as Error;
214        let cargo_metadata: toml::Value = toml::from_str(source)?;
215        let Some(package) = cargo_metadata.get("package") else {
216            Error::metadata_load_from_file(
217                &path_hint,
218                &format!("package not defined in {path_hint:?}"),
219            )?
220        };
221        let Some(metadata) = package.get("metadata") else {
222            Error::metadata_load_from_file(
223                &path_hint,
224                &format!("package.metadata not defined in {path_hint:?}"),
225            )?
226        };
227        let Some(database) = metadata.get("database") else {
228            Error::metadata_load_from_file(
229                &path_hint,
230                &format!("package.metadata.database not defined in {path_hint:?}"),
231            )?
232        };
233
234        Self::from_value(database)
235    }
236
237    /// deserialize form TOML Value
238    ///
239    /// # Errors
240    ///
241    /// Returns [`MetadataError`] when TOML conversion fails.
242    pub(crate) fn from_value(database: &toml::Value) -> Result<Self, MetadataError> {
243        let db_matadata: Self = database.clone().try_into()?;
244        Ok(db_matadata)
245    }
246
247    /// get CTE catalog directory path
248    ///
249    /// # Errors
250    ///
251    /// Returns [`MetadataError`] if the catalog path does not exist.
252    pub fn cte_catalog(&self) -> Result<Option<&PathBuf>, MetadataError> {
253        let Some(cte_catalog) = &self.cte_catalog else {
254            return Ok(None);
255        };
256        if cte_catalog.exists() {
257            Ok(Some(cte_catalog))
258        } else {
259            MetadataError::cte_catalog_not_exist(cte_catalog)?
260        }
261    }
262
263    /// get SQL dialect
264    #[must_use]
265    pub fn sql_dialect(&self) -> SqlDialect {
266        self.dialect
267    }
268
269    /// get db engine
270    #[must_use]
271    pub fn engine(&self) -> &DatabaseEngine {
272        &self.engine
273    }
274
275    /// get engine version
276    ///
277    ///  this function returns just metadata defined.
278    ///
279    /// See [`crate::SqlFunArgs::postgres_version`] for argument precedence.
280    ///
281    #[must_use]
282    pub fn engine_version(&self) -> &Option<EngineVersion> {
283        &self.engine_version
284    }
285
286    /// get search path
287    ///
288    ///  this function returns just metadata defined.
289    ///
290    /// See [`crate::SqlFunArgs::postgres_search_path`] for argument precedence.
291    ///
292    #[must_use]
293    pub fn search_path(&self) -> &Option<Vec<String>> {
294        &self.search_path
295    }
296
297    /// get `PostgreSQL` extensions allowed
298    #[must_use]
299    pub fn postgres_extensions(&self) -> &Option<PostgresExtensionsCollection> {
300        &self.extensions
301    }
302
303    /// get database connection driver / middle-ware
304    #[must_use]
305    pub fn connector(&self) -> &Option<String> {
306        &self.connector
307    }
308}