1#[allow(deprecated)]
6use crate::{
7 AuthorityGrant, CredentialSubject, CredentialSubjectAuthority, CredentialSubjectBasic,
8 CredentialSubjectDelegation, CredentialSubjectEndorsement, CredentialSubjectMembership,
9 CredentialSubjectRCard, CredentialSubjectWitness, DTGCommon, DTGCredential, DTGCredentialError,
10 DTGCredentialType, DelegationGrant, WitnessContext,
11};
12use chrono::{DateTime, Utc};
13use serde_json::Value;
14
15impl DTGCredential {
16 pub fn new_vmc(
40 issuer: String,
41 subject: String,
42 valid_from: DateTime<Utc>,
43 valid_until: Option<DateTime<Utc>>,
44 personhood: bool,
45 ) -> Self {
46 let mut vmc = DTGCommon {
47 issuer,
48 valid_from,
49 valid_until,
50 credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
51 id: subject,
52 digest_multibase: None,
53 }),
54 ..Default::default()
55 };
56
57 vmc.type_.push(DTGCredentialType::Membership.to_string());
58
59 if personhood {
60 vmc.type_.push("PersonhoodCredential".to_string());
61 }
62
63 DTGCredential {
64 credential: vmc,
65 type_: DTGCredentialType::Membership,
66 version: crate::W3CVCVersion::V2_0,
67 }
68 }
69
70 pub fn new_member_vmc(
109 grant: &Value,
110 valid_from: DateTime<Utc>,
111 valid_until: Option<DateTime<Utc>>,
112 ) -> Result<Self, DTGCredentialError> {
113 let object = grant
114 .as_object()
115 .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("not a JSON object".into()))?;
116
117 let is_membership = object
118 .get("type")
119 .and_then(Value::as_array)
120 .is_some_and(|types| {
121 types
122 .iter()
123 .filter_map(Value::as_str)
124 .any(|t| t == "MembershipCredential")
125 });
126 if !is_membership {
127 return Err(DTGCredentialError::NotAMembershipGrant(
128 "`type` does not include `MembershipCredential`".into(),
129 ));
130 }
131
132 let subject = object
133 .get("credentialSubject")
134 .and_then(Value::as_object)
135 .ok_or_else(|| {
136 DTGCredentialError::NotAMembershipGrant("no `credentialSubject`".into())
137 })?;
138
139 if subject.contains_key("digestMultibase") || subject.contains_key("digest") {
144 return Err(DTGCredentialError::NotAMembershipGrant(
145 "the credential carries a digest of another credential, so it is itself a \
146 member-issued acknowledgement rather than a community-issued grant"
147 .into(),
148 ));
149 }
150
151 let member = subject
156 .get("id")
157 .and_then(Value::as_str)
158 .ok_or_else(|| {
159 DTGCredentialError::NotAMembershipGrant("no `credentialSubject.id`".into())
160 })?
161 .to_string();
162
163 let community = object
165 .get("issuer")
166 .and_then(|i| {
167 i.as_str()
168 .map(str::to_string)
169 .or_else(|| i.get("id").and_then(Value::as_str).map(str::to_string))
170 })
171 .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("no `issuer`".into()))?;
172
173 let mut vmc = DTGCommon {
174 issuer: member,
175 valid_from,
176 valid_until,
177 credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
178 id: community,
179 digest_multibase: Some(crate::digest_multibase_json(grant)?),
180 }),
181 ..Default::default()
182 };
183
184 vmc.type_.push(DTGCredentialType::Membership.to_string());
185
186 Ok(DTGCredential {
187 credential: vmc,
188 type_: DTGCredentialType::Membership,
189 version: crate::W3CVCVersion::V2_0,
190 })
191 }
192
193 pub fn new_vrc(
199 issuer: String,
200 subject: String,
201 valid_from: DateTime<Utc>,
202 valid_until: Option<DateTime<Utc>>,
203 ) -> Self {
204 let mut vrc = DTGCommon {
205 issuer,
206 valid_from,
207 valid_until,
208 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
209 ..Default::default()
210 };
211
212 vrc.type_.push(DTGCredentialType::Relationship.to_string());
213
214 DTGCredential {
215 credential: vrc,
216 type_: DTGCredentialType::Relationship,
217 version: crate::W3CVCVersion::V2_0,
218 }
219 }
220
221 pub fn new_vic(
227 issuer: String,
228 subject: String,
229 valid_from: DateTime<Utc>,
230 valid_until: Option<DateTime<Utc>>,
231 ) -> Self {
232 let mut vic = DTGCommon {
233 issuer,
234 valid_from,
235 valid_until,
236 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
237 ..Default::default()
238 };
239
240 vic.type_.push(DTGCredentialType::Invitation.to_string());
241
242 DTGCredential {
243 credential: vic,
244 type_: DTGCredentialType::Invitation,
245 version: crate::W3CVCVersion::V2_0,
246 }
247 }
248
249 pub fn new_vac(
265 issuer: String,
266 subject: String,
267 scope: String,
268 actions: Vec<String>,
269 valid_from: DateTime<Utc>,
270 valid_until: DateTime<Utc>,
271 ) -> Result<Self, DTGCredentialError> {
272 if actions.is_empty() {
273 return Err(DTGCredentialError::EmptyAuthorityActions);
274 }
275 let mut vac = DTGCommon {
276 issuer,
277 valid_from,
278 valid_until: Some(valid_until),
279 credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
280 id: subject,
281 authority: AuthorityGrant {
282 scope,
283 actions,
284 parent: None,
285 },
286 }),
287 ..Default::default()
288 };
289
290 vac.type_.push(DTGCredentialType::Authority.to_string());
291
292 Ok(DTGCredential {
293 credential: vac,
294 type_: DTGCredentialType::Authority,
295 version: crate::W3CVCVersion::V2_0,
296 })
297 }
298
299 pub fn attenuate(
332 &self,
333 subject: String,
334 actions: Vec<String>,
335 valid_from: DateTime<Utc>,
336 valid_until: DateTime<Utc>,
337 ) -> Result<Self, DTGCredentialError> {
338 let parent_grant = self
339 .credential()
340 .authority()
341 .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
342
343 Self::attenuate_inner(
344 parent_grant.clone(),
345 self.credential().subject().to_string(),
346 self.credential().valid_until(),
347 self.digest_multibase()?,
348 subject,
349 actions,
350 valid_from,
351 valid_until,
352 )
353 }
354
355 pub fn attenuate_from_json(
369 parent: &Value,
370 subject: String,
371 actions: Vec<String>,
372 valid_from: DateTime<Utc>,
373 valid_until: DateTime<Utc>,
374 ) -> Result<Self, DTGCredentialError> {
375 let object = parent
376 .as_object()
377 .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
378
379 let is_authority = object
380 .get("type")
381 .and_then(Value::as_array)
382 .is_some_and(|types| {
383 types
384 .iter()
385 .filter_map(Value::as_str)
386 .any(|t| t == "AuthorityCredential")
387 });
388 if !is_authority {
389 return Err(DTGCredentialError::NotAnAuthorityCredential);
390 }
391
392 let parent_subject = object
393 .get("credentialSubject")
394 .and_then(Value::as_object)
395 .ok_or(DTGCredentialError::NotAnAuthorityCredential)?;
396
397 let holder = parent_subject
400 .get("id")
401 .and_then(Value::as_str)
402 .ok_or(DTGCredentialError::NotAnAuthorityCredential)?
403 .to_string();
404
405 let parent_grant: AuthorityGrant = parent_subject
406 .get("authority")
407 .ok_or(DTGCredentialError::NotAnAuthorityCredential)
408 .and_then(|a| {
409 serde_json::from_value(a.clone())
410 .map_err(|_| DTGCredentialError::NotAnAuthorityCredential)
411 })?;
412
413 let parent_until = object
414 .get("validUntil")
415 .or_else(|| object.get("expirationDate"))
416 .and_then(Value::as_str)
417 .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
418 .map(|t| t.with_timezone(&Utc));
419
420 Self::attenuate_inner(
421 parent_grant,
422 holder,
423 parent_until,
424 crate::digest_multibase_json(parent)?,
425 subject,
426 actions,
427 valid_from,
428 valid_until,
429 )
430 }
431
432 #[allow(clippy::too_many_arguments)]
434 fn attenuate_inner(
435 parent_grant: AuthorityGrant,
436 holder: String,
437 parent_until: Option<DateTime<Utc>>,
438 parent_digest: String,
439 subject: String,
440 actions: Vec<String>,
441 valid_from: DateTime<Utc>,
442 valid_until: DateTime<Utc>,
443 ) -> Result<Self, DTGCredentialError> {
444 if actions.is_empty() {
445 return Err(DTGCredentialError::EmptyAuthorityActions);
446 }
447 for action in &actions {
448 if !parent_grant.actions.contains(action) {
449 return Err(DTGCredentialError::AttenuationWidens(format!(
450 "action `{action}` is not conferred by the parent"
451 )));
452 }
453 }
454 if let Some(parent_until) = parent_until
455 && valid_until > parent_until
456 {
457 return Err(DTGCredentialError::AttenuationWidens(format!(
458 "validUntil {valid_until} is beyond the parent's {parent_until}"
459 )));
460 }
461
462 let mut vac = DTGCommon {
463 issuer: holder,
465 valid_from,
466 valid_until: Some(valid_until),
467 credential_subject: CredentialSubject::Authority(CredentialSubjectAuthority {
468 id: subject,
469 authority: AuthorityGrant {
470 scope: parent_grant.scope.clone(),
472 actions,
473 parent: Some(parent_digest),
474 },
475 }),
476 ..Default::default()
477 };
478
479 vac.type_.push(DTGCredentialType::Authority.to_string());
480
481 Ok(DTGCredential {
482 credential: vac,
483 type_: DTGCredentialType::Authority,
484 version: crate::W3CVCVersion::V2_0,
485 })
486 }
487
488 pub fn new_vdc(
530 issuer: String,
531 subject: String,
532 valid_from: DateTime<Utc>,
533 valid_until: DateTime<Utc>,
534 scope: Vec<String>,
535 max_depth: Option<u32>,
536 ) -> Result<Self, DTGCredentialError> {
537 if scope.is_empty() {
538 return Err(DTGCredentialError::MalformedDelegation(
539 "a grant MUST carry at least one `scope` entry — a VDC cannot express an \
540 unbounded appointment by emptying it"
541 .into(),
542 ));
543 }
544
545 let mut vdc = DTGCommon {
546 issuer,
547 valid_from,
548 valid_until: Some(valid_until),
549 credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
550 id: subject,
551 delegation: DelegationGrant {
552 scope: Some(scope),
553 parent: None,
554 max_depth,
555 accepts: None,
556 },
557 }),
558 ..Default::default()
559 };
560
561 vdc.type_.push(DTGCredentialType::Delegation.to_string());
562
563 Ok(DTGCredential {
564 credential: vdc,
565 type_: DTGCredentialType::Delegation,
566 version: crate::W3CVCVersion::V2_0,
567 })
568 }
569
570 pub fn redelegate(
590 &self,
591 subject: String,
592 scope: Vec<String>,
593 valid_from: DateTime<Utc>,
594 valid_until: DateTime<Utc>,
595 ) -> Result<Self, DTGCredentialError> {
596 let parent = self.credential().delegation().ok_or_else(|| {
597 DTGCredentialError::MalformedDelegation("not a DelegationCredential".into())
598 })?;
599
600 Self::redelegate_inner(
601 parent.clone(),
602 self.credential().subject().to_string(),
603 self.credential().valid_until(),
604 self.digest_multibase()?,
605 subject,
606 scope,
607 valid_from,
608 valid_until,
609 )
610 }
611
612 pub fn redelegate_from_json(
618 parent: &Value,
619 subject: String,
620 scope: Vec<String>,
621 valid_from: DateTime<Utc>,
622 valid_until: DateTime<Utc>,
623 ) -> Result<Self, DTGCredentialError> {
624 let (delegate, grant, parent_until) = Self::read_delegation_json(parent)?;
625
626 Self::redelegate_inner(
627 grant,
628 delegate,
629 parent_until,
630 crate::digest_multibase_json(parent)?,
631 subject,
632 scope,
633 valid_from,
634 valid_until,
635 )
636 }
637
638 #[allow(clippy::too_many_arguments)]
640 fn redelegate_inner(
641 parent_grant: DelegationGrant,
642 holder: String,
643 parent_until: Option<DateTime<Utc>>,
644 parent_digest: String,
645 subject: String,
646 scope: Vec<String>,
647 valid_from: DateTime<Utc>,
648 valid_until: DateTime<Utc>,
649 ) -> Result<Self, DTGCredentialError> {
650 if parent_grant.accepts.is_some() {
651 return Err(DTGCredentialError::MalformedDelegation(
652 "the parent is an acceptance, not a grant — an acceptance appoints nobody \
653 and cannot be re-delegated from"
654 .into(),
655 ));
656 }
657
658 let parent_depth = parent_grant.max_depth.unwrap_or(0);
662 if parent_depth == 0 {
663 return Err(DTGCredentialError::MalformedDelegation(
664 "the parent does not permit re-delegation — `maxDepth` is absent or zero, \
665 and setting it above zero is the delegator's only way to authorise one"
666 .into(),
667 ));
668 }
669
670 if scope.is_empty() {
671 return Err(DTGCredentialError::MalformedDelegation(
672 "a grant MUST carry at least one `scope` entry".into(),
673 ));
674 }
675 let parent_scope = parent_grant.scope.as_deref().unwrap_or(&[]);
676 for act in &scope {
677 if !parent_scope.contains(act) {
678 return Err(DTGCredentialError::MalformedDelegation(format!(
679 "`{act}` is not in the scope this delegation derives from"
680 )));
681 }
682 }
683 if let Some(parent_until) = parent_until
684 && valid_until > parent_until
685 {
686 return Err(DTGCredentialError::MalformedDelegation(format!(
687 "validUntil {valid_until} is beyond the parent's {parent_until}"
688 )));
689 }
690
691 let mut vdc = DTGCommon {
692 issuer: holder,
693 valid_from,
694 valid_until: Some(valid_until),
695 credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
696 id: subject,
697 delegation: DelegationGrant {
698 scope: Some(scope),
699 parent: Some(parent_digest),
700 max_depth: Some(parent_depth - 1),
701 accepts: None,
702 },
703 }),
704 ..Default::default()
705 };
706
707 vdc.type_.push(DTGCredentialType::Delegation.to_string());
708
709 Ok(DTGCredential {
710 credential: vdc,
711 type_: DTGCredentialType::Delegation,
712 version: crate::W3CVCVersion::V2_0,
713 })
714 }
715
716 pub fn new_delegate_vdc(
744 grant: &Value,
745 valid_from: DateTime<Utc>,
746 valid_until: DateTime<Utc>,
747 ) -> Result<Self, DTGCredentialError> {
748 let object = grant
749 .as_object()
750 .ok_or_else(|| DTGCredentialError::NotADelegationGrant("not a JSON object".into()))?;
751
752 let is_delegation = object
753 .get("type")
754 .and_then(Value::as_array)
755 .is_some_and(|types| {
756 types
757 .iter()
758 .filter_map(Value::as_str)
759 .any(|t| t == "DelegationCredential")
760 });
761 if !is_delegation {
762 return Err(DTGCredentialError::NotADelegationGrant(
763 "`type` does not include `DelegationCredential`".into(),
764 ));
765 }
766
767 let subject = object
768 .get("credentialSubject")
769 .and_then(Value::as_object)
770 .ok_or_else(|| {
771 DTGCredentialError::NotADelegationGrant("no `credentialSubject`".into())
772 })?;
773
774 let delegation = subject
775 .get("delegation")
776 .and_then(Value::as_object)
777 .ok_or_else(|| {
778 DTGCredentialError::NotADelegationGrant("no `credentialSubject.delegation`".into())
779 })?;
780
781 if delegation.contains_key("accepts") {
782 return Err(DTGCredentialError::NotADelegationGrant(
783 "the credential carries `accepts`, so it is itself an acceptance rather \
784 than a grant"
785 .into(),
786 ));
787 }
788 if !delegation.contains_key("scope") {
789 return Err(DTGCredentialError::NotADelegationGrant(
790 "the grant carries no `scope`, so there is no appointment to accept".into(),
791 ));
792 }
793
794 let delegate = subject
799 .get("id")
800 .and_then(Value::as_str)
801 .ok_or_else(|| {
802 DTGCredentialError::NotADelegationGrant("no `credentialSubject.id`".into())
803 })?
804 .to_string();
805
806 let delegator = object
808 .get("issuer")
809 .and_then(|i| {
810 i.as_str()
811 .map(str::to_string)
812 .or_else(|| i.get("id").and_then(Value::as_str).map(str::to_string))
813 })
814 .ok_or_else(|| DTGCredentialError::NotADelegationGrant("no `issuer`".into()))?;
815
816 let mut vdc = DTGCommon {
817 issuer: delegate,
818 valid_from,
819 valid_until: Some(valid_until),
820 credential_subject: CredentialSubject::Delegation(CredentialSubjectDelegation {
821 id: delegator,
822 delegation: DelegationGrant {
823 scope: None,
824 parent: None,
825 max_depth: None,
826 accepts: Some(crate::digest_multibase_json(grant)?),
827 },
828 }),
829 ..Default::default()
830 };
831
832 vdc.type_.push(DTGCredentialType::Delegation.to_string());
833
834 Ok(DTGCredential {
835 credential: vdc,
836 type_: DTGCredentialType::Delegation,
837 version: crate::W3CVCVersion::V2_0,
838 })
839 }
840
841 fn read_delegation_json(
843 doc: &Value,
844 ) -> Result<(String, DelegationGrant, Option<DateTime<Utc>>), DTGCredentialError> {
845 let object = doc
846 .as_object()
847 .ok_or_else(|| DTGCredentialError::MalformedDelegation("not a JSON object".into()))?;
848
849 let is_delegation = object
850 .get("type")
851 .and_then(Value::as_array)
852 .is_some_and(|types| {
853 types
854 .iter()
855 .filter_map(Value::as_str)
856 .any(|t| t == "DelegationCredential")
857 });
858 if !is_delegation {
859 return Err(DTGCredentialError::MalformedDelegation(
860 "`type` does not include `DelegationCredential`".into(),
861 ));
862 }
863
864 let subject = object
865 .get("credentialSubject")
866 .and_then(Value::as_object)
867 .ok_or_else(|| {
868 DTGCredentialError::MalformedDelegation("no `credentialSubject`".into())
869 })?;
870
871 let delegate = subject
872 .get("id")
873 .and_then(Value::as_str)
874 .ok_or_else(|| {
875 DTGCredentialError::MalformedDelegation("no `credentialSubject.id`".into())
876 })?
877 .to_string();
878
879 let grant: DelegationGrant = subject
880 .get("delegation")
881 .ok_or_else(|| {
882 DTGCredentialError::MalformedDelegation("no `credentialSubject.delegation`".into())
883 })
884 .and_then(|d| {
885 serde_json::from_value(d.clone()).map_err(|e| {
886 DTGCredentialError::MalformedDelegation(format!("malformed `delegation`: {e}"))
887 })
888 })?;
889
890 let until = object
891 .get("validUntil")
892 .or_else(|| object.get("expirationDate"))
893 .and_then(Value::as_str)
894 .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
895 .map(|t| t.with_timezone(&Utc));
896
897 Ok((delegate, grant, until))
898 }
899
900 pub fn new_vpc(
906 issuer: String,
907 subject: String,
908 valid_from: DateTime<Utc>,
909 valid_until: Option<DateTime<Utc>>,
910 ) -> Self {
911 let mut vpc = DTGCommon {
912 issuer,
913 valid_from,
914 valid_until,
915 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
916 ..Default::default()
917 };
918
919 vpc.type_.push(DTGCredentialType::Persona.to_string());
920
921 DTGCredential {
922 credential: vpc,
923 type_: DTGCredentialType::Persona,
924 version: crate::W3CVCVersion::V2_0,
925 }
926 }
927
928 pub fn new_vec(
935 issuer: String,
936 subject: String,
937 valid_from: DateTime<Utc>,
938 valid_until: Option<DateTime<Utc>>,
939 endorsement: Value,
940 ) -> Self {
941 let mut vec = DTGCommon {
942 issuer,
943 valid_from,
944 valid_until,
945 credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
946 id: subject,
947 endorsement,
948 }),
949 ..Default::default()
950 };
951
952 vec.type_.push(DTGCredentialType::Endorsement.to_string());
953
954 DTGCredential {
955 credential: vec,
956 type_: DTGCredentialType::Endorsement,
957 version: crate::W3CVCVersion::V2_0,
958 }
959 }
960
961 pub fn new_vwc(
979 issuer: String,
980 subject: String,
981 valid_from: DateTime<Utc>,
982 valid_until: Option<DateTime<Utc>>,
983 task_context: String,
984 digest: Option<String>,
985 witness_context: Option<WitnessContext>,
986 ) -> Self {
987 let mut vwc = DTGCommon {
988 issuer,
989 valid_from,
990 valid_until,
991 task_context: Some(task_context),
992 credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
993 id: subject,
994 digest_multibase: digest,
995 witness_context,
996 }),
997 ..Default::default()
998 };
999
1000 vwc.type_.push(DTGCredentialType::Witness.to_string());
1001
1002 DTGCredential {
1003 credential: vwc,
1004 type_: DTGCredentialType::Witness,
1005 version: crate::W3CVCVersion::V2_0,
1006 }
1007 }
1008
1009 #[deprecated(
1016 since = "0.2.0",
1017 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
1018 It was removed from the DTG Core Credentials specification in Working Draft 01 \
1019 and will be defined by the planned DTG Verifiable Data Structures specification. \
1020 This constructor will be removed in a future release."
1021 )]
1022 #[allow(deprecated)]
1023 pub fn new_rcard(
1024 issuer: String,
1025 subject: String,
1026 valid_from: DateTime<Utc>,
1027 valid_until: Option<DateTime<Utc>>,
1028 card: Value,
1029 ) -> Self {
1030 let mut rcard = DTGCommon {
1031 issuer,
1032 valid_from,
1033 valid_until,
1034 credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
1035 id: subject,
1036 card,
1037 }),
1038 ..Default::default()
1039 };
1040
1041 rcard.type_.push(DTGCredentialType::RCard.to_string());
1042
1043 DTGCredential {
1044 credential: rcard,
1045 type_: DTGCredentialType::RCard,
1046 version: crate::W3CVCVersion::V2_0,
1047 }
1048 }
1049
1050 pub fn with_id(mut self, id: impl Into<String>) -> Self {
1076 self.credential.id = Some(id.into());
1077 self
1078 }
1079
1080 pub fn set_id(&mut self, id: impl Into<String>) {
1085 self.credential.id = Some(id.into());
1086 }
1087}
1088
1089#[cfg(test)]
1090#[allow(deprecated)]
1091mod tests {
1092 use crate::{DTGCredential, WitnessContext};
1093 use chrono::{DateTime, Utc};
1094 use serde_json::json;
1095
1096 #[test]
1097 fn test_vmc_serialization() {
1098 let vmc = DTGCredential::new_vmc(
1099 "did:example:issuer".to_string(),
1100 "did:example:subject".to_string(),
1101 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1102 .unwrap()
1103 .with_timezone(&Utc),
1104 None,
1105 false,
1106 );
1107
1108 let txt = serde_json::to_string_pretty(&vmc).unwrap();
1109 let sample = r#"{
1110 "@context": [
1111 "https://www.w3.org/ns/credentials/v2",
1112 "https://firstperson.network/credentials/dtg/v1"
1113 ],
1114 "type": [
1115 "VerifiableCredential",
1116 "DTGCredential",
1117 "MembershipCredential"
1118 ],
1119 "issuer": "did:example:issuer",
1120 "validFrom": "2025-12-11T00:00:00Z",
1121 "credentialSubject": {
1122 "id": "did:example:subject"
1123 }
1124}"#;
1125
1126 assert_eq!(txt, sample);
1127 }
1128
1129 #[test]
1130 fn test_vmc_phc_serialization() {
1131 let vmc = DTGCredential::new_vmc(
1132 "did:example:issuer".to_string(),
1133 "did:example:subject".to_string(),
1134 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1135 .unwrap()
1136 .with_timezone(&Utc),
1137 None,
1138 true,
1139 );
1140
1141 let txt = serde_json::to_string_pretty(&vmc).unwrap();
1142 let sample = r#"{
1143 "@context": [
1144 "https://www.w3.org/ns/credentials/v2",
1145 "https://firstperson.network/credentials/dtg/v1"
1146 ],
1147 "type": [
1148 "VerifiableCredential",
1149 "DTGCredential",
1150 "MembershipCredential",
1151 "PersonhoodCredential"
1152 ],
1153 "issuer": "did:example:issuer",
1154 "validFrom": "2025-12-11T00:00:00Z",
1155 "credentialSubject": {
1156 "id": "did:example:subject"
1157 }
1158}"#;
1159
1160 assert_eq!(txt, sample);
1161 }
1162 #[test]
1165 fn test_vmc_without_id_omits_the_property() {
1166 let vmc = DTGCredential::new_vmc(
1167 "did:example:issuer".to_string(),
1168 "did:example:subject".to_string(),
1169 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1170 .unwrap()
1171 .with_timezone(&Utc),
1172 None,
1173 false,
1174 );
1175
1176 assert_eq!(vmc.id(), None);
1177 let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
1178 assert!(
1179 value.get("id").is_none(),
1180 "an unset id must not appear on the wire at all: {value}"
1181 );
1182 }
1183
1184 #[test]
1188 fn test_vmc_with_id_serialization() {
1189 let vmc = DTGCredential::new_vmc(
1190 "did:example:issuer".to_string(),
1191 "did:example:subject".to_string(),
1192 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1193 .unwrap()
1194 .with_timezone(&Utc),
1195 None,
1196 false,
1197 )
1198 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
1199
1200 let txt = serde_json::to_string_pretty(&vmc).unwrap();
1201 let sample = r#"{
1202 "@context": [
1203 "https://www.w3.org/ns/credentials/v2",
1204 "https://firstperson.network/credentials/dtg/v1"
1205 ],
1206 "type": [
1207 "VerifiableCredential",
1208 "DTGCredential",
1209 "MembershipCredential"
1210 ],
1211 "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
1212 "issuer": "did:example:issuer",
1213 "validFrom": "2025-12-11T00:00:00Z",
1214 "credentialSubject": {
1215 "id": "did:example:subject"
1216 }
1217}"#;
1218
1219 assert_eq!(txt, sample);
1220 }
1221
1222 #[test]
1226 fn test_id_round_trips_through_deserialization() {
1227 let vmc = DTGCredential::new_vmc(
1228 "did:example:issuer".to_string(),
1229 "did:example:subject".to_string(),
1230 Utc::now(),
1231 None,
1232 false,
1233 )
1234 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
1235
1236 let txt = serde_json::to_string(&vmc).unwrap();
1237 let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
1238 assert_eq!(
1239 parsed.id(),
1240 Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
1241 );
1242 }
1243
1244 #[test]
1247 fn test_missing_id_deserializes_as_none() {
1248 let parsed: DTGCredential = serde_json::from_str(
1249 r#"{
1250 "@context": ["https://www.w3.org/ns/credentials/v2"],
1251 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
1252 "issuer": "did:example:issuer",
1253 "validFrom": "2025-12-11T00:00:00Z",
1254 "credentialSubject": { "id": "did:example:subject" }
1255 }"#,
1256 )
1257 .unwrap();
1258 assert_eq!(parsed.id(), None);
1259 }
1260
1261 #[test]
1263 fn test_set_id_matches_with_id() {
1264 let build = || {
1265 DTGCredential::new_vrc(
1266 "did:example:issuer".to_string(),
1267 "did:example:subject".to_string(),
1268 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1269 .unwrap()
1270 .with_timezone(&Utc),
1271 None,
1272 )
1273 };
1274 let mut in_place = build();
1275 in_place.set_id("urn:uuid:abc");
1276 assert_eq!(
1277 serde_json::to_value(&in_place).unwrap(),
1278 serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
1279 );
1280 }
1281
1282 #[test]
1283 fn test_vrc_serialization() {
1284 let vrc = DTGCredential::new_vrc(
1285 "did:example:issuer".to_string(),
1286 "did:example:subject".to_string(),
1287 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1288 .unwrap()
1289 .with_timezone(&Utc),
1290 None,
1291 );
1292
1293 let txt = serde_json::to_string_pretty(&vrc).unwrap();
1294 let sample = r#"{
1295 "@context": [
1296 "https://www.w3.org/ns/credentials/v2",
1297 "https://firstperson.network/credentials/dtg/v1"
1298 ],
1299 "type": [
1300 "VerifiableCredential",
1301 "DTGCredential",
1302 "RelationshipCredential"
1303 ],
1304 "issuer": "did:example:issuer",
1305 "validFrom": "2025-12-11T00:00:00Z",
1306 "credentialSubject": {
1307 "id": "did:example:subject"
1308 }
1309}"#;
1310
1311 assert_eq!(txt, sample);
1312 }
1313
1314 #[test]
1315 fn test_vic_serialization() {
1316 let vic = DTGCredential::new_vic(
1317 "did:example:issuer".to_string(),
1318 "did:example:subject".to_string(),
1319 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1320 .unwrap()
1321 .with_timezone(&Utc),
1322 None,
1323 );
1324
1325 let txt = serde_json::to_string_pretty(&vic).unwrap();
1326 let sample = r#"{
1327 "@context": [
1328 "https://www.w3.org/ns/credentials/v2",
1329 "https://firstperson.network/credentials/dtg/v1"
1330 ],
1331 "type": [
1332 "VerifiableCredential",
1333 "DTGCredential",
1334 "InvitationCredential"
1335 ],
1336 "issuer": "did:example:issuer",
1337 "validFrom": "2025-12-11T00:00:00Z",
1338 "credentialSubject": {
1339 "id": "did:example:subject"
1340 }
1341}"#;
1342
1343 assert_eq!(txt, sample);
1344 }
1345
1346 #[test]
1347 fn test_vpc_serialization() {
1348 let vpc = DTGCredential::new_vpc(
1349 "did:example:issuer".to_string(),
1350 "did:example:subject".to_string(),
1351 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1352 .unwrap()
1353 .with_timezone(&Utc),
1354 None,
1355 );
1356
1357 let txt = serde_json::to_string_pretty(&vpc).unwrap();
1358 let sample = r#"{
1359 "@context": [
1360 "https://www.w3.org/ns/credentials/v2",
1361 "https://firstperson.network/credentials/dtg/v1"
1362 ],
1363 "type": [
1364 "VerifiableCredential",
1365 "DTGCredential",
1366 "PersonaCredential"
1367 ],
1368 "issuer": "did:example:issuer",
1369 "validFrom": "2025-12-11T00:00:00Z",
1370 "credentialSubject": {
1371 "id": "did:example:subject"
1372 }
1373}"#;
1374
1375 assert_eq!(txt, sample);
1376 }
1377
1378 #[test]
1379 fn test_vec_serialization() {
1380 let vec = DTGCredential::new_vec(
1381 "did:example:issuer".to_string(),
1382 "did:example:subject".to_string(),
1383 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1384 .unwrap()
1385 .with_timezone(&Utc),
1386 None,
1387 json!({
1388 "type": "SkillEndorsement",
1389 "name": "Software Development",
1390 "competencyLevel": "expert"
1391 }),
1392 );
1393
1394 let txt = serde_json::to_string_pretty(&vec).unwrap();
1395 let sample = r#"{
1396 "@context": [
1397 "https://www.w3.org/ns/credentials/v2",
1398 "https://firstperson.network/credentials/dtg/v1"
1399 ],
1400 "type": [
1401 "VerifiableCredential",
1402 "DTGCredential",
1403 "EndorsementCredential"
1404 ],
1405 "issuer": "did:example:issuer",
1406 "validFrom": "2025-12-11T00:00:00Z",
1407 "credentialSubject": {
1408 "id": "did:example:subject",
1409 "endorsement": {
1410 "competencyLevel": "expert",
1411 "name": "Software Development",
1412 "type": "SkillEndorsement"
1413 }
1414 }
1415}"#;
1416
1417 assert_eq!(txt, sample);
1418 }
1419
1420 #[test]
1421 fn test_vwc_serialization() {
1422 let vwc = DTGCredential::new_vwc(
1423 "did:example:issuer".to_string(),
1424 "did:example:subject".to_string(),
1425 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1426 .unwrap()
1427 .with_timezone(&Utc),
1428 None,
1429 "thread-abc-123".to_string(),
1430 Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
1431 Some(WitnessContext {
1432 event: Some("EthDenver 2024".to_string()),
1433 session_id: Some("session-8822-nonce".to_string()),
1434 method: Some("in-person-proximity".to_string()),
1435 }),
1436 );
1437
1438 let txt = serde_json::to_string_pretty(&vwc).unwrap();
1439
1440 let sample = r#"{
1441 "@context": [
1442 "https://www.w3.org/ns/credentials/v2",
1443 "https://firstperson.network/credentials/dtg/v1"
1444 ],
1445 "type": [
1446 "VerifiableCredential",
1447 "DTGCredential",
1448 "WitnessCredential"
1449 ],
1450 "issuer": "did:example:issuer",
1451 "validFrom": "2025-12-11T00:00:00Z",
1452 "taskContext": "thread-abc-123",
1453 "credentialSubject": {
1454 "id": "did:example:subject",
1455 "digestMultibase": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
1456 "witnessContext": {
1457 "event": "EthDenver 2024",
1458 "sessionId": "session-8822-nonce",
1459 "method": "in-person-proximity"
1460 }
1461 }
1462}"#;
1463
1464 assert_eq!(txt, sample);
1465 }
1466
1467 #[test]
1468 fn test_rcard_serialization() {
1469 let rcard = DTGCredential::new_rcard(
1470 "did:example:issuer".to_string(),
1471 "did:example:subject".to_string(),
1472 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
1473 .unwrap()
1474 .with_timezone(&Utc),
1475 None,
1476 json!([
1477 "vcard",
1478 [
1479 ["fn", {}, "text", "Alice Smith"],
1480 ["email", {}, "text", "alice@example.com"]
1481 ]
1482 ]),
1483 );
1484
1485 let txt = serde_json::to_string_pretty(&rcard).unwrap();
1486
1487 let sample = r#"{
1488 "@context": [
1489 "https://www.w3.org/ns/credentials/v2",
1490 "https://firstperson.network/credentials/dtg/v1"
1491 ],
1492 "type": [
1493 "VerifiableCredential",
1494 "DTGCredential",
1495 "RCardCredential"
1496 ],
1497 "issuer": "did:example:issuer",
1498 "validFrom": "2025-12-11T00:00:00Z",
1499 "credentialSubject": {
1500 "id": "did:example:subject",
1501 "card": [
1502 "vcard",
1503 [
1504 [
1505 "fn",
1506 {},
1507 "text",
1508 "Alice Smith"
1509 ],
1510 [
1511 "email",
1512 {},
1513 "text",
1514 "alice@example.com"
1515 ]
1516 ]
1517 ]
1518 }
1519}"#;
1520
1521 assert_eq!(txt, sample);
1522 }
1523}