1#[allow(deprecated)]
6use crate::{
7 CredentialSubject, CredentialSubjectBasic, CredentialSubjectEndorsement,
8 CredentialSubjectMembership, CredentialSubjectRCard, CredentialSubjectWitness, DTGCommon,
9 DTGCredential, DTGCredentialError, DTGCredentialType, WitnessContext,
10};
11use chrono::{DateTime, Utc};
12use serde_json::Value;
13
14impl DTGCredential {
15 pub fn new_vmc(
39 issuer: String,
40 subject: String,
41 valid_from: DateTime<Utc>,
42 valid_until: Option<DateTime<Utc>>,
43 personhood: bool,
44 ) -> Self {
45 let mut vmc = DTGCommon {
46 issuer,
47 valid_from,
48 valid_until,
49 credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
50 id: subject,
51 digest: None,
52 }),
53 ..Default::default()
54 };
55
56 vmc.type_.push(DTGCredentialType::Membership.to_string());
57
58 if personhood {
59 vmc.type_.push("PersonhoodCredential".to_string());
60 }
61
62 DTGCredential {
63 credential: vmc,
64 type_: DTGCredentialType::Membership,
65 version: crate::W3CVCVersion::V2_0,
66 }
67 }
68
69 pub fn new_member_vmc(
105 grant: &DTGCredential,
106 valid_from: DateTime<Utc>,
107 valid_until: Option<DateTime<Utc>>,
108 ) -> Result<Self, DTGCredentialError> {
109 if !matches!(grant.type_(), DTGCredentialType::Membership) {
110 return Err(DTGCredentialError::WrongCredentialType {
111 expected: DTGCredentialType::Membership.to_string(),
112 got: grant.type_().to_string(),
113 });
114 }
115
116 if grant.subject_digest().is_some() {
117 return Err(DTGCredentialError::NotAMembershipGrant(
118 "the credential carries a `digest`, so it is itself a member-issued \
119 acknowledgement rather than a community-issued grant"
120 .to_string(),
121 ));
122 }
123
124 let mut vmc = DTGCommon {
129 issuer: grant.subject().to_string(),
130 valid_from,
131 valid_until,
132 credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
133 id: grant.issuer().to_string(),
134 digest: Some(grant.digest()?),
135 }),
136 ..Default::default()
137 };
138
139 vmc.type_.push(DTGCredentialType::Membership.to_string());
140
141 Ok(DTGCredential {
142 credential: vmc,
143 type_: DTGCredentialType::Membership,
144 version: crate::W3CVCVersion::V2_0,
145 })
146 }
147
148 pub fn new_vrc(
154 issuer: String,
155 subject: String,
156 valid_from: DateTime<Utc>,
157 valid_until: Option<DateTime<Utc>>,
158 ) -> Self {
159 let mut vrc = DTGCommon {
160 issuer,
161 valid_from,
162 valid_until,
163 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
164 ..Default::default()
165 };
166
167 vrc.type_.push(DTGCredentialType::Relationship.to_string());
168
169 DTGCredential {
170 credential: vrc,
171 type_: DTGCredentialType::Relationship,
172 version: crate::W3CVCVersion::V2_0,
173 }
174 }
175
176 pub fn new_vic(
182 issuer: String,
183 subject: String,
184 valid_from: DateTime<Utc>,
185 valid_until: Option<DateTime<Utc>>,
186 ) -> Self {
187 let mut vic = DTGCommon {
188 issuer,
189 valid_from,
190 valid_until,
191 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
192 ..Default::default()
193 };
194
195 vic.type_.push(DTGCredentialType::Invitation.to_string());
196
197 DTGCredential {
198 credential: vic,
199 type_: DTGCredentialType::Invitation,
200 version: crate::W3CVCVersion::V2_0,
201 }
202 }
203
204 pub fn new_vpc(
210 issuer: String,
211 subject: String,
212 valid_from: DateTime<Utc>,
213 valid_until: Option<DateTime<Utc>>,
214 ) -> Self {
215 let mut vpc = DTGCommon {
216 issuer,
217 valid_from,
218 valid_until,
219 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
220 ..Default::default()
221 };
222
223 vpc.type_.push(DTGCredentialType::Persona.to_string());
224
225 DTGCredential {
226 credential: vpc,
227 type_: DTGCredentialType::Persona,
228 version: crate::W3CVCVersion::V2_0,
229 }
230 }
231
232 pub fn new_vec(
239 issuer: String,
240 subject: String,
241 valid_from: DateTime<Utc>,
242 valid_until: Option<DateTime<Utc>>,
243 endorsement: Value,
244 ) -> Self {
245 let mut vec = DTGCommon {
246 issuer,
247 valid_from,
248 valid_until,
249 credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
250 id: subject,
251 endorsement,
252 }),
253 ..Default::default()
254 };
255
256 vec.type_.push(DTGCredentialType::Endorsement.to_string());
257
258 DTGCredential {
259 credential: vec,
260 type_: DTGCredentialType::Endorsement,
261 version: crate::W3CVCVersion::V2_0,
262 }
263 }
264
265 pub fn new_vwc(
282 issuer: String,
283 subject: String,
284 valid_from: DateTime<Utc>,
285 valid_until: Option<DateTime<Utc>>,
286 task_context: String,
287 digest: Option<String>,
288 witness_context: Option<WitnessContext>,
289 ) -> Self {
290 let mut vwc = DTGCommon {
291 issuer,
292 valid_from,
293 valid_until,
294 task_context: Some(task_context),
295 credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
296 id: subject,
297 digest,
298 witness_context,
299 }),
300 ..Default::default()
301 };
302
303 vwc.type_.push(DTGCredentialType::Witness.to_string());
304
305 DTGCredential {
306 credential: vwc,
307 type_: DTGCredentialType::Witness,
308 version: crate::W3CVCVersion::V2_0,
309 }
310 }
311
312 #[deprecated(
319 since = "0.2.0",
320 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
321 It was removed from the DTG Core Credentials specification in Working Draft 01 \
322 and will be defined by the planned DTG Verifiable Data Structures specification. \
323 This constructor will be removed in a future release."
324 )]
325 #[allow(deprecated)]
326 pub fn new_rcard(
327 issuer: String,
328 subject: String,
329 valid_from: DateTime<Utc>,
330 valid_until: Option<DateTime<Utc>>,
331 card: Value,
332 ) -> Self {
333 let mut rcard = DTGCommon {
334 issuer,
335 valid_from,
336 valid_until,
337 credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
338 id: subject,
339 card,
340 }),
341 ..Default::default()
342 };
343
344 rcard.type_.push(DTGCredentialType::RCard.to_string());
345
346 DTGCredential {
347 credential: rcard,
348 type_: DTGCredentialType::RCard,
349 version: crate::W3CVCVersion::V2_0,
350 }
351 }
352
353 pub fn with_id(mut self, id: impl Into<String>) -> Self {
379 self.credential.id = Some(id.into());
380 self
381 }
382
383 pub fn set_id(&mut self, id: impl Into<String>) {
388 self.credential.id = Some(id.into());
389 }
390}
391
392#[cfg(test)]
393#[allow(deprecated)]
394mod tests {
395 use crate::{DTGCredential, WitnessContext};
396 use chrono::{DateTime, Utc};
397 use serde_json::json;
398
399 #[test]
400 fn test_vmc_serialization() {
401 let vmc = DTGCredential::new_vmc(
402 "did:example:issuer".to_string(),
403 "did:example:subject".to_string(),
404 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
405 .unwrap()
406 .with_timezone(&Utc),
407 None,
408 false,
409 );
410
411 let txt = serde_json::to_string_pretty(&vmc).unwrap();
412 let sample = r#"{
413 "@context": [
414 "https://www.w3.org/ns/credentials/v2",
415 "https://firstperson.network/credentials/dtg/v1"
416 ],
417 "type": [
418 "VerifiableCredential",
419 "DTGCredential",
420 "MembershipCredential"
421 ],
422 "issuer": "did:example:issuer",
423 "validFrom": "2025-12-11T00:00:00Z",
424 "credentialSubject": {
425 "id": "did:example:subject"
426 }
427}"#;
428
429 assert_eq!(txt, sample);
430 }
431
432 #[test]
433 fn test_vmc_phc_serialization() {
434 let vmc = DTGCredential::new_vmc(
435 "did:example:issuer".to_string(),
436 "did:example:subject".to_string(),
437 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
438 .unwrap()
439 .with_timezone(&Utc),
440 None,
441 true,
442 );
443
444 let txt = serde_json::to_string_pretty(&vmc).unwrap();
445 let sample = r#"{
446 "@context": [
447 "https://www.w3.org/ns/credentials/v2",
448 "https://firstperson.network/credentials/dtg/v1"
449 ],
450 "type": [
451 "VerifiableCredential",
452 "DTGCredential",
453 "MembershipCredential",
454 "PersonhoodCredential"
455 ],
456 "issuer": "did:example:issuer",
457 "validFrom": "2025-12-11T00:00:00Z",
458 "credentialSubject": {
459 "id": "did:example:subject"
460 }
461}"#;
462
463 assert_eq!(txt, sample);
464 }
465 #[test]
468 fn test_vmc_without_id_omits_the_property() {
469 let vmc = DTGCredential::new_vmc(
470 "did:example:issuer".to_string(),
471 "did:example:subject".to_string(),
472 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
473 .unwrap()
474 .with_timezone(&Utc),
475 None,
476 false,
477 );
478
479 assert_eq!(vmc.id(), None);
480 let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
481 assert!(
482 value.get("id").is_none(),
483 "an unset id must not appear on the wire at all: {value}"
484 );
485 }
486
487 #[test]
491 fn test_vmc_with_id_serialization() {
492 let vmc = DTGCredential::new_vmc(
493 "did:example:issuer".to_string(),
494 "did:example:subject".to_string(),
495 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
496 .unwrap()
497 .with_timezone(&Utc),
498 None,
499 false,
500 )
501 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
502
503 let txt = serde_json::to_string_pretty(&vmc).unwrap();
504 let sample = r#"{
505 "@context": [
506 "https://www.w3.org/ns/credentials/v2",
507 "https://firstperson.network/credentials/dtg/v1"
508 ],
509 "type": [
510 "VerifiableCredential",
511 "DTGCredential",
512 "MembershipCredential"
513 ],
514 "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
515 "issuer": "did:example:issuer",
516 "validFrom": "2025-12-11T00:00:00Z",
517 "credentialSubject": {
518 "id": "did:example:subject"
519 }
520}"#;
521
522 assert_eq!(txt, sample);
523 }
524
525 #[test]
529 fn test_id_round_trips_through_deserialization() {
530 let vmc = DTGCredential::new_vmc(
531 "did:example:issuer".to_string(),
532 "did:example:subject".to_string(),
533 Utc::now(),
534 None,
535 false,
536 )
537 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
538
539 let txt = serde_json::to_string(&vmc).unwrap();
540 let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
541 assert_eq!(
542 parsed.id(),
543 Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
544 );
545 }
546
547 #[test]
550 fn test_missing_id_deserializes_as_none() {
551 let parsed: DTGCredential = serde_json::from_str(
552 r#"{
553 "@context": ["https://www.w3.org/ns/credentials/v2"],
554 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
555 "issuer": "did:example:issuer",
556 "validFrom": "2025-12-11T00:00:00Z",
557 "credentialSubject": { "id": "did:example:subject" }
558 }"#,
559 )
560 .unwrap();
561 assert_eq!(parsed.id(), None);
562 }
563
564 #[test]
566 fn test_set_id_matches_with_id() {
567 let build = || {
568 DTGCredential::new_vrc(
569 "did:example:issuer".to_string(),
570 "did:example:subject".to_string(),
571 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
572 .unwrap()
573 .with_timezone(&Utc),
574 None,
575 )
576 };
577 let mut in_place = build();
578 in_place.set_id("urn:uuid:abc");
579 assert_eq!(
580 serde_json::to_value(&in_place).unwrap(),
581 serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
582 );
583 }
584
585 #[test]
586 fn test_vrc_serialization() {
587 let vrc = DTGCredential::new_vrc(
588 "did:example:issuer".to_string(),
589 "did:example:subject".to_string(),
590 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
591 .unwrap()
592 .with_timezone(&Utc),
593 None,
594 );
595
596 let txt = serde_json::to_string_pretty(&vrc).unwrap();
597 let sample = r#"{
598 "@context": [
599 "https://www.w3.org/ns/credentials/v2",
600 "https://firstperson.network/credentials/dtg/v1"
601 ],
602 "type": [
603 "VerifiableCredential",
604 "DTGCredential",
605 "RelationshipCredential"
606 ],
607 "issuer": "did:example:issuer",
608 "validFrom": "2025-12-11T00:00:00Z",
609 "credentialSubject": {
610 "id": "did:example:subject"
611 }
612}"#;
613
614 assert_eq!(txt, sample);
615 }
616
617 #[test]
618 fn test_vic_serialization() {
619 let vic = DTGCredential::new_vic(
620 "did:example:issuer".to_string(),
621 "did:example:subject".to_string(),
622 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
623 .unwrap()
624 .with_timezone(&Utc),
625 None,
626 );
627
628 let txt = serde_json::to_string_pretty(&vic).unwrap();
629 let sample = r#"{
630 "@context": [
631 "https://www.w3.org/ns/credentials/v2",
632 "https://firstperson.network/credentials/dtg/v1"
633 ],
634 "type": [
635 "VerifiableCredential",
636 "DTGCredential",
637 "InvitationCredential"
638 ],
639 "issuer": "did:example:issuer",
640 "validFrom": "2025-12-11T00:00:00Z",
641 "credentialSubject": {
642 "id": "did:example:subject"
643 }
644}"#;
645
646 assert_eq!(txt, sample);
647 }
648
649 #[test]
650 fn test_vpc_serialization() {
651 let vpc = DTGCredential::new_vpc(
652 "did:example:issuer".to_string(),
653 "did:example:subject".to_string(),
654 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
655 .unwrap()
656 .with_timezone(&Utc),
657 None,
658 );
659
660 let txt = serde_json::to_string_pretty(&vpc).unwrap();
661 let sample = r#"{
662 "@context": [
663 "https://www.w3.org/ns/credentials/v2",
664 "https://firstperson.network/credentials/dtg/v1"
665 ],
666 "type": [
667 "VerifiableCredential",
668 "DTGCredential",
669 "PersonaCredential"
670 ],
671 "issuer": "did:example:issuer",
672 "validFrom": "2025-12-11T00:00:00Z",
673 "credentialSubject": {
674 "id": "did:example:subject"
675 }
676}"#;
677
678 assert_eq!(txt, sample);
679 }
680
681 #[test]
682 fn test_vec_serialization() {
683 let vec = DTGCredential::new_vec(
684 "did:example:issuer".to_string(),
685 "did:example:subject".to_string(),
686 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
687 .unwrap()
688 .with_timezone(&Utc),
689 None,
690 json!({
691 "type": "SkillEndorsement",
692 "name": "Software Development",
693 "competencyLevel": "expert"
694 }),
695 );
696
697 let txt = serde_json::to_string_pretty(&vec).unwrap();
698 let sample = r#"{
699 "@context": [
700 "https://www.w3.org/ns/credentials/v2",
701 "https://firstperson.network/credentials/dtg/v1"
702 ],
703 "type": [
704 "VerifiableCredential",
705 "DTGCredential",
706 "EndorsementCredential"
707 ],
708 "issuer": "did:example:issuer",
709 "validFrom": "2025-12-11T00:00:00Z",
710 "credentialSubject": {
711 "id": "did:example:subject",
712 "endorsement": {
713 "competencyLevel": "expert",
714 "name": "Software Development",
715 "type": "SkillEndorsement"
716 }
717 }
718}"#;
719
720 assert_eq!(txt, sample);
721 }
722
723 #[test]
724 fn test_vwc_serialization() {
725 let vwc = DTGCredential::new_vwc(
726 "did:example:issuer".to_string(),
727 "did:example:subject".to_string(),
728 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
729 .unwrap()
730 .with_timezone(&Utc),
731 None,
732 "thread-abc-123".to_string(),
733 Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
734 Some(WitnessContext {
735 event: Some("EthDenver 2024".to_string()),
736 session_id: Some("session-8822-nonce".to_string()),
737 method: Some("in-person-proximity".to_string()),
738 }),
739 );
740
741 let txt = serde_json::to_string_pretty(&vwc).unwrap();
742
743 let sample = r#"{
744 "@context": [
745 "https://www.w3.org/ns/credentials/v2",
746 "https://firstperson.network/credentials/dtg/v1"
747 ],
748 "type": [
749 "VerifiableCredential",
750 "DTGCredential",
751 "WitnessCredential"
752 ],
753 "issuer": "did:example:issuer",
754 "validFrom": "2025-12-11T00:00:00Z",
755 "taskContext": "thread-abc-123",
756 "credentialSubject": {
757 "id": "did:example:subject",
758 "digest": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
759 "witnessContext": {
760 "event": "EthDenver 2024",
761 "sessionId": "session-8822-nonce",
762 "method": "in-person-proximity"
763 }
764 }
765}"#;
766
767 assert_eq!(txt, sample);
768 }
769
770 #[test]
771 fn test_rcard_serialization() {
772 let rcard = DTGCredential::new_rcard(
773 "did:example:issuer".to_string(),
774 "did:example:subject".to_string(),
775 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
776 .unwrap()
777 .with_timezone(&Utc),
778 None,
779 json!([
780 "vcard",
781 [
782 ["fn", {}, "text", "Alice Smith"],
783 ["email", {}, "text", "alice@example.com"]
784 ]
785 ]),
786 );
787
788 let txt = serde_json::to_string_pretty(&rcard).unwrap();
789
790 let sample = r#"{
791 "@context": [
792 "https://www.w3.org/ns/credentials/v2",
793 "https://firstperson.network/credentials/dtg/v1"
794 ],
795 "type": [
796 "VerifiableCredential",
797 "DTGCredential",
798 "RCardCredential"
799 ],
800 "issuer": "did:example:issuer",
801 "validFrom": "2025-12-11T00:00:00Z",
802 "credentialSubject": {
803 "id": "did:example:subject",
804 "card": [
805 "vcard",
806 [
807 [
808 "fn",
809 {},
810 "text",
811 "Alice Smith"
812 ],
813 [
814 "email",
815 {},
816 "text",
817 "alice@example.com"
818 ]
819 ]
820 ]
821 }
822}"#;
823
824 assert_eq!(txt, sample);
825 }
826}