use serde::{Deserialize, Serialize};
use super::{ReviewComment, ReviewCommentId, ReviewThread};
use crate::ModelError;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum FindingSeverity {
Critical,
Major,
Minor,
Nit,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FindingResolutionReason {
Addressed,
Invalid,
WontFix,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FindingResolution {
pub reason: FindingResolutionReason,
pub addressing_severity: FindingSeverity,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(try_from = "String", into = "String")]
pub struct FindingResolutionReply(String);
impl FindingResolutionReply {
pub fn new(value: impl Into<String>) -> std::result::Result<Self, ModelError> {
let value = value.into();
if value.trim().is_empty() {
return Err(ModelError::Empty {
field: "finding resolution reply",
});
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for FindingResolutionReply {
type Error = ModelError;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<FindingResolutionReply> for String {
fn from(value: FindingResolutionReply) -> Self {
value.0
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case", tag = "compatibility")]
pub enum FindingResolutionRecord {
Supported {
resolution: FindingResolution,
source_reply_id: ReviewCommentId,
},
Unsupported {
metadata_format: String,
source_reply_id: ReviewCommentId,
},
}
impl FindingResolutionRecord {
#[must_use]
pub fn supported_resolution(&self) -> Option<FindingResolution> {
match self {
Self::Supported { resolution, .. } => Some(*resolution),
Self::Unsupported { .. } => None,
}
}
#[must_use]
pub fn source_reply_id(&self) -> &ReviewCommentId {
match self {
Self::Supported {
source_reply_id, ..
}
| Self::Unsupported {
source_reply_id, ..
} => source_reply_id,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ReviewFinding {
#[serde(flatten)]
pub thread: ReviewThread,
pub resolution: Option<FindingResolutionRecord>,
}
impl std::ops::Deref for ReviewFinding {
type Target = ReviewThread;
fn deref(&self) -> &Self::Target {
&self.thread
}
}
impl std::ops::DerefMut for ReviewFinding {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.thread
}
}
impl ReviewFinding {
#[must_use]
pub fn resolution_reply(&self) -> Option<&ReviewComment> {
let reply_id = self.resolution.as_ref()?.source_reply_id();
self.replies.iter().find(|reply| &reply.id == reply_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn finding_resolution_reasons_use_githubs_enum_spellings() {
for (reason, expected) in [
(FindingResolutionReason::Addressed, "ADDRESSED"),
(FindingResolutionReason::Invalid, "INVALID"),
(FindingResolutionReason::WontFix, "WONT_FIX"),
] {
let resolution = FindingResolution {
reason,
addressing_severity: FindingSeverity::Major,
};
assert_eq!(
serde_json::to_value(resolution).expect("serializes resolution"),
serde_json::json!({
"reason": expected,
"addressing_severity": "major"
})
);
}
}
#[test]
fn finding_resolution_replies_require_visible_explanatory_text() {
assert!(FindingResolutionReply::new("\n\t").is_err());
let reply = FindingResolutionReply::new("Addressed in the current revision.")
.expect("visible explanation");
assert_eq!(reply.as_str(), "Addressed in the current revision.");
}
}