1use crate::wire::area::{AreaPatch, AreaProps};
2use crate::wire::checklist::{ChecklistItemPatch, ChecklistItemProps};
3use crate::wire::tags::{TagPatch, TagProps};
4use crate::wire::task::{TaskPatch, TaskProps};
5use crate::wire::tombstone::{CommandProps, TombstoneProps};
6use num_enum::{FromPrimitive, IntoPrimitive};
7use serde::de::DeserializeOwned;
8use serde::ser::SerializeStruct;
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10use serde_json::Value;
11use std::collections::BTreeMap;
12use strum::{Display, EnumString};
13
14pub type WireItem = BTreeMap<String, WireObject>;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct WireObject {
19 pub operation_type: OperationType,
20 pub entity_type: Option<EntityType>,
21 pub payload: Properties,
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum Properties {
26 TaskCreate(TaskProps),
27 TaskUpdate(TaskPatch),
28 ChecklistCreate(ChecklistItemProps),
29 ChecklistUpdate(ChecklistItemPatch),
30 TagCreate(TagProps),
31 TagUpdate(TagPatch),
32 AreaCreate(AreaProps),
33 AreaUpdate(AreaPatch),
34 TombstoneCreate(TombstoneProps),
35 CommandCreate(CommandProps),
36 Delete,
37 Ignored(BTreeMap<String, Value>),
39 Unknown(BTreeMap<String, Value>),
41}
42
43impl From<BTreeMap<String, Value>> for Properties {
44 fn from(value: BTreeMap<String, Value>) -> Self {
45 Self::Unknown(value)
46 }
47}
48
49macro_rules! impl_properties_from {
50 ($($source:ty => $variant:ident),+ $(,)?) => {
51 $(
52 impl From<$source> for Properties {
53 fn from(value: $source) -> Self {
54 Self::$variant(value)
55 }
56 }
57 )+
58 };
59}
60
61impl_properties_from!(
62 TaskProps => TaskCreate,
63 TaskPatch => TaskUpdate,
64 ChecklistItemProps => ChecklistCreate,
65 ChecklistItemPatch => ChecklistUpdate,
66 TagProps => TagCreate,
67 TagPatch => TagUpdate,
68 AreaProps => AreaCreate,
69 AreaPatch => AreaUpdate,
70 TombstoneProps => TombstoneCreate,
71 CommandProps => CommandCreate,
72);
73
74impl WireObject {
75 pub fn properties(&self) -> Result<Properties, serde_json::Error> {
76 match &self.payload {
77 Properties::Unknown(map) => WireObject::properties_from(
78 self.operation_type,
79 self.entity_type.as_ref(),
80 map.clone(),
81 ),
82 other => Ok(other.clone()),
83 }
84 }
85
86 pub fn properties_map(&self) -> BTreeMap<String, Value> {
87 match &self.payload {
88 Properties::TaskCreate(props) => to_map(props),
89 Properties::TaskUpdate(patch) => to_map(patch),
90 Properties::ChecklistCreate(props) => to_map(props),
91 Properties::ChecklistUpdate(patch) => to_map(patch),
92 Properties::TagCreate(props) => to_map(props),
93 Properties::TagUpdate(patch) => to_map(patch),
94 Properties::AreaCreate(props) => to_map(props),
95 Properties::AreaUpdate(patch) => to_map(patch),
96 Properties::TombstoneCreate(props) => to_map(props),
97 Properties::CommandCreate(props) => to_map(props),
98 Properties::Delete => BTreeMap::new(),
99 Properties::Ignored(map) => map.clone(),
100 Properties::Unknown(map) => map.clone(),
101 }
102 }
103
104 pub fn create(entity_type: EntityType, payload: impl Into<Properties>) -> Self {
105 let payload =
106 Self::coerce_known_payload(OperationType::Create, &entity_type, payload.into());
107 Self {
108 operation_type: OperationType::Create,
109 entity_type: Some(entity_type),
110 payload,
111 }
112 }
113
114 pub fn update(entity_type: EntityType, payload: impl Into<Properties>) -> Self {
115 let payload =
116 Self::coerce_known_payload(OperationType::Update, &entity_type, payload.into());
117 Self {
118 operation_type: OperationType::Update,
119 entity_type: Some(entity_type),
120 payload,
121 }
122 }
123
124 pub fn delete(entity_type: EntityType) -> Self {
125 Self {
126 operation_type: OperationType::Delete,
127 entity_type: Some(entity_type),
128 payload: Properties::Delete,
129 }
130 }
131
132 fn properties_from(
133 operation_type: OperationType,
134 entity_type: Option<&EntityType>,
135 properties: BTreeMap<String, Value>,
136 ) -> Result<Properties, serde_json::Error> {
137 use EntityType::*;
138 use Properties::*;
139 let p = properties;
140
141 fn parse<T: DeserializeOwned>(
142 properties: BTreeMap<String, Value>,
143 ) -> Result<T, serde_json::Error> {
144 parse_props_from_map(properties)
145 }
146
147 let payload = match operation_type {
148 OperationType::Delete => Delete,
149 OperationType::Create => match entity_type {
150 Some(Task3 | Task4 | Task6) => TaskCreate(parse(p)?),
151 Some(ChecklistItem | ChecklistItem2 | ChecklistItem3) => ChecklistCreate(parse(p)?),
152 Some(Tag3 | Tag4) => TagCreate(parse(p)?),
153 Some(Area2 | Area3) => AreaCreate(parse(p)?),
154 Some(Tombstone | Tombstone2) => TombstoneCreate(parse(p)?),
155 Some(Command) => CommandCreate(parse(p)?),
156 Some(Settings3 | Settings4 | Settings5) => Ignored(p),
157 _ => Properties::Unknown(p),
158 },
159 OperationType::Update => match entity_type {
160 Some(Task3 | Task4 | Task6) => TaskUpdate(parse(p)?),
161 Some(ChecklistItem | ChecklistItem2 | ChecklistItem3) => ChecklistUpdate(parse(p)?),
162 Some(Tag3 | Tag4) => TagUpdate(parse(p)?),
163 Some(Area2 | Area3) => AreaUpdate(parse(p)?),
164 Some(Settings3 | Settings4 | Settings5) => Ignored(p),
165 _ => Properties::Unknown(p),
166 },
167 OperationType::Unknown(_) => Properties::Unknown(p),
168 };
169
170 Ok(payload)
171 }
172
173 fn coerce_known_payload(
174 operation_type: OperationType,
175 entity_type: &EntityType,
176 payload: Properties,
177 ) -> Properties {
178 match payload {
179 Properties::Unknown(map) => {
180 match Self::properties_from(operation_type, Some(entity_type), map.clone()) {
181 Ok(parsed) => parsed,
182 Err(_) => Properties::Unknown(map),
183 }
184 }
185 other => other,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191struct RawWireObject {
192 #[serde(rename = "t")]
193 operation_type: OperationType,
194 #[serde(rename = "e")]
195 entity_type: Option<EntityType>,
196 #[serde(rename = "p", default)]
197 properties: BTreeMap<String, Value>,
198}
199
200impl Serialize for WireObject {
201 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
202 let mut state = serializer.serialize_struct("WireObject", 3)?;
203 state.serialize_field("t", &self.operation_type)?;
204 state.serialize_field("e", &self.entity_type)?;
205 state.serialize_field("p", &self.properties_map())?;
206 state.end()
207 }
208}
209
210impl<'de> Deserialize<'de> for WireObject {
211 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
212 let raw = RawWireObject::deserialize(deserializer)?;
213 let payload = WireObject::properties_from(
214 raw.operation_type,
215 raw.entity_type.as_ref(),
216 raw.properties,
217 )
218 .map_err(serde::de::Error::custom)?;
219 Ok(Self {
220 operation_type: raw.operation_type,
221 entity_type: raw.entity_type,
222 payload,
223 })
224 }
225}
226
227fn parse_props_from_map<T: DeserializeOwned>(
228 properties: BTreeMap<String, Value>,
229) -> Result<T, serde_json::Error> {
230 serde_json::from_value(Value::Object(
231 properties
232 .into_iter()
233 .collect::<serde_json::Map<String, Value>>(),
234 ))
235}
236
237fn to_map<T: Serialize>(value: &T) -> BTreeMap<String, Value> {
238 match serde_json::to_value(value) {
239 Ok(Value::Object(map)) => map.into_iter().collect(),
240 _ => BTreeMap::new(),
241 }
242}
243
244#[derive(
246 Debug,
247 Clone,
248 Copy,
249 Serialize,
250 Deserialize,
251 PartialEq,
252 Eq,
253 Display,
254 EnumString,
255 FromPrimitive,
256 IntoPrimitive,
257)]
258#[repr(i32)]
259#[serde(from = "i32", into = "i32")]
260pub enum OperationType {
261 Create = 0,
263 Update = 1,
265 Delete = 2,
267
268 #[num_enum(catch_all)]
270 #[strum(disabled, to_string = "{0}")]
271 Unknown(i32),
272}
273
274#[expect(
275 clippy::derivable_impls,
276 reason = "num_enum(catch_all) conflicts with #[default]"
277)]
278impl Default for OperationType {
279 fn default() -> Self {
280 Self::Create
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display, EnumString)]
288#[serde(from = "String", into = "String")]
289pub enum EntityType {
290 Task3,
292 Task4,
294 Task6,
296
297 ChecklistItem,
299 ChecklistItem2,
301 ChecklistItem3,
303
304 Tag3,
306 Tag4,
308
309 Area2,
311 Area3,
313
314 Settings3,
316 Settings4,
317 Settings5,
318
319 Tombstone,
321 Tombstone2,
323
324 Command,
326 #[strum(default, to_string = "{0}")]
328 Unknown(String),
329}
330
331impl From<String> for EntityType {
332 fn from(value: String) -> Self {
333 value.parse().unwrap_or(Self::Unknown(value))
334 }
335}
336
337impl From<EntityType> for String {
338 fn from(value: EntityType) -> Self {
339 value.to_string()
340 }
341}