shuttle_common/
lib.rs

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