pg_connection_string/
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 (
96            n,
97            HostSpec {
98                host,
99                port,
100            },
101        ) in self.hostspecs.iter().enumerate()
102        {
103            if let Host::Path(_) = host {
104                continue;
105            }
106
107            write!(f, "{host}")?;
108
109            if let Some(p) = port {
110                write!(f, ":{p}")?;
111            }
112
113            if n + 1 < self.hostspecs.len() {
114                write!(f, ",")?;
115            }
116        }
117
118        if let Some(database) = &self.database {
119            write!(f, "/{database}")?;
120        }
121
122        for (
123            n,
124            Parameter {
125                keyword,
126                value,
127            },
128        ) in self.parameters.iter().enumerate()
129        {
130            if n == 0 {
131                write!(f, "?")?;
132            }
133            write!(f, "{keyword}={value}")?;
134
135            if n + 1 < self.parameters.len() {
136                write!(f, "&")?;
137            }
138        }
139
140        // Write an host params to the end.
141        for HostSpec {
142            host,
143            ..
144        } in &self.hostspecs
145        {
146            match host {
147                Host::Path(path) => {
148                    write!(
149                        f,
150                        "{}",
151                        if self.parameters.is_empty() {
152                            "?"
153                        } else {
154                            "&"
155                        }
156                    )?;
157
158                    write!(f, "host={}", path.to_str().unwrap_or("invalid"))?;
159                },
160                _ => continue,
161            }
162        }
163
164        if let Some(frag) = &self.fragment {
165            write!(f, "#{frag}")?;
166        }
167
168        Ok(())
169    }
170}
171
172impl TryFrom<ConnectionUri> for ConnectionString {
173    type Error = anyhow::Error;
174
175    fn try_from(mut uri: ConnectionUri) -> Result<Self, Self::Error> {
176        // Extract any additional host from the query params before setting them
177        // on the URIs.
178        let mut addtl_hosts = vec![];
179        if let Some(params) = &mut uri.parameters {
180            if let Some(pos) = params.iter().position(|p| p.keyword == "host") {
181                let param = params.remove(pos);
182
183                addtl_hosts.push(param);
184            }
185        }
186
187        // Set up a base object that has all the unchanging parameters about the
188        // URL (properties that can't be specified multiple times).
189        let mut out = ConnectionString {
190            database: uri.database,
191            parameters: uri.parameters.unwrap_or(vec![]),
192            fragment: uri.fragment,
193            ..ConnectionString::default()
194        };
195        trace!(?out, "populated unchanging pieces");
196
197        // If there's an authority section, we'll add that to the output object.
198        if let Some(Authority {
199            userspec,
200            hostspec,
201        }) = uri.authority
202        {
203            trace!(?userspec, ?hostspec, "found authority");
204
205            // If a user/password were passed, set them.
206            if let Some(UserSpec {
207                user,
208                password,
209            }) = userspec
210            {
211                trace!(?user, ?password, "found userspec");
212
213                out.user = Some(user);
214                out.password = password;
215            }
216
217            // If there's a hostspec, set that.
218            for spec in hostspec {
219                trace!(?spec, "adding hostspec");
220
221                if let Some(host) = spec.host {
222                    out.hostspecs.push(HostSpec {
223                        host,
224                        port: spec.port,
225                    });
226                }
227            }
228        }
229
230        for Parameter {
231            value,
232            ..
233        } in addtl_hosts
234        {
235            let host = if value.starts_with('/') {
236                Host::Path(PathBuf::from(value))
237            } else {
238                value.parse()?
239            };
240
241            out.hostspecs.push(HostSpec {
242                host,
243                port: None,
244            });
245        }
246
247        Ok(out)
248    }
249}
250
251/// Parse a PostgreSQL connection string.
252impl FromStr for ConnectionString {
253    type Err = anyhow::Error;
254
255    fn from_str(s: &str) -> Result<Self, Self::Err> {
256        let parsed = parser::consuming_connection_string(s)?;
257        debug!(?parsed);
258
259        parsed.try_into()
260    }
261}
262
263/// Parse a delimited list of PostgreSQL connection strings into a list of
264/// `ConnectionString`s.
265///
266/// This function can be useful if you want to take a string input list of
267/// connection strings.
268///
269/// # Example
270///
271/// ```
272/// use pg_connection_string::{HostSpec, from_multi_str, ConnectionString, Parameter};
273///
274/// assert_eq!(
275///     from_multi_str("postgres://jack@besthost:34/mydb?host=foohost", ",").unwrap(),
276///     [
277///         ConnectionString {
278///             user: Some("jack".to_string()),
279///             password: None,
280///             hostspecs: vec![
281///                 HostSpec {
282///                     host: "besthost".to_string(),
283///                     port: Some(34),
284///                 },
285///                 HostSpec {
286///                     host: "foohost".to_string(),
287///                     port: None,
288///                 },
289///             ],
290///             database: Some("mydb".to_string()),
291///             parameters: vec![],
292///             fragment: None,
293///         },
294///     ]
295/// );
296/// ```
297pub fn from_multi_str(
298    i: &str,
299    sep: &str,
300) -> anyhow::Result<Vec<ConnectionString>> {
301    let parsed = parser::multi_connection_string(i, sep)?;
302    eprintln!("{parsed:?}");
303
304    let mut out: Vec<ConnectionString> = vec![];
305
306    for c in parsed {
307        out.push(TryFrom::try_from(c)?);
308    }
309
310    Ok(out)
311}