Skip to content

Commit 5a11e6f

Browse files
committed
feat(notify-zulip): support notify-zulip.<label> getting mapped to multiple actions
1 parent 527bf21 commit 5a11e6f

File tree

2 files changed

+145
-60
lines changed

2 files changed

+145
-60
lines changed

src/config.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,51 @@ pub(crate) struct AutolabelLabelConfig {
265265
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
266266
pub(crate) struct NotifyZulipConfig {
267267
#[serde(flatten)]
268-
pub(crate) labels: HashMap<String, NotifyZulipLabelConfig>,
268+
pub(crate) labels: HashMap<String, NotifyZulipTablesConfig>,
269+
}
270+
271+
#[derive(PartialEq, Eq, Debug)]
272+
pub(crate) struct NotifyZulipTablesConfig {
273+
pub(crate) subtables: HashMap<String, NotifyZulipLabelConfig>,
274+
}
275+
276+
impl<'de> serde::Deserialize<'de> for NotifyZulipTablesConfig {
277+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
278+
where
279+
D: serde::Deserializer<'de>,
280+
{
281+
use serde::de::Error;
282+
use toml::Value;
283+
284+
// Deserialize into a toml::value::Table for dynamic inspection
285+
let table = toml::Value::deserialize(deserializer)?
286+
.as_table()
287+
.cloned()
288+
.ok_or_else(|| Error::custom("expected a TOML table"))?;
289+
290+
let mut subtables = HashMap::new();
291+
let mut direct_fields = toml::value::Table::new();
292+
293+
for (k, v) in table {
294+
if let Some(subtable) = v.as_table() {
295+
// This is a subtable; deserialize as NotifyZulipLabelConfig
296+
let sub = NotifyZulipLabelConfig::deserialize(Value::Table(subtable.clone()))
297+
.map_err(Error::custom)?;
298+
subtables.insert(k, sub);
299+
} else {
300+
// This is a direct field; collect for the "" entry
301+
direct_fields.insert(k, v);
302+
}
303+
}
304+
305+
if !direct_fields.is_empty() {
306+
let direct = NotifyZulipLabelConfig::deserialize(Value::Table(direct_fields))
307+
.map_err(Error::custom)?;
308+
subtables.insert("".to_string(), direct);
309+
}
310+
311+
Ok(NotifyZulipTablesConfig { subtables })
312+
}
269313
}
270314

271315
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]

src/handlers/notify_zulip.rs

Lines changed: 100 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
config::{NotifyZulipConfig, NotifyZulipLabelConfig},
2+
config::{NotifyZulipConfig, NotifyZulipLabelConfig, NotifyZulipTablesConfig},
33
github::{Issue, IssuesAction, IssuesEvent, Label},
44
handlers::Context,
55
};
@@ -12,6 +12,9 @@ pub(super) struct NotifyZulipInput {
1212
/// For example, if an `I-prioritize` issue is closed,
1313
/// this field will be `I-prioritize`.
1414
label: Label,
15+
/// List of strings for tables such as [notify-zulip."beta-nominated"]
16+
/// and/or [notify-zulip."beta-nominated".compiler]
17+
include_config_names: Vec<String>,
1518
}
1619

1720
pub(super) enum NotificationType {
@@ -52,26 +55,40 @@ pub(super) async fn parse_input(
5255
fn parse_label_change_input(
5356
event: &IssuesEvent,
5457
label: Label,
55-
config: &NotifyZulipLabelConfig,
58+
config: &NotifyZulipTablesConfig,
5659
) -> Option<NotifyZulipInput> {
57-
if !has_all_required_labels(&event.issue, config) {
58-
// Issue misses a required label, ignore this event
60+
let mut include_config_names: Vec<String> = vec![];
61+
62+
for (name, label_config) in &config.subtables {
63+
if has_all_required_labels(&event.issue, &label_config) {
64+
match event.action {
65+
IssuesAction::Labeled { .. } if !label_config.messages_on_add.is_empty() => {
66+
include_config_names.push(name.to_string());
67+
}
68+
IssuesAction::Unlabeled { .. } if !label_config.messages_on_remove.is_empty() => {
69+
include_config_names.push(name.to_string());
70+
}
71+
_ => (),
72+
}
73+
}
74+
}
75+
76+
if include_config_names.is_empty() {
77+
// It seems that there is no match between this event and any notify-zulip config, ignore this event
5978
return None;
6079
}
6180

6281
match event.action {
63-
IssuesAction::Labeled { .. } if !config.messages_on_add.is_empty() => {
64-
Some(NotifyZulipInput {
65-
notification_type: NotificationType::Labeled,
66-
label,
67-
})
68-
}
69-
IssuesAction::Unlabeled { .. } if !config.messages_on_remove.is_empty() => {
70-
Some(NotifyZulipInput {
71-
notification_type: NotificationType::Unlabeled,
72-
label,
73-
})
74-
}
82+
IssuesAction::Labeled { .. } => Some(NotifyZulipInput {
83+
notification_type: NotificationType::Labeled,
84+
label,
85+
include_config_names,
86+
}),
87+
IssuesAction::Unlabeled { .. } => Some(NotifyZulipInput {
88+
notification_type: NotificationType::Unlabeled,
89+
label,
90+
include_config_names,
91+
}),
7592
_ => None,
7693
}
7794
}
@@ -92,24 +109,38 @@ fn parse_close_reopen_input(
92109
.map(|config| (label, config))
93110
})
94111
.flat_map(|(label, config)| {
95-
if !has_all_required_labels(&event.issue, config) {
96-
// Issue misses a required label, ignore this event
112+
let mut include_config_names: Vec<String> = vec![];
113+
114+
for (name, label_config) in &config.subtables {
115+
if has_all_required_labels(&event.issue, &label_config) {
116+
match event.action {
117+
IssuesAction::Closed if !label_config.messages_on_close.is_empty() => {
118+
include_config_names.push(name.to_string());
119+
}
120+
IssuesAction::Reopened if !label_config.messages_on_reopen.is_empty() => {
121+
include_config_names.push(name.to_string());
122+
}
123+
_ => (),
124+
}
125+
}
126+
}
127+
128+
if include_config_names.is_empty() {
129+
// It seems that there is no match between this event and any notify-zulip config, ignore this event
97130
return None;
98131
}
99132

100133
match event.action {
101-
IssuesAction::Closed if !config.messages_on_close.is_empty() => {
102-
Some(NotifyZulipInput {
103-
notification_type: NotificationType::Closed,
104-
label,
105-
})
106-
}
107-
IssuesAction::Reopened if !config.messages_on_reopen.is_empty() => {
108-
Some(NotifyZulipInput {
109-
notification_type: NotificationType::Reopened,
110-
label,
111-
})
112-
}
134+
IssuesAction::Closed => Some(NotifyZulipInput {
135+
notification_type: NotificationType::Closed,
136+
label,
137+
include_config_names,
138+
}),
139+
IssuesAction::Reopened => Some(NotifyZulipInput {
140+
notification_type: NotificationType::Reopened,
141+
label,
142+
include_config_names,
143+
}),
113144
_ => None,
114145
}
115146
})
@@ -140,41 +171,51 @@ pub(super) async fn handle_input<'a>(
140171
inputs: Vec<NotifyZulipInput>,
141172
) -> anyhow::Result<()> {
142173
for input in inputs {
143-
let config = &config.labels[&input.label.name];
144-
145-
let topic = &config.topic;
146-
let topic = topic.replace("{number}", &event.issue.number.to_string());
147-
let mut topic = topic.replace("{title}", &event.issue.title);
148-
// Truncate to 60 chars (a Zulip limitation)
149-
let mut chars = topic.char_indices().skip(59);
150-
if let (Some((len, _)), Some(_)) = (chars.next(), chars.next()) {
151-
topic.truncate(len);
152-
topic.push('…');
174+
let tables_config = &config.labels[&input.label.name];
175+
176+
// Get valid label configs
177+
let mut label_configs: Vec<&NotifyZulipLabelConfig> = vec![];
178+
for name in input.include_config_names {
179+
label_configs.push(&tables_config.subtables[&name]);
153180
}
154181

155-
let msgs = match input.notification_type {
156-
NotificationType::Labeled => &config.messages_on_add,
157-
NotificationType::Unlabeled => &config.messages_on_remove,
158-
NotificationType::Closed => &config.messages_on_close,
159-
NotificationType::Reopened => &config.messages_on_reopen,
160-
};
182+
for label_config in label_configs {
183+
let config = label_config;
161184

162-
let recipient = crate::zulip::Recipient::Stream {
163-
id: config.zulip_stream,
164-
topic: &topic,
165-
};
185+
let topic = &config.topic;
186+
let topic = topic.replace("{number}", &event.issue.number.to_string());
187+
let mut topic = topic.replace("{title}", &event.issue.title);
188+
// Truncate to 60 chars (a Zulip limitation)
189+
let mut chars = topic.char_indices().skip(59);
190+
if let (Some((len, _)), Some(_)) = (chars.next(), chars.next()) {
191+
topic.truncate(len);
192+
topic.push('…');
193+
}
194+
195+
let msgs = match input.notification_type {
196+
NotificationType::Labeled => &config.messages_on_add,
197+
NotificationType::Unlabeled => &config.messages_on_remove,
198+
NotificationType::Closed => &config.messages_on_close,
199+
NotificationType::Reopened => &config.messages_on_reopen,
200+
};
166201

167-
for msg in msgs {
168-
let msg = msg.replace("{number}", &event.issue.number.to_string());
169-
let msg = msg.replace("{title}", &event.issue.title);
170-
let msg = replace_team_to_be_nominated(&event.issue.labels, msg);
202+
let recipient = crate::zulip::Recipient::Stream {
203+
id: config.zulip_stream,
204+
topic: &topic,
205+
};
171206

172-
crate::zulip::MessageApiRequest {
173-
recipient,
174-
content: &msg,
207+
for msg in msgs {
208+
let msg = msg.replace("{number}", &event.issue.number.to_string());
209+
let msg = msg.replace("{title}", &event.issue.title);
210+
let msg = replace_team_to_be_nominated(&event.issue.labels, msg);
211+
212+
crate::zulip::MessageApiRequest {
213+
recipient,
214+
content: &msg,
215+
}
216+
.send(&ctx.github.raw())
217+
.await?;
175218
}
176-
.send(&ctx.github.raw())
177-
.await?;
178219
}
179220
}
180221

0 commit comments

Comments
 (0)