1use std::borrow::Cow;
12
13use utoipa::openapi::RefOr;
14use utoipa::openapi::schema::{
15 AdditionalProperties, ArrayBuilder, ArrayItems, ObjectBuilder, OneOfBuilder, Ref, RefBuilder,
16 Schema, SchemaFormat, SchemaType, Type,
17};
18use utoipa::{PartialSchema, ToSchema};
19
20use crate::did::{DidDocument, MethodRef, Service, VerificationMethod};
21use crate::jose::Jwk;
22
23const DID_PATTERN: &str =
26 r"^did:[a-z0-9]+:(([A-Za-z0-9._-]|%[0-9A-Fa-f]{2})*:)*([A-Za-z0-9._-]|%[0-9A-Fa-f]{2})+$";
27
28type Schemas = Vec<(String, RefOr<Schema>)>;
29
30fn collect<T: ToSchema>(schemas: &mut Schemas) {
32 let name = T::name();
33 if schemas.iter().any(|(present, _)| *present == name) {
34 return;
35 }
36 schemas.push((name.into_owned(), T::schema()));
37 T::schemas(schemas);
38}
39
40fn schema_ref<T: ToSchema>() -> Ref {
41 Ref::from_schema_name(T::name().into_owned())
42}
43
44fn string(description: &str) -> ObjectBuilder {
45 ObjectBuilder::new()
46 .schema_type(Type::String)
47 .description(Some(description))
48}
49
50fn did(description: &str) -> ObjectBuilder {
51 string(description).pattern(Some(DID_PATTERN))
52}
53
54fn uri(description: &str) -> ObjectBuilder {
55 string(description).format(Some(SchemaFormat::Custom("uri".to_owned())))
56}
57
58fn any_object(description: &str) -> ObjectBuilder {
60 ObjectBuilder::new()
61 .description(Some(description))
62 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
63}
64
65fn array(items: impl Into<RefOr<Schema>>, description: &str) -> ArrayBuilder {
66 ArrayBuilder::new()
67 .items(ArrayItems::RefOrSchema(Box::new(items.into())))
68 .description(Some(description))
69}
70
71fn one_of<const N: usize>(items: [RefOr<Schema>; N], description: &str) -> OneOfBuilder {
72 items
73 .into_iter()
74 .fold(OneOfBuilder::new(), OneOfBuilder::item)
75 .description(Some(description))
76}
77
78fn string_or_strings(item: &str, description: &str) -> OneOfBuilder {
80 one_of(
81 [
82 string(item).into(),
83 array(string(item), "")
84 .min_items(Some(1))
85 .description(None::<String>)
86 .into(),
87 ],
88 description,
89 )
90}
91
92#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
97impl PartialSchema for DidDocument {
98 fn schema() -> RefOr<Schema> {
99 let relationship = |name: &str, section: &str| {
100 array(
101 schema_ref::<MethodRef>(),
102 &format!("The `{name}` relationship (DID Core §{section})."),
103 )
104 };
105 ObjectBuilder::new()
106 .description(Some(
107 "A DID document (DID Core §4): the core properties, plus any extension \
108 property.",
109 ))
110 .property(
111 "@context",
112 one_of(
113 [
114 uri("A context URI.").into(),
115 array(
116 ObjectBuilder::new().schema_type(SchemaType::AnyValue),
117 "Context URIs and objects.",
118 )
119 .min_items(Some(1))
120 .into(),
121 any_object("A context object.").into(),
122 ],
123 "The JSON-LD context, carried as data (DID Core §6.3.1).",
124 ),
125 )
126 .property("id", did("The DID of the subject (DID Core §5.1.1)."))
127 .required("id")
128 .property(
129 "alsoKnownAs",
130 array(
131 uri("A URI."),
132 "Other identifiers of the subject (DID Core §5.1.3).",
133 ),
134 )
135 .property(
136 "controller",
137 one_of(
138 [
139 did("The DID of the controller.").into(),
140 array(did("The DID of a controller."), "")
141 .description(None::<String>)
142 .into(),
143 ],
144 "The DIDs whose controllers may change this document (DID Core §5.1.2); \
145 an empty set means none.",
146 ),
147 )
148 .property(
149 "verificationMethod",
150 array(
151 schema_ref::<VerificationMethod>(),
152 "Verification methods the relationships may reference (DID Core §5.2).",
153 ),
154 )
155 .property("authentication", relationship("authentication", "5.3.1"))
156 .property("assertionMethod", relationship("assertionMethod", "5.3.2"))
157 .property("keyAgreement", relationship("keyAgreement", "5.3.3"))
158 .property(
159 "capabilityInvocation",
160 relationship("capabilityInvocation", "5.3.4"),
161 )
162 .property(
163 "capabilityDelegation",
164 relationship("capabilityDelegation", "5.3.5"),
165 )
166 .property(
167 "service",
168 array(
169 schema_ref::<Service>(),
170 "Ways of interacting with the subject (DID Core §5.4).",
171 ),
172 )
173 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
174 .into()
175 }
176}
177
178#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
179impl ToSchema for DidDocument {
180 fn name() -> Cow<'static, str> {
181 Cow::Borrowed("DidDocument")
182 }
183
184 fn schemas(schemas: &mut Schemas) {
185 collect::<VerificationMethod>(schemas);
186 collect::<MethodRef>(schemas);
187 collect::<Service>(schemas);
188 }
189}
190
191#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
192impl PartialSchema for VerificationMethod {
193 fn schema() -> RefOr<Schema> {
194 ObjectBuilder::new()
195 .description(Some(
196 "A verification method (DID Core §5.2): a public key, carried as a JWK or a \
197 multibase string, with its type and controller.",
198 ))
199 .property(
200 "id",
201 string("The DID URL of the method, usually the document's DID with a fragment."),
202 )
203 .required("id")
204 .property(
205 "type",
206 string(
207 "The verification method type, such as `JsonWebKey2020`, `JsonWebKey` or \
208 `Multikey`.",
209 ),
210 )
211 .required("type")
212 .property("controller", did("The DID of the controller of the key."))
213 .required("controller")
214 .property(
215 "publicKeyJwk",
216 RefBuilder::new()
217 .ref_location_from_schema_name(Jwk::name().into_owned())
218 .description(Some("The public key as a JWK (DID Core §5.2.1)."))
219 .build(),
220 )
221 .property(
222 "publicKeyMultibase",
223 string("The public key as a multibase string (DID Core §5.2.1)."),
224 )
225 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
226 .into()
227 }
228}
229
230#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
231impl ToSchema for VerificationMethod {
232 fn name() -> Cow<'static, str> {
233 Cow::Borrowed("VerificationMethod")
234 }
235
236 fn schemas(schemas: &mut Schemas) {
237 collect::<Jwk>(schemas);
238 }
239}
240
241#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
242impl PartialSchema for MethodRef {
243 fn schema() -> RefOr<Schema> {
244 one_of(
245 [
246 string("A DID URL, possibly relative, of a method under `verificationMethod`.")
247 .into(),
248 schema_ref::<VerificationMethod>().into(),
249 ],
250 "An entry of a verification relationship (DID Core §5.3): a reference to a \
251 verification method, or a method embedded in the relationship.",
252 )
253 .into()
254 }
255}
256
257#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
258impl ToSchema for MethodRef {
259 fn name() -> Cow<'static, str> {
260 Cow::Borrowed("MethodRef")
261 }
262
263 fn schemas(schemas: &mut Schemas) {
264 collect::<VerificationMethod>(schemas);
265 }
266}
267
268#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
269impl PartialSchema for Service {
270 fn schema() -> RefOr<Schema> {
271 ObjectBuilder::new()
272 .description(Some(
273 "A service (DID Core §5.4): a way of interacting with the DID subject.",
274 ))
275 .property(
276 "id",
277 string("The DID URL of the service, usually the document's DID with a fragment."),
278 )
279 .required("id")
280 .property(
281 "type",
282 string_or_strings("A service type.", "The service type or types."),
283 )
284 .required("type")
285 .property(
286 "serviceEndpoint",
287 one_of(
288 [
289 uri("The endpoint URI.").into(),
290 array(uri("An endpoint URI."), "")
291 .min_items(Some(1))
292 .description(None::<String>)
293 .into(),
294 any_object("An endpoint map.").into(),
295 ],
296 "Where the service is: a URI, a set of URIs, or a map.",
297 ),
298 )
299 .required("serviceEndpoint")
300 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
301 .into()
302 }
303}
304
305#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
306impl ToSchema for Service {
307 fn name() -> Cow<'static, str> {
308 Cow::Borrowed("Service")
309 }
310}
311
312#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
317impl PartialSchema for Jwk {
318 fn schema() -> RefOr<Schema> {
319 let base64url = |description: &str| string(description);
320 ObjectBuilder::new()
321 .description(Some(
322 "A JSON Web Key (RFC 7517 §4): the key type, the parameters of RFC 7518 §6 and \
323 RFC 8037 §2 for that type, and the common members. Parameters are \
324 base64url-encoded without padding.",
325 ))
326 .property(
327 "kty",
328 string("The key type (RFC 7517 §4.1): `EC`, `OKP`, `RSA` or `oct`."),
329 )
330 .required("kty")
331 .property(
332 "crv",
333 string("The curve of an `EC` or `OKP` key: `P-256`, `secp256k1`, `Ed25519`, ..."),
334 )
335 .property("x", base64url("The x coordinate of an `EC` key, or the `OKP` public key."))
336 .property("y", base64url("The y coordinate of an `EC` key."))
337 .property("n", base64url("The modulus of an `RSA` key."))
338 .property("e", base64url("The public exponent of an `RSA` key."))
339 .property(
340 "d",
341 base64url("The private key of an `EC` or `OKP` key, or the private exponent of an `RSA` key."),
342 )
343 .property("p", base64url("The first prime factor of an `RSA` private key."))
344 .property("q", base64url("The second prime factor of an `RSA` private key."))
345 .property("dp", base64url("The first factor CRT exponent of an `RSA` private key."))
346 .property("dq", base64url("The second factor CRT exponent of an `RSA` private key."))
347 .property("qi", base64url("The first CRT coefficient of an `RSA` private key."))
348 .property(
349 "oth",
350 array(
351 ObjectBuilder::new()
352 .property("r", base64url("The prime factor."))
353 .required("r")
354 .property("d", base64url("The factor CRT exponent."))
355 .required("d")
356 .property("t", base64url("The factor CRT coefficient."))
357 .required("t"),
358 "Other primes of an `RSA` private key (RFC 7518 §6.3.2.7).",
359 ),
360 )
361 .property("k", base64url("The value of an `oct` key."))
362 .property(
363 "use",
364 string("The intended use of the key (RFC 7517 §4.2): `sig` or `enc`."),
365 )
366 .property(
367 "key_ops",
368 array(
369 string("An operation, such as `sign` or `verify`."),
370 "The operations the key is intended for (RFC 7517 §4.3).",
371 ),
372 )
373 .property(
374 "alg",
375 string("The algorithm intended for use with the key (RFC 7517 §4.4)."),
376 )
377 .property("kid", string("The key id (RFC 7517 §4.5)."))
378 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
379 .into()
380 }
381}
382
383#[cfg_attr(docsrs, doc(cfg(feature = "utoipa")))]
384impl ToSchema for Jwk {
385 fn name() -> Cow<'static, str> {
386 Cow::Borrowed("Jwk")
387 }
388}
389
390#[cfg(feature = "domain-linkage")]
395#[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "domain-linkage"))))]
396impl PartialSchema for crate::domain_linkage::DomainLinkageConfiguration {
397 fn schema() -> RefOr<Schema> {
398 ObjectBuilder::new()
399 .description(Some(
400 "A DID configuration resource (Well Known DID Configuration §3.1): the context \
401 and the domain linkage credentials, one per linked DID.",
402 ))
403 .property(
404 "@context",
405 string("The DID configuration context.")
406 .enum_values(Some([crate::domain_linkage::CONTEXT])),
407 )
408 .required("@context")
409 .property(
410 "linked_dids",
411 array(
412 one_of(
413 [
414 string(
415 "A domain linkage credential in the JSON Web Token proof \
416 format: a compact JWS.",
417 )
418 .into(),
419 any_object(
420 "A domain linkage credential in the Linked Data Proof format.",
421 )
422 .into(),
423 ],
424 "A domain linkage credential.",
425 ),
426 "The domain linkage credentials.",
427 ),
428 )
429 .required("linked_dids")
430 .additional_properties(Some(AdditionalProperties::FreeForm(false)))
431 .into()
432 }
433}
434
435#[cfg(feature = "domain-linkage")]
436#[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "domain-linkage"))))]
437impl ToSchema for crate::domain_linkage::DomainLinkageConfiguration {
438 fn name() -> Cow<'static, str> {
439 Cow::Borrowed("DomainLinkageConfiguration")
440 }
441}
442
443#[cfg(feature = "vc")]
448mod claim_path {
449 use super::*;
450 use crate::vc::ClaimPath;
451
452 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
453 impl PartialSchema for ClaimPath {
454 fn schema() -> RefOr<Schema> {
455 array(
456 one_of(
457 [
458 string("An object key.").into(),
459 ObjectBuilder::new()
460 .schema_type(Type::Integer)
461 .minimum(Some(0))
462 .description(Some("An array index."))
463 .into(),
464 ObjectBuilder::new()
465 .schema_type(Type::Null)
466 .description(Some("Every element of an array."))
467 .into(),
468 ],
469 "A path component.",
470 ),
471 "A claim path (SD-JWT VC §5.6.1), the claims path pointer of OID4VP §7: where a \
472 claim sits in a JSON credential.",
473 )
474 .min_items(Some(1))
475 .into()
476 }
477 }
478
479 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "vc"))))]
480 impl ToSchema for ClaimPath {
481 fn name() -> Cow<'static, str> {
482 Cow::Borrowed("ClaimPath")
483 }
484 }
485}
486
487#[cfg(feature = "sd-jwt")]
492mod type_metadata {
493 use super::*;
494 use crate::sd_jwt_vc::{ClaimDisplay, ClaimMetadata, DisplayMetadata, TypeMetadata};
495 use crate::vc::ClaimPath;
496
497 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
498 impl PartialSchema for TypeMetadata {
499 fn schema() -> RefOr<Schema> {
500 ObjectBuilder::new()
501 .description(Some(
502 "SD-JWT VC type metadata (SD-JWT VC §5): what a `vct` means, how to \
503 display it, and which claims it carries.",
504 ))
505 .property(
506 "vct",
507 string("The type described: a case-sensitive collision-resistant name."),
508 )
509 .required("vct")
510 .property("name", string("A name for developers."))
511 .property("description", string("A description for developers."))
512 .property("extends", uri("The type this one extends."))
513 .property(
514 "extends#integrity",
515 string(
516 "The integrity metadata of the extended type's document (SD-JWT VC §6).",
517 ),
518 )
519 .property(
520 "display",
521 array(
522 schema_ref::<DisplayMetadata>(),
523 "Display information per locale (SD-JWT VC §5.5).",
524 ),
525 )
526 .property(
527 "claims",
528 array(
529 schema_ref::<ClaimMetadata>(),
530 "Claim metadata (SD-JWT VC §5.6).",
531 ),
532 )
533 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
534 .into()
535 }
536 }
537
538 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
539 impl ToSchema for TypeMetadata {
540 fn name() -> Cow<'static, str> {
541 Cow::Borrowed("TypeMetadata")
542 }
543
544 fn schemas(schemas: &mut Schemas) {
545 collect::<DisplayMetadata>(schemas);
546 collect::<ClaimMetadata>(schemas);
547 }
548 }
549
550 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
551 impl PartialSchema for DisplayMetadata {
552 fn schema() -> RefOr<Schema> {
553 ObjectBuilder::new()
554 .description(Some(
555 "Display information for a type in one locale (SD-JWT VC §5.5).",
556 ))
557 .property("locale", string("The language tag (RFC 5646)."))
558 .required("locale")
559 .property("name", string("The name of the type for end users."))
560 .required("name")
561 .property("description", string("A description for end users."))
562 .property(
563 "rendering",
564 any_object("Rendering information: `simple`, `svg_templates`."),
565 )
566 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
567 .into()
568 }
569 }
570
571 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
572 impl ToSchema for DisplayMetadata {
573 fn name() -> Cow<'static, str> {
574 Cow::Borrowed("DisplayMetadata")
575 }
576 }
577
578 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
579 impl PartialSchema for ClaimMetadata {
580 fn schema() -> RefOr<Schema> {
581 ObjectBuilder::new()
582 .description(Some(
583 "Metadata for the claim or claims at a path (SD-JWT VC §5.6).",
584 ))
585 .property(
586 "path",
587 RefBuilder::new()
588 .ref_location_from_schema_name(ClaimPath::name())
589 .description(Some("The claim path (SD-JWT VC §5.6.1).")),
590 )
591 .required("path")
592 .property(
593 "display",
594 array(
595 schema_ref::<ClaimDisplay>(),
596 "Display information per locale (SD-JWT VC §5.6.2).",
597 ),
598 )
599 .property(
600 "mandatory",
601 ObjectBuilder::new()
602 .schema_type(Type::Boolean)
603 .description(Some(
604 "Whether the issuer must include the claim; default false.",
605 )),
606 )
607 .property(
608 "sd",
609 string(
610 "Whether the claim is selectively disclosable (SD-JWT VC §5.6.4); \
611 default `allowed`.",
612 )
613 .enum_values(Some(["always", "allowed", "never"])),
614 )
615 .property("svg_id", string("The id of the claim in SVG templates."))
616 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
617 .into()
618 }
619 }
620
621 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
622 impl ToSchema for ClaimMetadata {
623 fn name() -> Cow<'static, str> {
624 Cow::Borrowed("ClaimMetadata")
625 }
626
627 fn schemas(schemas: &mut Schemas) {
628 collect::<ClaimDisplay>(schemas);
629 collect::<ClaimPath>(schemas);
630 }
631 }
632
633 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
634 impl PartialSchema for ClaimDisplay {
635 fn schema() -> RefOr<Schema> {
636 ObjectBuilder::new()
637 .description(Some(
638 "Display information for a claim in one locale (SD-JWT VC §5.6.2).",
639 ))
640 .property("locale", string("The language tag (RFC 5646)."))
641 .required("locale")
642 .property("label", string("The label of the claim for end users."))
643 .required("label")
644 .property("description", string("A description for end users."))
645 .additional_properties(Some(AdditionalProperties::FreeForm(true)))
646 .into()
647 }
648 }
649
650 #[cfg_attr(docsrs, doc(cfg(all(feature = "utoipa", feature = "sd-jwt"))))]
651 impl ToSchema for ClaimDisplay {
652 fn name() -> Cow<'static, str> {
653 Cow::Borrowed("ClaimDisplay")
654 }
655 }
656}