From 0bd766d4310ef8c16af355785c3058af9b1aa3bf Mon Sep 17 00:00:00 2001 From: Phil Booth Date: Wed, 31 Oct 2018 14:24:42 +0000 Subject: [PATCH] feat(settings): extract provider type to a fully-fledged enum The provider type was modelled as a newtype struct around a `String` but there's a finite number of values, so really it should be an enum. This wasn't too pressing until now but will shortly come in useful for the configuration stuff, which needs a provider type too. --- src/providers/mod.rs | 41 ++++++++-------- src/providers/test.rs | 4 +- src/settings/mod.rs | 8 ++-- src/settings/test.rs | 15 +++--- src/types/mod.rs | 1 + src/types/provider/mod.rs | 97 ++++++++++++++++++++++++++++++++++++++ src/types/provider/test.rs | 44 +++++++++++++++++ src/types/validate/mod.rs | 6 --- src/types/validate/test.rs | 17 ------- 9 files changed, 176 insertions(+), 57 deletions(-) create mode 100644 src/types/provider/mod.rs create mode 100644 src/types/provider/test.rs diff --git a/src/providers/mod.rs b/src/providers/mod.rs index 8a81b50..eaba23b 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -4,7 +4,7 @@ //! Generic abstraction of specific email providers. -use std::{boxed::Box, collections::HashMap}; +use std::{boxed::Box, collections::HashMap, convert::TryFrom}; use emailmessage::{header::ContentType, Message, MessageBuilder, MultiPart, SinglePart}; @@ -12,10 +12,11 @@ use self::{ mock::MockProvider as Mock, sendgrid::SendgridProvider as Sendgrid, ses::SesProvider as Ses, smtp::SmtpProvider as Smtp, socketlabs::SocketLabsProvider as SocketLabs, }; -use settings::{DefaultProvider, Settings}; +use settings::Settings; use types::{ error::{AppErrorKind, AppResult}, headers::*, + provider::Provider as ProviderType, }; mod mock; @@ -120,40 +121,38 @@ trait Provider { /// Generic provider wrapper. pub struct Providers { - default_provider: String, + default_provider: ProviderType, force_default_provider: bool, - providers: HashMap>, + providers: HashMap>, } impl Providers { /// Instantiate the provider clients. pub fn new(settings: &Settings) -> Providers { - let mut providers: HashMap> = HashMap::new(); + let mut providers: HashMap> = HashMap::new(); macro_rules! set_provider { - ($id:expr, $constructor:expr) => { - if !settings.provider.forcedefault - || settings.provider.default == DefaultProvider(String::from($id)) - { - providers.insert(String::from($id), Box::new($constructor)); + ($type:expr, $constructor:expr) => { + if !settings.provider.forcedefault || settings.provider.default == $type { + providers.insert($type, Box::new($constructor)); } }; } - set_provider!("mock", Mock); - set_provider!("ses", Ses::new(settings)); - set_provider!("smtp", Smtp::new(settings)); + set_provider!(ProviderType::Mock, Mock); + set_provider!(ProviderType::Ses, Ses::new(settings)); + set_provider!(ProviderType::Smtp, Smtp::new(settings)); if let Some(ref sendgrid) = settings.sendgrid { - set_provider!("sendgrid", Sendgrid::new(sendgrid, settings)); + set_provider!(ProviderType::Sendgrid, Sendgrid::new(sendgrid, settings)); } if settings.socketlabs.is_some() { - set_provider!("socketlabs", SocketLabs::new(settings)); + set_provider!(ProviderType::SocketLabs, SocketLabs::new(settings)); } Providers { - default_provider: settings.provider.default.to_string(), + default_provider: settings.provider.default, force_default_provider: settings.provider.forcedefault, providers, } @@ -171,13 +170,17 @@ impl Providers { provider_id: Option<&str>, ) -> AppResult { let resolved_provider_id = if self.force_default_provider { - &self.default_provider + self.default_provider } else { - provider_id.unwrap_or(&self.default_provider) + if let Some(provider_id) = provider_id { + ProviderType::try_from(provider_id)? + } else { + self.default_provider + } }; self.providers - .get(resolved_provider_id) + .get(&resolved_provider_id) .ok_or_else(|| { AppErrorKind::InvalidPayload(format!( "provider `{}` is not enabled", diff --git a/src/providers/test.rs b/src/providers/test.rs index b27774c..137f0fb 100644 --- a/src/providers/test.rs +++ b/src/providers/test.rs @@ -125,7 +125,7 @@ fn constructor() { fn send() { let mut settings = Settings::new().expect("config error"); settings.provider.forcedefault = true; - settings.provider.default = DefaultProvider(String::from("mock")); + settings.provider.default = ProviderType::Mock; let providers = Providers::new(&settings); let result = providers.send("foo", &vec![], None, "bar", "baz", None, Some("ses")); assert!(result.is_ok(), "Providers::send should not have failed"); @@ -134,7 +134,7 @@ fn send() { } settings.provider.forcedefault = false; - settings.provider.default = DefaultProvider(String::from("ses")); + settings.provider.default = ProviderType::Ses; let providers = Providers::new(&settings); let result = providers.send("foo", &vec![], None, "bar", "baz", None, Some("mock")); assert!(result.is_ok(), "Providers::send should not have failed"); diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 188f348..3448818 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -21,7 +21,9 @@ use rocket::config::{ use serde::de::{Deserialize, Deserializer, Error, Unexpected}; use logging::MozlogLogger; -use types::{duration::Duration, email_address::EmailAddress, validate}; +use types::{ + duration::Duration, email_address::EmailAddress, provider::Provider as ProviderType, validate, +}; macro_rules! deserialize_and_validate { ($(#[$docs:meta] ($type:ident, $validator:ident, $expected:expr)),+) => ($( @@ -67,8 +69,6 @@ deserialize_and_validate! { (AwsSecret, aws_secret, "AWS secret key"), /// Base URI type. (BaseUri, base_uri, "base URI"), - /// Default email provider. - (DefaultProvider, provider, "'ses', 'sendgrid', 'socketlabs' or 'smtp'"), /// Env type. (Env, env, "'dev', 'staging', 'production' or 'test'"), /// Host name or IP address type. @@ -172,7 +172,7 @@ pub struct Provider { /// Note that this setting can be overridden /// on a per-request basis /// unless `forcedefault` is `true`. - pub default: DefaultProvider, + pub default: ProviderType, /// Flag indicating whether the default provider should be enforced /// in preference to the per-request `provider` param. diff --git a/src/settings/test.rs b/src/settings/test.rs index aa5e99c..8f5d32c 100644 --- a/src/settings/test.rs +++ b/src/settings/test.rs @@ -131,14 +131,11 @@ fn env_vars_take_precedence() { let current_env = Env(String::from("test")); let hmac_key = String::from("something else"); let provider = Provider { - default: DefaultProvider( - if settings.provider.default == DefaultProvider("ses".to_string()) { - "sendgrid" - } else { - "ses" - } - .to_string(), - ), + default: if settings.provider.default == ProviderType::Ses { + ProviderType::Sendgrid + } else { + ProviderType::Ses + }, forcedefault: !settings.provider.forcedefault, }; let redis_host = format!("{}1", &settings.redis.host); @@ -216,7 +213,7 @@ fn env_vars_take_precedence() { env::set_var("FXA_EMAIL_ENV", ¤t_env.0); env::set_var("FXA_EMAIL_LOG_LEVEL", &log.level.0); env::set_var("FXA_EMAIL_LOG_FORMAT", &log.format.0); - env::set_var("FXA_EMAIL_PROVIDER_DEFAULT", &provider.default.0); + env::set_var("FXA_EMAIL_PROVIDER_DEFAULT", provider.default.as_ref()); env::set_var( "FXA_EMAIL_PROVIDER_FORCEDEFAULT", provider.forcedefault.to_string(), diff --git a/src/types/mod.rs b/src/types/mod.rs index 9d64d73..fd89358 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,4 +10,5 @@ pub mod duration; pub mod email_address; pub mod error; pub mod headers; +pub mod provider; pub mod validate; diff --git a/src/types/provider/mod.rs b/src/types/provider/mod.rs new file mode 100644 index 0000000..1f335c8 --- /dev/null +++ b/src/types/provider/mod.rs @@ -0,0 +1,97 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. + +//! Email provider type. + +#[cfg(test)] +mod test; + +use std::{ + convert::TryFrom, + fmt::{self, Display, Formatter}, +}; + +use serde::{ + de::{Deserialize, Deserializer, Error as SerdeError, Unexpected}, + ser::{Serialize, Serializer}, +}; + +use types::error::{AppError, AppErrorKind}; + +/// Identifies the underlying email provider. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Provider { + Mock, + Sendgrid, + Ses, + Smtp, + SocketLabs, +} + +impl AsRef for Provider { + /// Return the provider as a string slice. + fn as_ref(&self) -> &str { + match *self { + Provider::Mock => "mock", + Provider::Sendgrid => "sendgrid", + Provider::Ses => "ses", + Provider::Smtp => "smtp", + Provider::SocketLabs => "socketlabs", + } + } +} + +impl Default for Provider { + fn default() -> Self { + Provider::Ses + } +} + +impl Display for Provider { + /// Format the provider as a `String`. + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + write!(formatter, "{}", self.as_ref()) + } +} + +impl<'d> Deserialize<'d> for Provider { + /// Deserialize a provider from its string representation. + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'d>, + { + let value: String = Deserialize::deserialize(deserializer)?; + Provider::try_from(value.as_str()) + .map_err(|_| D::Error::invalid_value(Unexpected::Str(&value), &"provider")) + } +} + +impl Serialize for Provider { + /// Serialize a provider. + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_ref()) + } +} + +impl<'v> TryFrom<&'v str> for Provider { + type Error = AppError; + + /// Parse a provider from its string representation. + fn try_from(value: &str) -> Result { + match value { + "mock" => Ok(Provider::Mock), + "sendgrid" => Ok(Provider::Sendgrid), + "ses" => Ok(Provider::Ses), + "smtp" => Ok(Provider::Smtp), + "socketlabs" => Ok(Provider::SocketLabs), + _ => Err(AppErrorKind::InvalidPayload(format!( + "provider `{}`", + value + )))?, + } + } +} diff --git a/src/types/provider/test.rs b/src/types/provider/test.rs new file mode 100644 index 0000000..2ec52c7 --- /dev/null +++ b/src/types/provider/test.rs @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, you can obtain one at https://mozilla.org/MPL/2.0/. + +use super::*; + +#[test] +fn try_from() { + match Provider::try_from("mock") { + Ok(provider) => assert_eq!(provider, Provider::Mock), + Err(error) => assert!(false, error.to_string()), + } + match Provider::try_from("sendgrid") { + Ok(provider) => assert_eq!(provider, Provider::Sendgrid), + Err(error) => assert!(false, error.to_string()), + } + match Provider::try_from("ses") { + Ok(provider) => assert_eq!(provider, Provider::Ses), + Err(error) => assert!(false, error.to_string()), + } + match Provider::try_from("smtp") { + Ok(provider) => assert_eq!(provider, Provider::Smtp), + Err(error) => assert!(false, error.to_string()), + } + match Provider::try_from("socketlabs") { + Ok(provider) => assert_eq!(provider, Provider::SocketLabs), + Err(error) => assert!(false, error.to_string()), + } + match Provider::try_from("wibble") { + Ok(_) => assert!(false, "Provider::try_from should have failed"), + Err(error) => { + assert_eq!(error.to_string(), "Invalid payload: provider `wibble`"); + } + } +} + +#[test] +fn as_ref() { + assert_eq!(Provider::Mock.as_ref(), "mock"); + assert_eq!(Provider::Sendgrid.as_ref(), "sendgrid"); + assert_eq!(Provider::Ses.as_ref(), "ses"); + assert_eq!(Provider::Smtp.as_ref(), "smtp"); + assert_eq!(Provider::SocketLabs.as_ref(), "socketlabs"); +} diff --git a/src/types/validate/mod.rs b/src/types/validate/mod.rs index 471e708..baacbc2 100644 --- a/src/types/validate/mod.rs +++ b/src/types/validate/mod.rs @@ -32,7 +32,6 @@ lazy_static! { static ref HOST_FORMAT: Regex = Regex::new(r"^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*$").unwrap(); static ref LOGGING_LEVEL: Regex = Regex::new(r"^(?:normal|debug|critical|off)$").unwrap(); static ref LOGGING_FORMAT: Regex = Regex::new(r"^(?:mozlog|pretty|null)$").unwrap(); - static ref PROVIDER_FORMAT: Regex = Regex::new(r"^(?:mock|sendgrid|ses|smtp|socketlabs)$").unwrap(); static ref SENDER_NAME_FORMAT: Regex = Regex::new(r"^[A-Za-z0-9-]+(?: [A-Za-z0-9-]+)*$").unwrap(); static ref SENDGRID_API_KEY_FORMAT: Regex = Regex::new("^[A-Za-z0-9._-]+$").unwrap(); @@ -87,11 +86,6 @@ pub fn logging_format(value: &str) -> bool { LOGGING_FORMAT.is_match(value) } -/// Validate an email provider. -pub fn provider(value: &str) -> bool { - PROVIDER_FORMAT.is_match(value) -} - /// Validate a sender name. pub fn sender_name(value: &str) -> bool { SENDER_NAME_FORMAT.is_match(value) diff --git a/src/types/validate/test.rs b/src/types/validate/test.rs index e812b79..60fed42 100644 --- a/src/types/validate/test.rs +++ b/src/types/validate/test.rs @@ -170,23 +170,6 @@ fn invalid_host() { assert_eq!(validate::host("127.0.0.1:25"), false); } -#[test] -fn provider() { - assert!(validate::provider("mock")); - assert!(validate::provider("smtp")); - assert!(validate::provider("ses")); - assert!(validate::provider("sendgrid")); - assert!(validate::provider("socketlabs")); -} - -#[test] -fn invalid_provider() { - assert_eq!(validate::provider("sses"), false); - assert_eq!(validate::provider("sendgrids"), false); - assert_eq!(validate::provider("ses "), false); - assert_eq!(validate::provider(" sendgrid"), false); -} - #[test] fn sender_name() { assert!(validate::sender_name("foo"));