Skip to main content

ephemeris_forge_forgejo/plan/
webhooks.rs

1use ephemeris_forge::{Collection, FieldDiff, NameSet, Report};
2
3use crate::Live;
4use crate::api::model::Hook;
5use crate::api::request::{CreateHookOption, EditHookOption};
6use crate::config::{Webhook, Webhooks};
7use crate::operation::webhooks::Operation;
8
9pub(super) fn plan(config: Option<&Webhooks>, live: &Live, report: &mut Report) -> Vec<Operation> {
10    let Some(config) = config else {
11        return Vec::new();
12    };
13    Collection {
14        noun: "webhook",
15        mode: config.mode,
16    }
17    .plan(
18        &config.webhooks,
19        &live.webhooks,
20        report,
21        |hook, url| hook.url.as_deref() == Some(url),
22        |url, desired, actual, _| {
23            let mut diff = FieldDiff::default();
24            let kind_differs = diff.check(
25                "type",
26                actual.is_some_and(|hook| hook.kind.as_deref() == Some(desired.kind.as_str())),
27                &actual.and_then(|hook| hook.kind.as_deref()),
28                &desired.kind.as_str(),
29            );
30            let recreate = kind_differs && actual.is_some();
31            let edit = compare(desired, actual, &mut diff);
32            let operations = match actual {
33                Some(hook) if recreate => vec![
34                    Operation::Delete {
35                        id: hook.id,
36                        url: url.to_owned(),
37                    },
38                    create(url, desired),
39                ],
40                Some(hook) if !edit.is_empty() => vec![Operation::Patch {
41                    id: hook.id,
42                    url: url.to_owned(),
43                    edit: with_kept_fields(edit, desired, hook),
44                }],
45                Some(_) => Vec::new(),
46                None => vec![create(url, desired)],
47            };
48            (diff, operations)
49        },
50        |hook, _| {
51            let url = hook.url.clone().unwrap_or_else(|| format!("#{}", hook.id));
52            Some((url.clone(), Operation::Delete { id: hook.id, url }))
53        },
54    )
55}
56
57fn create(url: &str, desired: &Webhook) -> Operation {
58    Operation::Create {
59        url: url.to_owned(),
60        body: CreateHookOption::new(url, desired),
61    }
62}
63
64/// The edit for the fields that differ, ignoring the kind.
65fn compare(desired: &Webhook, actual: Option<&Hook>, diff: &mut FieldDiff) -> EditHookOption {
66    let mut edit = EditHookOption::default();
67    let content_type = desired.content_type.as_str().to_owned();
68    let live_content_type = actual.and_then(|hook| hook.content_type.clone());
69    if let Some(wanted) = diff.want("content_type", &content_type, live_content_type.as_ref()) {
70        edit.config = Some([("content_type".to_owned(), wanted)].into());
71    }
72    let events = actual.map(|hook| NameSet::new(hook.events.clone().unwrap_or_default()));
73    edit.events = diff
74        .want_set(
75            "events",
76            &NameSet::new(desired.events.clone()),
77            events.as_ref(),
78        )
79        .map(NameSet::into_vec);
80    let active = actual.map(|hook| hook.active.unwrap_or(false));
81    edit.active = diff.want("active", &desired.active, active.as_ref());
82    let branch_filter = actual.map(|hook| hook.branch_filter.clone().unwrap_or_default());
83    edit.branch_filter = diff.want(
84        "branch_filter",
85        &desired.branch_filter,
86        branch_filter.as_ref(),
87    );
88    edit
89}
90
91/// Forgejo resets the events, the branch filter and the authorization header
92/// on every edit, so the edit repeats the ones it does not change.
93fn with_kept_fields(mut edit: EditHookOption, desired: &Webhook, actual: &Hook) -> EditHookOption {
94    edit.events.get_or_insert_with(|| desired.events.clone());
95    edit.branch_filter
96        .get_or_insert_with(|| desired.branch_filter.clone());
97    edit.authorization_header = actual
98        .authorization_header
99        .clone()
100        .filter(|header| !header.is_empty());
101    edit
102}