-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Events] Add project notification settings
ghstack-source-id: 2452ad00e890fa87497a785348ac29808eec1a40 Pull Request resolved: #18 This PR adds a new model for project notifications. The semantics of the model is explained in `notifications.proto`. 1. Adds a the model to the db model in metadata_svc and exposed it in the project store. 2. Expose setters/getters to notification settings in metadata svc. 3. Expose APIs to manipulate the notification settings. I opted into having a single endpoint for updating the notification setting json for convenience. Later, we can also add this to the CLI. Test plan: ``` ~/repos/cronback/cronback ❯❯❯ cargo cli --localhost --secret-token adminkey admin projects create ✘ 130 Project 'prj_026601H699SE7VY0YEFHRXXE75117B' was created successfully. ~/repos/cronback/cronback ❯❯❯ http -b --auth adminkey --auth-type bearer POST http://localhost:8888/v1/admin/projects/prj_026601H699SE7VY0YEFHRXXE75117B/notification_settings @examples/notifications.json ~/repos/cronback/cronback ❯❯❯ http -b --auth adminkey --auth-type bearer GET http://localhost:8888/v1/admin/projects/prj_026601H699SE7VY0YEFHRXXE75117B/notification_settings { "channels": { "email": { "address": "[email protected]", "type": "email", "verified": false } }, "subscriptions": [ { "channel_names": [ "email" ], "event": { "type": "on_run_failure" } } ] } ```
- Loading branch information
1 parent
060ba8b
commit a24ed9f
Showing
18 changed files
with
694 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
mod api_keys; | ||
mod notifications; | ||
mod projects; | ||
|
||
pub use api_keys::*; | ||
pub use notifications::*; | ||
pub use projects::*; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
use std::collections::HashMap; | ||
|
||
#[cfg(feature = "dto")] | ||
use dto::{FromProto, IntoProto}; | ||
use monostate::MustBe; | ||
use serde::{Deserialize, Serialize}; | ||
#[cfg(feature = "validation")] | ||
use validator::Validate; | ||
|
||
#[cfg(feature = "validation")] | ||
use crate::validation_util::validation_error; | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
#[cfg_attr( | ||
feature = "dto", | ||
derive(IntoProto, FromProto), | ||
proto(target = "proto::notifications::ProjectNotificationSettings") | ||
)] | ||
#[cfg_attr( | ||
feature = "validation", | ||
derive(Validate), | ||
validate(schema( | ||
function = "validate_settings", | ||
skip_on_field_errors = false | ||
)) | ||
)] | ||
#[serde(deny_unknown_fields)] | ||
pub struct NotificationSettings { | ||
#[cfg_attr(feature = "validation", validate)] | ||
pub default_subscriptions: Vec<NotificationSubscription>, | ||
// The key of the hashmap is the channel name | ||
#[cfg_attr(feature = "validation", validate)] | ||
pub channels: HashMap<String, NotificationChannel>, | ||
} | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
#[cfg_attr( | ||
feature = "dto", | ||
derive(IntoProto, FromProto), | ||
proto(target = "proto::notifications::NotificationSubscription") | ||
)] | ||
#[cfg_attr(feature = "validation", derive(Validate))] | ||
#[serde(deny_unknown_fields)] | ||
pub struct NotificationSubscription { | ||
#[cfg_attr(feature = "validation", validate(length(max = 20)))] | ||
pub channel_names: Vec<String>, | ||
#[cfg_attr(feature = "dto", proto(required))] | ||
#[cfg_attr(feature = "validation", validate)] | ||
pub event: NotificationEvent, | ||
} | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
#[cfg_attr( | ||
feature = "dto", | ||
derive(IntoProto, FromProto), | ||
proto( | ||
target = "proto::notifications::NotificationChannel", | ||
oneof = "channel" | ||
) | ||
)] | ||
#[serde(rename_all = "snake_case")] | ||
#[serde(untagged)] | ||
pub enum NotificationChannel { | ||
Email(EmailNotification), | ||
} | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
#[cfg_attr( | ||
feature = "dto", | ||
derive(IntoProto, FromProto), | ||
proto(target = "proto::notifications::NotificationEvent", oneof = "event") | ||
)] | ||
#[serde(rename_all = "snake_case")] | ||
#[serde(untagged)] | ||
pub enum NotificationEvent { | ||
OnRunFailure(OnRunFailure), | ||
} | ||
|
||
// Channel configs | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
#[cfg_attr( | ||
feature = "dto", | ||
derive(IntoProto, FromProto), | ||
proto(target = "proto::notifications::Email") | ||
)] | ||
#[serde(deny_unknown_fields)] | ||
#[cfg_attr(feature = "validation", derive(Validate))] | ||
pub struct EmailNotification { | ||
#[serde(rename = "type")] | ||
_kind: MustBe!("email"), | ||
#[cfg_attr(feature = "validation", validate(email))] | ||
pub address: String, | ||
pub verified: bool, | ||
} | ||
|
||
// Subscription configs | ||
|
||
#[derive(Clone, Debug, Serialize, Deserialize)] | ||
#[cfg_attr( | ||
feature = "dto", | ||
derive(IntoProto, FromProto), | ||
proto(target = "proto::notifications::OnRunFailure") | ||
)] | ||
#[cfg_attr(feature = "validation", derive(Validate))] | ||
#[serde(deny_unknown_fields)] | ||
pub struct OnRunFailure { | ||
#[serde(rename = "type")] | ||
_kind: MustBe!("on_run_failure"), | ||
} | ||
|
||
#[cfg(feature = "validation")] | ||
impl Validate for NotificationEvent { | ||
fn validate(&self) -> Result<(), validator::ValidationErrors> { | ||
match self { | ||
| NotificationEvent::OnRunFailure(o) => o.validate(), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(feature = "validation")] | ||
impl Validate for NotificationChannel { | ||
fn validate(&self) -> Result<(), validator::ValidationErrors> { | ||
match self { | ||
| NotificationChannel::Email(e) => e.validate(), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(feature = "validation")] | ||
fn validate_settings( | ||
settings: &NotificationSettings, | ||
) -> Result<(), validator::ValidationError> { | ||
// Validate that any channel referenced in a subscription actually exists. | ||
|
||
for sub in &settings.default_subscriptions { | ||
for channel in &sub.channel_names { | ||
if !settings.channels.contains_key(channel) { | ||
return Err(validation_error( | ||
"invalid_channel_name", | ||
format!( | ||
"Channel name '{}' is not configured in channel list", | ||
channel | ||
), | ||
)); | ||
} | ||
} | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::collections::HashMap; | ||
|
||
use super::*; | ||
|
||
#[test] | ||
fn test_valid_settings() -> anyhow::Result<()> { | ||
let email = EmailNotification { | ||
_kind: Default::default(), | ||
address: "[email protected]".to_string(), | ||
verified: true, | ||
}; | ||
let mut channels = HashMap::new(); | ||
channels.insert("email".to_string(), NotificationChannel::Email(email)); | ||
let setting = NotificationSettings { | ||
channels, | ||
default_subscriptions: vec![NotificationSubscription { | ||
channel_names: vec!["email".to_string()], | ||
event: NotificationEvent::OnRunFailure(OnRunFailure { | ||
_kind: Default::default(), | ||
}), | ||
}], | ||
}; | ||
|
||
setting.validate()?; | ||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn test_invalid_email() -> anyhow::Result<()> { | ||
let email = EmailNotification { | ||
_kind: Default::default(), | ||
address: "wrong_email".to_string(), | ||
verified: false, | ||
}; | ||
let mut channels = HashMap::new(); | ||
channels.insert("email".to_string(), NotificationChannel::Email(email)); | ||
let setting = NotificationSettings { | ||
channels, | ||
default_subscriptions: vec![], | ||
}; | ||
|
||
let validated = setting.validate(); | ||
|
||
assert!(validated.is_err()); | ||
assert_eq!( | ||
validated.unwrap_err().to_string(), | ||
"channels[0].address: Validation error: email [{\"value\": \ | ||
String(\"wrong_email\")}]" | ||
.to_string() | ||
); | ||
|
||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn test_invalid_channel() { | ||
let email = EmailNotification { | ||
_kind: Default::default(), | ||
address: "[email protected]".to_string(), | ||
verified: false, | ||
}; | ||
let mut channels = HashMap::new(); | ||
channels.insert("email".to_string(), NotificationChannel::Email(email)); | ||
let setting = NotificationSettings { | ||
channels, | ||
default_subscriptions: vec![NotificationSubscription { | ||
channel_names: vec![ | ||
"email".to_string(), | ||
"wrong_channel".to_string(), | ||
], | ||
event: NotificationEvent::OnRunFailure(OnRunFailure { | ||
_kind: Default::default(), | ||
}), | ||
}], | ||
}; | ||
|
||
let validated = setting.validate(); | ||
|
||
assert!(validated.is_err()); | ||
assert_eq!( | ||
validated.unwrap_err().to_string(), | ||
"__all__: Channel name 'wrong_channel' is not configured in \ | ||
channel list" | ||
.to_string() | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.