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}