1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
6pub struct SlopNode {
7 pub id: String,
8 #[serde(rename = "type")]
9 pub node_type: String,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 pub properties: Option<Map<String, Value>>,
12 #[serde(skip_serializing_if = "Option::is_none")]
13 pub children: Option<Vec<SlopNode>>,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub affordances: Option<Vec<Affordance>>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub meta: Option<NodeMeta>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub content_ref: Option<ContentRef>,
20}
21
22impl SlopNode {
23 pub fn new(id: impl Into<String>, node_type: impl Into<String>) -> Self {
24 Self {
25 id: id.into(),
26 node_type: node_type.into(),
27 properties: None,
28 children: None,
29 affordances: None,
30 meta: None,
31 content_ref: None,
32 }
33 }
34
35 pub fn root(id: impl Into<String>, name: impl Into<String>) -> Self {
36 let mut props = Map::new();
37 props.insert("label".into(), Value::String(name.into()));
38 Self {
39 id: id.into(),
40 node_type: "root".into(),
41 properties: Some(props),
42 children: Some(Vec::new()),
43 affordances: None,
44 meta: None,
45 content_ref: None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
52pub struct Affordance {
53 pub action: String,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub label: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub description: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub params: Option<Value>,
60 #[serde(default, skip_serializing_if = "is_false")]
61 pub dangerous: bool,
62 #[serde(default, skip_serializing_if = "is_false")]
63 pub idempotent: bool,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub estimate: Option<Estimate>,
66}
67
68impl Affordance {
69 pub fn new(action: impl Into<String>) -> Self {
70 Self {
71 action: action.into(),
72 label: None,
73 description: None,
74 params: None,
75 dangerous: false,
76 idempotent: false,
77 estimate: None,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
84#[serde(rename_all = "lowercase")]
85pub enum Estimate {
86 Instant,
87 Fast,
88 Slow,
89 Async,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
94pub struct NodeMeta {
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub summary: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub salience: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub pinned: Option<bool>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub changed: Option<bool>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub focus: Option<bool>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub urgency: Option<Urgency>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub reason: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub total_children: Option<usize>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub window: Option<(usize, usize)>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub created: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub updated: Option<String>,
117}
118
119impl NodeMeta {
120 pub fn new() -> Self {
121 Self {
122 summary: None,
123 salience: None,
124 pinned: None,
125 changed: None,
126 focus: None,
127 urgency: None,
128 reason: None,
129 total_children: None,
130 window: None,
131 created: None,
132 updated: None,
133 }
134 }
135
136 pub fn is_empty(&self) -> bool {
137 self.summary.is_none()
138 && self.salience.is_none()
139 && self.pinned.is_none()
140 && self.changed.is_none()
141 && self.focus.is_none()
142 && self.urgency.is_none()
143 && self.reason.is_none()
144 && self.total_children.is_none()
145 && self.window.is_none()
146 && self.created.is_none()
147 && self.updated.is_none()
148 }
149}
150
151impl Default for NodeMeta {
152 fn default() -> Self {
153 Self::new()
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
159#[serde(rename_all = "lowercase")]
160pub enum Urgency {
161 None,
162 Low,
163 Medium,
164 High,
165 Critical,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct PatchOp {
171 pub op: PatchOpKind,
172 pub path: String,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub value: Option<Value>,
175 #[serde(skip_serializing_if = "Option::is_none")]
178 pub index: Option<usize>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
182#[serde(rename_all = "lowercase")]
183pub enum PatchOpKind {
184 Add,
185 Remove,
186 Replace,
187 Move,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192pub struct ContentRef {
193 #[serde(rename = "type")]
194 pub content_type: ContentType,
195 pub mime: String,
196 pub summary: String,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub size: Option<usize>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub uri: Option<String>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub preview: Option<String>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub encoding: Option<String>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub hash: Option<String>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
210#[serde(rename_all = "lowercase")]
211pub enum ContentType {
212 Text,
213 Binary,
214 Stream,
215}
216
217fn is_false(v: &bool) -> bool {
218 !v
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use serde_json::json;
225
226 #[test]
227 fn test_slop_node_roundtrip() {
228 let node = SlopNode::root("app", "My App");
229 let json = serde_json::to_value(&node).unwrap();
230 assert_eq!(json["id"], "app");
231 assert_eq!(json["type"], "root");
232 assert_eq!(json["properties"]["label"], "My App");
233
234 let back: SlopNode = serde_json::from_value(json).unwrap();
235 assert_eq!(back.id, "app");
236 assert_eq!(back.node_type, "root");
237 }
238
239 #[test]
240 fn test_affordance_skip_false_fields() {
241 let aff = Affordance::new("toggle");
242 let json = serde_json::to_value(&aff).unwrap();
243 assert!(json.get("dangerous").is_none());
244 assert!(json.get("idempotent").is_none());
245 }
246
247 #[test]
248 fn test_affordance_with_dangerous() {
249 let json = json!({"action": "delete", "dangerous": true});
250 let aff: Affordance = serde_json::from_value(json).unwrap();
251 assert!(aff.dangerous);
252 assert!(!aff.idempotent);
253 }
254
255 #[test]
256 fn test_estimate_serialization() {
257 let est = Estimate::Async;
258 let json = serde_json::to_value(&est).unwrap();
259 assert_eq!(json, "async");
260 }
261
262 #[test]
263 fn test_patch_op() {
264 let op = PatchOp {
265 op: PatchOpKind::Replace,
266 path: "/properties/count".into(),
267 value: Some(json!(42)),
268 index: None,
269 };
270 let json = serde_json::to_value(&op).unwrap();
271 assert_eq!(json["op"], "replace");
272 assert_eq!(json["path"], "/properties/count");
273 assert_eq!(json["value"], 42);
274 }
275}