| + |
//! Representation of a [`patch::Review`] for deserializing.
|
| + |
//!
|
| + |
//! The [`Review`] type contains fields that are:
|
| + |
//!
|
| + |
//! - Shared within previous versions, e.g. V1 fields that are shared with V2
|
| + |
//! - Introduced at a later version, but can use a [`Default`] instance
|
| + |
//! - Can be migrated from one version to another
|
| + |
//!
|
| + |
//! [`patch::Review`]: crate::cob::patch::Review
|
| + |
|
| + |
use nonempty::NonEmpty;
|
| + |
use serde::Deserialize;
|
| + |
use serde_untagged::UntaggedEnumVisitor;
|
| + |
|
| + |
use crate::cob::patch;
|
| + |
use crate::cob::patch::{
|
| + |
Author, CodeLocation, Comment, Edit, Label, Reactions, ReviewId, Thread, Timestamp, Verdict,
|
| + |
};
|
| + |
|
| + |
/// The encoding for a `patch::Review` that can be deserialized and migrated.
|
| + |
///
|
| + |
/// To maintain backwards-compatibility, [`Review`] must implement:
|
| + |
/// ```rust, no_run
|
| + |
/// From<Review> for patch::Review
|
| + |
/// ```
|
| + |
#[derive(Deserialize)]
|
| + |
pub(in crate::cob::patch) struct Review {
|
| + |
// V1 fields
|
| + |
id: ReviewId,
|
| + |
author: Author,
|
| + |
verdict: Option<Verdict>,
|
| + |
comments: Thread<Comment<CodeLocation>>,
|
| + |
labels: Vec<Label>,
|
| + |
timestamp: Timestamp,
|
| + |
|
| + |
// V2 fields
|
| + |
#[serde(default)]
|
| + |
reactions: Reactions,
|
| + |
|
| + |
// V1 -> V2 conversion
|
| + |
#[serde(default)]
|
| + |
summary: Summary,
|
| + |
}
|
| + |
|
| + |
/// The [`Summary`] type represents the different versions of the `summary`
|
| + |
/// field of a [`Review`].
|
| + |
///
|
| + |
/// The `V1` variant holds an `Option<String>`, which can be converted into an
|
| + |
/// `Edit` given the `ActorId` and `Timestamp` – supplying an empty `Vec` of
|
| + |
/// `Embed`s.
|
| + |
///
|
| + |
/// The `V2` variant holds the current type `NonEmpty<Edit>`.
|
| + |
///
|
| + |
/// Using [`Summary::into_edits`], we can get the latest representation of a
|
| + |
/// [`patch::Review::summary`].
|
| + |
///
|
| + |
/// [`patch::Review::summary`]: crate::cob::patch::Review::summary
|
| + |
#[derive(Debug, PartialEq, Eq)]
|
| + |
pub(in crate::cob::patch) enum Summary {
|
| + |
V2(NonEmpty<Edit>),
|
| + |
V1(Option<String>),
|
| + |
}
|
| + |
|
| + |
impl<'de> Deserialize<'de> for Summary {
|
| + |
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
| + |
where
|
| + |
D: serde::Deserializer<'de>,
|
| + |
{
|
| + |
UntaggedEnumVisitor::new()
|
| + |
.unit(|| Ok(Self::V1(None)))
|
| + |
.string(|body| Ok(Self::V1(Some(body.to_owned()))))
|
| + |
.seq(|edits| edits.deserialize().map(Self::V2))
|
| + |
.deserialize(deserializer)
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl Default for Summary {
|
| + |
fn default() -> Self {
|
| + |
Self::V1(None)
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl Summary {
|
| + |
fn into_edits(self, author: &Author, timestamp: &Timestamp) -> NonEmpty<Edit> {
|
| + |
match self {
|
| + |
Summary::V1(summary) => NonEmpty::new(Edit::new(
|
| + |
*author.public_key(),
|
| + |
summary.unwrap_or_default(),
|
| + |
*timestamp,
|
| + |
vec![],
|
| + |
)),
|
| + |
Summary::V2(edits) => edits,
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
|
| + |
impl From<Review> for patch::Review {
|
| + |
fn from(review: Review) -> Self {
|
| + |
let Review {
|
| + |
id,
|
| + |
author,
|
| + |
verdict,
|
| + |
comments,
|
| + |
labels,
|
| + |
timestamp,
|
| + |
reactions,
|
| + |
summary,
|
| + |
} = review;
|
| + |
let summary = summary.into_edits(&author, ×tamp);
|
| + |
Self {
|
| + |
id,
|
| + |
author,
|
| + |
verdict,
|
| + |
summary,
|
| + |
comments,
|
| + |
labels,
|
| + |
reactions,
|
| + |
timestamp,
|
| + |
}
|
| + |
}
|
| + |
}
|
| + |
|
| + |
#[allow(clippy::unwrap_used)]
|
| + |
#[cfg(test)]
|
| + |
mod test {
|
| + |
use nonempty::nonempty;
|
| + |
use serde_json::json;
|
| + |
|
| + |
use crate::{
|
| + |
cob::{thread::Edit, Timestamp},
|
| + |
patch,
|
| + |
};
|
| + |
|
| + |
use super::{Review, Summary};
|
| + |
|
| + |
#[test]
|
| + |
fn test_review_summary() {
|
| + |
let summary_null = json!(null);
|
| + |
let summary_string = json!("lgtm");
|
| + |
let summary_edits = json!([{
|
| + |
"author": "z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
|
| + |
"timestamp": 1710947885000_i64,
|
| + |
"body": "lgtm",
|
| + |
"embeds": [],
|
| + |
}]);
|
| + |
assert_eq!(
|
| + |
serde_json::from_value::<Summary>(summary_null).unwrap(),
|
| + |
Summary::V1(None)
|
| + |
);
|
| + |
assert_eq!(
|
| + |
serde_json::from_value::<Summary>(summary_string).unwrap(),
|
| + |
Summary::V1(Some("lgtm".to_string()))
|
| + |
);
|
| + |
assert_eq!(
|
| + |
serde_json::from_value::<Summary>(summary_edits).unwrap(),
|
| + |
Summary::V2(nonempty![Edit::new(
|
| + |
"z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx"
|
| + |
.parse()
|
| + |
.unwrap(),
|
| + |
"lgtm".to_string(),
|
| + |
Timestamp::from_secs(1710947885),
|
| + |
vec![]
|
| + |
)])
|
| + |
);
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn test_review_deserialize_summary_migration_null_summary() {
|
| + |
let review = json!({
|
| + |
"id": "89d45fb371eb2622ba88188d474347cc526d80bb",
|
| + |
"author": { "id": "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx" },
|
| + |
"verdict": "accept",
|
| + |
"summary": null,
|
| + |
"comments": {
|
| + |
"comments": {},
|
| + |
"timeline": []
|
| + |
},
|
| + |
"labels": [],
|
| + |
"timestamp": 1710947885000_i64
|
| + |
});
|
| + |
let v1 = serde_json::from_value::<Review>(review.clone()).unwrap();
|
| + |
assert_eq!(
|
| + |
serde_json::from_value::<patch::Review>(review).unwrap(),
|
| + |
v1.into()
|
| + |
);
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn test_review_deserialize_summary_migration_without_summary() {
|
| + |
let review = json!({
|
| + |
"id": "89d45fb371eb2622ba88188d474347cc526d80bb",
|
| + |
"author": { "id": "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx" },
|
| + |
"verdict": "accept",
|
| + |
"comments": {
|
| + |
"comments": {},
|
| + |
"timeline": []
|
| + |
},
|
| + |
"labels": [],
|
| + |
"timestamp": 1710947885000_i64
|
| + |
});
|
| + |
let v1 = serde_json::from_value::<Review>(review.clone()).unwrap();
|
| + |
assert_eq!(
|
| + |
serde_json::from_value::<patch::Review>(review).unwrap(),
|
| + |
v1.into()
|
| + |
);
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn test_review_deserialize_summary_migration_with_summary() {
|
| + |
let review = json!({
|
| + |
"id": "89d45fb371eb2622ba88188d474347cc526d80bb",
|
| + |
"author": { "id": "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx" },
|
| + |
"verdict": "accept",
|
| + |
"summary": "lgtm",
|
| + |
"comments": {
|
| + |
"comments": {},
|
| + |
"timeline": []
|
| + |
},
|
| + |
"labels": [],
|
| + |
"timestamp": 1710947885000_i64
|
| + |
});
|
| + |
let v1 = serde_json::from_value::<Review>(review.clone()).unwrap();
|
| + |
assert_eq!(
|
| + |
serde_json::from_value::<patch::Review>(review).unwrap(),
|
| + |
v1.into()
|
| + |
);
|
| + |
}
|
| + |
|
| + |
#[test]
|
| + |
fn test_review_deserialize_summary_v2() {
|
| + |
let review = json!({
|
| + |
"id": "89d45fb371eb2622ba88188d474347cc526d80bb",
|
| + |
"author": { "id": "did:key:z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx" },
|
| + |
"verdict": "accept",
|
| + |
"summary": [{
|
| + |
"author": "z6MkwPUeUS2fJMfc2HZN1RQTQcTTuhw4HhPySB8JeUg2mVvx",
|
| + |
"timestamp": 1710947885000_i64,
|
| + |
"body": "lgtm",
|
| + |
"embeds": [],
|
| + |
}],
|
| + |
"comments": {
|
| + |
"comments": {},
|
| + |
"timeline": []
|
| + |
},
|
| + |
"labels": [],
|
| + |
"timestamp": 1710947885000_i64
|
| + |
});
|
| + |
let v1 = serde_json::from_value::<Review>(review.clone()).unwrap();
|
| + |
assert_eq!(
|
| + |
serde_json::from_value::<patch::Review>(review).unwrap(),
|
| + |
v1.into()
|
| + |
);
|
| + |
}
|
| + |
}
|