1use heddle_object_model::object::{
3 CollaborationMention as Mention, CollaborationRecordKind as Kind, ContentHash, StateId,
4};
5use uuid::Uuid;
6
7use crate::{contract::*, transport::Error};
8
9fn spool(value: Option<&SpoolRef>) -> Result<Uuid, Error> {
10 let id = value
11 .ok_or(Error::Protocol("mention requires spool"))?
12 .id
13 .parse::<Uuid>()
14 .map_err(|_| Error::Protocol("mention spool must be UUID"))?;
15 if id.is_nil() {
16 return Err(Error::Protocol("mention spool cannot be nil"));
17 }
18 Ok(id)
19}
20fn key(value: &[u8]) -> Result<[u8; 32], Error> {
21 value
22 .try_into()
23 .map_err(|_| Error::Protocol("mention identity must contain 32 bytes"))
24}
25fn endpoint(value: Option<&EndpointRef>) -> Result<[u8; 32], Error> {
26 let endpoint = value.ok_or(Error::Protocol("device mention requires endpoint"))?;
27 if endpoint.kind != EndpointKind::Device as i32 {
28 return Err(Error::Protocol("device mention must target a device"));
29 }
30 key(&endpoint.public_key)
31}
32pub fn mention(value: &EntityRef) -> Result<Mention, Error> {
33 use entity_ref::Entity;
34 Ok(
35 match value
36 .entity
37 .as_ref()
38 .ok_or(Error::Protocol("empty mention"))?
39 {
40 Entity::Spool(r) => Mention::Spool {
41 spool: spool(Some(r))?,
42 },
43 Entity::Thread(r) => Mention::Thread {
44 spool: spool(r.spool.as_ref())?,
45 thread: ContentHash::from_bytes(key(&r
46 .id
47 .as_ref()
48 .ok_or(Error::Protocol("Thread mention requires ID"))?
49 .value)?),
50 },
51 Entity::Checkout(r) => Mention::Checkout {
52 spool: spool(r.spool.as_ref())?,
53 device: endpoint(r.device.as_ref())?,
54 id: r.id.clone(),
55 },
56 Entity::Revision(r) => match r
57 .revision
58 .as_ref()
59 .ok_or(Error::Protocol("revision mention requires exact revision"))?
60 {
61 revision_ref::Revision::State(id) => Mention::State {
62 spool: spool(r.spool.as_ref())?,
63 state: StateId::from_bytes(key(&id.value)?),
64 },
65 revision_ref::Revision::GitCommitOid(oid) => Mention::GitCommit {
66 spool: spool(r.spool.as_ref())?,
67 oid: oid.clone(),
68 },
69 },
70 Entity::Device(r) => Mention::Device {
71 key: endpoint(Some(r))?,
72 },
73 Entity::Discussion(r) => record(r, Kind::Discussion)?,
74 Entity::Context(r) => record(r, Kind::Context)?,
75 Entity::Operation(r) => record(r, Kind::Operation)?,
76 Entity::Run(r) => record(r, Kind::Run)?,
77 Entity::Policy(r) => record(r, Kind::Policy)?,
78 Entity::Analysis(r) => record(r, Kind::Analysis)?,
79 Entity::Invitation(r) => record(r, Kind::Invitation)?,
80 Entity::Grant(r) => record(r, Kind::Grant)?,
81 Entity::DiscussionTurn(r) => record(r, Kind::DiscussionTurn)?,
82 Entity::Review(r) => record(r, Kind::Review)?,
83 Entity::Notification(r) => record(r, Kind::Notification)?,
84 Entity::AttentionItem(r) => record(r, Kind::AttentionItem)?,
85 Entity::Member(r) => record(r, Kind::Member)?,
86 Entity::ApprovalGroup(r) => record(r, Kind::ApprovalGroup)?,
87 Entity::Session(r) => record(r, Kind::Session)?,
88 Entity::SignupInvitation(r) => record(r, Kind::SignupInvitation)?,
89 Entity::TimelineEvent(r) => record(r, Kind::TimelineEvent)?,
90 Entity::Artifact(r) => record(r, Kind::Artifact)?,
91 Entity::Mount(r) => record(r, Kind::Mount)?,
92 Entity::SupportAccess(r) => record(r, Kind::SupportAccess)?,
93 Entity::DeviceRecord(r) => record(r, Kind::DeviceRecord)?,
94 Entity::Delegation(r) => record(r, Kind::Delegation)?,
95 Entity::Recovery(r) => record(r, Kind::Recovery)?,
96 Entity::OwnerTransition(r) => record(r, Kind::OwnerTransition)?,
97 Entity::Billing(r) => record(r, Kind::Billing)?,
98 Entity::Evidence(r) => record(r, Kind::Evidence)?,
99 Entity::CheckAcknowledgement(r) => record(r, Kind::CheckAcknowledgement)?,
100 Entity::ProviderConnection(r) => record(r, Kind::ProviderConnection)?,
101 Entity::RemoteLink(r) => record(r, Kind::RemoteLink)?,
102 Entity::Bookmark(_) => {
103 return Err(Error::Protocol(
104 "bookmarks are account-private; mention their Spool or Thread instead",
105 ));
106 }
107 Entity::Passkey(_) => {
108 return Err(Error::Protocol(
109 "passkeys are account-private credentials and cannot be mentioned",
110 ));
111 }
112 Entity::Principal(_) | Entity::Agent(_) => {
113 return Err(Error::Protocol(
114 "principal and agent references require an explicit mention contract",
115 ));
116 }
117 },
118 )
119}
120fn record(r: &RecordRef, record_kind: Kind) -> Result<Mention, Error> {
121 Ok(Mention::Record {
122 spool: r.spool.as_ref().map(|r| spool(Some(r))).transpose()?,
123 record_kind,
124 id: r.id.clone(),
125 })
126}
127fn wire_spool(id: Uuid) -> Option<SpoolRef> {
128 Some(SpoolRef { id: id.to_string() })
129}
130fn wire_device(key: &[u8; 32]) -> EndpointRef {
131 EndpointRef {
132 kind: EndpointKind::Device as i32,
133 public_key: key.to_vec(),
134 }
135}
136pub fn mention_ref(value: &Mention) -> EntityRef {
137 use entity_ref::Entity;
138 let entity = match value {
139 Mention::Spool { spool } => Entity::Spool(SpoolRef {
140 id: spool.to_string(),
141 }),
142 Mention::Thread { spool, thread } => Entity::Thread(ThreadRef {
143 spool: wire_spool(*spool),
144 id: Some(ThreadId {
145 value: thread.as_bytes().to_vec(),
146 }),
147 }),
148 Mention::Checkout { spool, device, id } => Entity::Checkout(CheckoutRef {
149 spool: wire_spool(*spool),
150 device: Some(wire_device(device)),
151 id: id.clone(),
152 }),
153 Mention::State { spool, state } => Entity::Revision(RevisionRef {
154 spool: wire_spool(*spool),
155 revision: Some(revision_ref::Revision::State(
156 api::heddle::api::common::StateId {
157 value: state.as_bytes().to_vec(),
158 },
159 )),
160 }),
161 Mention::GitCommit { spool, oid } => Entity::Revision(RevisionRef {
162 spool: wire_spool(*spool),
163 revision: Some(revision_ref::Revision::GitCommitOid(oid.clone())),
164 }),
165 Mention::Device { key } => Entity::Device(wire_device(key)),
166 Mention::Record {
167 spool,
168 record_kind,
169 id,
170 } => {
171 let r = RecordRef {
172 spool: spool.and_then(wire_spool),
173 id: id.clone(),
174 };
175 match record_kind {
176 Kind::Discussion => Entity::Discussion(r),
177 Kind::Context => Entity::Context(r),
178 Kind::Operation => Entity::Operation(r),
179 Kind::Run => Entity::Run(r),
180 Kind::Policy => Entity::Policy(r),
181 Kind::Analysis => Entity::Analysis(r),
182 Kind::Invitation => Entity::Invitation(r),
183 Kind::Grant => Entity::Grant(r),
184 Kind::DiscussionTurn => Entity::DiscussionTurn(r),
185 Kind::Review => Entity::Review(r),
186 Kind::Notification => Entity::Notification(r),
187 Kind::AttentionItem => Entity::AttentionItem(r),
188 Kind::Member => Entity::Member(r),
189 Kind::ApprovalGroup => Entity::ApprovalGroup(r),
190 Kind::Session => Entity::Session(r),
191 Kind::SignupInvitation => Entity::SignupInvitation(r),
192 Kind::TimelineEvent => Entity::TimelineEvent(r),
193 Kind::Artifact => Entity::Artifact(r),
194 Kind::Mount => Entity::Mount(r),
195 Kind::SupportAccess => Entity::SupportAccess(r),
196 Kind::DeviceRecord => Entity::DeviceRecord(r),
197 Kind::Delegation => Entity::Delegation(r),
198 Kind::Recovery => Entity::Recovery(r),
199 Kind::OwnerTransition => Entity::OwnerTransition(r),
200 Kind::Billing => Entity::Billing(r),
201 Kind::Evidence => Entity::Evidence(r),
202 Kind::CheckAcknowledgement => Entity::CheckAcknowledgement(r),
203 Kind::ProviderConnection => Entity::ProviderConnection(r),
204 Kind::RemoteLink => Entity::RemoteLink(r),
205 }
206 }
207 };
208 EntityRef {
209 entity: Some(entity),
210 }
211}
212
213pub(super) fn source_target(
214 value: &SourceTargetReference,
215) -> Result<heddle_object_model::object::source_target::SourceTargetReference, Error> {
216 use heddle_object_model::object::{
217 CollaborationRevision, CollaborationScope,
218 source_target::{SourceTargetBinding, SourceTargetReference as Target},
219 };
220 fn named(value: &ThreadRef) -> Result<CollaborationScope, Error> {
221 Ok(CollaborationScope {
222 spool: spool(value.spool.as_ref())?,
223 thread: Some(ContentHash::from_bytes(key(&value
224 .id
225 .as_ref()
226 .ok_or(Error::Protocol("target requires Thread ID"))?
227 .value)?)),
228 })
229 }
230 let binding = match value
231 .binding
232 .as_ref()
233 .ok_or(Error::Protocol("explicit source target binding required"))?
234 {
235 source_target_reference::Binding::ViewedThread(true) => SourceTargetBinding::ViewedThread,
236 source_target_reference::Binding::ViewedThread(false) => {
237 return Err(Error::Protocol("viewed Thread binding must be true"));
238 }
239 source_target_reference::Binding::NamedThread(value) => SourceTargetBinding::NamedThread {
240 scope: named(value)?,
241 },
242 source_target_reference::Binding::PinnedRevision(value) => {
243 let revision = value
244 .revision
245 .as_ref()
246 .ok_or(Error::Protocol("pinned target requires revision"))?;
247 let spool = spool(revision.spool.as_ref())?;
248 let scope = match &value.thread {
249 Some(thread) => {
250 let scope = named(thread)?;
251 if scope.spool != spool {
252 return Err(Error::Protocol(
253 "pinned target Thread belongs to another Spool",
254 ));
255 }
256 scope
257 }
258 None => CollaborationScope {
259 spool,
260 thread: None,
261 },
262 };
263 let revision = match revision
264 .revision
265 .as_ref()
266 .ok_or(Error::Protocol("pinned target requires exact revision"))?
267 {
268 revision_ref::Revision::State(id) => CollaborationRevision::State {
269 state_id: StateId::from_bytes(key(&id.value)?),
270 },
271 revision_ref::Revision::GitCommitOid(oid) => {
272 CollaborationRevision::GitCommit { oid: oid.clone() }
273 }
274 };
275 SourceTargetBinding::PinnedRevision { scope, revision }
276 }
277 };
278 let target = Target {
279 target: ContentHash::from_bytes(key(&value.target_id)?),
280 binding,
281 };
282 target
283 .validate()
284 .map_err(|_| Error::Protocol("invalid source target binding"))?;
285 Ok(target)
286}
287pub fn source_target_ref(
288 value: &heddle_object_model::object::source_target::SourceTargetReference,
289) -> SourceTargetReference {
290 use heddle_object_model::object::{
291 CollaborationRevision, CollaborationScope, source_target::SourceTargetBinding,
292 };
293 fn thread(scope: &CollaborationScope) -> Option<ThreadRef> {
294 scope.thread.map(|id| ThreadRef {
295 spool: wire_spool(scope.spool),
296 id: Some(ThreadId {
297 value: id.as_bytes().to_vec(),
298 }),
299 })
300 }
301 let binding = match &value.binding {
302 SourceTargetBinding::ViewedThread => source_target_reference::Binding::ViewedThread(true),
303 SourceTargetBinding::NamedThread { scope } => {
304 source_target_reference::Binding::NamedThread(ThreadRef {
305 spool: wire_spool(scope.spool),
306 id: scope.thread.map(|id| ThreadId {
307 value: id.as_bytes().to_vec(),
308 }),
309 })
310 }
311 SourceTargetBinding::PinnedRevision { scope, revision } => {
312 source_target_reference::Binding::PinnedRevision(PinnedSourceTargetRevision {
313 thread: thread(scope),
314 revision: Some(RevisionRef {
315 spool: wire_spool(scope.spool),
316 revision: Some(match revision {
317 CollaborationRevision::State { state_id } => {
318 revision_ref::Revision::State(api::heddle::api::common::StateId {
319 value: state_id.as_bytes().to_vec(),
320 })
321 }
322 CollaborationRevision::GitCommit { oid } => {
323 revision_ref::Revision::GitCommitOid(oid.clone())
324 }
325 }),
326 }),
327 })
328 }
329 };
330 SourceTargetReference {
331 target_id: value.target.as_bytes().to_vec(),
332 binding: Some(binding),
333 }
334}
335
336pub fn anchor(
338 value: &CollaborationAnchor,
339 scope: &heddle_object_model::object::CollaborationScope,
340) -> Result<heddle_object_model::object::CollaborationAnchor, Error> {
341 use heddle_object_model::object::{
342 CollaborationAnchor as Anchor, CollaborationRevision, CollaborationSourceAnchor,
343 };
344 fn thread(
345 value: &ThreadRef,
346 scope: &heddle_object_model::object::CollaborationScope,
347 ) -> Result<(), Error> {
348 if spool(value.spool.as_ref())? != scope.spool
349 || Some(ContentHash::from_bytes(key(&value
350 .id
351 .as_ref()
352 .ok_or(Error::Protocol("anchor Thread requires ID"))?
353 .value)?))
354 != scope.thread
355 {
356 return Err(Error::Protocol("anchor belongs to another Thread or spool"));
357 }
358 Ok(())
359 }
360 Ok(
361 match value
362 .target
363 .as_ref()
364 .ok_or(Error::Protocol("collaboration anchor required"))?
365 {
366 collaboration_anchor::Target::Thread(value) => {
367 thread(value, scope)?;
368 Anchor::Repository
369 }
370 collaboration_anchor::Target::Spool(value) => {
371 if scope.thread.is_some() || spool(Some(value))? != scope.spool {
372 return Err(Error::Protocol(
373 "spool anchor requires independent spool scope",
374 ));
375 }
376 Anchor::Repository
377 }
378 collaboration_anchor::Target::Source(value) => {
379 reject_path_kind(value)?;
380 thread(
381 value
382 .thread
383 .as_ref()
384 .ok_or(Error::Protocol("source anchor requires owning Thread"))?,
385 scope,
386 )?;
387 let revision = value
388 .revision
389 .as_ref()
390 .ok_or(Error::Protocol("source anchor requires exact revision"))?;
391 if spool(revision.spool.as_ref())? != scope.spool {
392 return Err(Error::Protocol("source revision belongs to another spool"));
393 }
394 let revision = match revision
395 .revision
396 .as_ref()
397 .ok_or(Error::Protocol("exact revision required"))?
398 {
399 revision_ref::Revision::State(id) => CollaborationRevision::State {
400 state_id: StateId::from_bytes(key(&id.value)?),
401 },
402 revision_ref::Revision::GitCommitOid(oid) => {
403 CollaborationRevision::GitCommit { oid: oid.clone() }
404 }
405 };
406 Anchor::Source {
407 source: CollaborationSourceAnchor {
408 revision,
409 path: value.path.clone(),
410 symbol_id: value.symbol_id.clone(),
411 start_line: value.start_line,
412 end_line: value.end_line,
413 target: value.target.as_ref().map(source_target).transpose()?,
414 },
415 }
416 }
417 },
418 )
419}
420
421pub(crate) fn reject_path_kind(value: &SourceAnchor) -> Result<(), Error> {
425 if value.path_kind != 0 || value.path_kind_source != 0 {
426 return Err(Error::Protocol(
427 "source anchor path_kind is not supported yet (heddle#1959)",
428 ));
429 }
430 Ok(())
431}
432
433pub fn visibility(
436 audience: i32,
437 label: &str,
438) -> Result<heddle_object_model::object::VisibilityTier, Error> {
439 use heddle_object_model::object::VisibilityTier as Tier;
440 match Audience::try_from(audience) {
441 Ok(Audience::Public) if label.is_empty() => Ok(Tier::Public),
442 Ok(Audience::Members) if label.is_empty() => Ok(Tier::Internal),
443 Ok(Audience::Private)
444 if !label.trim().is_empty()
445 && label.len() <= 512
446 && !label.chars().any(char::is_control) =>
447 {
448 Ok(Tier::Private {
449 scope_label: label.into(),
450 })
451 }
452 _ => Err(Error::Protocol(
453 "explicit audience and matching private label required",
454 )),
455 }
456}
457pub fn audience(tier: &heddle_object_model::object::VisibilityTier) -> Option<(Audience, String)> {
460 use heddle_object_model::object::VisibilityTier as Tier;
461 match tier {
462 Tier::Public => Some((Audience::Public, String::new())),
463 Tier::Internal => Some((Audience::Members, String::new())),
464 Tier::Private { scope_label } => Some((Audience::Private, scope_label.clone())),
465 Tier::TeamScoped { .. } | Tier::Restricted { .. } => None,
466 }
467}
468
469pub fn anchor_ref(
472 value: &heddle_object_model::object::CollaborationAnchor,
473 scope: &heddle_object_model::object::CollaborationScope,
474) -> Result<CollaborationAnchor, Error> {
475 use heddle_object_model::object::{
476 CollaborationAnchor as Anchor, CollaborationRevision as Revision,
477 };
478 let thread = scope.thread.map(|id| ThreadRef {
479 spool: wire_spool(scope.spool),
480 id: Some(ThreadId {
481 value: id.as_bytes().to_vec(),
482 }),
483 });
484 let source = match value {
485 Anchor::Repository => {
486 return Ok(CollaborationAnchor {
487 target: Some(match thread {
488 Some(thread) => collaboration_anchor::Target::Thread(thread),
489 None => collaboration_anchor::Target::Spool(SpoolRef {
490 id: scope.spool.to_string(),
491 }),
492 }),
493 });
494 }
495 Anchor::Source { source } => SourceAnchor {
496 revision: Some(RevisionRef {
497 spool: wire_spool(scope.spool),
498 revision: Some(match &source.revision {
499 Revision::State { state_id } => {
500 revision_ref::Revision::State(api::heddle::api::common::StateId {
501 value: state_id.as_bytes().to_vec(),
502 })
503 }
504 Revision::GitCommit { oid } => {
505 revision_ref::Revision::GitCommitOid(oid.clone())
506 }
507 }),
508 }),
509 path: source.path.clone(),
510 symbol_id: source.symbol_id.clone(),
511 start_line: source.start_line,
512 end_line: source.end_line,
513 target: source.target.as_ref().map(source_target_ref),
514 thread,
515 path_kind: api::heddle::api::v1alpha2::SourcePathKind::Unspecified as i32,
517 path_kind_source: api::heddle::api::v1alpha2::SourcePathKindSource::Unspecified as i32,
518 },
519 Anchor::State { state_id }
520 | Anchor::Path { state_id, .. }
521 | Anchor::Symbol { state_id, .. } => SourceAnchor {
522 revision: Some(RevisionRef {
523 spool: wire_spool(scope.spool),
524 revision: Some(revision_ref::Revision::State(
525 api::heddle::api::common::StateId {
526 value: state_id.as_bytes().to_vec(),
527 },
528 )),
529 }),
530 path: match value {
531 Anchor::Path { path, .. } | Anchor::Symbol { path, .. } => path.clone(),
532 _ => String::new(),
533 },
534 symbol_id: match value {
535 Anchor::Symbol { symbol, .. } => symbol.clone(),
536 _ => String::new(),
537 },
538 start_line: None,
539 end_line: None,
540 target: None,
541 thread,
542 path_kind: api::heddle::api::v1alpha2::SourcePathKind::Unspecified as i32,
543 path_kind_source: api::heddle::api::v1alpha2::SourcePathKindSource::Unspecified as i32,
544 },
545 Anchor::Change { .. } => {
546 return Err(Error::Protocol(
547 "change anchor requires exact source revision",
548 ));
549 }
550 };
551 Ok(CollaborationAnchor {
552 target: Some(collaboration_anchor::Target::Source(source)),
553 })
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 #[test]
560 fn explicit_target_bindings_round_trip_and_reject_incomplete_identity() {
561 use heddle_object_model::object::{
562 CollaborationRevision, CollaborationScope,
563 source_target::{SourceTargetBinding, SourceTargetReference as Target},
564 };
565 let scope = CollaborationScope {
566 spool: Uuid::from_u128(1),
567 thread: Some(ContentHash::from_bytes([2; 32])),
568 };
569 for binding in [
570 SourceTargetBinding::ViewedThread,
571 SourceTargetBinding::NamedThread {
572 scope: scope.clone(),
573 },
574 SourceTargetBinding::PinnedRevision {
575 scope: scope.clone(),
576 revision: CollaborationRevision::GitCommit {
577 oid: "a".repeat(40),
578 },
579 },
580 SourceTargetBinding::PinnedRevision {
581 scope: CollaborationScope {
582 thread: None,
583 ..scope.clone()
584 },
585 revision: CollaborationRevision::State {
586 state_id: StateId::from_bytes([5; 32]),
587 },
588 },
589 ] {
590 let target = Target {
591 target: ContentHash::from_bytes([6; 32]),
592 binding,
593 };
594 assert_eq!(
595 source_target(&source_target_ref(&target)).expect("lossless binding"),
596 target
597 );
598 let source = heddle_object_model::object::CollaborationSourceAnchor {
599 revision: CollaborationRevision::State {
600 state_id: StateId::from_bytes([5; 32]),
601 },
602 path: "src/main.rs".into(),
603 symbol_id: "run".into(),
604 start_line: Some(12),
605 end_line: Some(18),
606 target: Some(target),
607 };
608 let original = heddle_object_model::object::CollaborationAnchor::Source {
609 source: source.clone(),
610 };
611 assert_eq!(
612 anchor(&anchor_ref(&original, &scope).expect("wire anchor"), &scope)
613 .expect("canonical anchor"),
614 original
615 );
616 let reference = heddle_object_model::object::AnnotationSourceReference {
617 scope: scope.clone(),
618 source,
619 };
620 assert_eq!(
621 super::super::annotation_source(&super::super::annotation_source_ref(&reference))
622 .expect("canonical tag reference"),
623 reference
624 );
625 }
626 for binding in [
627 None,
628 Some(source_target_reference::Binding::ViewedThread(false)),
629 Some(source_target_reference::Binding::NamedThread(ThreadRef {
630 spool: wire_spool(Uuid::from_u128(1)),
631 id: None,
632 })),
633 Some(source_target_reference::Binding::PinnedRevision(
634 PinnedSourceTargetRevision {
635 revision: None,
636 thread: None,
637 },
638 )),
639 ] {
640 assert!(
641 source_target(&SourceTargetReference {
642 target_id: vec![6; 32],
643 binding
644 })
645 .is_err()
646 );
647 }
648 assert!(
649 source_target(&SourceTargetReference {
650 target_id: vec![6; 31],
651 binding: Some(source_target_reference::Binding::ViewedThread(true))
652 })
653 .is_err()
654 );
655 }
656 #[test]
657 fn every_native_mention_retains_kind_scope_and_identity() {
658 let spool = Uuid::from_u128(1);
659 let mut mentions = vec![
660 Mention::Spool { spool },
661 Mention::Thread {
662 spool,
663 thread: ContentHash::from_bytes([2; 32]),
664 },
665 Mention::State {
666 spool,
667 state: StateId::from_bytes([3; 32]),
668 },
669 Mention::GitCommit {
670 spool,
671 oid: "a".repeat(40),
672 },
673 Mention::Checkout {
674 spool,
675 device: [4; 32],
676 id: "checkout".into(),
677 },
678 Mention::Device { key: [5; 32] },
679 ];
680 for record_kind in [
681 Kind::Discussion,
682 Kind::Context,
683 Kind::Operation,
684 Kind::Run,
685 Kind::Policy,
686 Kind::Analysis,
687 Kind::Invitation,
688 Kind::Grant,
689 Kind::DiscussionTurn,
690 Kind::Review,
691 Kind::Notification,
692 Kind::AttentionItem,
693 Kind::Member,
694 Kind::ApprovalGroup,
695 Kind::Session,
696 Kind::SignupInvitation,
697 Kind::TimelineEvent,
698 Kind::Artifact,
699 Kind::Mount,
700 Kind::SupportAccess,
701 Kind::DeviceRecord,
702 Kind::Delegation,
703 Kind::Recovery,
704 Kind::OwnerTransition,
705 Kind::Billing,
706 Kind::Evidence,
707 Kind::CheckAcknowledgement,
708 Kind::ProviderConnection,
709 Kind::RemoteLink,
710 ] {
711 for spool in [None, Some(spool)] {
712 mentions.push(Mention::Record {
713 spool,
714 record_kind,
715 id: "record".into(),
716 });
717 }
718 }
719 assert_eq!(mentions.len(), 64);
720 for original in mentions {
721 assert_eq!(
722 mention(&mention_ref(&original)).expect("typed mention"),
723 original
724 );
725 }
726 assert!(
727 mention(&EntityRef {
728 entity: Some(entity_ref::Entity::Device(EndpointRef {
729 kind: EndpointKind::Weft as i32,
730 public_key: vec![5; 32]
731 }))
732 })
733 .is_err()
734 );
735 }
736 #[test]
737 fn source_anchor_binds_owning_thread_and_preserves_lines() {
738 use heddle_object_model::object::{CollaborationAnchor as Anchor, CollaborationScope};
739 let scope = CollaborationScope {
740 spool: Uuid::from_u128(1),
741 thread: Some(ContentHash::from_bytes([2; 32])),
742 };
743 let thread = ThreadRef {
744 spool: wire_spool(scope.spool),
745 id: Some(ThreadId { value: vec![2; 32] }),
746 };
747 let mut source = SourceAnchor {
748 revision: Some(RevisionRef {
749 spool: wire_spool(scope.spool),
750 revision: Some(revision_ref::Revision::GitCommitOid("a".repeat(40))),
751 }),
752 path: "src/main.rs".into(),
753 symbol_id: "run".into(),
754 start_line: Some(12),
755 end_line: Some(18),
756 target: None,
757 thread: Some(thread),
758 path_kind: 0,
759 path_kind_source: 0,
760 };
761 let value = CollaborationAnchor {
762 target: Some(collaboration_anchor::Target::Source(source.clone())),
763 };
764 let Anchor::Source { source: actual } =
765 anchor(&value, &scope).expect("bound source anchor")
766 else {
767 panic!("source");
768 };
769 assert_eq!(
770 (
771 actual.path.as_str(),
772 actual.symbol_id.as_str(),
773 actual.start_line,
774 actual.end_line
775 ),
776 ("src/main.rs", "run", Some(12), Some(18))
777 );
778 assert_eq!(
779 anchor_ref(&Anchor::Source { source: actual }, &scope).expect("source projection"),
780 value,
781 "source fields survive both directions"
782 );
783 source
784 .thread
785 .as_mut()
786 .expect("Thread")
787 .id
788 .as_mut()
789 .expect("ID")
790 .value[0] ^= 1;
791 assert!(
792 anchor(
793 &CollaborationAnchor {
794 target: Some(collaboration_anchor::Target::Source(source))
795 },
796 &scope
797 )
798 .is_err(),
799 "source must not escape the Thread"
800 );
801 }
802 #[test]
803 fn set_source_path_kind_is_rejected_not_dropped() {
804 use heddle_object_model::object::CollaborationScope;
805 let scope = CollaborationScope {
806 spool: Uuid::from_u128(1),
807 thread: Some(ContentHash::from_bytes([2; 32])),
808 };
809 let source = SourceAnchor {
810 revision: Some(RevisionRef {
811 spool: wire_spool(scope.spool),
812 revision: Some(revision_ref::Revision::GitCommitOid("a".repeat(40))),
813 }),
814 path: "src/main.rs".into(),
815 thread: Some(ThreadRef {
816 spool: wire_spool(scope.spool),
817 id: Some(ThreadId { value: vec![2; 32] }),
818 }),
819 ..Default::default()
820 };
821 let convert = |source: &SourceAnchor| {
822 (
823 anchor(
824 &CollaborationAnchor {
825 target: Some(collaboration_anchor::Target::Source(source.clone())),
826 },
827 &scope,
828 ),
829 super::super::tags::annotation_source(&AnnotationSourceReference {
830 source: Some(source.clone()),
831 }),
832 )
833 };
834 let (native, tag) = convert(&source);
835 native.expect("an unset kind converts");
836 tag.expect("an unset kind converts in a source tag");
837 for (kind, provenance) in [
838 (SourcePathKind::File, SourcePathKindSource::Recorded),
839 (SourcePathKind::Directory, SourcePathKindSource::Derived),
840 (SourcePathKind::File, SourcePathKindSource::Unspecified),
841 (SourcePathKind::Unspecified, SourcePathKindSource::Recorded),
842 ] {
843 let set = SourceAnchor {
844 path_kind: kind as i32,
845 path_kind_source: provenance as i32,
846 ..source.clone()
847 };
848 let (native, tag) = convert(&set);
849 for result in [native.map(|_| ()), tag.map(|_| ())] {
850 assert!(
851 matches!(result, Err(Error::Protocol(message)) if message.contains("heddle#1959")),
852 "{kind:?}/{provenance:?} must be refused, not dropped"
853 );
854 }
855 }
856 }
857 #[test]
858 fn audience_mapping_is_explicit_and_lossless() {
859 use heddle_object_model::object::VisibilityTier as Tier;
860 for (kind, label) in [
861 (Audience::Public, ""),
862 (Audience::Members, ""),
863 (Audience::Private, "review-team"),
864 ] {
865 let tier = visibility(kind as i32, label).expect("valid audience");
866 assert_eq!(audience(&tier), Some((kind, label.into())));
867 }
868 for (kind, label) in [
869 (Audience::Unspecified, ""),
870 (Audience::Private, ""),
871 (Audience::Public, "review-team"),
872 (Audience::Members, "review-team"),
873 ] {
874 assert!(
875 visibility(kind as i32, label).is_err(),
876 "must not change audience or fabricate label"
877 );
878 }
879 assert!(
880 audience(&Tier::TeamScoped {
881 team_id: "team".into()
882 })
883 .is_none()
884 );
885 assert!(
886 audience(&Tier::Restricted {
887 scope_label: "restricted".into()
888 })
889 .is_none()
890 );
891 }
892}