Skip to main content

pg_ephemeral/
lib.rs

1pub mod certificate;
2pub mod cli;
3pub mod config;
4pub mod container;
5pub mod definition;
6pub mod image;
7pub mod label;
8pub mod seed;
9pub mod session;
10
11pub use config::{Config, Instance};
12pub use container::Container;
13pub use definition::{Definition, TransparentWorkdir, TransparentWorkdirError};
14pub use image::Image;
15pub use seed::Command;
16pub use seed::DuplicateSeedName;
17pub use seed::LoadError;
18pub use seed::Seed;
19pub use seed::SeedCacheConfig;
20pub use seed::SeedName;
21pub use seed::SeedNameError;
22
23pub(crate) const VERSION_STR: &str = env!("CARGO_PKG_VERSION");
24pub(crate) const LOCALHOST_IP: std::net::IpAddr =
25    std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST);
26pub(crate) const UNSPECIFIED_IP: std::net::IpAddr =
27    std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED);
28pub(crate) const LOCALHOST_HOST_ADDR: pg_client::config::HostAddr =
29    pg_client::config::HostAddr::new(LOCALHOST_IP);
30pub(crate) const ENV_DATABASE_URL: cmd_proc::EnvVariableName =
31    cmd_proc::EnvVariableName::from_static_or_panic("DATABASE_URL");
32
33#[must_use]
34pub fn version() -> &'static semver::Version {
35    static VERSION: std::sync::LazyLock<semver::Version> =
36        std::sync::LazyLock::new(|| semver::Version::parse(VERSION_STR).unwrap());
37    &VERSION
38}
39
40pub(crate) fn convert_schema(value: &[u8]) -> String {
41    std::str::from_utf8(value)
42        .expect("schema contains invalid utf8")
43        .to_string()
44}
45
46/// Error parsing an instance name.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum InstanceNameError {
49    /// Instance name cannot be empty.
50    Empty,
51    /// Instance name contains an invalid character.
52    InvalidCharacter,
53    /// Instance name starts with a dash.
54    StartsWithDash,
55    /// Instance name ends with a dash.
56    EndsWithDash,
57}
58
59impl InstanceNameError {
60    #[must_use]
61    const fn message(&self) -> &'static str {
62        match self {
63            Self::Empty => "instance name cannot be empty",
64            Self::InvalidCharacter => {
65                "instance name must contain only lowercase ASCII alphanumeric characters or dashes"
66            }
67            Self::StartsWithDash => "instance name cannot start with a dash",
68            Self::EndsWithDash => "instance name cannot end with a dash",
69        }
70    }
71}
72
73impl std::fmt::Display for InstanceNameError {
74    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(formatter, "{}", self.message())
76    }
77}
78
79impl std::error::Error for InstanceNameError {}
80
81const fn validate_instance_name(input: &str) -> Option<InstanceNameError> {
82    let bytes = input.as_bytes();
83
84    if bytes.is_empty() {
85        return Some(InstanceNameError::Empty);
86    }
87
88    if bytes[0] == b'-' {
89        return Some(InstanceNameError::StartsWithDash);
90    }
91
92    if bytes[bytes.len() - 1] == b'-' {
93        return Some(InstanceNameError::EndsWithDash);
94    }
95
96    let mut index = 0;
97
98    while index < bytes.len() {
99        let byte = bytes[index];
100        if !(byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') {
101            return Some(InstanceNameError::InvalidCharacter);
102        }
103        index += 1;
104    }
105
106    None
107}
108
109#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize, serde::Deserialize)]
110#[serde(try_from = "String")]
111pub struct InstanceName(std::borrow::Cow<'static, str>);
112
113impl TryFrom<String> for InstanceName {
114    type Error = InstanceNameError;
115
116    fn try_from(value: String) -> Result<Self, Self::Error> {
117        match validate_instance_name(&value) {
118            Some(error) => Err(error),
119            None => Ok(Self(std::borrow::Cow::Owned(value))),
120        }
121    }
122}
123
124impl InstanceName {
125    pub const MAIN: Self = Self::from_static_or_panic("main");
126
127    /// Creates a new instance name from a static string.
128    ///
129    /// # Panics
130    ///
131    /// Panics if the input is empty, contains non-alphanumeric/dash characters,
132    /// or starts/ends with a dash.
133    #[must_use]
134    pub const fn from_static_or_panic(input: &'static str) -> Self {
135        match validate_instance_name(input) {
136            Some(error) => panic!("{}", error.message()),
137            None => Self(std::borrow::Cow::Borrowed(input)),
138        }
139    }
140
141    /// Returns the instance name as a string slice.
142    #[must_use]
143    pub fn as_str(&self) -> &str {
144        &self.0
145    }
146}
147
148impl std::default::Default for InstanceName {
149    fn default() -> Self {
150        Self::MAIN
151    }
152}
153
154impl std::str::FromStr for InstanceName {
155    type Err = InstanceNameError;
156
157    fn from_str(value: &str) -> Result<Self, Self::Err> {
158        match validate_instance_name(value) {
159            Some(error) => Err(error),
160            None => Ok(Self(std::borrow::Cow::Owned(value.to_owned()))),
161        }
162    }
163}
164
165impl std::fmt::Display for InstanceName {
166    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        write!(formatter, "{}", self.0)
168    }
169}
170
171impl AsRef<str> for InstanceName {
172    fn as_ref(&self) -> &str {
173        &self.0
174    }
175}
176
177pub type InstanceMap = std::collections::BTreeMap<InstanceName, config::Instance>;
178
179#[cfg(test)]
180mod test {
181    use super::*;
182
183    #[test]
184    fn parse_valid_simple() {
185        let name: InstanceName = "main".parse().unwrap();
186        assert_eq!(name.to_string(), "main");
187    }
188
189    #[test]
190    fn parse_valid_with_dash() {
191        let name: InstanceName = "my-instance".parse().unwrap();
192        assert_eq!(name.to_string(), "my-instance");
193    }
194
195    #[test]
196    fn parse_valid_single_char() {
197        let name: InstanceName = "a".parse().unwrap();
198        assert_eq!(name.to_string(), "a");
199    }
200
201    #[test]
202    fn parse_valid_numeric() {
203        let name: InstanceName = "123".parse().unwrap();
204        assert_eq!(name.to_string(), "123");
205    }
206
207    #[test]
208    fn parse_empty_fails() {
209        assert_eq!("".parse::<InstanceName>(), Err(InstanceNameError::Empty));
210    }
211
212    #[test]
213    fn parse_starts_with_dash_fails() {
214        assert_eq!(
215            "-foo".parse::<InstanceName>(),
216            Err(InstanceNameError::StartsWithDash)
217        );
218    }
219
220    #[test]
221    fn parse_ends_with_dash_fails() {
222        assert_eq!(
223            "foo-".parse::<InstanceName>(),
224            Err(InstanceNameError::EndsWithDash)
225        );
226    }
227
228    #[test]
229    fn parse_invalid_character_fails() {
230        assert_eq!(
231            "foo bar".parse::<InstanceName>(),
232            Err(InstanceNameError::InvalidCharacter)
233        );
234    }
235
236    #[test]
237    fn parse_underscore_fails() {
238        assert_eq!(
239            "foo_bar".parse::<InstanceName>(),
240            Err(InstanceNameError::InvalidCharacter)
241        );
242    }
243
244    #[test]
245    fn parse_uppercase_fails() {
246        assert_eq!(
247            "Main".parse::<InstanceName>(),
248            Err(InstanceNameError::InvalidCharacter)
249        );
250    }
251
252    #[test]
253    fn default_is_main() {
254        assert_eq!(InstanceName::default(), InstanceName::MAIN);
255        assert_eq!(InstanceName::default().to_string(), "main");
256    }
257}