Skip to main content

identity_kit/
openapi.rs

1//! OpenAPI schemas for the JSON types (feature `utoipa`): [`utoipa::ToSchema`] for
2//! [`DidDocument`], [`VerificationMethod`], [`MethodRef`], [`Service`], [`Jwk`],
3//! [`JwsAlgorithm`], `DomainLinkageConfiguration`, the credentials and presentations of `vc`
4//! (`Credential`, `Presentation`, `CredentialV2`, `PresentationV2`, `EnvelopedVc`,
5//! `EnvelopedVp`), `ClaimPath`, `TypeMetadata` with its display and claim metadata, and
6//! `StatusType`.
7//!
8//! The schemas are written by hand rather than derived: every type carries open extension
9//! properties and custom serialization that a derive cannot express. Each schema describes the
10//! properties its specification defines and, where the specification leaves the object open,
11//! allows additional properties.
12
13use std::borrow::Cow;
14
15use utoipa::openapi::RefOr;
16use utoipa::openapi::schema::{
17    AdditionalProperties, ArrayBuilder, ArrayItems, ObjectBuilder, OneOfBuilder, Ref, RefBuilder,
18    Schema, SchemaFormat, SchemaType, Type,
19};
20use utoipa::{PartialSchema, ToSchema};
21
22use crate::did::{DidDocument, MethodRef, Service, VerificationMethod};
23use crate::jose::{Jwk, JwsAlgorithm};
24
25/// DID Core §3.1 as a regular expression: `did:`, a method name of lowercase letters and
26/// digits, then a method-specific id of `idchar`s and `:` that does not end in `:`.
27const DID_PATTERN: &str =
28    r"^did:[a-z0-9]+:(([A-Za-z0-9._-]|%[0-9A-Fa-f]{2})*:)*([A-Za-z0-9._-]|%[0-9A-Fa-f]{2})+$";
29
30type Schemas = Vec<(String, RefOr<Schema>)>;
31
32/// Adds the schema of `T`, and the schemas it references, unless already present.
33fn collect<T: ToSchema>(schemas: &mut Schemas) {
34    let name = T::name();
35    if schemas.iter().any(|(present, _)| *present == name) {
36        return;
37    }
38    schemas.push((name.into_owned(), T::schema()));
39    T::schemas(schemas);
40}
41
42fn schema_ref<T: ToSchema>() -> Ref {
43    Ref::from_schema_name(T::name().into_owned())
44}
45
46fn string(description: &str) -> ObjectBuilder {
47    ObjectBuilder::new()
48        .schema_type(Type::String)
49        .description(Some(description))
50}
51
52fn did(description: &str) -> ObjectBuilder {
53    string(description).pattern(Some(DID_PATTERN))
54}
55
56fn uri(description: &str) -> ObjectBuilder {
57    string(description).format(Some(SchemaFormat::Custom("uri".to_owned())))
58}
59
60/// An object with any properties.
61fn any_object(description: &str) -> ObjectBuilder {
62    ObjectBuilder::new()
63        .description(Some(description))
64        .additional_properties(Some(AdditionalProperties::FreeForm(true)))
65}
66
67fn array(items: impl Into<RefOr<Schema>>, description: &str) -> ArrayBuilder {
68    ArrayBuilder::new()
69        .items(ArrayItems::RefOrSchema(Box::new(items.into())))
70        .description(Some(description))
71}
72
73fn one_of<const N: usize>(items: [RefOr<Schema>; N], description: &str) -> OneOfBuilder {
74    items
75        .into_iter()
76        .fold(OneOfBuilder::new(), OneOfBuilder::item)
77        .description(Some(description))
78}
79
80/// A string, or a non-empty array of strings.
81fn string_or_strings(item: &str, description: &str) -> OneOfBuilder {
82    one_of(
83        [
84            string(item).into(),
85            array(string(item), "")
86                .min_items(Some(1))
87                .description(None::<String>)
88                .into(),
89        ],
90        description,
91    )
92}
93
94// ---------------------------------------------------------------------------------------------
95// DID documents
96// ---------------------------------------------------------------------------------------------
97
98#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
99impl PartialSchema for DidDocument {
100    fn schema() -> RefOr<Schema> {
101        let relationship = |name: &str, section: &str| {
102            array(
103                schema_ref::<MethodRef>(),
104                &format!("The `{name}` relationship (DID Core §{section})."),
105            )
106        };
107        ObjectBuilder::new()
108            .description(Some(
109                "A DID document (DID Core §4): the core properties, plus any extension \
110                 property.",
111            ))
112            .property(
113                "@context",
114                one_of(
115                    [
116                        uri("A context URI.").into(),
117                        array(
118                            ObjectBuilder::new().schema_type(SchemaType::AnyValue),
119                            "Context URIs and objects.",
120                        )
121                        .min_items(Some(1))
122                        .into(),
123                        any_object("A context object.").into(),
124                    ],
125                    "The JSON-LD context, carried as data (DID Core §6.3.1).",
126                ),
127            )
128            .property("id", did("The DID of the subject (DID Core §5.1.1)."))
129            .required("id")
130            .property(
131                "alsoKnownAs",
132                array(
133                    uri("A URI."),
134                    "Other identifiers of the subject (DID Core §5.1.3).",
135                ),
136            )
137            .property(
138                "controller",
139                one_of(
140                    [
141                        did("The DID of the controller.").into(),
142                        array(did("The DID of a controller."), "")
143                            .description(None::<String>)
144                            .into(),
145                    ],
146                    "The DIDs whose controllers may change this document (DID Core §5.1.2); \
147                     an empty set means none.",
148                ),
149            )
150            .property(
151                "verificationMethod",
152                array(
153                    schema_ref::<VerificationMethod>(),
154                    "Verification methods the relationships may reference (DID Core §5.2).",
155                ),
156            )
157            .property("authentication", relationship("authentication", "5.3.1"))
158            .property("assertionMethod", relationship("assertionMethod", "5.3.2"))
159            .property("keyAgreement", relationship("keyAgreement", "5.3.3"))
160            .property(
161                "capabilityInvocation",
162                relationship("capabilityInvocation", "5.3.4"),
163            )
164            .property(
165                "capabilityDelegation",
166                relationship("capabilityDelegation", "5.3.5"),
167            )
168            .property(
169                "service",
170                array(
171                    schema_ref::<Service>(),
172                    "Ways of interacting with the subject (DID Core §5.4).",
173                ),
174            )
175            .additional_properties(Some(AdditionalProperties::FreeForm(true)))
176            .into()
177    }
178}
179
180#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
181impl ToSchema for DidDocument {
182    fn name() -> Cow<'static, str> {
183        Cow::Borrowed("DidDocument")
184    }
185
186    fn schemas(schemas: &mut Schemas) {
187        collect::<VerificationMethod>(schemas);
188        collect::<MethodRef>(schemas);
189        collect::<Service>(schemas);
190    }
191}
192
193#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
194impl PartialSchema for VerificationMethod {
195    fn schema() -> RefOr<Schema> {
196        ObjectBuilder::new()
197            .description(Some(
198                "A verification method (DID Core §5.2): a public key, carried as a JWK or a \
199                 multibase string, with its type and controller.",
200            ))
201            .property(
202                "id",
203                string("The DID URL of the method, usually the document's DID with a fragment."),
204            )
205            .required("id")
206            .property(
207                "type",
208                string(
209                    "The verification method type, such as `JsonWebKey2020`, `JsonWebKey` or \
210                     `Multikey`.",
211                ),
212            )
213            .required("type")
214            .property("controller", did("The DID of the controller of the key."))
215            .required("controller")
216            .property(
217                "publicKeyJwk",
218                RefBuilder::new()
219                    .ref_location_from_schema_name(Jwk::name().into_owned())
220                    .description(Some("The public key as a JWK (DID Core §5.2.1)."))
221                    .build(),
222            )
223            .property(
224                "publicKeyMultibase",
225                string("The public key as a multibase string (DID Core §5.2.1)."),
226            )
227            .additional_properties(Some(AdditionalProperties::FreeForm(true)))
228            .into()
229    }
230}
231
232#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
233impl ToSchema for VerificationMethod {
234    fn name() -> Cow<'static, str> {
235        Cow::Borrowed("VerificationMethod")
236    }
237
238    fn schemas(schemas: &mut Schemas) {
239        collect::<Jwk>(schemas);
240    }
241}
242
243#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
244impl PartialSchema for MethodRef {
245    fn schema() -> RefOr<Schema> {
246        one_of(
247            [
248                string("A DID URL, possibly relative, of a method under `verificationMethod`.")
249                    .into(),
250                schema_ref::<VerificationMethod>().into(),
251            ],
252            "An entry of a verification relationship (DID Core §5.3): a reference to a \
253             verification method, or a method embedded in the relationship.",
254        )
255        .into()
256    }
257}
258
259#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
260impl ToSchema for MethodRef {
261    fn name() -> Cow<'static, str> {
262        Cow::Borrowed("MethodRef")
263    }
264
265    fn schemas(schemas: &mut Schemas) {
266        collect::<VerificationMethod>(schemas);
267    }
268}
269
270#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
271impl PartialSchema for Service {
272    fn schema() -> RefOr<Schema> {
273        ObjectBuilder::new()
274            .description(Some(
275                "A service (DID Core §5.4): a way of interacting with the DID subject.",
276            ))
277            .property(
278                "id",
279                string("The DID URL of the service, usually the document's DID with a fragment."),
280            )
281            .required("id")
282            .property(
283                "type",
284                string_or_strings("A service type.", "The service type or types."),
285            )
286            .required("type")
287            .property(
288                "serviceEndpoint",
289                one_of(
290                    [
291                        uri("The endpoint URI.").into(),
292                        array(uri("An endpoint URI."), "")
293                            .min_items(Some(1))
294                            .description(None::<String>)
295                            .into(),
296                        any_object("An endpoint map.").into(),
297                    ],
298                    "Where the service is: a URI, a set of URIs, or a map.",
299                ),
300            )
301            .required("serviceEndpoint")
302            .additional_properties(Some(AdditionalProperties::FreeForm(true)))
303            .into()
304    }
305}
306
307#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
308impl ToSchema for Service {
309    fn name() -> Cow<'static, str> {
310        Cow::Borrowed("Service")
311    }
312}
313
314// ---------------------------------------------------------------------------------------------
315// JOSE
316// ---------------------------------------------------------------------------------------------
317
318#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
319impl PartialSchema for Jwk {
320    fn schema() -> RefOr<Schema> {
321        let base64url = |description: &str| string(description);
322        ObjectBuilder::new()
323            .description(Some(
324                "A JSON Web Key (RFC 7517 §4): the key type, the parameters of RFC 7518 §6 and \
325                 RFC 8037 §2 for that type, and the common members. Parameters are \
326                 base64url-encoded without padding.",
327            ))
328            .property(
329                "kty",
330                string("The key type (RFC 7517 §4.1): `EC`, `OKP`, `RSA` or `oct`."),
331            )
332            .required("kty")
333            .property(
334                "crv",
335                string("The curve of an `EC` or `OKP` key: `P-256`, `secp256k1`, `Ed25519`, ..."),
336            )
337            .property("x", base64url("The x coordinate of an `EC` key, or the `OKP` public key."))
338            .property("y", base64url("The y coordinate of an `EC` key."))
339            .property("n", base64url("The modulus of an `RSA` key."))
340            .property("e", base64url("The public exponent of an `RSA` key."))
341            .property(
342                "d",
343                base64url("The private key of an `EC` or `OKP` key, or the private exponent of an `RSA` key."),
344            )
345            .property("p", base64url("The first prime factor of an `RSA` private key."))
346            .property("q", base64url("The second prime factor of an `RSA` private key."))
347            .property("dp", base64url("The first factor CRT exponent of an `RSA` private key."))
348            .property("dq", base64url("The second factor CRT exponent of an `RSA` private key."))
349            .property("qi", base64url("The first CRT coefficient of an `RSA` private key."))
350            .property(
351                "oth",
352                array(
353                    ObjectBuilder::new()
354                        .property("r", base64url("The prime factor."))
355                        .required("r")
356                        .property("d", base64url("The factor CRT exponent."))
357                        .required("d")
358                        .property("t", base64url("The factor CRT coefficient."))
359                        .required("t"),
360                    "Other primes of an `RSA` private key (RFC 7518 §6.3.2.7).",
361                ),
362            )
363            .property("k", base64url("The value of an `oct` key."))
364            .property(
365                "use",
366                string("The intended use of the key (RFC 7517 §4.2): `sig` or `enc`."),
367            )
368            .property(
369                "key_ops",
370                array(
371                    string("An operation, such as `sign` or `verify`."),
372                    "The operations the key is intended for (RFC 7517 §4.3).",
373                ),
374            )
375            .property(
376                "alg",
377                string("The algorithm intended for use with the key (RFC 7517 §4.4)."),
378            )
379            .property("kid", string("The key id (RFC 7517 §4.5)."))
380            .additional_properties(Some(AdditionalProperties::FreeForm(true)))
381            .into()
382    }
383}
384
385#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
386impl ToSchema for Jwk {
387    fn name() -> Cow<'static, str> {
388        Cow::Borrowed("Jwk")
389    }
390}
391
392#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
393impl PartialSchema for JwsAlgorithm {
394    fn schema() -> RefOr<Schema> {
395        string(
396            "A JWS algorithm (RFC 7518 §3.1, RFC 8037, RFC 8812): its registered name, among \
397             the asymmetric algorithms this crate speaks.",
398        )
399        .enum_values(Some(JwsAlgorithm::ALL.iter().map(|alg| alg.as_str())))
400        .into()
401    }
402}
403
404#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
405impl ToSchema for JwsAlgorithm {
406    fn name() -> Cow<'static, str> {
407        Cow::Borrowed("JwsAlgorithm")
408    }
409}
410
411// ---------------------------------------------------------------------------------------------
412// DIF Well Known DID Configuration
413// ---------------------------------------------------------------------------------------------
414
415#[cfg(feature = "domain-linkage")]
416#[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "domain-linkage"))))]
417impl PartialSchema for crate::domain_linkage::DomainLinkageConfiguration {
418    fn schema() -> RefOr<Schema> {
419        ObjectBuilder::new()
420            .description(Some(
421                "A DID configuration resource (Well Known DID Configuration §3.1): the context \
422                 and the domain linkage credentials, one per linked DID.",
423            ))
424            .property(
425                "@context",
426                string("The DID configuration context.")
427                    .enum_values(Some([crate::domain_linkage::CONTEXT])),
428            )
429            .required("@context")
430            .property(
431                "linked_dids",
432                array(
433                    one_of(
434                        [
435                            string(
436                                "A domain linkage credential in the JSON Web Token proof \
437                                 format: a compact JWS.",
438                            )
439                            .into(),
440                            any_object(
441                                "A domain linkage credential in the Linked Data Proof format.",
442                            )
443                            .into(),
444                        ],
445                        "A domain linkage credential.",
446                    ),
447                    "The domain linkage credentials.",
448                ),
449            )
450            .required("linked_dids")
451            .additional_properties(Some(AdditionalProperties::FreeForm(false)))
452            .into()
453    }
454}
455
456#[cfg(feature = "domain-linkage")]
457#[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "domain-linkage"))))]
458impl ToSchema for crate::domain_linkage::DomainLinkageConfiguration {
459    fn name() -> Cow<'static, str> {
460        Cow::Borrowed("DomainLinkageConfiguration")
461    }
462}
463
464// ---------------------------------------------------------------------------------------------
465// Verifiable credentials and presentations
466// ---------------------------------------------------------------------------------------------
467
468#[cfg(feature = "vc")]
469mod credentials {
470    use super::{
471        AdditionalProperties, ArrayBuilder, Cow, ObjectBuilder, OneOfBuilder, PartialSchema, RefOr,
472        Schema, SchemaFormat, SchemaType, Schemas, ToSchema, array, collect, one_of, schema_ref,
473        string, string_or_strings, uri,
474    };
475    use crate::vc::{
476        CONTEXT_V1, CONTEXT_V2, Credential, CredentialV2, EnvelopedVc, EnvelopedVp, Presentation,
477        PresentationV2,
478    };
479
480    /// A compact JWS: three base64url parts joined by dots.
481    const JWS_PATTERN: &str = r"^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*$";
482
483    /// `@context` beginning with `base`: the URI alone, or an array of URIs and objects whose
484    /// first element is it.
485    fn context(base: &'static str, section: &str) -> OneOfBuilder {
486        one_of(
487            [
488                string("The base context alone.")
489                    .enum_values(Some([base]))
490                    .into(),
491                ArrayBuilder::new()
492                    .prefix_items([Schema::from(
493                        string("The base context.").enum_values(Some([base])),
494                    )])
495                    .items(ObjectBuilder::new().schema_type(SchemaType::AnyValue))
496                    .min_items(Some(1))
497                    .description(Some("The base context first, then further contexts."))
498                    .into(),
499            ],
500            &format!("The JSON-LD context, carried as data ({section}); it begins with `{base}`."),
501        )
502    }
503
504    /// The `type` of an object that must include `required`.
505    fn types(required: &str, section: &str) -> OneOfBuilder {
506        string_or_strings(
507            "A type.",
508            &format!("The types ({section}), including `{required}`."),
509        )
510    }
511
512    fn date_time(description: &str) -> ObjectBuilder {
513        string(description).format(Some(SchemaFormat::KnownFormat(
514            utoipa::openapi::KnownFormat::DateTime,
515        )))
516    }
517
518    /// An issuer or holder: its URI, or an object with its URI as `id`.
519    fn party(description: &str) -> OneOfBuilder {
520        one_of(
521            [
522                uri("The identifier.").into(),
523                ObjectBuilder::new()
524                    .property("id", uri("The identifier."))
525                    .required("id")
526                    .additional_properties(Some(AdditionalProperties::FreeForm(true)))
527                    .description(Some("The identifier with further properties."))
528                    .into(),
529            ],
530            description,
531        )
532    }
533
534    fn subject() -> ObjectBuilder {
535        ObjectBuilder::new()
536            .property("id", uri("The identifier of the subject."))
537            .additional_properties(Some(AdditionalProperties::FreeForm(true)))
538            .description(Some("A subject and the claims about it."))
539    }
540
541    /// One subject, or a non-empty array of them.
542    fn subjects(section: &str) -> OneOfBuilder {
543        one_of(
544            [
545                subject().into(),
546                array(subject(), "")
547                    .min_items(Some(1))
548                    .description(None::<String>)
549                    .into(),
550            ],
551            &format!("The subjects and the claims about them ({section})."),
552        )
553    }
554
555    /// An object with a `type`, such as a `credentialStatus` entry.
556    fn typed(description: &str) -> ObjectBuilder {
557        ObjectBuilder::new()
558            .property("id", uri("The identifier."))
559            .property("type", string_or_strings("A type.", "The types."))
560            .required("type")
561            .additional_properties(Some(AdditionalProperties::FreeForm(true)))
562            .description(Some(description))
563    }
564
565    /// One typed object, or an array of them.
566    fn typed_one_or_many(item: &str, description: &str) -> OneOfBuilder {
567        one_of(
568            [
569                typed(item).into(),
570                array(typed(item), "").description(None::<String>).into(),
571            ],
572            description,
573        )
574    }
575
576    /// A string, a language value object, or an array of language value objects (VCDM 2.0
577    /// §11.1).
578    fn language_value(description: &str) -> ObjectBuilder {
579        ObjectBuilder::new()
580            .schema_type(SchemaType::AnyValue)
581            .description(Some(description))
582    }
583
584    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
585    impl PartialSchema for Credential {
586        fn schema() -> RefOr<Schema> {
587            ObjectBuilder::new()
588                .description(Some(
589                    "A verifiable credential of the VC Data Model 1.1 (§4), as the `vc` claim \
590                     of a JWT carries it once `iss`, `jti`, `nbf`, `exp` and `sub` are mapped \
591                     back.",
592                ))
593                .property("@context", context(CONTEXT_V1, "VCDM 1.1 §4.1"))
594                .required("@context")
595                .property("id", uri("The identifier of the credential (VCDM 1.1 §4.2)."))
596                .property("type", types(Credential::TYPE, "VCDM 1.1 §4.3"))
597                .required("type")
598                .property("issuer", party("The issuer (VCDM 1.1 §4.5)."))
599                .required("issuer")
600                .property(
601                    "issuanceDate",
602                    date_time("When the credential becomes valid (VCDM 1.1 §4.6)."),
603                )
604                .required("issuanceDate")
605                .property(
606                    "expirationDate",
607                    date_time("When the credential ceases to be valid (VCDM 1.1 §4.8)."),
608                )
609                .property("credentialSubject", subjects("VCDM 1.1 §4.4"))
610                .required("credentialSubject")
611                .property(
612                    "credentialStatus",
613                    typed("Where to learn whether the credential is revoked or suspended (VCDM 1.1 §4.9)."),
614                )
615                .property(
616                    "credentialSchema",
617                    typed_one_or_many("A data schema.", "Data schemas (VCDM 1.1 §5.4)."),
618                )
619                .property(
620                    "refreshService",
621                    typed_one_or_many("A refresh service.", "Refresh services (VCDM 1.1 §5.6)."),
622                )
623                .property(
624                    "termsOfUse",
625                    typed_one_or_many("Terms of use.", "Terms of use (VCDM 1.1 §5.7)."),
626                )
627                .property(
628                    "evidence",
629                    typed_one_or_many("Evidence.", "Evidence (VCDM 1.1 §5.8)."),
630                )
631                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
632                .into()
633        }
634    }
635
636    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
637    impl ToSchema for Credential {
638        fn name() -> Cow<'static, str> {
639            Cow::Borrowed("Credential")
640        }
641    }
642
643    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
644    impl PartialSchema for Presentation {
645        fn schema() -> RefOr<Schema> {
646            ObjectBuilder::new()
647                .description(Some(
648                    "A verifiable presentation of the VC Data Model 1.1 (§4.10), as the `vp` \
649                     claim of a JWT carries it once `iss` and `jti` are mapped back.",
650                ))
651                .property("@context", context(CONTEXT_V1, "VCDM 1.1 §4.1"))
652                .required("@context")
653                .property(
654                    "id",
655                    uri("The identifier of the presentation (VCDM 1.1 §4.2)."),
656                )
657                .property("type", types(Presentation::TYPE, "VCDM 1.1 §4.3"))
658                .required("type")
659                .property(
660                    "verifiableCredential",
661                    array(
662                        string("A credential JWT: a compact JWS.").pattern(Some(JWS_PATTERN)),
663                        "The presented credentials, as JWTs (VCDM 1.1 §6.3.1).",
664                    ),
665                )
666                .property("holder", party("The holder presenting the credentials."))
667                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
668                .into()
669        }
670    }
671
672    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
673    impl ToSchema for Presentation {
674        fn name() -> Cow<'static, str> {
675            Cow::Borrowed("Presentation")
676        }
677    }
678
679    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
680    impl PartialSchema for CredentialV2 {
681        fn schema() -> RefOr<Schema> {
682            ObjectBuilder::new()
683                .description(Some(
684                    "A verifiable credential of the VC Data Model 2.0 (§4), the payload of a \
685                     `vc+jwt` (VC-JOSE-COSE §3.1.1).",
686                ))
687                .property("@context", context(CONTEXT_V2, "VCDM 2.0 §4.3"))
688                .required("@context")
689                .property("id", uri("The identifier of the credential (VCDM 2.0 §4.4)."))
690                .property("type", types(CredentialV2::TYPE, "VCDM 2.0 §4.5"))
691                .required("type")
692                .property(
693                    "name",
694                    language_value("A name for the credential (VCDM 2.0 §4.6)."),
695                )
696                .property(
697                    "description",
698                    language_value("A description of the credential (VCDM 2.0 §4.6)."),
699                )
700                .property("issuer", party("The issuer (VCDM 2.0 §4.7)."))
701                .required("issuer")
702                .property(
703                    "validFrom",
704                    date_time("When the credential becomes valid (VCDM 2.0 §4.9)."),
705                )
706                .property(
707                    "validUntil",
708                    date_time("When the credential ceases to be valid (VCDM 2.0 §4.9)."),
709                )
710                .property(
711                    "credentialStatus",
712                    typed_one_or_many(
713                        "A status entry.",
714                        "Where to learn whether the credential is revoked or suspended (VCDM 2.0 §4.10).",
715                    ),
716                )
717                .property("credentialSubject", subjects("VCDM 2.0 §4.8"))
718                .required("credentialSubject")
719                .property(
720                    "credentialSchema",
721                    typed_one_or_many("A data schema.", "Data schemas (VCDM 2.0 §4.11)."),
722                )
723                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
724                .into()
725        }
726    }
727
728    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
729    impl ToSchema for CredentialV2 {
730        fn name() -> Cow<'static, str> {
731            Cow::Borrowed("CredentialV2")
732        }
733    }
734
735    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
736    impl PartialSchema for PresentationV2 {
737        fn schema() -> RefOr<Schema> {
738            ObjectBuilder::new()
739                .description(Some(
740                    "A verifiable presentation of the VC Data Model 2.0 (§4.13), the payload of \
741                     a `vp+jwt` (VC-JOSE-COSE §3.1.2), carrying enveloped credentials.",
742                ))
743                .property("@context", context(CONTEXT_V2, "VCDM 2.0 §4.3"))
744                .required("@context")
745                .property(
746                    "id",
747                    uri("The identifier of the presentation (VCDM 2.0 §4.4)."),
748                )
749                .property("type", types(PresentationV2::TYPE, "VCDM 2.0 §4.5"))
750                .required("type")
751                .property("holder", party("The holder presenting the credentials."))
752                .property(
753                    "verifiableCredential",
754                    array(
755                        schema_ref::<EnvelopedVc>(),
756                        "The presented credentials, enveloped (VCDM 2.0 §4.13).",
757                    ),
758                )
759                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
760                .into()
761        }
762    }
763
764    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
765    impl ToSchema for PresentationV2 {
766        fn name() -> Cow<'static, str> {
767            Cow::Borrowed("PresentationV2")
768        }
769
770        fn schemas(schemas: &mut Schemas) {
771            collect::<EnvelopedVc>(schemas);
772        }
773    }
774
775    /// An enveloped credential or presentation of `object_type`, secured as `media_type`.
776    fn enveloped(object_type: &str, media_type: &str, section: &str) -> RefOr<Schema> {
777        ObjectBuilder::new()
778            .description(Some(format!(
779                "An `{object_type}` (VCDM 2.0 {section}): a secured object carried in a `data:` \
780                 URL, `{media_type}` for a JWT."
781            )))
782            .property("@context", context(CONTEXT_V2, "VCDM 2.0 §4.3"))
783            .required("@context")
784            .property(
785                "id",
786                uri("The `data:` URL holding the secured form (RFC 2397).").pattern(Some("^data:")),
787            )
788            .required("id")
789            .property("type", types(object_type, "VCDM 2.0 §4.5"))
790            .required("type")
791            .into()
792    }
793
794    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
795    impl PartialSchema for EnvelopedVc {
796        fn schema() -> RefOr<Schema> {
797            enveloped(EnvelopedVc::TYPE, EnvelopedVc::JWT_MEDIA_TYPE, "§4.12.1")
798        }
799    }
800
801    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
802    impl ToSchema for EnvelopedVc {
803        fn name() -> Cow<'static, str> {
804            Cow::Borrowed("EnvelopedVc")
805        }
806    }
807
808    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
809    impl PartialSchema for EnvelopedVp {
810        fn schema() -> RefOr<Schema> {
811            enveloped(EnvelopedVp::TYPE, EnvelopedVp::JWT_MEDIA_TYPE, "§4.13.1")
812        }
813    }
814
815    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
816    impl ToSchema for EnvelopedVp {
817        fn name() -> Cow<'static, str> {
818            Cow::Borrowed("EnvelopedVp")
819        }
820    }
821}
822
823// ---------------------------------------------------------------------------------------------
824// Claim paths
825// ---------------------------------------------------------------------------------------------
826
827#[cfg(feature = "vc")]
828mod claim_path {
829    use super::{
830        Cow, ObjectBuilder, PartialSchema, RefOr, Schema, ToSchema, Type, array, one_of, string,
831    };
832    use crate::vc::ClaimPath;
833
834    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
835    impl PartialSchema for ClaimPath {
836        fn schema() -> RefOr<Schema> {
837            array(
838                one_of(
839                    [
840                        string("An object key.").into(),
841                        ObjectBuilder::new()
842                            .schema_type(Type::Integer)
843                            .minimum(Some(0))
844                            .description(Some("An array index."))
845                            .into(),
846                        ObjectBuilder::new()
847                            .schema_type(Type::Null)
848                            .description(Some("Every element of an array."))
849                            .into(),
850                    ],
851                    "A path component.",
852                ),
853                "A claim path (SD-JWT VC §5.6.1), the claims path pointer of OID4VP §7: where a \
854                 claim sits in a JSON credential.",
855            )
856            .min_items(Some(1))
857            .into()
858        }
859    }
860
861    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
862    impl ToSchema for ClaimPath {
863        fn name() -> Cow<'static, str> {
864            Cow::Borrowed("ClaimPath")
865        }
866    }
867}
868
869// ---------------------------------------------------------------------------------------------
870// SD-JWT VC type metadata
871// ---------------------------------------------------------------------------------------------
872
873#[cfg(feature = "sd-jwt")]
874mod type_metadata {
875    use super::{
876        AdditionalProperties, Cow, ObjectBuilder, PartialSchema, RefBuilder, RefOr, Schema,
877        Schemas, ToSchema, Type, any_object, array, collect, schema_ref, string, uri,
878    };
879    use crate::sd_jwt_vc::{ClaimDisplay, ClaimMetadata, DisplayMetadata, TypeMetadata};
880    use crate::vc::ClaimPath;
881
882    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
883    impl PartialSchema for TypeMetadata {
884        fn schema() -> RefOr<Schema> {
885            ObjectBuilder::new()
886                .description(Some(
887                    "SD-JWT VC type metadata (SD-JWT VC §5): what a `vct` means, how to \
888                     display it, and which claims it carries.",
889                ))
890                .property(
891                    "vct",
892                    string("The type described: a case-sensitive collision-resistant name."),
893                )
894                .required("vct")
895                .property("name", string("A name for developers."))
896                .property("description", string("A description for developers."))
897                .property("extends", uri("The type this one extends."))
898                .property(
899                    "extends#integrity",
900                    string(
901                        "The integrity metadata of the extended type's document (SD-JWT VC §6).",
902                    ),
903                )
904                .property(
905                    "display",
906                    array(
907                        schema_ref::<DisplayMetadata>(),
908                        "Display information per locale (SD-JWT VC §5.5).",
909                    ),
910                )
911                .property(
912                    "claims",
913                    array(
914                        schema_ref::<ClaimMetadata>(),
915                        "Claim metadata (SD-JWT VC §5.6).",
916                    ),
917                )
918                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
919                .into()
920        }
921    }
922
923    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
924    impl ToSchema for TypeMetadata {
925        fn name() -> Cow<'static, str> {
926            Cow::Borrowed("TypeMetadata")
927        }
928
929        fn schemas(schemas: &mut Schemas) {
930            collect::<DisplayMetadata>(schemas);
931            collect::<ClaimMetadata>(schemas);
932        }
933    }
934
935    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
936    impl PartialSchema for DisplayMetadata {
937        fn schema() -> RefOr<Schema> {
938            ObjectBuilder::new()
939                .description(Some(
940                    "Display information for a type in one locale (SD-JWT VC §5.5).",
941                ))
942                .property("locale", string("The language tag (RFC 5646)."))
943                .required("locale")
944                .property("name", string("The name of the type for end users."))
945                .required("name")
946                .property("description", string("A description for end users."))
947                .property(
948                    "rendering",
949                    any_object("Rendering information: `simple`, `svg_templates`."),
950                )
951                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
952                .into()
953        }
954    }
955
956    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
957    impl ToSchema for DisplayMetadata {
958        fn name() -> Cow<'static, str> {
959            Cow::Borrowed("DisplayMetadata")
960        }
961    }
962
963    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
964    impl PartialSchema for ClaimMetadata {
965        fn schema() -> RefOr<Schema> {
966            ObjectBuilder::new()
967                .description(Some(
968                    "Metadata for the claim or claims at a path (SD-JWT VC §5.6).",
969                ))
970                .property(
971                    "path",
972                    RefBuilder::new()
973                        .ref_location_from_schema_name(ClaimPath::name())
974                        .description(Some("The claim path (SD-JWT VC §5.6.1).")),
975                )
976                .required("path")
977                .property(
978                    "display",
979                    array(
980                        schema_ref::<ClaimDisplay>(),
981                        "Display information per locale (SD-JWT VC §5.6.2).",
982                    ),
983                )
984                .property(
985                    "mandatory",
986                    ObjectBuilder::new()
987                        .schema_type(Type::Boolean)
988                        .description(Some(
989                            "Whether the issuer must include the claim; default false.",
990                        )),
991                )
992                .property(
993                    "sd",
994                    string(
995                        "Whether the claim is selectively disclosable (SD-JWT VC §5.6.4); \
996                         default `allowed`.",
997                    )
998                    .enum_values(Some(["always", "allowed", "never"])),
999                )
1000                .property("svg_id", string("The id of the claim in SVG templates."))
1001                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
1002                .into()
1003        }
1004    }
1005
1006    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
1007    impl ToSchema for ClaimMetadata {
1008        fn name() -> Cow<'static, str> {
1009            Cow::Borrowed("ClaimMetadata")
1010        }
1011
1012        fn schemas(schemas: &mut Schemas) {
1013            collect::<ClaimDisplay>(schemas);
1014            collect::<ClaimPath>(schemas);
1015        }
1016    }
1017
1018    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
1019    impl PartialSchema for ClaimDisplay {
1020        fn schema() -> RefOr<Schema> {
1021            ObjectBuilder::new()
1022                .description(Some(
1023                    "Display information for a claim in one locale (SD-JWT VC §5.6.2).",
1024                ))
1025                .property("locale", string("The language tag (RFC 5646)."))
1026                .required("locale")
1027                .property("label", string("The label of the claim for end users."))
1028                .required("label")
1029                .property("description", string("A description for end users."))
1030                .additional_properties(Some(AdditionalProperties::FreeForm(true)))
1031                .into()
1032        }
1033    }
1034
1035    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
1036    impl ToSchema for ClaimDisplay {
1037        fn name() -> Cow<'static, str> {
1038            Cow::Borrowed("ClaimDisplay")
1039        }
1040    }
1041}
1042
1043// ---------------------------------------------------------------------------------------------
1044// Token Status List
1045// ---------------------------------------------------------------------------------------------
1046
1047#[cfg(feature = "status-list")]
1048mod status {
1049    use super::{Cow, ObjectBuilder, PartialSchema, RefOr, Schema, ToSchema, Type, one_of, string};
1050    use crate::status_list::StatusType;
1051
1052    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "status-list"))))]
1053    impl PartialSchema for StatusType {
1054        fn schema() -> RefOr<Schema> {
1055            one_of(
1056                [
1057                    string("A status type with a registered name of its own.")
1058                        .enum_values(Some(["VALID", "INVALID", "SUSPENDED"]))
1059                        .into(),
1060                    ObjectBuilder::new()
1061                        .schema_type(Type::Integer)
1062                        .minimum(Some(0))
1063                        .maximum(Some(255))
1064                        .description(Some("Any other status type value."))
1065                        .into(),
1066                ],
1067                "A status type (Token Status List §7): its registered name, or its value.",
1068            )
1069            .into()
1070        }
1071    }
1072
1073    #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "status-list"))))]
1074    impl ToSchema for StatusType {
1075        fn name() -> Cow<'static, str> {
1076            Cow::Borrowed("StatusType")
1077        }
1078    }
1079}