1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "UPPERCASE")]
12pub enum DeltaOperationType {
13 Add,
14 Update,
15 Tag,
16 Remove,
17}
18
19impl fmt::Display for DeltaOperationType {
20 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21 match self {
22 Self::Add => write!(f, "ADD"),
23 Self::Update => write!(f, "UPDATE"),
24 Self::Tag => write!(f, "TAG"),
25 Self::Remove => write!(f, "REMOVE"),
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DeltaOperation {
33 #[serde(rename = "type")]
34 pub op_type: DeltaOperationType,
35 pub section: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub content: Option<String>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub bullet_id: Option<String>,
40 #[serde(default)]
41 pub metadata: HashMap<String, i64>,
42}
43
44impl DeltaOperation {
45 pub fn from_json(payload: &serde_json::Value) -> Option<Self> {
47 let op_type_str = payload["type"].as_str()?.to_uppercase();
48 let op_type = match op_type_str.as_str() {
49 "ADD" => DeltaOperationType::Add,
50 "UPDATE" => DeltaOperationType::Update,
51 "TAG" => DeltaOperationType::Tag,
52 "REMOVE" => DeltaOperationType::Remove,
53 _ => return None,
54 };
55
56 let section = payload
57 .get("section")
58 .and_then(|v| v.as_str())
59 .unwrap_or("")
60 .to_string();
61
62 let content = payload
63 .get("content")
64 .and_then(|v| v.as_str())
65 .map(String::from);
66
67 let bullet_id = payload
68 .get("bullet_id")
69 .and_then(|v| v.as_str())
70 .map(String::from);
71
72 let mut metadata = HashMap::new();
73 if let Some(meta_obj) = payload.get("metadata").and_then(|v| v.as_object()) {
74 let valid_tags: &[&str] = if op_type == DeltaOperationType::Tag {
75 &["helpful", "harmful", "neutral"]
76 } else {
77 &[]
79 };
80
81 for (k, v) in meta_obj {
82 if op_type == DeltaOperationType::Tag && !valid_tags.contains(&k.as_str()) {
83 continue;
84 }
85 if let Some(n) = v.as_i64() {
86 metadata.insert(k.clone(), n);
87 }
88 }
89 }
90
91 Some(Self {
92 op_type,
93 section,
94 content,
95 bullet_id,
96 metadata,
97 })
98 }
99
100 pub fn to_json(&self) -> serde_json::Value {
102 let mut data = serde_json::json!({
103 "type": self.op_type,
104 "section": self.section,
105 });
106 if let Some(ref content) = self.content {
107 data["content"] = serde_json::Value::String(content.clone());
108 }
109 if let Some(ref bullet_id) = self.bullet_id {
110 data["bullet_id"] = serde_json::Value::String(bullet_id.clone());
111 }
112 if !self.metadata.is_empty() {
113 data["metadata"] = serde_json::to_value(&self.metadata).unwrap_or_default();
114 }
115 data
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct DeltaBatch {
122 pub reasoning: String,
123 #[serde(default)]
124 pub operations: Vec<DeltaOperation>,
125}
126
127impl DeltaBatch {
128 pub fn from_json(payload: &serde_json::Value) -> Self {
130 let reasoning = payload
131 .get("reasoning")
132 .and_then(|v| v.as_str())
133 .unwrap_or("")
134 .to_string();
135
136 let mut operations = Vec::new();
137 if let Some(ops_array) = payload.get("operations").and_then(|v| v.as_array()) {
138 for item in ops_array {
139 if let Some(op) = DeltaOperation::from_json(item) {
140 operations.push(op);
141 }
142 }
143 }
144
145 Self {
146 reasoning,
147 operations,
148 }
149 }
150
151 pub fn to_json(&self) -> serde_json::Value {
153 serde_json::json!({
154 "reasoning": self.reasoning,
155 "operations": self.operations.iter().map(|op| op.to_json()).collect::<Vec<_>>(),
156 })
157 }
158}
159
160#[cfg(test)]
161#[path = "delta_tests.rs"]
162mod tests;