Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle src cob patch actions.rs
//! Keep track of the patch [`Action`] versions, to ensure compatibility where
//! possible.
//!
//! [`Action`]: super::Action

use serde::{Deserialize, Serialize};

use crate::cob::{ActorId, Embed, Label, Timestamp, Uri, thread::Edit};

use super::{Error, Patch, ReviewId, Verdict, lookup};

/// A review edit that keeps track of the different versions of actions.
///
/// [`ReviewEdit::new`] will create the latest version of the action.
///
/// [`ReviewEdit::run`] will apply the action to the given [`Patch`].
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ReviewEdit {
    /// The initial version of editing a review.
    ///
    /// This allowed editing the `summary`, `verdict`, and `labels` of a
    /// [`Patch`], where the `summary` value was optional.
    #[serde(rename = "review.edit")]
    V1(ReviewEditV1),
    /// The latest version of editing a review.
    ///
    /// This allows editing the `summary`, `verdict`, `labels` of [`Patch`], and
    /// introduces `embeds` to the review summary.
    ///
    /// The `summary` of a [`super::Review`] is now an edit-history.
    #[serde(rename = "review.edit.v2")]
    V2(ReviewEditV2),
}

impl ReviewEdit {
    /// Create the latest version of [`ReviewEdit`].
    pub fn new(
        review: ReviewId,
        summary: String,
        verdict: Option<Verdict>,
        labels: Vec<Label>,
        embeds: Vec<Embed<Uri>>,
    ) -> Self {
        Self::V2(ReviewEditV2 {
            review,
            summary,
            verdict,
            labels,
            embeds,
        })
    }

    /// Get the [`ReviewId`] that this edit is applying to.
    pub fn review_id(&self) -> &ReviewId {
        match self {
            ReviewEdit::V1(ReviewEditV1 { review, .. }) => review,
            ReviewEdit::V2(ReviewEditV2 { review, .. }) => review,
        }
    }

    /// Get the summary of the [`ReviewEdit`].
    ///
    /// The summary was optional in the first version, so it may be `None`.
    pub fn summary(&self) -> Option<&String> {
        match self {
            ReviewEdit::V1(ReviewEditV1 { summary, .. }) => summary.as_ref(),
            ReviewEdit::V2(ReviewEditV2 { summary, .. }) => Some(summary),
        }
    }

    /// Get the [`Verdict`] of the [`ReviewEdit`].
    pub fn verdict(&self) -> Option<&Verdict> {
        match self {
            ReviewEdit::V1(ReviewEditV1 { verdict, .. }) => verdict.as_ref(),
            ReviewEdit::V2(ReviewEditV2 { verdict, .. }) => verdict.as_ref(),
        }
    }

    /// Get the list of [`Label`]s of the [`ReviewEdit`].
    pub fn labels(&self) -> &[Label] {
        match self {
            ReviewEdit::V1(ReviewEditV1 { labels, .. }) => labels,
            ReviewEdit::V2(ReviewEditV2 { labels, .. }) => labels,
        }
    }

    /// Get the [`Embed`]s of the [`ReviewEdit`].
    ///
    /// [`Embed`]s were introduced in the second version of edits. For this
    /// reason, an [`Option`] is returned instead of a [`Vec`] – this allows to
    /// avoid an unnecessary clone of the [`Vec`] when it is present.
    pub fn embeds(&self) -> Option<&Vec<Embed<Uri>>> {
        match self {
            ReviewEdit::V1(_) => None,
            ReviewEdit::V2(ReviewEditV2 { embeds, .. }) => Some(embeds),
        }
    }

    /// Apply the action to the given [`Patch`].
    pub fn run(
        self,
        author: ActorId,
        timestamp: Timestamp,
        patch: &mut Patch,
    ) -> Result<(), Error> {
        match self {
            ReviewEdit::V1(ReviewEditV1 {
                review,
                summary,
                verdict,
                labels,
            }) => {
                if summary.is_none() && verdict.is_none() {
                    return Err(Error::EmptyReview);
                }
                let Some(review) = lookup::review_mut(patch, &review)? else {
                    return Ok(());
                };

                if let Some(body) = summary {
                    review
                        .summary
                        .push(Edit::new(author, body, timestamp, vec![]));
                }
                review.verdict = verdict;
                review.labels = labels;
                Ok(())
            }
            ReviewEdit::V2(ReviewEditV2 {
                review,
                summary,
                verdict,
                labels,
                embeds,
            }) => {
                if summary.is_empty() && verdict.is_none() {
                    return Err(Error::EmptyReview);
                }
                let Some(review) = lookup::review_mut(patch, &review)? else {
                    return Ok(());
                };

                review
                    .summary
                    .push(Edit::new(author, summary, timestamp, embeds));
                review.verdict = verdict;
                review.labels = labels;
                Ok(())
            }
        }
    }
}

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReviewEditV2 {
    review: ReviewId,
    #[serde(default, skip_serializing_if = "String::is_empty")]
    summary: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    verdict: Option<Verdict>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    labels: Vec<Label>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    embeds: Vec<Embed<Uri>>,
}

#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReviewEditV1 {
    review: ReviewId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    summary: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    verdict: Option<Verdict>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    labels: Vec<Label>,
}

#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod test {
    use serde_json::json;

    use crate::patch;

    use super::ReviewEdit;

    #[test]
    fn test_review_edit() {
        let v1 = json!({
            "type": "review.edit",
            "review": "89d45fb371eb2622ba88188d474347cc526d80bb",
            "summary": "lgtm",
            "verdict": "accept",
            "labels": [],
        });
        let v2 = json!({
            "type": "review.edit.v2",
            "review": "89d45fb371eb2622ba88188d474347cc526d80bb",
            "summary": "lgtm",
            "verdict": "accept",
            "labels": [],
            "embeds": [],
        });
        serde_json::from_value::<ReviewEdit>(v1.clone()).unwrap();
        serde_json::from_value::<ReviewEdit>(v2.clone()).unwrap();
        assert!(matches!(
            serde_json::from_value::<patch::Action>(v1).unwrap(),
            patch::Action::ReviewEdit { .. }
        ));
        assert!(matches!(
            serde_json::from_value::<patch::Action>(v2).unwrap(),
            patch::Action::ReviewEdit { .. }
        ));
    }
}