1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4use std::path::PathBuf;
5
6pub const ARTIFACT_REVIEW_UI_KIND: &str = "artifactReview";
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "lowercase")]
10pub enum ArtifactReviewDecision {
11 Approved,
12 Feedback,
13}
14
15impl ArtifactReviewDecision {
16 pub const ALL: [Self; 2] = [Self::Approved, Self::Feedback];
17
18 pub const fn as_str(self) -> &'static str {
19 match self {
20 Self::Approved => "approved",
21 Self::Feedback => "feedback",
22 }
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[serde(tag = "decision", rename_all = "lowercase", try_from = "ReviewForm")]
28pub enum ArtifactReviewSubmission {
29 Approved,
30 Feedback { feedback: String },
31}
32
33impl ArtifactReviewSubmission {
34 pub fn fields(&self) -> impl Iterator<Item = (&'static str, &str)> {
35 let (decision, feedback) = match self {
36 Self::Approved => (ArtifactReviewDecision::Approved, None),
37 Self::Feedback { feedback } => (ArtifactReviewDecision::Feedback, Some(feedback.as_str())),
38 };
39 std::iter::once(("decision", decision.as_str())).chain(feedback.map(|feedback| ("feedback", feedback)))
40 }
41}
42
43impl std::fmt::Display for ArtifactReviewDecision {
44 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 formatter.write_str(self.as_str())
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50#[serde(rename_all = "camelCase")]
51pub struct ArtifactReviewElicitationMeta {
52 pub ui: String,
53 pub path: Option<PathBuf>,
54 pub title: String,
55 pub markdown: String,
56}
57
58impl ArtifactReviewElicitationMeta {
59 pub fn new(path: Option<PathBuf>, title: impl Into<String>, markdown: impl Into<String>) -> Self {
60 Self { ui: ARTIFACT_REVIEW_UI_KIND.to_string(), path, title: title.into(), markdown: markdown.into() }
61 }
62
63 pub fn to_json(&self) -> Result<Map<String, Value>, serde_json::Error> {
64 serde_json::to_value(self).and_then(|value| match value {
65 Value::Object(map) => Ok(map),
66 _ => Err(serde_json::Error::io(std::io::Error::other(
67 "artifact review metadata did not serialize to an object",
68 ))),
69 })
70 }
71
72 pub fn parse(meta: Option<&Map<String, Value>>) -> Option<Self> {
73 let parsed = serde_json::from_value::<Self>(Value::Object(meta?.clone())).ok()?;
74 (parsed.ui == ARTIFACT_REVIEW_UI_KIND).then_some(parsed)
75 }
76}
77
78#[derive(Deserialize)]
79#[serde(deny_unknown_fields)]
80struct ReviewForm {
81 decision: ArtifactReviewDecision,
82 #[serde(default)]
83 feedback: String,
84}
85
86impl TryFrom<ReviewForm> for ArtifactReviewSubmission {
87 type Error = &'static str;
88
89 fn try_from(form: ReviewForm) -> Result<Self, Self::Error> {
90 match form.decision {
91 ArtifactReviewDecision::Approved if form.feedback.is_empty() => Ok(Self::Approved),
92 ArtifactReviewDecision::Approved => Err("approval cannot contain feedback"),
93 ArtifactReviewDecision::Feedback => Ok(Self::Feedback { feedback: form.feedback }),
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use std::path::Path;
102
103 #[test]
104 fn submission_fields_match_the_serialized_contract() {
105 for submission in [
106 ArtifactReviewSubmission::Approved,
107 ArtifactReviewSubmission::Feedback { feedback: "line one\nline two\n".into() },
108 ] {
109 let fields = submission
110 .fields()
111 .map(|(name, value)| (name.to_string(), Value::String(value.to_string())))
112 .collect::<Map<_, _>>();
113 assert_eq!(serde_json::to_value(&submission).unwrap(), Value::Object(fields.clone()));
114 assert_eq!(serde_json::from_value::<ArtifactReviewSubmission>(Value::Object(fields)).unwrap(), submission);
115 }
116 }
117
118 #[test]
119 fn metadata_round_trips() {
120 let meta = ArtifactReviewElicitationMeta::new(Some(PathBuf::from("docs/question.md")), "Review", "# Question");
121 let parsed = ArtifactReviewElicitationMeta::parse(Some(&meta.to_json().expect("serialize"))).expect("parse");
122 assert_eq!(parsed, meta);
123 assert_eq!(parsed.ui, "artifactReview");
124 assert_eq!(parsed.path.as_deref(), Some(Path::new("docs/question.md")));
125 }
126}