lib

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

commit 72df2e9646a144290af1c538ed033adab9418089
parent 4c72c964eed14fafd1cfdbe832d8b3003119878b
Author: triesap <tyson@radroots.org>
Date:   Fri, 12 Jun 2026 16:02:29 -0700

events: expand farm crdt contract

- add full farm CRDT document and semantic kind vocabularies
- preserve string serde shape with unknown Other variants
- roundtrip representative farm operations CRDT semantics
- align farm file owner document kind encoding and decoding

Diffstat:
Mcrates/events/src/farm_crdt.rs | 459++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/events_codec/src/farm_crdt/mod.rs | 109++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/events_codec/src/farm_file/decode.rs | 12++++--------
Mcrates/events_codec/src/farm_file/encode.rs | 17+++++------------
Mcrates/events_codec/src/farm_file/mod.rs | 26++++++++++++++++++++++++++
5 files changed, 597 insertions(+), 26 deletions(-)

diff --git a/crates/events/src/farm_crdt.rs b/crates/events/src/farm_crdt.rs @@ -4,7 +4,10 @@ use crate::farm_workspace::RadrootsFarmWorkspaceRef; use crate::kinds::KIND_FARM_CRDT_CHANGE as KIND_FARM_CRDT_CHANGE_EVENT; #[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; pub const KIND_FARM_CRDT_CHANGE: u32 = KIND_FARM_CRDT_CHANGE_EVENT; pub const RADROOTS_FARM_CRDT_CHANGE_SCHEMA: &str = "radroots.farm.crdt.change.v1"; @@ -31,14 +34,25 @@ pub struct RadrootsFarmCrdtChange { } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", serde(from = "String", into = "String"))] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsFarmCrdtDocumentKind { + FarmMembership, + FarmRolePolicy, FarmTask, FarmWorkSession, + FarmActivity, FarmHarvestRecord, + FarmLocation, + FarmCrop, + FarmCropVariety, + FarmCropCycle, + FarmAttachment, + FarmPayPeriod, FarmInventoryItem, FarmMediaAsset, FarmObservation, + Other { value: String }, } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -50,20 +64,216 @@ pub enum RadrootsCrdtBackend { } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", serde(from = "String", into = "String"))] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsFarmSemanticKind { FarmTaskCreate, + FarmTaskAssign, + FarmTaskJoin, + FarmTaskStatusSet, + FarmTaskChecklistItemAdd, + FarmTaskCommentAdd, + FarmTaskAttachmentAttach, + FarmTaskFollowUpCreate, FarmTaskUpdate, FarmTaskComplete, FarmWorkSessionStart, + FarmWorkSessionStop, + FarmWorkSessionSubmit, + FarmWorkSessionManualEntryCreate, + FarmWorkSessionApprove, + FarmWorkSessionReject, + FarmWorkSessionCorrect, FarmWorkSessionUpdate, FarmWorkSessionEnd, FarmHarvestRecordCreate, + FarmHarvestLineAdd, + FarmHarvestLineCorrect, + FarmHarvestLineVoid, + FarmHarvestAttachmentAttach, FarmHarvestRecordUpdate, + FarmActivityCreate, + FarmNoteCreate, + FarmLocationCreate, + FarmCropCreate, + FarmCropVarietyCreate, + FarmCropCycleCreate, + FarmMemberInviteCreate, + FarmMemberApprove, + FarmMemberRoleSet, + FarmMemberDeactivate, + FarmPayPeriodOpen, + FarmPayPeriodClose, + FarmReportExportMark, FarmInventoryItemUpdate, FarmMediaAssetAttach, FarmObservationCreate, FarmWorkspaceUpdate, + Other { value: String }, +} + +impl RadrootsFarmCrdtDocumentKind { + pub fn as_str(&self) -> &str { + match self { + Self::FarmMembership => "FarmMembership", + Self::FarmRolePolicy => "FarmRolePolicy", + Self::FarmTask => "FarmTask", + Self::FarmWorkSession => "FarmWorkSession", + Self::FarmActivity => "FarmActivity", + Self::FarmHarvestRecord => "FarmHarvestRecord", + Self::FarmLocation => "FarmLocation", + Self::FarmCrop => "FarmCrop", + Self::FarmCropVariety => "FarmCropVariety", + Self::FarmCropCycle => "FarmCropCycle", + Self::FarmAttachment => "FarmAttachment", + Self::FarmPayPeriod => "FarmPayPeriod", + Self::FarmInventoryItem => "FarmInventoryItem", + Self::FarmMediaAsset => "FarmMediaAsset", + Self::FarmObservation => "FarmObservation", + Self::Other { value } => value.as_str(), + } + } +} + +impl From<String> for RadrootsFarmCrdtDocumentKind { + fn from(value: String) -> Self { + match value.as_str() { + "FarmMembership" => Self::FarmMembership, + "FarmRolePolicy" => Self::FarmRolePolicy, + "FarmTask" => Self::FarmTask, + "FarmWorkSession" => Self::FarmWorkSession, + "FarmActivity" => Self::FarmActivity, + "FarmHarvestRecord" => Self::FarmHarvestRecord, + "FarmLocation" => Self::FarmLocation, + "FarmCrop" => Self::FarmCrop, + "FarmCropVariety" => Self::FarmCropVariety, + "FarmCropCycle" => Self::FarmCropCycle, + "FarmAttachment" => Self::FarmAttachment, + "FarmPayPeriod" => Self::FarmPayPeriod, + "FarmInventoryItem" => Self::FarmInventoryItem, + "FarmMediaAsset" => Self::FarmMediaAsset, + "FarmObservation" => Self::FarmObservation, + _ => Self::Other { value }, + } + } +} + +impl From<RadrootsFarmCrdtDocumentKind> for String { + fn from(value: RadrootsFarmCrdtDocumentKind) -> Self { + match value { + RadrootsFarmCrdtDocumentKind::Other { value } => value, + value => value.as_str().to_string(), + } + } +} + +impl RadrootsFarmSemanticKind { + pub fn as_str(&self) -> &str { + match self { + Self::FarmTaskCreate => "FarmTaskCreate", + Self::FarmTaskAssign => "FarmTaskAssign", + Self::FarmTaskJoin => "FarmTaskJoin", + Self::FarmTaskStatusSet => "FarmTaskStatusSet", + Self::FarmTaskChecklistItemAdd => "FarmTaskChecklistItemAdd", + Self::FarmTaskCommentAdd => "FarmTaskCommentAdd", + Self::FarmTaskAttachmentAttach => "FarmTaskAttachmentAttach", + Self::FarmTaskFollowUpCreate => "FarmTaskFollowUpCreate", + Self::FarmTaskUpdate => "FarmTaskUpdate", + Self::FarmTaskComplete => "FarmTaskComplete", + Self::FarmWorkSessionStart => "FarmWorkSessionStart", + Self::FarmWorkSessionStop => "FarmWorkSessionStop", + Self::FarmWorkSessionSubmit => "FarmWorkSessionSubmit", + Self::FarmWorkSessionManualEntryCreate => "FarmWorkSessionManualEntryCreate", + Self::FarmWorkSessionApprove => "FarmWorkSessionApprove", + Self::FarmWorkSessionReject => "FarmWorkSessionReject", + Self::FarmWorkSessionCorrect => "FarmWorkSessionCorrect", + Self::FarmWorkSessionUpdate => "FarmWorkSessionUpdate", + Self::FarmWorkSessionEnd => "FarmWorkSessionEnd", + Self::FarmHarvestRecordCreate => "FarmHarvestRecordCreate", + Self::FarmHarvestLineAdd => "FarmHarvestLineAdd", + Self::FarmHarvestLineCorrect => "FarmHarvestLineCorrect", + Self::FarmHarvestLineVoid => "FarmHarvestLineVoid", + Self::FarmHarvestAttachmentAttach => "FarmHarvestAttachmentAttach", + Self::FarmHarvestRecordUpdate => "FarmHarvestRecordUpdate", + Self::FarmActivityCreate => "FarmActivityCreate", + Self::FarmNoteCreate => "FarmNoteCreate", + Self::FarmLocationCreate => "FarmLocationCreate", + Self::FarmCropCreate => "FarmCropCreate", + Self::FarmCropVarietyCreate => "FarmCropVarietyCreate", + Self::FarmCropCycleCreate => "FarmCropCycleCreate", + Self::FarmMemberInviteCreate => "FarmMemberInviteCreate", + Self::FarmMemberApprove => "FarmMemberApprove", + Self::FarmMemberRoleSet => "FarmMemberRoleSet", + Self::FarmMemberDeactivate => "FarmMemberDeactivate", + Self::FarmPayPeriodOpen => "FarmPayPeriodOpen", + Self::FarmPayPeriodClose => "FarmPayPeriodClose", + Self::FarmReportExportMark => "FarmReportExportMark", + Self::FarmInventoryItemUpdate => "FarmInventoryItemUpdate", + Self::FarmMediaAssetAttach => "FarmMediaAssetAttach", + Self::FarmObservationCreate => "FarmObservationCreate", + Self::FarmWorkspaceUpdate => "FarmWorkspaceUpdate", + Self::Other { value } => value.as_str(), + } + } +} + +impl From<String> for RadrootsFarmSemanticKind { + fn from(value: String) -> Self { + match value.as_str() { + "FarmTaskCreate" => Self::FarmTaskCreate, + "FarmTaskAssign" => Self::FarmTaskAssign, + "FarmTaskJoin" => Self::FarmTaskJoin, + "FarmTaskStatusSet" => Self::FarmTaskStatusSet, + "FarmTaskChecklistItemAdd" => Self::FarmTaskChecklistItemAdd, + "FarmTaskCommentAdd" => Self::FarmTaskCommentAdd, + "FarmTaskAttachmentAttach" => Self::FarmTaskAttachmentAttach, + "FarmTaskFollowUpCreate" => Self::FarmTaskFollowUpCreate, + "FarmTaskUpdate" => Self::FarmTaskUpdate, + "FarmTaskComplete" => Self::FarmTaskComplete, + "FarmWorkSessionStart" => Self::FarmWorkSessionStart, + "FarmWorkSessionStop" => Self::FarmWorkSessionStop, + "FarmWorkSessionSubmit" => Self::FarmWorkSessionSubmit, + "FarmWorkSessionManualEntryCreate" => Self::FarmWorkSessionManualEntryCreate, + "FarmWorkSessionApprove" => Self::FarmWorkSessionApprove, + "FarmWorkSessionReject" => Self::FarmWorkSessionReject, + "FarmWorkSessionCorrect" => Self::FarmWorkSessionCorrect, + "FarmWorkSessionUpdate" => Self::FarmWorkSessionUpdate, + "FarmWorkSessionEnd" => Self::FarmWorkSessionEnd, + "FarmHarvestRecordCreate" => Self::FarmHarvestRecordCreate, + "FarmHarvestLineAdd" => Self::FarmHarvestLineAdd, + "FarmHarvestLineCorrect" => Self::FarmHarvestLineCorrect, + "FarmHarvestLineVoid" => Self::FarmHarvestLineVoid, + "FarmHarvestAttachmentAttach" => Self::FarmHarvestAttachmentAttach, + "FarmHarvestRecordUpdate" => Self::FarmHarvestRecordUpdate, + "FarmActivityCreate" => Self::FarmActivityCreate, + "FarmNoteCreate" => Self::FarmNoteCreate, + "FarmLocationCreate" => Self::FarmLocationCreate, + "FarmCropCreate" => Self::FarmCropCreate, + "FarmCropVarietyCreate" => Self::FarmCropVarietyCreate, + "FarmCropCycleCreate" => Self::FarmCropCycleCreate, + "FarmMemberInviteCreate" => Self::FarmMemberInviteCreate, + "FarmMemberApprove" => Self::FarmMemberApprove, + "FarmMemberRoleSet" => Self::FarmMemberRoleSet, + "FarmMemberDeactivate" => Self::FarmMemberDeactivate, + "FarmPayPeriodOpen" => Self::FarmPayPeriodOpen, + "FarmPayPeriodClose" => Self::FarmPayPeriodClose, + "FarmReportExportMark" => Self::FarmReportExportMark, + "FarmInventoryItemUpdate" => Self::FarmInventoryItemUpdate, + "FarmMediaAssetAttach" => Self::FarmMediaAssetAttach, + "FarmObservationCreate" => Self::FarmObservationCreate, + "FarmWorkspaceUpdate" => Self::FarmWorkspaceUpdate, + _ => Self::Other { value }, + } + } +} + +impl From<RadrootsFarmSemanticKind> for String { + fn from(value: RadrootsFarmSemanticKind) -> Self { + match value { + RadrootsFarmSemanticKind::Other { value } => value, + value => value.as_str().to_string(), + } + } } #[cfg(all(test, feature = "serde"))] @@ -107,6 +317,249 @@ mod tests { assert_eq!(value["business_time_ms"], 1_780_000_000_000_u64); } + #[test] + fn document_kinds_serialize_as_stable_strings() { + for (kind, expected) in [ + ( + RadrootsFarmCrdtDocumentKind::FarmMembership, + "FarmMembership", + ), + ( + RadrootsFarmCrdtDocumentKind::FarmRolePolicy, + "FarmRolePolicy", + ), + (RadrootsFarmCrdtDocumentKind::FarmTask, "FarmTask"), + ( + RadrootsFarmCrdtDocumentKind::FarmWorkSession, + "FarmWorkSession", + ), + (RadrootsFarmCrdtDocumentKind::FarmActivity, "FarmActivity"), + ( + RadrootsFarmCrdtDocumentKind::FarmHarvestRecord, + "FarmHarvestRecord", + ), + (RadrootsFarmCrdtDocumentKind::FarmLocation, "FarmLocation"), + (RadrootsFarmCrdtDocumentKind::FarmCrop, "FarmCrop"), + ( + RadrootsFarmCrdtDocumentKind::FarmCropVariety, + "FarmCropVariety", + ), + (RadrootsFarmCrdtDocumentKind::FarmCropCycle, "FarmCropCycle"), + ( + RadrootsFarmCrdtDocumentKind::FarmAttachment, + "FarmAttachment", + ), + (RadrootsFarmCrdtDocumentKind::FarmPayPeriod, "FarmPayPeriod"), + ( + RadrootsFarmCrdtDocumentKind::FarmInventoryItem, + "FarmInventoryItem", + ), + ( + RadrootsFarmCrdtDocumentKind::FarmMediaAsset, + "FarmMediaAsset", + ), + ( + RadrootsFarmCrdtDocumentKind::FarmObservation, + "FarmObservation", + ), + ] { + let encoded = serde_json::to_string(&kind).unwrap(); + assert_eq!(encoded, format!("\"{expected}\"")); + let decoded: RadrootsFarmCrdtDocumentKind = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decoded, kind); + } + } + + #[test] + fn semantic_kinds_serialize_as_stable_strings() { + for (kind, expected) in [ + (RadrootsFarmSemanticKind::FarmTaskCreate, "FarmTaskCreate"), + (RadrootsFarmSemanticKind::FarmTaskAssign, "FarmTaskAssign"), + (RadrootsFarmSemanticKind::FarmTaskJoin, "FarmTaskJoin"), + ( + RadrootsFarmSemanticKind::FarmTaskStatusSet, + "FarmTaskStatusSet", + ), + ( + RadrootsFarmSemanticKind::FarmTaskChecklistItemAdd, + "FarmTaskChecklistItemAdd", + ), + ( + RadrootsFarmSemanticKind::FarmTaskCommentAdd, + "FarmTaskCommentAdd", + ), + ( + RadrootsFarmSemanticKind::FarmTaskAttachmentAttach, + "FarmTaskAttachmentAttach", + ), + ( + RadrootsFarmSemanticKind::FarmTaskFollowUpCreate, + "FarmTaskFollowUpCreate", + ), + (RadrootsFarmSemanticKind::FarmTaskUpdate, "FarmTaskUpdate"), + ( + RadrootsFarmSemanticKind::FarmTaskComplete, + "FarmTaskComplete", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionStart, + "FarmWorkSessionStart", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionStop, + "FarmWorkSessionStop", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionSubmit, + "FarmWorkSessionSubmit", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionManualEntryCreate, + "FarmWorkSessionManualEntryCreate", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionApprove, + "FarmWorkSessionApprove", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionReject, + "FarmWorkSessionReject", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionCorrect, + "FarmWorkSessionCorrect", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionUpdate, + "FarmWorkSessionUpdate", + ), + ( + RadrootsFarmSemanticKind::FarmWorkSessionEnd, + "FarmWorkSessionEnd", + ), + ( + RadrootsFarmSemanticKind::FarmHarvestRecordCreate, + "FarmHarvestRecordCreate", + ), + ( + RadrootsFarmSemanticKind::FarmHarvestLineAdd, + "FarmHarvestLineAdd", + ), + ( + RadrootsFarmSemanticKind::FarmHarvestLineCorrect, + "FarmHarvestLineCorrect", + ), + ( + RadrootsFarmSemanticKind::FarmHarvestLineVoid, + "FarmHarvestLineVoid", + ), + ( + RadrootsFarmSemanticKind::FarmHarvestAttachmentAttach, + "FarmHarvestAttachmentAttach", + ), + ( + RadrootsFarmSemanticKind::FarmHarvestRecordUpdate, + "FarmHarvestRecordUpdate", + ), + ( + RadrootsFarmSemanticKind::FarmActivityCreate, + "FarmActivityCreate", + ), + (RadrootsFarmSemanticKind::FarmNoteCreate, "FarmNoteCreate"), + ( + RadrootsFarmSemanticKind::FarmLocationCreate, + "FarmLocationCreate", + ), + (RadrootsFarmSemanticKind::FarmCropCreate, "FarmCropCreate"), + ( + RadrootsFarmSemanticKind::FarmCropVarietyCreate, + "FarmCropVarietyCreate", + ), + ( + RadrootsFarmSemanticKind::FarmCropCycleCreate, + "FarmCropCycleCreate", + ), + ( + RadrootsFarmSemanticKind::FarmMemberInviteCreate, + "FarmMemberInviteCreate", + ), + ( + RadrootsFarmSemanticKind::FarmMemberApprove, + "FarmMemberApprove", + ), + ( + RadrootsFarmSemanticKind::FarmMemberRoleSet, + "FarmMemberRoleSet", + ), + ( + RadrootsFarmSemanticKind::FarmMemberDeactivate, + "FarmMemberDeactivate", + ), + ( + RadrootsFarmSemanticKind::FarmPayPeriodOpen, + "FarmPayPeriodOpen", + ), + ( + RadrootsFarmSemanticKind::FarmPayPeriodClose, + "FarmPayPeriodClose", + ), + ( + RadrootsFarmSemanticKind::FarmReportExportMark, + "FarmReportExportMark", + ), + ( + RadrootsFarmSemanticKind::FarmInventoryItemUpdate, + "FarmInventoryItemUpdate", + ), + ( + RadrootsFarmSemanticKind::FarmMediaAssetAttach, + "FarmMediaAssetAttach", + ), + ( + RadrootsFarmSemanticKind::FarmObservationCreate, + "FarmObservationCreate", + ), + ( + RadrootsFarmSemanticKind::FarmWorkspaceUpdate, + "FarmWorkspaceUpdate", + ), + ] { + let encoded = serde_json::to_string(&kind).unwrap(); + assert_eq!(encoded, format!("\"{expected}\"")); + let decoded: RadrootsFarmSemanticKind = serde_json::from_str(&encoded).unwrap(); + assert_eq!(decoded, kind); + } + } + + #[test] + fn unknown_crdt_kinds_roundtrip_as_other_strings() { + let document_kind: RadrootsFarmCrdtDocumentKind = + serde_json::from_str("\"FarmSoilTest\"").unwrap(); + let semantic_kind: RadrootsFarmSemanticKind = + serde_json::from_str("\"FarmSoilTestCreate\"").unwrap(); + + assert_eq!( + document_kind, + RadrootsFarmCrdtDocumentKind::Other { + value: "FarmSoilTest".to_string() + } + ); + assert_eq!( + semantic_kind, + RadrootsFarmSemanticKind::Other { + value: "FarmSoilTestCreate".to_string() + } + ); + assert_eq!( + serde_json::to_string(&document_kind).unwrap(), + "\"FarmSoilTest\"" + ); + assert_eq!( + serde_json::to_string(&semantic_kind).unwrap(), + "\"FarmSoilTestCreate\"" + ); + } + fn sample_change() -> RadrootsFarmCrdtChange { RadrootsFarmCrdtChange { schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(), diff --git a/crates/events_codec/src/farm_crdt/mod.rs b/crates/events_codec/src/farm_crdt/mod.rs @@ -59,6 +59,93 @@ mod tests { } #[test] + fn farm_crdt_change_roundtrips_representative_mvp_semantics() { + let cases = vec![ + ( + RadrootsFarmCrdtDocumentKind::FarmTask, + RadrootsFarmSemanticKind::FarmTaskCreate, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmTask, + RadrootsFarmSemanticKind::FarmTaskStatusSet, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmWorkSession, + RadrootsFarmSemanticKind::FarmWorkSessionStart, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmWorkSession, + RadrootsFarmSemanticKind::FarmWorkSessionStop, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmWorkSession, + RadrootsFarmSemanticKind::FarmWorkSessionSubmit, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmWorkSession, + RadrootsFarmSemanticKind::FarmWorkSessionApprove, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmWorkSession, + RadrootsFarmSemanticKind::FarmWorkSessionReject, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmWorkSession, + RadrootsFarmSemanticKind::FarmWorkSessionCorrect, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmHarvestRecord, + RadrootsFarmSemanticKind::FarmHarvestLineAdd, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmHarvestRecord, + RadrootsFarmSemanticKind::FarmHarvestLineCorrect, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmHarvestRecord, + RadrootsFarmSemanticKind::FarmHarvestLineVoid, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmMembership, + RadrootsFarmSemanticKind::FarmMemberInviteCreate, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmMembership, + RadrootsFarmSemanticKind::FarmMemberApprove, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmMembership, + RadrootsFarmSemanticKind::FarmMemberRoleSet, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmPayPeriod, + RadrootsFarmSemanticKind::FarmPayPeriodClose, + ), + ( + RadrootsFarmCrdtDocumentKind::FarmPayPeriod, + RadrootsFarmSemanticKind::FarmReportExportMark, + ), + ]; + + for (index, (document_kind, semantic_kind)) in cases.into_iter().enumerate() { + let document_id = document_id(index); + let change = sample_change_with(document_id.as_str(), document_kind, semantic_kind); + let parts = to_wire_parts_with_author(&change, AUTHOR).expect("crdt wire parts"); + let decoded = farm_crdt_change_from_event_with_author( + parts.kind, + &parts.tags, + &parts.content, + AUTHOR, + ) + .expect("crdt decode"); + + assert_eq!(decoded.document_id, document_id); + assert_eq!(decoded.document_kind, change.document_kind); + assert_eq!(decoded.semantic_kind, change.semantic_kind); + } + } + + #[test] fn farm_crdt_change_rejects_missing_t_and_d_mismatch() { let parts = to_wire_parts(&sample_change()).expect("crdt wire parts"); let without_t = parts @@ -164,6 +251,18 @@ mod tests { } fn sample_change() -> RadrootsFarmCrdtChange { + sample_change_with( + DOCUMENT_ID, + RadrootsFarmCrdtDocumentKind::FarmTask, + RadrootsFarmSemanticKind::FarmTaskCreate, + ) + } + + fn sample_change_with( + document_id: &str, + document_kind: RadrootsFarmCrdtDocumentKind, + semantic_kind: RadrootsFarmSemanticKind, + ) -> RadrootsFarmCrdtChange { RadrootsFarmCrdtChange { schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(), workspace: RadrootsFarmWorkspaceRef { @@ -171,21 +270,25 @@ mod tests { d_tag: WORKSPACE_D_TAG.to_string(), }, farm_group_id: GROUP_ID.to_string(), - document_id: DOCUMENT_ID.to_string(), - document_kind: RadrootsFarmCrdtDocumentKind::FarmTask, + document_id: document_id.to_string(), + document_kind, crdt_backend: RadrootsCrdtBackend::Automerge, crdt_backend_version: Some("0.x".to_string()), actor_id: "actor_abc".to_string(), change_hash: "crdt_hash_abc".to_string(), dependencies: Vec::new(), encoded_change: "abc-DEF_012".to_string(), - semantic_kind: RadrootsFarmSemanticKind::FarmTaskCreate, + semantic_kind, business_time_ms: 1_780_000_000_000, author_member_id: Some("member_abc".to_string()), app_version: Some("0.1.0".to_string()), } } + fn document_id(index: usize) -> String { + format!("{index:02}AAAAAAAAAAAAAAAAAAAA") + } + fn tag(key: &str, value: &str) -> Vec<String> { vec![key.to_string(), value.to_string()] } diff --git a/crates/events_codec/src/farm_file/decode.rs b/crates/events_codec/src/farm_file/decode.rs @@ -187,14 +187,10 @@ fn parse_owner_document( } fn parse_document_kind_tag(value: &str) -> Result<RadrootsFarmCrdtDocumentKind, EventParseError> { - match value { - "FarmTask" => Ok(RadrootsFarmCrdtDocumentKind::FarmTask), - "FarmWorkSession" => Ok(RadrootsFarmCrdtDocumentKind::FarmWorkSession), - "FarmHarvestRecord" => Ok(RadrootsFarmCrdtDocumentKind::FarmHarvestRecord), - "FarmInventoryItem" => Ok(RadrootsFarmCrdtDocumentKind::FarmInventoryItem), - "FarmMediaAsset" => Ok(RadrootsFarmCrdtDocumentKind::FarmMediaAsset), - "FarmObservation" => Ok(RadrootsFarmCrdtDocumentKind::FarmObservation), - _ => Err(EventParseError::InvalidTag(TAG_OWNER_DOCUMENT)), + if value.trim().is_empty() { + Err(EventParseError::InvalidTag(TAG_OWNER_DOCUMENT)) + } else { + Ok(RadrootsFarmCrdtDocumentKind::from(value.to_string())) } } diff --git a/crates/events_codec/src/farm_file/encode.rs b/crates/events_codec/src/farm_file/encode.rs @@ -53,9 +53,9 @@ pub fn farm_file_metadata_build_tags( push_tag_values( &mut tags, TAG_OWNER_DOCUMENT, - [ - metadata.owner_document_id.as_str(), - document_kind_tag(metadata.owner_document_kind), + vec![ + metadata.owner_document_id.clone(), + document_kind_tag(&metadata.owner_document_kind), ], ); push_optional_tag( @@ -135,15 +135,8 @@ pub(crate) fn validate_metadata( Ok(()) } -pub(crate) fn document_kind_tag(kind: RadrootsFarmCrdtDocumentKind) -> &'static str { - match kind { - RadrootsFarmCrdtDocumentKind::FarmTask => "FarmTask", - RadrootsFarmCrdtDocumentKind::FarmWorkSession => "FarmWorkSession", - RadrootsFarmCrdtDocumentKind::FarmHarvestRecord => "FarmHarvestRecord", - RadrootsFarmCrdtDocumentKind::FarmInventoryItem => "FarmInventoryItem", - RadrootsFarmCrdtDocumentKind::FarmMediaAsset => "FarmMediaAsset", - RadrootsFarmCrdtDocumentKind::FarmObservation => "FarmObservation", - } +pub(crate) fn document_kind_tag(kind: &RadrootsFarmCrdtDocumentKind) -> String { + kind.as_str().to_string() } fn validate_dimensions( diff --git a/crates/events_codec/src/farm_file/mod.rs b/crates/events_codec/src/farm_file/mod.rs @@ -117,6 +117,32 @@ mod tests { assert_eq!(decoded.caption, None); } + #[test] + fn farm_file_metadata_preserves_expanded_owner_document_kinds() { + for kind in [ + RadrootsFarmCrdtDocumentKind::FarmMembership, + RadrootsFarmCrdtDocumentKind::FarmRolePolicy, + RadrootsFarmCrdtDocumentKind::FarmActivity, + RadrootsFarmCrdtDocumentKind::FarmLocation, + RadrootsFarmCrdtDocumentKind::FarmCrop, + RadrootsFarmCrdtDocumentKind::FarmCropVariety, + RadrootsFarmCrdtDocumentKind::FarmCropCycle, + RadrootsFarmCrdtDocumentKind::FarmAttachment, + RadrootsFarmCrdtDocumentKind::FarmPayPeriod, + RadrootsFarmCrdtDocumentKind::Other { + value: "FarmSoilTest".to_string(), + }, + ] { + let mut metadata = sample_metadata(); + metadata.owner_document_kind = kind; + let parts = to_wire_parts(&metadata).expect("file metadata wire parts"); + let decoded = farm_file_metadata_from_event(parts.kind, &parts.tags, &parts.content) + .expect("file metadata decode"); + + assert_eq!(decoded.owner_document_kind, metadata.owner_document_kind); + } + } + fn sample_metadata() -> RadrootsFarmFileMetadata { RadrootsFarmFileMetadata { d_tag: FILE_D_TAG.to_string(),