Skip to main content

zerodds_web/
headers.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! HTTP-Headers — Spec §8.3.5 Tab 7 + Tab 8.
5
6use alloc::string::String;
7
8/// Spec §8.3.5 Tab 7 — Required HTTP-Request-Header `OMG-DDS-API-Key`.
9pub const REQUEST_API_KEY: &str = "OMG-DDS-API-Key";
10
11/// Spec §8.3.5 Tab 7 — Request-Header-Set.
12#[derive(Debug, Clone, Default, PartialEq, Eq)]
13pub struct RequestHeaders {
14    /// `Accept` header — Spec verlangt `application/zerodds-web+xml`.
15    pub accept: Option<String>,
16    /// `Content-Type` header (only POST/PUT).
17    pub content_type: Option<String>,
18    /// `Content-Length` header (only POST/PUT/DELETE).
19    pub content_length: Option<u64>,
20    /// `Cache-Control` header — Spec verlangt RFC 2616 §14.9.
21    pub cache_control: Option<String>,
22    /// `OMG-DDS-API-Key` — Spec §8.3.5 Required.
23    pub api_key: Option<String>,
24}
25
26impl RequestHeaders {
27    /// Spec §8.3.5 — Required-Felder pruefen.
28    ///
29    /// # Errors
30    /// `Err(MissingHeader::ApiKey)` wenn `api_key` fehlt.
31    /// `Err(MissingHeader::Accept)` wenn `accept` fehlt.
32    /// `Err(MissingHeader::CacheControl)` wenn `cache_control` fehlt.
33    pub fn validate_required(&self) -> Result<(), MissingHeader> {
34        if self.api_key.is_none() {
35            return Err(MissingHeader::ApiKey);
36        }
37        if self.accept.is_none() {
38            return Err(MissingHeader::Accept);
39        }
40        if self.cache_control.is_none() {
41            return Err(MissingHeader::CacheControl);
42        }
43        Ok(())
44    }
45}
46
47/// Spec §8.3.5 Tab 8 — Response-Header-Set.
48#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub struct ResponseHeaders {
50    /// `Authentication-Info` (auf Response-zu-Login Required).
51    pub authentication_info: Option<String>,
52    /// `Cache-Control` (Required).
53    pub cache_control: Option<String>,
54    /// `Content-Length` (Required).
55    pub content_length: Option<u64>,
56    /// `Content-Type` (Required) — `application/zerodds-web+xml`.
57    pub content_type: Option<String>,
58    /// `Date` (Optional).
59    pub date: Option<String>,
60    /// `Expires` (Optional).
61    pub expires: Option<String>,
62    /// `Location` (Required fuer 201-Response auf POST).
63    pub location: Option<String>,
64    /// `Last-Modified` (Required fuer 200-Response auf GET/HEAD).
65    pub last_modified: Option<String>,
66}
67
68/// Header-Validation-Fehler.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum MissingHeader {
71    /// `OMG-DDS-API-Key` fehlt.
72    ApiKey,
73    /// `Accept` fehlt.
74    Accept,
75    /// `Cache-Control` fehlt.
76    CacheControl,
77}
78
79impl core::fmt::Display for MissingHeader {
80    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
81        match self {
82            Self::ApiKey => f.write_str("missing OMG-DDS-API-Key"),
83            Self::Accept => f.write_str("missing Accept"),
84            Self::CacheControl => f.write_str("missing Cache-Control"),
85        }
86    }
87}
88
89#[cfg(feature = "std")]
90impl std::error::Error for MissingHeader {}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn request_api_key_constant_matches_spec() {
98        assert_eq!(REQUEST_API_KEY, "OMG-DDS-API-Key");
99    }
100
101    #[test]
102    fn validate_required_passes_with_all_three_headers() {
103        let h = RequestHeaders {
104            accept: Some(String::from("application/zerodds-web+xml")),
105            cache_control: Some(String::from("no-cache")),
106            api_key: Some(String::from("secret-key-1234")),
107            ..Default::default()
108        };
109        assert_eq!(h.validate_required(), Ok(()));
110    }
111
112    #[test]
113    fn missing_api_key_rejected() {
114        let h = RequestHeaders {
115            accept: Some(String::from("application/zerodds-web+xml")),
116            cache_control: Some(String::from("no-cache")),
117            ..Default::default()
118        };
119        assert_eq!(h.validate_required(), Err(MissingHeader::ApiKey));
120    }
121
122    #[test]
123    fn missing_accept_rejected() {
124        let h = RequestHeaders {
125            cache_control: Some(String::from("no-cache")),
126            api_key: Some(String::from("k")),
127            ..Default::default()
128        };
129        assert_eq!(h.validate_required(), Err(MissingHeader::Accept));
130    }
131
132    #[test]
133    fn missing_cache_control_rejected() {
134        let h = RequestHeaders {
135            accept: Some(String::from("application/zerodds-web+xml")),
136            api_key: Some(String::from("k")),
137            ..Default::default()
138        };
139        assert_eq!(h.validate_required(), Err(MissingHeader::CacheControl));
140    }
141
142    #[test]
143    fn response_headers_default_is_empty() {
144        let r = ResponseHeaders::default();
145        assert!(r.cache_control.is_none());
146        assert!(r.content_type.is_none());
147        assert!(r.location.is_none());
148    }
149}