postgres_conn_str/
lib.rs

1#![deny(unsafe_code)]
2
3//! `pg-connection-string` parses URIs in the ways that psql (and generally,
4//! libpq) will accept them. This is a more convenient and robust alternative to
5//! crates like `uri`.
6//!
7//! As outlined in the
8//! [Postgres docs](https://www.postgresql.org/docs/14/libpq-connect.html), the
9//! general form for a connection URI is:
10//!
11//! ```txt
12//! postgresql://[userspec@][hostspec][/dbname][?paramspec]
13//!
14//! where userspec is:
15//!
16//! user[:password]
17//!
18//! and hostspec is:
19//!
20//! [host][:port][,...]
21//!
22//! and paramspec is:
23//!
24//! name=value[&...]
25//! ```
26//!
27//! The URI scheme designator can be either `postgresql://` or `postgres://`.
28//! Each of the remaining URI parts is optional.
29
30#[cfg(feature = "serde")]
31mod de;
32pub(crate) mod parser;
33#[cfg(feature = "serde")]
34mod ser;
35#[cfg(test)]
36mod tests;
37
38use parser::{
39    authority::{userinfo::UserSpec, Authority},
40    ConnectionUri,
41};
42use std::{fmt::Display, path::PathBuf, str::FromStr};
43use tracing::{debug, trace};
44
45pub use parser::authority::host::Host;
46
47/// A query parameter attached to the connection string.
48///
49/// This can be used to pass various configuration options to `libpq`, or to
50/// override other parts of the URI.
51#[derive(Clone, Debug, Default, PartialEq, Eq)]
52pub struct Parameter {
53    /// The key portion of the key=value pair.
54    pub keyword: String,
55
56    /// The value portion of the key=value pair.
57    pub value: String,
58}
59
60/// Representation of the connection string.
61///
62/// This provides useful methods to access various parts of the connection
63/// string, taking into account PostgreSQL's idiosyncrasies (such as being able
64/// to pass most of the URI either in-place, or as query params).
65#[derive(Clone, Debug, Default, PartialEq, Eq)]
66pub struct ConnectionString {
67    pub user: Option<String>,
68    pub password: Option<String>,
69    pub hostspecs: Vec<HostSpec>,
70    pub database: Option<String>,
71    pub parameters: Vec<Parameter>,
72    pub fragment: Option<String>,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct HostSpec {
77    pub host: Host,
78    pub port: Option<u16>,
79}
80
81impl Display for ConnectionString {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(f, "postgresql://",)?;
84
85        if let Some(user) = &self.user {
86            write!(f, "{user}",)?;
87
88            if let Some(password) = &self.password {
89                write!(f, ":{password}",)?;
90            }
91
92            write!(f, "@")?;
93        }
94
95        for (n, HostSpec { host, port }) in self.hostspecs.iter().enumerate() {
96            if let Host::Path(_) = host {
97                continue;
98            }
99
100            write!(f, "{host}")?;
101
102            if let Some(p) = port {
103                write!(f, ":{p}")?;
104            }
105
106            if n + 1 < self.hostspecs.len() {
107                write!(f, ",")?;
108            }
109        }
110
111        if let Some(database) = &self.database {
112            write!(f, "/{database}")?;
113        }
114
115        for (n, Parameter { keyword, value }) in self.parameters.iter().enumerate() {
116            if n == 0 {
117                write!(f, "?")?;
118            }
119            write!(f, "{keyword}={value}")?;
120
121            if n + 1 < self.parameters.len() {
122                write!(f, "&")?;
123            }
124        }
125
126        // Write an host params to the end.
127        for HostSpec { host, .. } in &self.hostspecs {
128            match host {
129                Host::Path(path) => {
130                    write!(f, "{}", if self.parameters.is_empty() { "?" } else { "&" })?;
131
132                    write!(f, "host={}", path.to_str().unwrap_or("invalid"))?;
133                }
134                _ => continue,
135            }
136        }
137
138        if let Some(frag) = &self.fragment {
139            write!(f, "#{frag}")?;
140        }
141
142        Ok(())
143    }
144}
145
146impl TryFrom<ConnectionUri> for ConnectionString {
147    type Error = anyhow::Error;
148
149    fn try_from(mut uri: ConnectionUri) -> Result<Self, Self::Error> {
150        // Extract any additional host from the query params before setting them
151        // on the URIs.
152        let mut addtl_hosts = vec![];
153        if let Some(params) = &mut uri.parameters {
154            if let Some(pos) = params.iter().position(|p| p.keyword == "host") {
155                let param = params.remove(pos);
156
157                addtl_hosts.push(param);
158            }
159        }
160
161        // Set up a base object that has all the unchanging parameters about the
162        // URL (properties that can't be specified multiple times).
163        let mut out = ConnectionString {
164            database: uri.database,
165            parameters: uri.parameters.unwrap_or(vec![]),
166            fragment: uri.fragment,
167            ..ConnectionString::default()
168        };
169        trace!(?out, "populated unchanging pieces");
170
171        // If there's an authority section, we'll add that to the output object.
172        if let Some(Authority { userspec, hostspec }) = uri.authority {
173            trace!(?userspec, ?hostspec, "found authority");
174
175            // If a user/password were passed, set them.
176            if let Some(UserSpec { user, password }) = userspec {
177                trace!(?user, ?password, "found userspec");
178
179                out.user = Some(user);
180                out.password = password;
181            }
182
183            // If there's a hostspec, set that.
184            for spec in hostspec {
185                trace!(?spec, "adding hostspec");
186
187                if let Some(host) = spec.host {
188                    out.hostspecs.push(HostSpec {
189                        host,
190                        port: spec.port,
191                    });
192                }
193            }
194        }
195
196        for Parameter { value, .. } in addtl_hosts {
197            let host = if value.starts_with('/') {
198                Host::Path(PathBuf::from(value))
199            } else {
200                value.parse()?
201            };
202
203            out.hostspecs.push(HostSpec { host, port: None });
204        }
205
206        Ok(out)
207    }
208}
209
210/// Parse a PostgreSQL connection string.
211impl FromStr for ConnectionString {
212    type Err = anyhow::Error;
213
214    fn from_str(s: &str) -> Result<Self, Self::Err> {
215        let parsed = parser::consuming_connection_string(s)?;
216        debug!(?parsed);
217
218        parsed.try_into()
219    }
220}
221
222/// Parse a delimited list of PostgreSQL connection strings into a list of
223/// `ConnectionString`s.
224///
225/// This function can be useful if you want to take a string input list of
226/// connection strings.
227///
228/// # Example
229///
230/// ```
231/// use pg_connection_string::{HostSpec, from_multi_str, ConnectionString, Parameter};
232///
233/// assert_eq!(
234///     from_multi_str("postgres://jack@besthost:34/mydb?host=foohost", ",").unwrap(),
235///     [
236///         ConnectionString {
237///             user: Some("jack".to_string()),
238///             password: None,
239///             hostspecs: vec![
240///                 HostSpec {
241///                     host: "besthost".to_string(),
242///                     port: Some(34),
243///                 },
244///                 HostSpec {
245///                     host: "foohost".to_string(),
246///                     port: None,
247///                 },
248///             ],
249///             database: Some("mydb".to_string()),
250///             parameters: vec![],
251///             fragment: None,
252///         },
253///     ]
254/// );
255/// ```
256pub fn from_multi_str(i: &str, sep: &str) -> anyhow::Result<Vec<ConnectionString>> {
257    let parsed = parser::multi_connection_string(i, sep)?;
258    // eprintln!("{parsed:?}");
259    debug!("{parsed:?}");
260
261    let mut out: Vec<ConnectionString> = vec![];
262
263    for c in parsed {
264        out.push(TryFrom::try_from(c)?);
265    }
266
267    Ok(out)
268}