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