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