lib

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

farm_crdt.rs (22904B)


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