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(
107 grant: &Value,
108 valid_from: DateTime<Utc>,
109 valid_until: Option<DateTime<Utc>>,
110 ) -> Result<Self, DTGCredentialError> {
111 let object = grant
112 .as_object()
113 .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("not a JSON object".into()))?;
114
115 let is_membership = object
116 .get("type")
117 .and_then(Value::as_array)
118 .is_some_and(|types| {
119 types
120 .iter()
121 .filter_map(Value::as_str)
122 .any(|t| t == "MembershipCredential")
123 });
124 if !is_membership {
125 return Err(DTGCredentialError::NotAMembershipGrant(
126 "`type` does not include `MembershipCredential`".into(),
127 ));
128 }
129
130 let subject = object
131 .get("credentialSubject")
132 .and_then(Value::as_object)
133 .ok_or_else(|| {
134 DTGCredentialError::NotAMembershipGrant("no `credentialSubject`".into())
135 })?;
136
137 if subject.contains_key("digest") {
138 return Err(DTGCredentialError::NotAMembershipGrant(
139 "the credential carries a `digest`, so it is itself a member-issued \
140 acknowledgement rather than a community-issued grant"
141 .into(),
142 ));
143 }
144
145 let member = subject
150 .get("id")
151 .and_then(Value::as_str)
152 .ok_or_else(|| {
153 DTGCredentialError::NotAMembershipGrant("no `credentialSubject.id`".into())
154 })?
155 .to_string();
156
157 let community = object
159 .get("issuer")
160 .and_then(|i| {
161 i.as_str()
162 .map(str::to_string)
163 .or_else(|| i.get("id").and_then(Value::as_str).map(str::to_string))
164 })
165 .ok_or_else(|| DTGCredentialError::NotAMembershipGrant("no `issuer`".into()))?;
166
167 let mut vmc = DTGCommon {
168 issuer: member,
169 valid_from,
170 valid_until,
171 credential_subject: CredentialSubject::Membership(CredentialSubjectMembership {
172 id: community,
173 digest: Some(crate::digest_json(grant)?),
174 }),
175 ..Default::default()
176 };
177
178 vmc.type_.push(DTGCredentialType::Membership.to_string());
179
180 Ok(DTGCredential {
181 credential: vmc,
182 type_: DTGCredentialType::Membership,
183 version: crate::W3CVCVersion::V2_0,
184 })
185 }
186
187 pub fn new_vrc(
193 issuer: String,
194 subject: String,
195 valid_from: DateTime<Utc>,
196 valid_until: Option<DateTime<Utc>>,
197 ) -> Self {
198 let mut vrc = DTGCommon {
199 issuer,
200 valid_from,
201 valid_until,
202 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
203 ..Default::default()
204 };
205
206 vrc.type_.push(DTGCredentialType::Relationship.to_string());
207
208 DTGCredential {
209 credential: vrc,
210 type_: DTGCredentialType::Relationship,
211 version: crate::W3CVCVersion::V2_0,
212 }
213 }
214
215 pub fn new_vic(
221 issuer: String,
222 subject: String,
223 valid_from: DateTime<Utc>,
224 valid_until: Option<DateTime<Utc>>,
225 ) -> Self {
226 let mut vic = DTGCommon {
227 issuer,
228 valid_from,
229 valid_until,
230 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
231 ..Default::default()
232 };
233
234 vic.type_.push(DTGCredentialType::Invitation.to_string());
235
236 DTGCredential {
237 credential: vic,
238 type_: DTGCredentialType::Invitation,
239 version: crate::W3CVCVersion::V2_0,
240 }
241 }
242
243 pub fn new_vpc(
249 issuer: String,
250 subject: String,
251 valid_from: DateTime<Utc>,
252 valid_until: Option<DateTime<Utc>>,
253 ) -> Self {
254 let mut vpc = DTGCommon {
255 issuer,
256 valid_from,
257 valid_until,
258 credential_subject: CredentialSubject::Basic(CredentialSubjectBasic { id: subject }),
259 ..Default::default()
260 };
261
262 vpc.type_.push(DTGCredentialType::Persona.to_string());
263
264 DTGCredential {
265 credential: vpc,
266 type_: DTGCredentialType::Persona,
267 version: crate::W3CVCVersion::V2_0,
268 }
269 }
270
271 pub fn new_vec(
278 issuer: String,
279 subject: String,
280 valid_from: DateTime<Utc>,
281 valid_until: Option<DateTime<Utc>>,
282 endorsement: Value,
283 ) -> Self {
284 let mut vec = DTGCommon {
285 issuer,
286 valid_from,
287 valid_until,
288 credential_subject: CredentialSubject::Endorsement(CredentialSubjectEndorsement {
289 id: subject,
290 endorsement,
291 }),
292 ..Default::default()
293 };
294
295 vec.type_.push(DTGCredentialType::Endorsement.to_string());
296
297 DTGCredential {
298 credential: vec,
299 type_: DTGCredentialType::Endorsement,
300 version: crate::W3CVCVersion::V2_0,
301 }
302 }
303
304 pub fn new_vwc(
321 issuer: String,
322 subject: String,
323 valid_from: DateTime<Utc>,
324 valid_until: Option<DateTime<Utc>>,
325 task_context: String,
326 digest: Option<String>,
327 witness_context: Option<WitnessContext>,
328 ) -> Self {
329 let mut vwc = DTGCommon {
330 issuer,
331 valid_from,
332 valid_until,
333 task_context: Some(task_context),
334 credential_subject: CredentialSubject::Witness(CredentialSubjectWitness {
335 id: subject,
336 digest,
337 witness_context,
338 }),
339 ..Default::default()
340 };
341
342 vwc.type_.push(DTGCredentialType::Witness.to_string());
343
344 DTGCredential {
345 credential: vwc,
346 type_: DTGCredentialType::Witness,
347 version: crate::W3CVCVersion::V2_0,
348 }
349 }
350
351 #[deprecated(
358 since = "0.2.0",
359 note = "The r-card is a verifiable data structure (VDS), not a DTGCredential subtype. \
360 It was removed from the DTG Core Credentials specification in Working Draft 01 \
361 and will be defined by the planned DTG Verifiable Data Structures specification. \
362 This constructor will be removed in a future release."
363 )]
364 #[allow(deprecated)]
365 pub fn new_rcard(
366 issuer: String,
367 subject: String,
368 valid_from: DateTime<Utc>,
369 valid_until: Option<DateTime<Utc>>,
370 card: Value,
371 ) -> Self {
372 let mut rcard = DTGCommon {
373 issuer,
374 valid_from,
375 valid_until,
376 credential_subject: CredentialSubject::RCard(CredentialSubjectRCard {
377 id: subject,
378 card,
379 }),
380 ..Default::default()
381 };
382
383 rcard.type_.push(DTGCredentialType::RCard.to_string());
384
385 DTGCredential {
386 credential: rcard,
387 type_: DTGCredentialType::RCard,
388 version: crate::W3CVCVersion::V2_0,
389 }
390 }
391
392 pub fn with_id(mut self, id: impl Into<String>) -> Self {
418 self.credential.id = Some(id.into());
419 self
420 }
421
422 pub fn set_id(&mut self, id: impl Into<String>) {
427 self.credential.id = Some(id.into());
428 }
429}
430
431#[cfg(test)]
432#[allow(deprecated)]
433mod tests {
434 use crate::{DTGCredential, WitnessContext};
435 use chrono::{DateTime, Utc};
436 use serde_json::json;
437
438 #[test]
439 fn test_vmc_serialization() {
440 let vmc = DTGCredential::new_vmc(
441 "did:example:issuer".to_string(),
442 "did:example:subject".to_string(),
443 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
444 .unwrap()
445 .with_timezone(&Utc),
446 None,
447 false,
448 );
449
450 let txt = serde_json::to_string_pretty(&vmc).unwrap();
451 let sample = r#"{
452 "@context": [
453 "https://www.w3.org/ns/credentials/v2",
454 "https://firstperson.network/credentials/dtg/v1"
455 ],
456 "type": [
457 "VerifiableCredential",
458 "DTGCredential",
459 "MembershipCredential"
460 ],
461 "issuer": "did:example:issuer",
462 "validFrom": "2025-12-11T00:00:00Z",
463 "credentialSubject": {
464 "id": "did:example:subject"
465 }
466}"#;
467
468 assert_eq!(txt, sample);
469 }
470
471 #[test]
472 fn test_vmc_phc_serialization() {
473 let vmc = DTGCredential::new_vmc(
474 "did:example:issuer".to_string(),
475 "did:example:subject".to_string(),
476 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
477 .unwrap()
478 .with_timezone(&Utc),
479 None,
480 true,
481 );
482
483 let txt = serde_json::to_string_pretty(&vmc).unwrap();
484 let sample = r#"{
485 "@context": [
486 "https://www.w3.org/ns/credentials/v2",
487 "https://firstperson.network/credentials/dtg/v1"
488 ],
489 "type": [
490 "VerifiableCredential",
491 "DTGCredential",
492 "MembershipCredential",
493 "PersonhoodCredential"
494 ],
495 "issuer": "did:example:issuer",
496 "validFrom": "2025-12-11T00:00:00Z",
497 "credentialSubject": {
498 "id": "did:example:subject"
499 }
500}"#;
501
502 assert_eq!(txt, sample);
503 }
504 #[test]
507 fn test_vmc_without_id_omits_the_property() {
508 let vmc = DTGCredential::new_vmc(
509 "did:example:issuer".to_string(),
510 "did:example:subject".to_string(),
511 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
512 .unwrap()
513 .with_timezone(&Utc),
514 None,
515 false,
516 );
517
518 assert_eq!(vmc.id(), None);
519 let value: serde_json::Value = serde_json::to_value(&vmc).unwrap();
520 assert!(
521 value.get("id").is_none(),
522 "an unset id must not appear on the wire at all: {value}"
523 );
524 }
525
526 #[test]
530 fn test_vmc_with_id_serialization() {
531 let vmc = DTGCredential::new_vmc(
532 "did:example:issuer".to_string(),
533 "did:example:subject".to_string(),
534 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
535 .unwrap()
536 .with_timezone(&Utc),
537 None,
538 false,
539 )
540 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
541
542 let txt = serde_json::to_string_pretty(&vmc).unwrap();
543 let sample = r#"{
544 "@context": [
545 "https://www.w3.org/ns/credentials/v2",
546 "https://firstperson.network/credentials/dtg/v1"
547 ],
548 "type": [
549 "VerifiableCredential",
550 "DTGCredential",
551 "MembershipCredential"
552 ],
553 "id": "urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff",
554 "issuer": "did:example:issuer",
555 "validFrom": "2025-12-11T00:00:00Z",
556 "credentialSubject": {
557 "id": "did:example:subject"
558 }
559}"#;
560
561 assert_eq!(txt, sample);
562 }
563
564 #[test]
568 fn test_id_round_trips_through_deserialization() {
569 let vmc = DTGCredential::new_vmc(
570 "did:example:issuer".to_string(),
571 "did:example:subject".to_string(),
572 Utc::now(),
573 None,
574 false,
575 )
576 .with_id("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff");
577
578 let txt = serde_json::to_string(&vmc).unwrap();
579 let parsed: DTGCredential = serde_json::from_str(&txt).unwrap();
580 assert_eq!(
581 parsed.id(),
582 Some("urn:uuid:1e2d3c4b-5a69-4788-9099-aabbccddeeff")
583 );
584 }
585
586 #[test]
589 fn test_missing_id_deserializes_as_none() {
590 let parsed: DTGCredential = serde_json::from_str(
591 r#"{
592 "@context": ["https://www.w3.org/ns/credentials/v2"],
593 "type": ["VerifiableCredential", "DTGCredential", "MembershipCredential"],
594 "issuer": "did:example:issuer",
595 "validFrom": "2025-12-11T00:00:00Z",
596 "credentialSubject": { "id": "did:example:subject" }
597 }"#,
598 )
599 .unwrap();
600 assert_eq!(parsed.id(), None);
601 }
602
603 #[test]
605 fn test_set_id_matches_with_id() {
606 let build = || {
607 DTGCredential::new_vrc(
608 "did:example:issuer".to_string(),
609 "did:example:subject".to_string(),
610 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
611 .unwrap()
612 .with_timezone(&Utc),
613 None,
614 )
615 };
616 let mut in_place = build();
617 in_place.set_id("urn:uuid:abc");
618 assert_eq!(
619 serde_json::to_value(&in_place).unwrap(),
620 serde_json::to_value(build().with_id("urn:uuid:abc")).unwrap()
621 );
622 }
623
624 #[test]
625 fn test_vrc_serialization() {
626 let vrc = DTGCredential::new_vrc(
627 "did:example:issuer".to_string(),
628 "did:example:subject".to_string(),
629 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
630 .unwrap()
631 .with_timezone(&Utc),
632 None,
633 );
634
635 let txt = serde_json::to_string_pretty(&vrc).unwrap();
636 let sample = r#"{
637 "@context": [
638 "https://www.w3.org/ns/credentials/v2",
639 "https://firstperson.network/credentials/dtg/v1"
640 ],
641 "type": [
642 "VerifiableCredential",
643 "DTGCredential",
644 "RelationshipCredential"
645 ],
646 "issuer": "did:example:issuer",
647 "validFrom": "2025-12-11T00:00:00Z",
648 "credentialSubject": {
649 "id": "did:example:subject"
650 }
651}"#;
652
653 assert_eq!(txt, sample);
654 }
655
656 #[test]
657 fn test_vic_serialization() {
658 let vic = DTGCredential::new_vic(
659 "did:example:issuer".to_string(),
660 "did:example:subject".to_string(),
661 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
662 .unwrap()
663 .with_timezone(&Utc),
664 None,
665 );
666
667 let txt = serde_json::to_string_pretty(&vic).unwrap();
668 let sample = r#"{
669 "@context": [
670 "https://www.w3.org/ns/credentials/v2",
671 "https://firstperson.network/credentials/dtg/v1"
672 ],
673 "type": [
674 "VerifiableCredential",
675 "DTGCredential",
676 "InvitationCredential"
677 ],
678 "issuer": "did:example:issuer",
679 "validFrom": "2025-12-11T00:00:00Z",
680 "credentialSubject": {
681 "id": "did:example:subject"
682 }
683}"#;
684
685 assert_eq!(txt, sample);
686 }
687
688 #[test]
689 fn test_vpc_serialization() {
690 let vpc = DTGCredential::new_vpc(
691 "did:example:issuer".to_string(),
692 "did:example:subject".to_string(),
693 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
694 .unwrap()
695 .with_timezone(&Utc),
696 None,
697 );
698
699 let txt = serde_json::to_string_pretty(&vpc).unwrap();
700 let sample = r#"{
701 "@context": [
702 "https://www.w3.org/ns/credentials/v2",
703 "https://firstperson.network/credentials/dtg/v1"
704 ],
705 "type": [
706 "VerifiableCredential",
707 "DTGCredential",
708 "PersonaCredential"
709 ],
710 "issuer": "did:example:issuer",
711 "validFrom": "2025-12-11T00:00:00Z",
712 "credentialSubject": {
713 "id": "did:example:subject"
714 }
715}"#;
716
717 assert_eq!(txt, sample);
718 }
719
720 #[test]
721 fn test_vec_serialization() {
722 let vec = DTGCredential::new_vec(
723 "did:example:issuer".to_string(),
724 "did:example:subject".to_string(),
725 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
726 .unwrap()
727 .with_timezone(&Utc),
728 None,
729 json!({
730 "type": "SkillEndorsement",
731 "name": "Software Development",
732 "competencyLevel": "expert"
733 }),
734 );
735
736 let txt = serde_json::to_string_pretty(&vec).unwrap();
737 let sample = r#"{
738 "@context": [
739 "https://www.w3.org/ns/credentials/v2",
740 "https://firstperson.network/credentials/dtg/v1"
741 ],
742 "type": [
743 "VerifiableCredential",
744 "DTGCredential",
745 "EndorsementCredential"
746 ],
747 "issuer": "did:example:issuer",
748 "validFrom": "2025-12-11T00:00:00Z",
749 "credentialSubject": {
750 "id": "did:example:subject",
751 "endorsement": {
752 "competencyLevel": "expert",
753 "name": "Software Development",
754 "type": "SkillEndorsement"
755 }
756 }
757}"#;
758
759 assert_eq!(txt, sample);
760 }
761
762 #[test]
763 fn test_vwc_serialization() {
764 let vwc = DTGCredential::new_vwc(
765 "did:example:issuer".to_string(),
766 "did:example:subject".to_string(),
767 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
768 .unwrap()
769 .with_timezone(&Utc),
770 None,
771 "thread-abc-123".to_string(),
772 Some("zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw".to_string()),
773 Some(WitnessContext {
774 event: Some("EthDenver 2024".to_string()),
775 session_id: Some("session-8822-nonce".to_string()),
776 method: Some("in-person-proximity".to_string()),
777 }),
778 );
779
780 let txt = serde_json::to_string_pretty(&vwc).unwrap();
781
782 let sample = r#"{
783 "@context": [
784 "https://www.w3.org/ns/credentials/v2",
785 "https://firstperson.network/credentials/dtg/v1"
786 ],
787 "type": [
788 "VerifiableCredential",
789 "DTGCredential",
790 "WitnessCredential"
791 ],
792 "issuer": "did:example:issuer",
793 "validFrom": "2025-12-11T00:00:00Z",
794 "taskContext": "thread-abc-123",
795 "credentialSubject": {
796 "id": "did:example:subject",
797 "digest": "zQmbGXRT3v1RmfWkQ7Y3Z5Uj9pKq2NcXhLd8sVtA4eB6nMw",
798 "witnessContext": {
799 "event": "EthDenver 2024",
800 "sessionId": "session-8822-nonce",
801 "method": "in-person-proximity"
802 }
803 }
804}"#;
805
806 assert_eq!(txt, sample);
807 }
808
809 #[test]
810 fn test_rcard_serialization() {
811 let rcard = DTGCredential::new_rcard(
812 "did:example:issuer".to_string(),
813 "did:example:subject".to_string(),
814 DateTime::parse_from_rfc3339("2025-12-11T00:00:00Z")
815 .unwrap()
816 .with_timezone(&Utc),
817 None,
818 json!([
819 "vcard",
820 [
821 ["fn", {}, "text", "Alice Smith"],
822 ["email", {}, "text", "alice@example.com"]
823 ]
824 ]),
825 );
826
827 let txt = serde_json::to_string_pretty(&rcard).unwrap();
828
829 let sample = r#"{
830 "@context": [
831 "https://www.w3.org/ns/credentials/v2",
832 "https://firstperson.network/credentials/dtg/v1"
833 ],
834 "type": [
835 "VerifiableCredential",
836 "DTGCredential",
837 "RCardCredential"
838 ],
839 "issuer": "did:example:issuer",
840 "validFrom": "2025-12-11T00:00:00Z",
841 "credentialSubject": {
842 "id": "did:example:subject",
843 "card": [
844 "vcard",
845 [
846 [
847 "fn",
848 {},
849 "text",
850 "Alice Smith"
851 ],
852 [
853 "email",
854 {},
855 "text",
856 "alice@example.com"
857 ]
858 ]
859 ]
860 }
861}"#;
862
863 assert_eq!(txt, sample);
864 }
865}