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:
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(),