Skip to main content

dtg_credentials/
create.rs

1/*!
2*   Builder methods for creating new entities.
3*/
4
5#[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    /// Creates a new community-issued Verifiable Membership Credential (VMC) — the
16    /// membership **grant**, the community → member half of a membership edge.
17    ///
18    /// A membership edge is a *pair* of VMCs, and this is only one of them. The member
19    /// answers with [DTGCredential::new_member_vmc], and the edge is not complete until
20    /// they have: a community can always issue a credential naming somebody as a member,
21    /// but it cannot produce the acknowledgement without that party's signature. The pair
22    /// is what makes an unconsented membership claim unprovable.
23    ///
24    /// The grant MUST NOT carry a `digest` — that property is what marks the other
25    /// direction — and this constructor does not set one.
26    ///
27    /// issuer: The C-DID of the VTC or VTN granting membership
28    /// subject: The M-DID of the member, or the member VTC's C-DID for VTN membership
29    /// valid_from: The datetime from which this credential is valid
30    /// valid_until: Optional: The datetime this credential is valid until
31    /// personhood: Whether this VMC can be used as a form of Personhood Credential
32    ///             - Adds PersonhoodCredential to the type array if true
33    ///
34    /// # Give it an `id`
35    ///
36    /// Chain [DTGCredential::with_id] on: the member stores the grant under its `id`, and
37    /// re-issuing is only recognisable as a renewal rather than a duplicate if there is one.
38    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    /// Creates a new member-issued Verifiable Membership Credential (VMC) — the membership
70    /// **acknowledgement**, the member → community half of a membership edge.
71    ///
72    /// The roles of [DTGCredential::new_vmc] are reversed (the member issues, the community
73    /// is the subject) and the subject carries a `digest` of the grant being acknowledged.
74    /// That digest is what binds the two halves into one edge: an acknowledgement whose
75    /// digest matches no valid grant does not complete anything, and the binding forces an
76    /// order — the grant must exist before this can reference it.
77    ///
78    /// This is the member's consent artifact. Because the member is its issuer, withdrawing
79    /// consent needs no cooperation from the community.
80    ///
81    /// # Takes the grant in its wire form, deliberately
82    ///
83    /// `grant` is the JSON the community sent, not a parsed [DTGCredential]. The digest has
84    /// to cover the document the community will recompute it over, and this library does
85    /// not model every member a credential may carry — `credentialStatus`, which every VMC
86    /// issued against a status list carries, is dropped by a parse-then-re-serialise round
87    /// trip. Building the acknowledgement from a parsed grant would produce a digest that
88    /// verifies nowhere, and would do it silently.
89    ///
90    /// So: keep the bytes you were given, and pass them here.
91    ///
92    /// valid_from: The datetime from which this credential is valid
93    /// valid_until: Optional: The datetime this credential is valid until
94    ///
95    /// # Errors
96    ///
97    /// [DTGCredentialError::NotAMembershipGrant] if `grant` is not a JSON object, does not
98    /// carry `MembershipCredential` in its `type`, has no `issuer` or
99    /// `credentialSubject.id`, or already carries a `digest` — that last is an
100    /// acknowledgement, and acknowledging one does not form an edge.
101    ///
102    /// # Give it an `id`
103    ///
104    /// Chain [DTGCredential::with_id] on before signing. A community keys a member's VMC by
105    /// `id` to tell a re-send from a renewal.
106    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        // The member is the grant's subject and the community its issuer: reading both off
146        // the grant is what keeps the two halves naming the same pair. Taking them as
147        // parameters would let a caller acknowledge one grant while naming the parties of
148        // another, which verifies as a digest match and means nothing.
149        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        // `issuer` is a string or an object with an `id`, per the W3C data model.
158        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    /// Creates a new Verified Relationship Credential (VRC)
188    /// issuer: The issuer DID of the credential
189    /// subject: The DID of the subject of this credential
190    /// valid_from: The datetime from which this credential is valid
191    /// valid_until: Optional: The datetime this credential is valid until
192    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    /// Creates a new Verified Invitation Credential (VIC)
216    /// issuer: The issuer DID of the credential
217    /// subject: The DID of the subject of this credential
218    /// valid_from: The datetime from which this credential is valid
219    /// valid_until: Optional: The datetime this credential is valid until
220    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    /// Creates a new Verified Persona Credential (VPC)
244    /// issuer: The issuer DID of the credential
245    /// subject: The DID of the subject of this credential
246    /// valid_from: The datetime from which this credential is valid
247    /// valid_until: Optional: The datetime this credential is valid until
248    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    /// Creates a new Verified Endorsement Credential (VEC)
272    /// issuer: The issuer DID of the credential
273    /// subject: The DID of the subject of this credential
274    /// valid_from: The datetime from which this credential is valid
275    /// valid_until: Optional: The datetime this credential is valid until
276    /// endorsement: The endorsement details for this credential
277    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    /// Creates a new Verified Witness Credential (VWC)
305    /// issuer: The issuer DID of the credential - an M-DID, or the DID of a VTA acting
306    ///         according to VTC policy
307    /// subject: The DID of the observed party. For a witnessed bi-directional exchange this
308    ///          MUST be the issuer of the VRC that this VWC attests (the VRC referenced by
309    ///          `digest`), so that the two VWCs of an exchange are unambiguously bound to
310    ///          their respective directions. The witness should issue one VWC per direction.
311    /// valid_from: The datetime from which this credential is valid
312    /// valid_until: Optional: The datetime this credential is valid until
313    /// task_context: Required `threadId` of the trust task exchange the witnessing occurred in
314    /// digest: Cryptographic hash of the witnessed edge credential, binding this VWC to the
315    ///         specific edge. Produce it with [DTGCredential::digest] on that credential.
316    ///         REQUIRED by the specification; `Option` here because a VWC that predates the
317    ///         requirement still has to deserialize. A VWC without one identifies the
318    ///         observed party and the exchange, but not which edge was witnessed.
319    /// witness_context: Optional Semantic context for the witness
320    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    /// Creates a new Verified RCard Credential (VWC)
352    /// issuer: The issuer DID of the credential
353    /// subject: The DID of the subject of this credential
354    /// valid_from: The datetime from which this credential is valid
355    /// valid_until: Optional: The datetime this credential is valid until
356    /// card: JSON Value representing a Jcard (RFC 7095) format
357    #[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    /// Sets this credential's own identifier, consuming and returning it so it chains onto
393    /// any of the `new_*` constructors above.
394    ///
395    /// `id` MUST be a single URL per the W3C VC Data Model; `urn:uuid:<uuid>` is the usual
396    /// choice for a credential with no dereferenceable home. This crate does not validate it.
397    ///
398    /// ```
399    /// # use chrono::Utc;
400    /// # use dtg_credentials::DTGCredential;
401    /// let vmc = DTGCredential::new_vmc(
402    ///     "did:example:member".to_string(),
403    ///     "did:example:community".to_string(),
404    ///     Utc::now(),
405    ///     None,
406    ///     false,
407    /// )
408    /// .with_id("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52");
409    /// assert_eq!(vmc.id(), Some("urn:uuid:2a4e1d90-6e0c-4d3f-9a4a-6d0a8f7c1b52"));
410    /// ```
411    ///
412    /// # Set it before signing
413    ///
414    /// A Data Integrity proof covers the credential minus its `proof`, so `id` is part of what
415    /// is signed. Chain this onto the constructor, before [DTGCredential::sign] — adding an id
416    /// to an already-signed credential leaves a document whose proof no longer verifies.
417    pub fn with_id(mut self, id: impl Into<String>) -> Self {
418        self.credential.id = Some(id.into());
419        self
420    }
421
422    /// Sets this credential's own identifier in place.
423    ///
424    /// The non-consuming form of [DTGCredential::with_id]; the same "before signing" caveat
425    /// applies.
426    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    /// `id` is OPTIONAL, and a credential that was never given one must keep serializing the
505    /// shape it always did — no `"id": null`, no empty string.
506    #[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    /// `with_id` puts the identifier at the top level of the credential — a sibling of
527    /// `issuer`, not something nested under `credentialSubject` (which carries the *subject's*
528    /// id, a different thing entirely).
529    #[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    /// The identifier has to survive a round trip. It arrives on the wire and is read back
565    /// through `TryFrom<DTGCommon>`, which is where `taskContext` was previously being dropped
566    /// — a field that deserializes into nothing breaks signing and verification silently.
567    #[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    /// A credential with no `id` still deserializes — the property is OPTIONAL, and every
587    /// credential issued before this field existed has none.
588    #[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    /// `set_id` is the in-place form of `with_id`; both write the same property.
604    #[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}