Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit bf50469

Browse files
committed
Flatten the database config
1 parent cba431d commit bf50469

File tree

6 files changed

+215
-181
lines changed

6 files changed

+215
-181
lines changed

crates/cli/src/util.rs

Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ use std::time::Duration;
1616

1717
use anyhow::Context;
1818
use mas_config::{
19-
BrandingConfig, DatabaseConfig, DatabaseConnectConfig, EmailConfig, EmailSmtpMode,
20-
EmailTransportConfig, PasswordsConfig, PolicyConfig, TemplatesConfig,
19+
BrandingConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, EmailTransportConfig,
20+
PasswordsConfig, PolicyConfig, TemplatesConfig,
2121
};
2222
use mas_email::{MailTransport, Mailer};
2323
use mas_handlers::{passwords::PasswordManager, ActivityTracker};
@@ -151,47 +151,37 @@ pub async fn templates_from_config(
151151
fn database_connect_options_from_config(
152152
config: &DatabaseConfig,
153153
) -> Result<PgConnectOptions, anyhow::Error> {
154-
let options = match &config.options {
155-
DatabaseConnectConfig::Uri { uri } => uri
156-
.parse()
157-
.context("could not parse database connection string")?,
158-
DatabaseConnectConfig::Options {
159-
host,
160-
port,
161-
socket,
162-
username,
163-
password,
164-
database,
165-
} => {
166-
let mut opts =
167-
PgConnectOptions::new().application_name("matrix-authentication-service");
168-
169-
if let Some(host) = host {
170-
opts = opts.host(host);
171-
}
172-
173-
if let Some(port) = port {
174-
opts = opts.port(*port);
175-
}
154+
let options = if let Some(uri) = config.uri.as_deref() {
155+
uri.parse()
156+
.context("could not parse database connection string")?
157+
} else {
158+
let mut opts = PgConnectOptions::new().application_name("matrix-authentication-service");
159+
160+
if let Some(host) = config.host.as_deref() {
161+
opts = opts.host(host);
162+
}
176163

177-
if let Some(socket) = socket {
178-
opts = opts.socket(socket);
179-
}
164+
if let Some(port) = config.port {
165+
opts = opts.port(port);
166+
}
180167

181-
if let Some(username) = username {
182-
opts = opts.username(username);
183-
}
168+
if let Some(socket) = config.socket.as_deref() {
169+
opts = opts.socket(socket);
170+
}
184171

185-
if let Some(password) = password {
186-
opts = opts.password(password);
187-
}
172+
if let Some(username) = config.username.as_deref() {
173+
opts = opts.username(username);
174+
}
188175

189-
if let Some(database) = database {
190-
opts = opts.database(database);
191-
}
176+
if let Some(password) = config.password.as_deref() {
177+
opts = opts.password(password);
178+
}
192179

193-
opts
180+
if let Some(database) = config.database.as_deref() {
181+
opts = opts.database(database);
194182
}
183+
184+
opts
195185
};
196186

197187
let options = options

crates/config/src/schema.rs

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,27 @@
1616
1717
use schemars::{
1818
gen::SchemaGenerator,
19-
schema::{InstanceType, NumberValidation, Schema, SchemaObject},
19+
schema::{InstanceType, Schema, SchemaObject},
20+
JsonSchema,
2021
};
2122

22-
/// A network port
23-
pub fn port(_gen: &mut SchemaGenerator) -> Schema {
24-
Schema::Object(SchemaObject {
25-
instance_type: Some(InstanceType::Integer.into()),
26-
number: Some(Box::new(NumberValidation {
27-
minimum: Some(1.0),
28-
maximum: Some(65535.0),
29-
..NumberValidation::default()
30-
})),
31-
..SchemaObject::default()
32-
})
33-
}
34-
3523
/// A network hostname
36-
pub fn hostname(_gen: &mut SchemaGenerator) -> Schema {
37-
Schema::Object(SchemaObject {
38-
instance_type: Some(InstanceType::String.into()),
39-
format: Some("hostname".to_owned()),
40-
..SchemaObject::default()
41-
})
24+
pub struct Hostname;
25+
26+
impl JsonSchema for Hostname {
27+
fn schema_name() -> String {
28+
"Hostname".to_string()
29+
}
30+
31+
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
32+
hostname(gen)
33+
}
4234
}
4335

44-
/// An email address
45-
pub fn mailbox(_gen: &mut SchemaGenerator) -> Schema {
36+
fn hostname(_gen: &mut SchemaGenerator) -> Schema {
4637
Schema::Object(SchemaObject {
4738
instance_type: Some(InstanceType::String.into()),
48-
format: Some("email".to_owned()),
39+
format: Some("hostname".to_owned()),
4940
..SchemaObject::default()
5041
})
5142
}

crates/config/src/sections/database.rs

Lines changed: 92 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ use camino::Utf8PathBuf;
1919
use rand::Rng;
2020
use schemars::JsonSchema;
2121
use serde::{Deserialize, Serialize};
22-
use serde_with::{serde_as, skip_serializing_none};
22+
use serde_with::serde_as;
2323

2424
use super::ConfigurationSection;
2525
use crate::schema;
2626

27-
fn default_connection_string() -> String {
28-
"postgresql://".to_owned()
27+
#[allow(clippy::unnecessary_wraps)]
28+
fn default_connection_string() -> Option<String> {
29+
Some("postgresql://".to_owned())
2930
}
3031

3132
fn default_max_connections() -> NonZeroU32 {
@@ -49,7 +50,13 @@ fn default_max_lifetime() -> Option<Duration> {
4950
impl Default for DatabaseConfig {
5051
fn default() -> Self {
5152
Self {
52-
options: ConnectConfig::default(),
53+
uri: default_connection_string(),
54+
host: None,
55+
port: None,
56+
socket: None,
57+
username: None,
58+
password: None,
59+
database: None,
5360
max_connections: default_max_connections(),
5461
min_connections: Default::default(),
5562
connect_timeout: default_connect_timeout(),
@@ -59,63 +66,56 @@ impl Default for DatabaseConfig {
5966
}
6067
}
6168

62-
/// Database connection configuration
63-
#[derive(Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
64-
#[serde(untagged)]
65-
pub enum ConnectConfig {
66-
/// Connect via a full URI
67-
Uri {
68-
/// Connection URI
69-
#[schemars(url, default = "default_connection_string")]
70-
uri: String,
71-
},
72-
/// Connect via a map of options
73-
Options {
74-
/// Name of host to connect to
75-
#[schemars(schema_with = "schema::hostname")]
76-
#[serde(default)]
77-
host: Option<String>,
78-
79-
/// Port number to connect at the server host
80-
#[schemars(schema_with = "schema::port")]
81-
#[serde(default)]
82-
port: Option<u16>,
83-
84-
/// Directory containing the UNIX socket to connect to
85-
#[serde(default)]
86-
#[schemars(with = "Option<String>")]
87-
socket: Option<Utf8PathBuf>,
88-
89-
/// PostgreSQL user name to connect as
90-
#[serde(default)]
91-
username: Option<String>,
92-
93-
/// Password to be used if the server demands password authentication
94-
#[serde(default)]
95-
password: Option<String>,
96-
97-
/// The database name
98-
#[serde(default)]
99-
database: Option<String>,
100-
},
101-
}
102-
103-
impl Default for ConnectConfig {
104-
fn default() -> Self {
105-
Self::Uri {
106-
uri: default_connection_string(),
107-
}
108-
}
109-
}
110-
11169
/// Database connection configuration
11270
#[serde_as]
113-
#[skip_serializing_none]
11471
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
11572
pub struct DatabaseConfig {
116-
/// Options related to how to connect to the database
117-
#[serde(default, flatten)]
118-
pub options: ConnectConfig,
73+
/// Connection URI
74+
///
75+
/// This must not be specified if `host`, `port`, `socket`, `username`,
76+
/// `password`, or `database` are specified.
77+
#[serde(skip_serializing_if = "Option::is_none")]
78+
#[schemars(url, default = "default_connection_string")]
79+
pub uri: Option<String>,
80+
81+
/// Name of host to connect to
82+
///
83+
/// This must not be specified if `uri` is specified.
84+
#[serde(skip_serializing_if = "Option::is_none")]
85+
#[schemars(with = "Option::<schema::Hostname>")]
86+
pub host: Option<String>,
87+
88+
/// Port number to connect at the server host
89+
///
90+
/// This must not be specified if `uri` is specified.
91+
#[serde(skip_serializing_if = "Option::is_none")]
92+
#[schemars(range(min = 1, max = 65535))]
93+
pub port: Option<u16>,
94+
95+
/// Directory containing the UNIX socket to connect to
96+
///
97+
/// This must not be specified if `uri` is specified.
98+
#[serde(skip_serializing_if = "Option::is_none")]
99+
#[schemars(with = "Option<String>")]
100+
pub socket: Option<Utf8PathBuf>,
101+
102+
/// PostgreSQL user name to connect as
103+
///
104+
/// This must not be specified if `uri` is specified.
105+
#[serde(skip_serializing_if = "Option::is_none")]
106+
pub username: Option<String>,
107+
108+
/// Password to be used if the server demands password authentication
109+
///
110+
/// This must not be specified if `uri` is specified.
111+
#[serde(skip_serializing_if = "Option::is_none")]
112+
pub password: Option<String>,
113+
114+
/// The database name
115+
///
116+
/// This must not be specified if `uri` is specified.
117+
#[serde(skip_serializing_if = "Option::is_none")]
118+
pub database: Option<String>,
119119

120120
/// Set the maximum number of connections the pool should maintain
121121
#[serde(default = "default_max_connections")]
@@ -133,13 +133,19 @@ pub struct DatabaseConfig {
133133

134134
/// Set a maximum idle duration for individual connections
135135
#[schemars(with = "Option<u64>")]
136-
#[serde(default = "default_idle_timeout")]
136+
#[serde(
137+
default = "default_idle_timeout",
138+
skip_serializing_if = "Option::is_none"
139+
)]
137140
#[serde_as(as = "Option<serde_with::DurationSeconds<u64>>")]
138141
pub idle_timeout: Option<Duration>,
139142

140143
/// Set the maximum lifetime of individual connections
141144
#[schemars(with = "u64")]
142-
#[serde(default = "default_max_lifetime")]
145+
#[serde(
146+
default = "default_max_lifetime",
147+
skip_serializing_if = "Option::is_none"
148+
)]
143149
#[serde_as(as = "Option<serde_with::DurationSeconds<u64>>")]
144150
pub max_lifetime: Option<Duration>,
145151
}
@@ -155,6 +161,31 @@ impl ConfigurationSection for DatabaseConfig {
155161
Ok(Self::default())
156162
}
157163

164+
fn validate(&self, figment: &figment::Figment) -> Result<(), figment::error::Error> {
165+
let metadata = figment.find_metadata(Self::PATH.unwrap());
166+
167+
// Check that the user did not specify both `uri` and the split options at the
168+
// same time
169+
let has_split_options = self.host.is_some()
170+
|| self.port.is_some()
171+
|| self.socket.is_some()
172+
|| self.username.is_some()
173+
|| self.password.is_some()
174+
|| self.database.is_some();
175+
176+
if self.uri.is_some() && has_split_options {
177+
let mut error = figment::error::Error::from(
178+
"uri must not be specified if host, port, socket, username, password, or database are specified".to_owned(),
179+
);
180+
error.metadata = metadata.cloned();
181+
error.profile = Some(figment::Profile::Default);
182+
error.path = vec![Self::PATH.unwrap().to_owned(), "uri".to_owned()];
183+
return Err(error);
184+
}
185+
186+
Ok(())
187+
}
188+
158189
fn test() -> Self {
159190
Self::default()
160191
}
@@ -185,10 +216,8 @@ mod tests {
185216
.extract_inner::<DatabaseConfig>("database")?;
186217

187218
assert_eq!(
188-
config.options,
189-
ConnectConfig::Uri {
190-
uri: "postgresql://user:password@host/database".to_string()
191-
}
219+
config.uri.as_deref(),
220+
Some("postgresql://user:password@host/database")
192221
);
193222

194223
Ok(())

crates/config/src/sections/email.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ pub enum EmailTransportConfig {
5959
mode: EmailSmtpMode,
6060

6161
/// Hostname to connect to
62-
#[schemars(schema_with = "crate::schema::hostname")]
62+
#[schemars(with = "crate::schema::Hostname")]
6363
hostname: String,
6464

6565
/// Port to connect to. Default is 25 for plain, 465 for TLS and 587 for
@@ -103,12 +103,12 @@ fn default_sendmail_command() -> String {
103103
pub struct EmailConfig {
104104
/// Email address to use as From when sending emails
105105
#[serde(default = "default_email")]
106-
#[schemars(schema_with = "crate::schema::mailbox")]
106+
#[schemars(email)]
107107
pub from: String,
108108

109109
/// Email address to use as Reply-To when sending emails
110110
#[serde(default = "default_email")]
111-
#[schemars(schema_with = "crate::schema::mailbox")]
111+
#[schemars(email)]
112112
pub reply_to: String,
113113

114114
/// What backend should be used when sending emails

0 commit comments

Comments
 (0)