lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }