heddle_object_model/object/collaboration/
context.rs1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use super::{
7 CollaborationAnchor, CollaborationCodecError, CollaborationMetadata, DiscussionRecordId,
8};
9use crate::object::{AnnotationKind, ContentHash, StateId};
10
11pub const CONTEXT_FORMAT: &str = "heddle-context-revision-v2";
12
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(deny_unknown_fields)]
18pub struct ContextProvenance {
19 pub revision_id: String,
20 pub kind: AnnotationKind,
21 pub attribution: String,
22 pub source_hash: Option<ContentHash>,
23 pub created_at_state: Option<StateId>,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct ContextRevision {
29 pub version: u16,
30 pub id: Uuid,
31 pub parents: Vec<ContentHash>,
33 pub metadata: CollaborationMetadata,
34 pub anchor: CollaborationAnchor,
35 pub content: String,
36 pub tags: Vec<super::AnnotationTag>,
37 pub supersedes: Option<Uuid>,
38 pub extracted_from: Option<DiscussionRecordId>,
39 pub occurred_at_ms: i64,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub provenance: Option<ContextProvenance>,
44}
45impl ContextRevision {
46 pub fn encode(&self) -> Result<Vec<u8>, CollaborationCodecError> {
47 self.metadata.validate()?;
48 super::validate_annotation_tags(&self.tags)?;
49 super::operation::validate_anchor(&self.anchor)?;
50 if self.version != 2
51 || self.id.is_nil()
52 || self
53 .supersedes
54 .is_some_and(|id| id.is_nil() || id == self.id)
55 || self.content.trim().is_empty()
56 || self.content.len() > 256 * 1024
57 || self.parents.len() > 128
58 || self.parents.windows(2).any(|pair| pair[0] >= pair[1])
59 {
60 return Err(CollaborationCodecError::Invalid(
61 "invalid or unbounded context revision".into(),
62 ));
63 }
64 rmp_serde::to_vec_named(self).map_err(|e| CollaborationCodecError::Encoding(e.to_string()))
65 }
66 pub fn decode(bytes: &[u8]) -> Result<Self, CollaborationCodecError> {
67 if bytes.len() > 512 * 1024 {
68 return Err(CollaborationCodecError::Invalid(
69 "context revision exceeds record bound".into(),
70 ));
71 }
72 let record: Self = rmp_serde::from_slice(bytes)
73 .map_err(|e| CollaborationCodecError::Decoding(e.to_string()))?;
74 if record.encode()? != bytes {
75 return Err(CollaborationCodecError::Invalid(
76 "context revision is not canonical".into(),
77 ));
78 }
79 Ok(record)
80 }
81 pub fn id(&self) -> Result<ContentHash, CollaborationCodecError> {
82 Ok(ContentHash::compute_typed(CONTEXT_FORMAT, &self.encode()?))
83 }
84}