Skip to main content

doxa_docs/
doc_params.rs

1//! [`DocHeaderEntry<H>`] — zero-sized marker type that implements
2//! [`utoipa::IntoParams`] via runtime resolution of
3//! [`DocumentedHeader::name`]. Used by the
4//! [`doxa-macros`](../../doxa_macros/index.html) macro
5//! pass to inject typed headers into a handler's `params(...)` block.
6
7use std::marker::PhantomData;
8
9use utoipa::openapi::path::{Parameter, ParameterBuilder, ParameterIn};
10use utoipa::openapi::{Object, RefOr, Required, Schema, Type};
11use utoipa::IntoParams;
12
13use crate::headers::DocumentedHeader;
14
15/// Generic [`IntoParams`] implementor that produces one header
16/// parameter from a [`DocumentedHeader`] marker, calling
17/// [`H::name`](DocumentedHeader::name),
18/// [`H::description`](DocumentedHeader::description), and
19/// [`H::example`](DocumentedHeader::example) at runtime.
20///
21/// You don't usually construct this yourself — the `#[get]` /
22/// `#[post]` / etc. macros emit it inside the synthesized
23/// `#[utoipa::path(params(...))]` block whenever a handler signature
24/// contains a [`crate::Header<H>`] extractor or a `headers(H, ...)`
25/// macro argument is supplied.
26///
27/// The runtime-name design (a fn on [`DocumentedHeader`], not a
28/// const) is what makes this possible: the proc-macro can't call
29/// `H::name()` during expansion, but the generated `params(DocHeaderEntry<H>)`
30/// reaches runtime where the call resolves cheaply.
31pub struct DocHeaderEntry<H: DocumentedHeader>(PhantomData<H>);
32
33impl<H: DocumentedHeader> IntoParams for DocHeaderEntry<H> {
34    fn into_params(_parameter_in_provider: impl Fn() -> Option<ParameterIn>) -> Vec<Parameter> {
35        let desc = H::description();
36        let mut b = ParameterBuilder::new()
37            .name(H::name())
38            .parameter_in(ParameterIn::Header)
39            .required(Required::True)
40            .schema(Some(RefOr::T(Schema::Object(Object::with_type(
41                Type::String,
42            )))));
43        if !desc.is_empty() {
44            b = b.description(Some(desc.to_string()));
45        }
46        if let Some(ex) = H::example() {
47            b = b.example(Some(serde_json::Value::String(ex.to_string())));
48        }
49        vec![b.build()]
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    struct XApiKey;
58    impl DocumentedHeader for XApiKey {
59        fn name() -> &'static str {
60            "X-Api-Key"
61        }
62        fn description() -> &'static str {
63            "Tenant API key"
64        }
65        fn example() -> Option<&'static str> {
66            Some("ak_live_42")
67        }
68    }
69
70    struct BareHeader;
71    impl DocumentedHeader for BareHeader {
72        fn name() -> &'static str {
73            "X-Bare"
74        }
75    }
76
77    #[test]
78    fn doc_header_entry_into_params_uses_runtime_name() {
79        let params = DocHeaderEntry::<XApiKey>::into_params(|| None);
80        assert_eq!(params.len(), 1);
81        assert_eq!(params[0].name, "X-Api-Key");
82        assert!(matches!(params[0].parameter_in, ParameterIn::Header));
83        assert!(matches!(params[0].required, Required::True));
84    }
85
86    #[test]
87    fn doc_header_entry_into_params_emits_description_when_provided() {
88        let params = DocHeaderEntry::<XApiKey>::into_params(|| None);
89        assert_eq!(params[0].description.as_deref(), Some("Tenant API key"));
90    }
91
92    #[test]
93    fn doc_header_entry_into_params_omits_description_when_empty() {
94        let params = DocHeaderEntry::<BareHeader>::into_params(|| None);
95        assert!(params[0].description.is_none());
96    }
97
98    #[test]
99    fn doc_header_entry_into_params_emits_example_when_provided() {
100        let params = DocHeaderEntry::<XApiKey>::into_params(|| None);
101        // Parameter::example is private, so round-trip via JSON.
102        let json = serde_json::to_value(&params[0]).unwrap();
103        assert_eq!(json["example"], "ak_live_42");
104    }
105}