Skip to main content

heddle_api/v2/
spool_settings.rs

1//! Atomic field-mask settings patches. Hosts own CAS and authorization lookup.
2use crate::heddle::api::v1alpha2::SpoolSettings;
3use prost_types::FieldMask;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
6pub enum SpoolSettingsPatchError {
7    #[error("invalid settings mask")]
8    Mask,
9    #[error("settings clearDenied")]
10    ClearDenied,
11}
12
13/// Apply only top-level masked fields; an absent selected message is a clear.
14/// `can_clear` must check permission to read AND clear the current stored value,
15/// including deleted/hidden targets. Replacing either filtered reference also
16/// removes its current value and requires this permission. An equal reference
17/// does not remove it. This is independent of validating the new target.
18/// The host must authorize the administrator, validate set references and the
19/// resulting audience/review policy, and bind the stored-value check and update
20/// to the same expected_version, with live authorization at atomic commit.
21pub fn apply_spool_settings_patch(
22    current: &SpoolSettings,
23    patch: Option<&SpoolSettings>,
24    mask: Option<&FieldMask>,
25    mut can_clear: impl FnMut(&str) -> bool,
26) -> Result<SpoolSettings, SpoolSettingsPatchError> {
27    let mut result = current.clone();
28    let empty = SpoolSettings::default();
29    let patch = patch.unwrap_or(&empty);
30    let Some(mask) = mask else { return Ok(result) };
31    for (index, path) in mask.paths.iter().enumerate() {
32        if mask.paths[..index].contains(path) {
33            return Err(SpoolSettingsPatchError::Mask);
34        }
35        macro_rules! assign {
36            ($field:ident, $replacing:expr) => {{
37                if (patch.$field == empty.$field || $replacing) && !can_clear(path) {
38                    return Err(SpoolSettingsPatchError::ClearDenied);
39                }
40                result.$field = patch.$field.to_owned();
41            }};
42        }
43        match path.as_str() {
44            "audience" => assign!(audience, false),
45            "default_state_audience" => assign!(default_state_audience, false),
46            "description" => assign!(description, false),
47            "allow_child_creation" => assign!(allow_child_creation, false),
48            "require_review_to_land" => assign!(require_review_to_land, false),
49            "abandoned_thread_retention" => assign!(abandoned_thread_retention, false),
50            "default_review_policy" => assign!(
51                default_review_policy,
52                current.default_review_policy.is_some()
53                    && patch.default_review_policy != current.default_review_policy
54            ),
55            "hold_lifecycle" => assign!(hold_lifecycle, false),
56            "blocking_discussion_resolve_rule" => assign!(blocking_discussion_resolve_rule, false),
57            "default_thread" => assign!(
58                default_thread,
59                current.default_thread.is_some() && patch.default_thread != current.default_thread
60            ),
61            _ => return Err(SpoolSettingsPatchError::Mask),
62        }
63    }
64    Ok(result)
65}