shuttle_common/
lib.rs

1pub mod constants;
2#[cfg(feature = "models")]
3pub mod models;
4pub mod secrets;
5#[cfg(feature = "tables")]
6pub mod tables;
7pub mod templates;
8
9use serde::{Deserialize, Serialize};
10
11////// Resource Input/Output types
12
13/// The input given to Shuttle DB resources
14#[derive(Clone, Deserialize, Serialize, Default)]
15pub struct DbInput {
16    pub local_uri: Option<String>,
17    /// Override the default db name. Only applies to RDS.
18    pub db_name: Option<String>,
19}
20
21/// The output produced by Shuttle DB resources
22#[derive(Deserialize, Serialize)]
23#[serde(untagged)]
24pub enum DatabaseResource {
25    ConnectionString(String),
26    Info(DatabaseInfo),
27}
28
29/// Holds the data for building a database connection string.
30#[derive(Clone, Serialize, Deserialize)]
31#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
32#[typeshare::typeshare]
33pub struct DatabaseInfo {
34    engine: String,
35    role_name: String,
36    role_password: String,
37    database_name: String,
38    port: String,
39    hostname: String,
40    /// The RDS instance name, which is required for deleting provisioned RDS instances, it's
41    /// optional because it isn't needed for shared PG deletion.
42    instance_name: Option<String>,
43}
44
45impl DatabaseInfo {
46    pub fn new(
47        engine: String,
48        role_name: String,
49        role_password: String,
50        database_name: String,
51        port: String,
52        hostname: String,
53        instance_name: Option<String>,
54    ) -> Self {
55        Self {
56            engine,
57            role_name,
58            role_password,
59            database_name,
60            port,
61            hostname,
62            instance_name,
63        }
64    }
65
66    /// For connecting to the database.
67    pub fn connection_string(&self, show_password: bool) -> String {
68        format!(
69            "{}://{}:{}@{}:{}/{}",
70            self.engine,
71            self.role_name,
72            if show_password {
73                &self.role_password
74            } else {
75                "********"
76            },
77            self.hostname,
78            self.port,
79            self.database_name,
80        )
81    }
82
83    pub fn role_name(&self) -> String {
84        self.role_name.to_string()
85    }
86
87    pub fn database_name(&self) -> String {
88        self.database_name.to_string()
89    }
90
91    pub fn instance_name(&self) -> Option<String> {
92        self.instance_name.clone()
93    }
94}
95
96// Don't leak password in Debug
97impl std::fmt::Debug for DatabaseInfo {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        write!(f, "DatabaseInfo {{ {:?} }}", self.connection_string(false))
100    }
101}
102
103/// Used to request a container from the local run provisioner
104#[derive(Serialize, Deserialize)]
105pub struct ContainerRequest {
106    pub project_name: String,
107    /// Type of container, used in the container name. ex "qdrant"
108    pub container_name: String,
109    /// ex. "qdrant/qdrant:latest"
110    pub image: String,
111    /// The internal port that the container should expose. ex. "6334/tcp"
112    pub port: String,
113    /// list of "KEY=value" strings
114    pub env: Vec<String>,
115}
116
117/// Response from requesting a container from the local run provisioner
118#[derive(Serialize, Deserialize)]
119pub struct ContainerResponse {
120    /// The port that the container exposes to the host.
121    /// Is a string for parity with the Docker respose.
122    pub host_port: String,
123}
124
125/// Check if two versions are compatible based on the rule used by cargo:
126/// "Versions `a` and `b` are compatible if their left-most nonzero digit is the same."
127pub fn semvers_are_compatible(a: &semver::Version, b: &semver::Version) -> bool {
128    if a.major != 0 || b.major != 0 {
129        a.major == b.major
130    } else if a.minor != 0 || b.minor != 0 {
131        a.minor == b.minor
132    } else {
133        a.patch == b.patch
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use std::str::FromStr;
140
141    #[test]
142    fn semver_compatibility_check_works() {
143        let semver_tests = &[
144            ("1.0.0", "1.0.0", true),
145            ("1.8.0", "1.0.0", true),
146            ("0.1.0", "0.2.1", false),
147            ("0.9.0", "0.2.0", false),
148        ];
149        for (version_a, version_b, are_compatible) in semver_tests {
150            let version_a = semver::Version::from_str(version_a).unwrap();
151            let version_b = semver::Version::from_str(version_b).unwrap();
152            assert_eq!(
153                super::semvers_are_compatible(&version_a, &version_b),
154                *are_compatible
155            );
156        }
157    }
158}