farm_crdt.rs (21163B)
1 #![forbid(unsafe_code)] 2 3 use crate::envelope::kind::KIND_FARM_CRDT_CHANGE as KIND_FARM_CRDT_CHANGE_EVENT; 4 use crate::farm::workspace::FarmWorkspaceRef; 5 6 #[cfg(not(feature = "std"))] 7 use alloc::{ 8 string::{String, ToString}, 9 vec::Vec, 10 }; 11 12 pub const KIND_FARM_CRDT_CHANGE: u32 = KIND_FARM_CRDT_CHANGE_EVENT; 13 pub const RADROOTS_FARM_CRDT_CHANGE_SCHEMA: &str = "radroots.farm.crdt.change.v1"; 14 pub const RADROOTS_FARM_CRDT_TAG: &str = "radroots:farm:crdt"; 15 16 #[cfg_attr( 17 any(feature = "serde", test), 18 derive(serde::Serialize, serde::Deserialize) 19 )] 20 #[derive(Clone, Debug, PartialEq, Eq)] 21 pub struct FarmCrdtChange { 22 pub schema: String, 23 pub workspace: FarmWorkspaceRef, 24 pub farm_group_id: String, 25 pub document_id: String, 26 pub document_kind: FarmCrdtDocumentKind, 27 pub crdt_backend: CrdtBackend, 28 pub crdt_backend_version: Option<String>, 29 pub actor_id: String, 30 pub change_hash: String, 31 pub dependencies: Vec<String>, 32 pub encoded_change: String, 33 pub semantic_kind: FarmSemanticKind, 34 pub business_time_ms: u64, 35 pub author_member_id: Option<String>, 36 pub app_version: Option<String>, 37 } 38 39 #[cfg_attr( 40 any(feature = "serde", test), 41 derive(serde::Serialize, serde::Deserialize) 42 )] 43 #[cfg_attr(any(feature = "serde", test), serde(from = "String", into = "String"))] 44 #[derive(Clone, Debug, PartialEq, Eq)] 45 pub enum FarmCrdtDocumentKind { 46 FarmMembership, 47 FarmRolePolicy, 48 FarmTask, 49 FarmWorkSession, 50 FarmActivity, 51 FarmHarvestRecord, 52 FarmLocation, 53 FarmCrop, 54 FarmCropVariety, 55 FarmCropCycle, 56 FarmAttachment, 57 FarmPayPeriod, 58 FarmInventoryItem, 59 FarmMediaAsset, 60 FarmObservation, 61 Other { value: String }, 62 } 63 64 #[cfg_attr( 65 any(feature = "serde", test), 66 derive(serde::Serialize, serde::Deserialize) 67 )] 68 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 69 pub enum CrdtBackend { 70 Automerge, 71 Yjs, 72 Loro, 73 } 74 75 #[cfg_attr( 76 any(feature = "serde", test), 77 derive(serde::Serialize, serde::Deserialize) 78 )] 79 #[cfg_attr(any(feature = "serde", test), serde(from = "String", into = "String"))] 80 #[derive(Clone, Debug, PartialEq, Eq)] 81 pub enum FarmSemanticKind { 82 FarmTaskCreate, 83 FarmTaskAssign, 84 FarmTaskJoin, 85 FarmTaskStatusSet, 86 FarmTaskChecklistItemAdd, 87 FarmTaskCommentAdd, 88 FarmTaskAttachmentAttach, 89 FarmTaskFollowUpCreate, 90 FarmTaskUpdate, 91 FarmTaskComplete, 92 FarmWorkSessionStart, 93 FarmWorkSessionStop, 94 FarmWorkSessionSubmit, 95 FarmWorkSessionManualEntryCreate, 96 FarmWorkSessionApprove, 97 FarmWorkSessionReject, 98 FarmWorkSessionCorrect, 99 FarmWorkSessionUpdate, 100 FarmWorkSessionEnd, 101 FarmHarvestRecordCreate, 102 FarmHarvestLineAdd, 103 FarmHarvestLineCorrect, 104 FarmHarvestLineVoid, 105 FarmHarvestAttachmentAttach, 106 FarmHarvestRecordUpdate, 107 FarmActivityCreate, 108 FarmNoteCreate, 109 FarmLocationCreate, 110 FarmCropCreate, 111 FarmCropVarietyCreate, 112 FarmCropCycleCreate, 113 FarmMemberInviteCreate, 114 FarmMemberApprove, 115 FarmMemberRoleSet, 116 FarmMemberDeactivate, 117 FarmPayPeriodOpen, 118 FarmPayPeriodClose, 119 FarmReportExportMark, 120 FarmInventoryItemUpdate, 121 FarmMediaAssetAttach, 122 FarmObservationCreate, 123 FarmWorkspaceUpdate, 124 Other { value: String }, 125 } 126 127 impl FarmCrdtDocumentKind { 128 pub fn as_str(&self) -> &str { 129 match self { 130 Self::FarmMembership => "FarmMembership", 131 Self::FarmRolePolicy => "FarmRolePolicy", 132 Self::FarmTask => "FarmTask", 133 Self::FarmWorkSession => "FarmWorkSession", 134 Self::FarmActivity => "FarmActivity", 135 Self::FarmHarvestRecord => "FarmHarvestRecord", 136 Self::FarmLocation => "FarmLocation", 137 Self::FarmCrop => "FarmCrop", 138 Self::FarmCropVariety => "FarmCropVariety", 139 Self::FarmCropCycle => "FarmCropCycle", 140 Self::FarmAttachment => "FarmAttachment", 141 Self::FarmPayPeriod => "FarmPayPeriod", 142 Self::FarmInventoryItem => "FarmInventoryItem", 143 Self::FarmMediaAsset => "FarmMediaAsset", 144 Self::FarmObservation => "FarmObservation", 145 Self::Other { value } => value.as_str(), 146 } 147 } 148 } 149 150 impl From<String> for FarmCrdtDocumentKind { 151 fn from(value: String) -> Self { 152 match value.as_str() { 153 "FarmMembership" => Self::FarmMembership, 154 "FarmRolePolicy" => Self::FarmRolePolicy, 155 "FarmTask" => Self::FarmTask, 156 "FarmWorkSession" => Self::FarmWorkSession, 157 "FarmActivity" => Self::FarmActivity, 158 "FarmHarvestRecord" => Self::FarmHarvestRecord, 159 "FarmLocation" => Self::FarmLocation, 160 "FarmCrop" => Self::FarmCrop, 161 "FarmCropVariety" => Self::FarmCropVariety, 162 "FarmCropCycle" => Self::FarmCropCycle, 163 "FarmAttachment" => Self::FarmAttachment, 164 "FarmPayPeriod" => Self::FarmPayPeriod, 165 "FarmInventoryItem" => Self::FarmInventoryItem, 166 "FarmMediaAsset" => Self::FarmMediaAsset, 167 "FarmObservation" => Self::FarmObservation, 168 _ => Self::Other { value }, 169 } 170 } 171 } 172 173 impl From<FarmCrdtDocumentKind> for String { 174 fn from(value: FarmCrdtDocumentKind) -> Self { 175 match value { 176 FarmCrdtDocumentKind::Other { value } => value, 177 value => value.as_str().to_string(), 178 } 179 } 180 } 181 182 impl FarmSemanticKind { 183 pub fn as_str(&self) -> &str { 184 match self { 185 Self::FarmTaskCreate => "FarmTaskCreate", 186 Self::FarmTaskAssign => "FarmTaskAssign", 187 Self::FarmTaskJoin => "FarmTaskJoin", 188 Self::FarmTaskStatusSet => "FarmTaskStatusSet", 189 Self::FarmTaskChecklistItemAdd => "FarmTaskChecklistItemAdd", 190 Self::FarmTaskCommentAdd => "FarmTaskCommentAdd", 191 Self::FarmTaskAttachmentAttach => "FarmTaskAttachmentAttach", 192 Self::FarmTaskFollowUpCreate => "FarmTaskFollowUpCreate", 193 Self::FarmTaskUpdate => "FarmTaskUpdate", 194 Self::FarmTaskComplete => "FarmTaskComplete", 195 Self::FarmWorkSessionStart => "FarmWorkSessionStart", 196 Self::FarmWorkSessionStop => "FarmWorkSessionStop", 197 Self::FarmWorkSessionSubmit => "FarmWorkSessionSubmit", 198 Self::FarmWorkSessionManualEntryCreate => "FarmWorkSessionManualEntryCreate", 199 Self::FarmWorkSessionApprove => "FarmWorkSessionApprove", 200 Self::FarmWorkSessionReject => "FarmWorkSessionReject", 201 Self::FarmWorkSessionCorrect => "FarmWorkSessionCorrect", 202 Self::FarmWorkSessionUpdate => "FarmWorkSessionUpdate", 203 Self::FarmWorkSessionEnd => "FarmWorkSessionEnd", 204 Self::FarmHarvestRecordCreate => "FarmHarvestRecordCreate", 205 Self::FarmHarvestLineAdd => "FarmHarvestLineAdd", 206 Self::FarmHarvestLineCorrect => "FarmHarvestLineCorrect", 207 Self::FarmHarvestLineVoid => "FarmHarvestLineVoid", 208 Self::FarmHarvestAttachmentAttach => "FarmHarvestAttachmentAttach", 209 Self::FarmHarvestRecordUpdate => "FarmHarvestRecordUpdate", 210 Self::FarmActivityCreate => "FarmActivityCreate", 211 Self::FarmNoteCreate => "FarmNoteCreate", 212 Self::FarmLocationCreate => "FarmLocationCreate", 213 Self::FarmCropCreate => "FarmCropCreate", 214 Self::FarmCropVarietyCreate => "FarmCropVarietyCreate", 215 Self::FarmCropCycleCreate => "FarmCropCycleCreate", 216 Self::FarmMemberInviteCreate => "FarmMemberInviteCreate", 217 Self::FarmMemberApprove => "FarmMemberApprove", 218 Self::FarmMemberRoleSet => "FarmMemberRoleSet", 219 Self::FarmMemberDeactivate => "FarmMemberDeactivate", 220 Self::FarmPayPeriodOpen => "FarmPayPeriodOpen", 221 Self::FarmPayPeriodClose => "FarmPayPeriodClose", 222 Self::FarmReportExportMark => "FarmReportExportMark", 223 Self::FarmInventoryItemUpdate => "FarmInventoryItemUpdate", 224 Self::FarmMediaAssetAttach => "FarmMediaAssetAttach", 225 Self::FarmObservationCreate => "FarmObservationCreate", 226 Self::FarmWorkspaceUpdate => "FarmWorkspaceUpdate", 227 Self::Other { value } => value.as_str(), 228 } 229 } 230 } 231 232 impl From<String> for FarmSemanticKind { 233 fn from(value: String) -> Self { 234 match value.as_str() { 235 "FarmTaskCreate" => Self::FarmTaskCreate, 236 "FarmTaskAssign" => Self::FarmTaskAssign, 237 "FarmTaskJoin" => Self::FarmTaskJoin, 238 "FarmTaskStatusSet" => Self::FarmTaskStatusSet, 239 "FarmTaskChecklistItemAdd" => Self::FarmTaskChecklistItemAdd, 240 "FarmTaskCommentAdd" => Self::FarmTaskCommentAdd, 241 "FarmTaskAttachmentAttach" => Self::FarmTaskAttachmentAttach, 242 "FarmTaskFollowUpCreate" => Self::FarmTaskFollowUpCreate, 243 "FarmTaskUpdate" => Self::FarmTaskUpdate, 244 "FarmTaskComplete" => Self::FarmTaskComplete, 245 "FarmWorkSessionStart" => Self::FarmWorkSessionStart, 246 "FarmWorkSessionStop" => Self::FarmWorkSessionStop, 247 "FarmWorkSessionSubmit" => Self::FarmWorkSessionSubmit, 248 "FarmWorkSessionManualEntryCreate" => Self::FarmWorkSessionManualEntryCreate, 249 "FarmWorkSessionApprove" => Self::FarmWorkSessionApprove, 250 "FarmWorkSessionReject" => Self::FarmWorkSessionReject, 251 "FarmWorkSessionCorrect" => Self::FarmWorkSessionCorrect, 252 "FarmWorkSessionUpdate" => Self::FarmWorkSessionUpdate, 253 "FarmWorkSessionEnd" => Self::FarmWorkSessionEnd, 254 "FarmHarvestRecordCreate" => Self::FarmHarvestRecordCreate, 255 "FarmHarvestLineAdd" => Self::FarmHarvestLineAdd, 256 "FarmHarvestLineCorrect" => Self::FarmHarvestLineCorrect, 257 "FarmHarvestLineVoid" => Self::FarmHarvestLineVoid, 258 "FarmHarvestAttachmentAttach" => Self::FarmHarvestAttachmentAttach, 259 "FarmHarvestRecordUpdate" => Self::FarmHarvestRecordUpdate, 260 "FarmActivityCreate" => Self::FarmActivityCreate, 261 "FarmNoteCreate" => Self::FarmNoteCreate, 262 "FarmLocationCreate" => Self::FarmLocationCreate, 263 "FarmCropCreate" => Self::FarmCropCreate, 264 "FarmCropVarietyCreate" => Self::FarmCropVarietyCreate, 265 "FarmCropCycleCreate" => Self::FarmCropCycleCreate, 266 "FarmMemberInviteCreate" => Self::FarmMemberInviteCreate, 267 "FarmMemberApprove" => Self::FarmMemberApprove, 268 "FarmMemberRoleSet" => Self::FarmMemberRoleSet, 269 "FarmMemberDeactivate" => Self::FarmMemberDeactivate, 270 "FarmPayPeriodOpen" => Self::FarmPayPeriodOpen, 271 "FarmPayPeriodClose" => Self::FarmPayPeriodClose, 272 "FarmReportExportMark" => Self::FarmReportExportMark, 273 "FarmInventoryItemUpdate" => Self::FarmInventoryItemUpdate, 274 "FarmMediaAssetAttach" => Self::FarmMediaAssetAttach, 275 "FarmObservationCreate" => Self::FarmObservationCreate, 276 "FarmWorkspaceUpdate" => Self::FarmWorkspaceUpdate, 277 _ => Self::Other { value }, 278 } 279 } 280 } 281 282 impl From<FarmSemanticKind> for String { 283 fn from(value: FarmSemanticKind) -> Self { 284 match value { 285 FarmSemanticKind::Other { value } => value, 286 value => value.as_str().to_string(), 287 } 288 } 289 } 290 291 #[cfg(all(test, feature = "serde"))] 292 #[cfg_attr(coverage_nightly, coverage(off))] 293 mod tests { 294 use super::*; 295 296 #[test] 297 fn crdt_change_kind_uses_custom_app_data_kind() { 298 assert_eq!(KIND_FARM_CRDT_CHANGE, 78); 299 } 300 301 #[test] 302 fn crdt_change_represents_required_envelope_fields() { 303 let change = sample_change(); 304 305 assert_eq!(change.schema, RADROOTS_FARM_CRDT_CHANGE_SCHEMA); 306 assert_eq!(change.workspace.pubkey, "workspace_pubkey"); 307 assert_eq!(change.farm_group_id, "BCDEFGHIJKLMNOPQRSTUVW"); 308 assert_eq!(change.document_id, "DEFGHIJKLMNOPQRSTUVWXY"); 309 assert_eq!(change.document_kind, FarmCrdtDocumentKind::FarmTask); 310 assert_eq!(change.crdt_backend, CrdtBackend::Automerge); 311 assert_eq!(change.dependencies, Vec::<String>::new()); 312 assert_eq!(change.semantic_kind, FarmSemanticKind::FarmTaskCreate); 313 assert_eq!(change.business_time_ms, 1_780_000_000_000); 314 assert_eq!(change.author_member_id.as_deref(), Some("member_abc")); 315 assert_eq!(change.app_version.as_deref(), Some("0.1.0")); 316 } 317 318 #[test] 319 fn crdt_change_serializes_stable_content_shape() { 320 let value = serde_json::to_value(sample_change()).unwrap(); 321 322 assert_eq!(value["schema"], RADROOTS_FARM_CRDT_CHANGE_SCHEMA); 323 assert_eq!(value["workspace"]["d_tag"], "ABCDEFGHIJKLMNOPQRSTUV"); 324 assert_eq!(value["document_kind"], "FarmTask"); 325 assert_eq!(value["crdt_backend"], "Automerge"); 326 assert_eq!(value["semantic_kind"], "FarmTaskCreate"); 327 assert_eq!(value["business_time_ms"], 1_780_000_000_000_u64); 328 } 329 330 #[test] 331 fn document_kinds_serialize_as_stable_strings() { 332 for (kind, expected) in [ 333 (FarmCrdtDocumentKind::FarmMembership, "FarmMembership"), 334 (FarmCrdtDocumentKind::FarmRolePolicy, "FarmRolePolicy"), 335 (FarmCrdtDocumentKind::FarmTask, "FarmTask"), 336 (FarmCrdtDocumentKind::FarmWorkSession, "FarmWorkSession"), 337 (FarmCrdtDocumentKind::FarmActivity, "FarmActivity"), 338 (FarmCrdtDocumentKind::FarmHarvestRecord, "FarmHarvestRecord"), 339 (FarmCrdtDocumentKind::FarmLocation, "FarmLocation"), 340 (FarmCrdtDocumentKind::FarmCrop, "FarmCrop"), 341 (FarmCrdtDocumentKind::FarmCropVariety, "FarmCropVariety"), 342 (FarmCrdtDocumentKind::FarmCropCycle, "FarmCropCycle"), 343 (FarmCrdtDocumentKind::FarmAttachment, "FarmAttachment"), 344 (FarmCrdtDocumentKind::FarmPayPeriod, "FarmPayPeriod"), 345 (FarmCrdtDocumentKind::FarmInventoryItem, "FarmInventoryItem"), 346 (FarmCrdtDocumentKind::FarmMediaAsset, "FarmMediaAsset"), 347 (FarmCrdtDocumentKind::FarmObservation, "FarmObservation"), 348 ] { 349 let encoded = serde_json::to_string(&kind).unwrap(); 350 assert_eq!(encoded, format!("\"{expected}\"")); 351 let decoded: FarmCrdtDocumentKind = serde_json::from_str(&encoded).unwrap(); 352 assert_eq!(decoded, kind); 353 } 354 } 355 356 #[test] 357 fn semantic_kinds_serialize_as_stable_strings() { 358 for (kind, expected) in [ 359 (FarmSemanticKind::FarmTaskCreate, "FarmTaskCreate"), 360 (FarmSemanticKind::FarmTaskAssign, "FarmTaskAssign"), 361 (FarmSemanticKind::FarmTaskJoin, "FarmTaskJoin"), 362 (FarmSemanticKind::FarmTaskStatusSet, "FarmTaskStatusSet"), 363 ( 364 FarmSemanticKind::FarmTaskChecklistItemAdd, 365 "FarmTaskChecklistItemAdd", 366 ), 367 (FarmSemanticKind::FarmTaskCommentAdd, "FarmTaskCommentAdd"), 368 ( 369 FarmSemanticKind::FarmTaskAttachmentAttach, 370 "FarmTaskAttachmentAttach", 371 ), 372 ( 373 FarmSemanticKind::FarmTaskFollowUpCreate, 374 "FarmTaskFollowUpCreate", 375 ), 376 (FarmSemanticKind::FarmTaskUpdate, "FarmTaskUpdate"), 377 (FarmSemanticKind::FarmTaskComplete, "FarmTaskComplete"), 378 ( 379 FarmSemanticKind::FarmWorkSessionStart, 380 "FarmWorkSessionStart", 381 ), 382 (FarmSemanticKind::FarmWorkSessionStop, "FarmWorkSessionStop"), 383 ( 384 FarmSemanticKind::FarmWorkSessionSubmit, 385 "FarmWorkSessionSubmit", 386 ), 387 ( 388 FarmSemanticKind::FarmWorkSessionManualEntryCreate, 389 "FarmWorkSessionManualEntryCreate", 390 ), 391 ( 392 FarmSemanticKind::FarmWorkSessionApprove, 393 "FarmWorkSessionApprove", 394 ), 395 ( 396 FarmSemanticKind::FarmWorkSessionReject, 397 "FarmWorkSessionReject", 398 ), 399 ( 400 FarmSemanticKind::FarmWorkSessionCorrect, 401 "FarmWorkSessionCorrect", 402 ), 403 ( 404 FarmSemanticKind::FarmWorkSessionUpdate, 405 "FarmWorkSessionUpdate", 406 ), 407 (FarmSemanticKind::FarmWorkSessionEnd, "FarmWorkSessionEnd"), 408 ( 409 FarmSemanticKind::FarmHarvestRecordCreate, 410 "FarmHarvestRecordCreate", 411 ), 412 (FarmSemanticKind::FarmHarvestLineAdd, "FarmHarvestLineAdd"), 413 ( 414 FarmSemanticKind::FarmHarvestLineCorrect, 415 "FarmHarvestLineCorrect", 416 ), 417 (FarmSemanticKind::FarmHarvestLineVoid, "FarmHarvestLineVoid"), 418 ( 419 FarmSemanticKind::FarmHarvestAttachmentAttach, 420 "FarmHarvestAttachmentAttach", 421 ), 422 ( 423 FarmSemanticKind::FarmHarvestRecordUpdate, 424 "FarmHarvestRecordUpdate", 425 ), 426 (FarmSemanticKind::FarmActivityCreate, "FarmActivityCreate"), 427 (FarmSemanticKind::FarmNoteCreate, "FarmNoteCreate"), 428 (FarmSemanticKind::FarmLocationCreate, "FarmLocationCreate"), 429 (FarmSemanticKind::FarmCropCreate, "FarmCropCreate"), 430 ( 431 FarmSemanticKind::FarmCropVarietyCreate, 432 "FarmCropVarietyCreate", 433 ), 434 (FarmSemanticKind::FarmCropCycleCreate, "FarmCropCycleCreate"), 435 ( 436 FarmSemanticKind::FarmMemberInviteCreate, 437 "FarmMemberInviteCreate", 438 ), 439 (FarmSemanticKind::FarmMemberApprove, "FarmMemberApprove"), 440 (FarmSemanticKind::FarmMemberRoleSet, "FarmMemberRoleSet"), 441 ( 442 FarmSemanticKind::FarmMemberDeactivate, 443 "FarmMemberDeactivate", 444 ), 445 (FarmSemanticKind::FarmPayPeriodOpen, "FarmPayPeriodOpen"), 446 (FarmSemanticKind::FarmPayPeriodClose, "FarmPayPeriodClose"), 447 ( 448 FarmSemanticKind::FarmReportExportMark, 449 "FarmReportExportMark", 450 ), 451 ( 452 FarmSemanticKind::FarmInventoryItemUpdate, 453 "FarmInventoryItemUpdate", 454 ), 455 ( 456 FarmSemanticKind::FarmMediaAssetAttach, 457 "FarmMediaAssetAttach", 458 ), 459 ( 460 FarmSemanticKind::FarmObservationCreate, 461 "FarmObservationCreate", 462 ), 463 (FarmSemanticKind::FarmWorkspaceUpdate, "FarmWorkspaceUpdate"), 464 ] { 465 let encoded = serde_json::to_string(&kind).unwrap(); 466 assert_eq!(encoded, format!("\"{expected}\"")); 467 let decoded: FarmSemanticKind = serde_json::from_str(&encoded).unwrap(); 468 assert_eq!(decoded, kind); 469 } 470 } 471 472 #[test] 473 fn unknown_crdt_kinds_roundtrip_as_other_strings() { 474 let document_kind: FarmCrdtDocumentKind = serde_json::from_str("\"FarmSoilTest\"").unwrap(); 475 let semantic_kind: FarmSemanticKind = 476 serde_json::from_str("\"FarmSoilTestCreate\"").unwrap(); 477 478 assert_eq!( 479 document_kind, 480 FarmCrdtDocumentKind::Other { 481 value: "FarmSoilTest".to_string() 482 } 483 ); 484 assert_eq!( 485 semantic_kind, 486 FarmSemanticKind::Other { 487 value: "FarmSoilTestCreate".to_string() 488 } 489 ); 490 assert_eq!(document_kind.as_str(), "FarmSoilTest"); 491 assert_eq!(semantic_kind.as_str(), "FarmSoilTestCreate"); 492 assert_eq!( 493 serde_json::to_string(&document_kind).unwrap(), 494 "\"FarmSoilTest\"" 495 ); 496 assert_eq!( 497 serde_json::to_string(&semantic_kind).unwrap(), 498 "\"FarmSoilTestCreate\"" 499 ); 500 } 501 502 fn sample_change() -> FarmCrdtChange { 503 FarmCrdtChange { 504 schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(), 505 workspace: FarmWorkspaceRef { 506 pubkey: "workspace_pubkey".to_string(), 507 d_tag: "ABCDEFGHIJKLMNOPQRSTUV".to_string(), 508 }, 509 farm_group_id: "BCDEFGHIJKLMNOPQRSTUVW".to_string(), 510 document_id: "DEFGHIJKLMNOPQRSTUVWXY".to_string(), 511 document_kind: FarmCrdtDocumentKind::FarmTask, 512 crdt_backend: CrdtBackend::Automerge, 513 crdt_backend_version: Some("0.x".to_string()), 514 actor_id: "actor_abc".to_string(), 515 change_hash: "crdt_hash_abc".to_string(), 516 dependencies: Vec::new(), 517 encoded_change: "base64url-encoded-change".to_string(), 518 semantic_kind: FarmSemanticKind::FarmTaskCreate, 519 business_time_ms: 1_780_000_000_000, 520 author_member_id: Some("member_abc".to_string()), 521 app_version: Some("0.1.0".to_string()), 522 } 523 } 524 }