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