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