1#[cfg(all(not(feature = "std"), feature = "alloc"))]
16use alloc::string::{String, ToString};
17#[cfg(all(not(feature = "std"), feature = "alloc"))]
18use alloc::vec::Vec;
19use core::fmt;
20use core::str::FromStr;
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Deserializer, Serialize};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum OrgIdError {
31 InvalidUuid(uuid::Error),
33}
34
35impl fmt::Display for OrgIdError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 Self::InvalidUuid(e) => write!(f, "invalid org ID: {e}"),
39 }
40 }
41}
42
43#[cfg(feature = "std")]
44impl std::error::Error for OrgIdError {
45 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46 match self {
47 Self::InvalidUuid(e) => Some(e),
48 }
49 }
50}
51
52#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(Serialize), serde(transparent))]
66#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
67#[cfg_attr(feature = "utoipa", schema(value_type = String, format = "uuid"))]
68#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
69pub struct OrgId(uuid::Uuid);
70
71impl OrgId {
72 #[must_use]
81 pub const fn new(id: uuid::Uuid) -> Self {
82 Self(id)
83 }
84
85 #[must_use]
94 pub fn generate() -> Self {
95 Self(uuid::Uuid::new_v4())
96 }
97
98 #[must_use]
108 pub fn inner(&self) -> uuid::Uuid {
109 self.0
110 }
111}
112
113#[cfg(feature = "std")]
118impl crate::header_id::HeaderId for OrgId {
119 const HEADER_NAME: &'static str = "X-Org-Id";
120
121 fn as_str(&self) -> std::borrow::Cow<'_, str> {
122 std::borrow::Cow::Owned(self.0.to_string())
123 }
124}
125
126#[cfg(all(not(feature = "std"), feature = "alloc"))]
127impl crate::header_id::HeaderId for OrgId {
128 const HEADER_NAME: &'static str = "X-Org-Id";
129
130 fn as_str(&self) -> alloc::borrow::Cow<'_, str> {
131 alloc::borrow::Cow::Owned(self.0.to_string())
132 }
133}
134
135impl Default for OrgId {
140 fn default() -> Self {
141 Self::generate()
142 }
143}
144
145impl fmt::Display for OrgId {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 fmt::Display::fmt(&self.0, f)
148 }
149}
150
151impl From<uuid::Uuid> for OrgId {
152 fn from(id: uuid::Uuid) -> Self {
153 Self(id)
154 }
155}
156
157impl From<OrgId> for uuid::Uuid {
158 fn from(o: OrgId) -> Self {
159 o.0
160 }
161}
162
163impl FromStr for OrgId {
164 type Err = OrgIdError;
165
166 fn from_str(s: &str) -> Result<Self, Self::Err> {
167 uuid::Uuid::parse_str(s)
168 .map(Self)
169 .map_err(OrgIdError::InvalidUuid)
170 }
171}
172
173impl TryFrom<&str> for OrgId {
174 type Error = OrgIdError;
175
176 fn try_from(s: &str) -> Result<Self, Self::Error> {
177 s.parse()
178 }
179}
180
181impl TryFrom<String> for OrgId {
182 type Error = OrgIdError;
183
184 fn try_from(s: String) -> Result<Self, Self::Error> {
185 s.parse()
186 }
187}
188
189#[cfg(feature = "serde")]
194impl<'de> Deserialize<'de> for OrgId {
195 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
196 let s = String::deserialize(deserializer)?;
197 s.parse::<Self>().map_err(serde::de::Error::custom)
198 }
199}
200
201#[cfg(feature = "http")]
207#[derive(Debug, Clone, PartialEq, Eq)]
208#[non_exhaustive]
209pub enum OrgIdHeaderError {
210 Missing,
212 NotUtf8,
214 Invalid(OrgIdError),
216}
217
218#[cfg(feature = "http")]
219impl fmt::Display for OrgIdHeaderError {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 match self {
222 Self::Missing => write!(f, "missing required header: X-Org-Id"),
223 Self::NotUtf8 => write!(f, "header X-Org-Id contains non-UTF-8 bytes"),
224 Self::Invalid(e) => write!(f, "invalid X-Org-Id: {e}"),
225 }
226 }
227}
228
229#[cfg(all(feature = "http", feature = "std"))]
230impl std::error::Error for OrgIdHeaderError {
231 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
232 match self {
233 Self::Invalid(e) => Some(e),
234 Self::Missing | Self::NotUtf8 => None,
235 }
236 }
237}
238
239#[cfg(feature = "http")]
240impl OrgId {
241 pub fn try_from_headers(headers: &http::HeaderMap) -> Result<Self, OrgIdHeaderError> {
270 let raw = headers
271 .get("x-org-id")
272 .ok_or(OrgIdHeaderError::Missing)?
273 .to_str()
274 .map_err(|_| OrgIdHeaderError::NotUtf8)?;
275 raw.parse::<Self>().map_err(OrgIdHeaderError::Invalid)
276 }
277}
278
279#[cfg(feature = "axum")]
293#[doc(hidden)]
294pub fn __adr_platform_0015_proof() {}
295
296#[derive(Clone, PartialEq, Eq, Debug, Default)]
305#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(transparent))]
306#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
307#[cfg_attr(feature = "utoipa", schema(value_type = String))]
308#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
309pub struct OrgPath(Vec<OrgId>);
310
311impl OrgPath {
312 #[must_use]
314 pub fn new(path: Vec<OrgId>) -> Self {
315 Self(path)
316 }
317
318 #[must_use]
320 pub fn as_slice(&self) -> &[OrgId] {
321 &self.0
322 }
323
324 #[must_use]
326 pub fn into_inner(self) -> Vec<OrgId> {
327 self.0
328 }
329}
330
331#[cfg(feature = "std")]
332impl crate::header_id::HeaderId for OrgPath {
333 const HEADER_NAME: &'static str = "X-Org-Path";
334 fn as_str(&self) -> std::borrow::Cow<'_, str> {
335 std::borrow::Cow::Owned(
336 self.0
337 .iter()
338 .map(std::string::ToString::to_string)
339 .collect::<Vec<_>>()
340 .join(","),
341 )
342 }
343}
344
345#[cfg(all(not(feature = "std"), feature = "alloc"))]
346impl crate::header_id::HeaderId for OrgPath {
347 const HEADER_NAME: &'static str = "X-Org-Path";
348 fn as_str(&self) -> alloc::borrow::Cow<'_, str> {
349 alloc::borrow::Cow::Owned(
350 self.0
351 .iter()
352 .map(|id| id.to_string())
353 .collect::<Vec<_>>()
354 .join(","),
355 )
356 }
357}
358
359impl FromStr for OrgPath {
361 type Err = OrgIdError;
362 fn from_str(s: &str) -> Result<Self, Self::Err> {
363 if s.is_empty() {
364 return Ok(Self(Vec::new()));
365 }
366 s.split(',')
367 .map(|part| part.trim().parse::<OrgId>())
368 .collect::<Result<Vec<_>, _>>()
369 .map(Self)
370 }
371}
372
373#[cfg(feature = "axum")]
374impl<S: Send + Sync> axum::extract::FromRequestParts<S> for OrgPath {
375 type Rejection = crate::error::ApiError;
376 async fn from_request_parts(
377 parts: &mut axum::http::request::Parts,
378 _state: &S,
379 ) -> Result<Self, Self::Rejection> {
380 let raw = parts
381 .headers
382 .get("x-org-path")
383 .ok_or_else(|| {
384 crate::error::ApiError::bad_request("missing required header: x-org-path")
385 })?
386 .to_str()
387 .map_err(|_| {
388 crate::error::ApiError::bad_request("header x-org-path contains non-UTF-8 bytes")
389 })?;
390 raw.parse::<Self>()
391 .map_err(|e| crate::error::ApiError::bad_request(format!("invalid X-Org-Path: {e}")))
392 }
393}
394
395#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::header_id::HeaderId as _;
403
404 #[test]
405 fn new_wraps_uuid() {
406 let uuid = uuid::Uuid::nil();
407 let id = OrgId::new(uuid);
408 assert_eq!(id.inner(), uuid);
409 }
410
411 #[test]
412 fn generate_is_v4() {
413 let id = OrgId::generate();
414 assert_eq!(id.inner().get_version_num(), 4);
415 }
416
417 #[test]
418 fn display_is_hyphenated_uuid() {
419 let id = OrgId::new(uuid::Uuid::nil());
420 assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000");
421 }
422
423 #[test]
424 fn from_str_valid() {
425 let s = "550e8400-e29b-41d4-a716-446655440000";
426 let id: OrgId = s.parse().unwrap();
427 assert_eq!(id.to_string(), s);
428 }
429
430 #[test]
431 fn from_str_invalid() {
432 assert!("not-a-uuid".parse::<OrgId>().is_err());
433 }
434
435 #[test]
436 fn from_into_uuid_roundtrip() {
437 let uuid = uuid::Uuid::new_v4();
438 let id = OrgId::from(uuid);
439 let back: uuid::Uuid = id.into();
440 assert_eq!(back, uuid);
441 }
442
443 #[test]
444 fn default_generates_v4() {
445 let id = OrgId::default();
446 assert_eq!(id.inner().get_version_num(), 4);
447 }
448
449 #[test]
450 fn error_display() {
451 let err = "not-a-uuid".parse::<OrgId>().unwrap_err();
452 let s = err.to_string();
453 assert!(s.contains("invalid org ID"));
454 }
455
456 #[cfg(feature = "std")]
457 #[test]
458 fn error_source_is_some() {
459 use std::error::Error as _;
460 let err = "not-a-uuid".parse::<OrgId>().unwrap_err();
461 assert!(err.source().is_some());
462 }
463
464 #[test]
465 fn try_from_str_valid() {
466 let s = "00000000-0000-0000-0000-000000000000";
467 let id = OrgId::try_from(s).unwrap();
468 assert_eq!(id.to_string(), s);
469 }
470
471 #[test]
472 fn try_from_string_valid() {
473 let s = "550e8400-e29b-41d4-a716-446655440000".to_owned();
474 let id = OrgId::try_from(s).unwrap();
475 assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
476 }
477
478 #[cfg(feature = "serde")]
479 #[test]
480 fn serde_roundtrip() {
481 let id = OrgId::new(uuid::Uuid::nil());
482 let json = serde_json::to_string(&id).unwrap();
483 assert_eq!(json, r#""00000000-0000-0000-0000-000000000000""#);
484 let back: OrgId = serde_json::from_str(&json).unwrap();
485 assert_eq!(back, id);
486 }
487
488 #[cfg(feature = "serde")]
489 #[test]
490 fn serde_invalid_rejects() {
491 let result: Result<OrgId, _> = serde_json::from_str(r#""not-a-uuid""#);
492 assert!(result.is_err());
493 }
494
495 #[test]
496 fn header_name_const() {
497 use crate::header_id::HeaderId as _;
498 let id = OrgId::new(uuid::Uuid::nil());
499 assert_eq!(OrgId::HEADER_NAME, "X-Org-Id");
500 assert_eq!(id.as_str().as_ref(), "00000000-0000-0000-0000-000000000000");
501 }
502
503 #[cfg(all(feature = "http", not(miri)))]
504 #[test]
505 fn try_from_headers_valid() {
506 use http::HeaderMap;
507 let mut headers = HeaderMap::new();
508 headers.insert(
509 "x-org-id",
510 "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
511 );
512 let id = OrgId::try_from_headers(&headers).unwrap();
513 assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
514 }
515
516 #[cfg(all(feature = "http", not(miri)))]
517 #[test]
518 fn try_from_headers_malformed() {
519 use http::HeaderMap;
520 let mut headers = HeaderMap::new();
521 headers.insert("x-org-id", "not-a-uuid".parse().unwrap());
522 let result = OrgId::try_from_headers(&headers);
523 assert!(matches!(result, Err(OrgIdHeaderError::Invalid(_))));
524 }
525
526 #[cfg(all(feature = "http", not(miri)))]
527 #[test]
528 fn try_from_headers_missing() {
529 use http::HeaderMap;
530 let headers = HeaderMap::new();
531 let result = OrgId::try_from_headers(&headers);
532 assert_eq!(result, Err(OrgIdHeaderError::Missing));
533 }
534
535 #[cfg(all(feature = "http", not(miri)))]
536 #[test]
537 fn try_from_headers_empty() {
538 use http::HeaderMap;
539 let mut headers = HeaderMap::new();
540 headers.insert("x-org-id", "".parse().unwrap());
541 let result = OrgId::try_from_headers(&headers);
542 assert!(matches!(result, Err(OrgIdHeaderError::Invalid(_))));
543 }
544
545 #[cfg(all(feature = "http", not(miri)))]
546 #[test]
547 fn try_from_headers_multiple_values_uses_first() {
548 use http::HeaderMap;
549 let mut headers = HeaderMap::new();
550 headers.append(
551 "x-org-id",
552 "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
553 );
554 headers.append(
555 "x-org-id",
556 "660e8400-e29b-41d4-a716-446655440001".parse().unwrap(),
557 );
558 let id = OrgId::try_from_headers(&headers).unwrap();
559 assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
560 }
561
562 #[cfg(all(feature = "http", not(miri)))]
563 #[test]
564 fn try_from_headers_non_utf8() {
565 use http::{HeaderMap, HeaderValue};
566 let mut headers = HeaderMap::new();
567 headers.insert("x-org-id", HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap());
568 let result = OrgId::try_from_headers(&headers);
569 assert_eq!(result, Err(OrgIdHeaderError::NotUtf8));
570 }
571
572 #[cfg(all(feature = "http", not(miri)))]
573 #[test]
574 fn try_from_headers_error_display_missing() {
575 let err = OrgIdHeaderError::Missing;
576 let s = err.to_string();
577 assert!(s.contains("missing"));
578 assert!(s.contains("X-Org-Id"));
579 }
580
581 #[cfg(all(feature = "http", not(miri)))]
582 #[test]
583 fn try_from_headers_error_display_not_utf8() {
584 let err = OrgIdHeaderError::NotUtf8;
585 let s = err.to_string();
586 assert!(s.contains("non-UTF-8"));
587 }
588
589 #[cfg(all(feature = "http", not(miri)))]
590 #[test]
591 fn try_from_headers_error_display_invalid() {
592 let err = OrgIdHeaderError::Invalid(OrgIdError::InvalidUuid(
593 uuid::Uuid::parse_str("not-a-uuid").unwrap_err(),
594 ));
595 let s = err.to_string();
596 assert!(s.contains("invalid"));
597 }
598
599 #[cfg(all(feature = "http", feature = "std", not(miri)))]
600 #[test]
601 fn try_from_headers_error_source_for_invalid() {
602 use std::error::Error as _;
603 let err = OrgIdHeaderError::Invalid(OrgIdError::InvalidUuid(
604 uuid::Uuid::parse_str("not-a-uuid").unwrap_err(),
605 ));
606 assert!(err.source().is_some());
607 }
608
609 #[cfg(all(feature = "http", feature = "std", not(miri)))]
610 #[test]
611 fn try_from_headers_error_source_for_missing() {
612 use std::error::Error as _;
613 let err = OrgIdHeaderError::Missing;
614 assert!(err.source().is_none());
615 }
616
617 #[test]
620 fn org_path_new_and_as_slice() {
621 let id1 = OrgId::generate();
622 let id2 = OrgId::generate();
623 let path = OrgPath::new(vec![id1, id2]);
624 assert_eq!(path.as_slice().len(), 2);
625 assert_eq!(path.as_slice()[0], id1);
626 assert_eq!(path.as_slice()[1], id2);
627 }
628
629 #[test]
630 fn org_path_into_inner() {
631 let id = OrgId::generate();
632 let path = OrgPath::new(vec![id]);
633 let inner = path.into_inner();
634 assert_eq!(inner.len(), 1);
635 assert_eq!(inner[0], id);
636 }
637
638 #[test]
639 fn org_path_header_name() {
640 use crate::header_id::HeaderId as _;
641 assert_eq!(OrgPath::HEADER_NAME, "X-Org-Path");
642 }
643
644 #[test]
645 fn org_path_header_as_str_empty() {
646 let path = OrgPath::new(Vec::new());
647 assert_eq!(path.as_str().as_ref(), "");
648 }
649
650 #[test]
651 fn org_path_header_as_str_single() {
652 let id = OrgId::new(uuid::Uuid::nil());
653 let path = OrgPath::new(vec![id]);
654 assert_eq!(
655 path.as_str().as_ref(),
656 "00000000-0000-0000-0000-000000000000"
657 );
658 }
659
660 #[test]
661 fn org_path_header_as_str_multiple() {
662 let id1 = OrgId::new(uuid::Uuid::nil());
663 let id2 = OrgId::generate();
664 let path = OrgPath::new(vec![id1, id2]);
665 let s = path.as_str();
666 assert!(s.as_ref().contains("00000000-0000-0000-0000-000000000000"));
667 assert!(s.as_ref().contains(','));
668 }
669
670 #[test]
671 fn org_path_from_str_empty() {
672 let path: OrgPath = "".parse().unwrap();
673 assert!(path.as_slice().is_empty());
674 }
675
676 #[test]
677 fn org_path_from_str_single() {
678 let s = "550e8400-e29b-41d4-a716-446655440000";
679 let path: OrgPath = s.parse().unwrap();
680 assert_eq!(path.as_slice().len(), 1);
681 assert_eq!(path.as_slice()[0].to_string(), s);
682 }
683
684 #[test]
685 fn org_path_from_str_multiple() {
686 let s = "550e8400-e29b-41d4-a716-446655440000,660e8400-e29b-41d4-a716-446655440001";
687 let path: OrgPath = s.parse().unwrap();
688 assert_eq!(path.as_slice().len(), 2);
689 }
690
691 #[test]
692 fn org_path_from_str_invalid() {
693 let result: Result<OrgPath, _> = "not-a-uuid".parse();
694 assert!(result.is_err());
695 }
696
697 #[cfg(feature = "axum")]
698 #[tokio::test]
699 async fn org_path_axum_extractor_valid() {
700 use axum::extract::FromRequestParts;
701 use axum::http::Request;
702
703 let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
704 let req = Request::builder()
705 .header("x-org-path", uuid_str)
706 .body(())
707 .unwrap();
708 let (mut parts, ()) = req.into_parts();
709 let path = OrgPath::from_request_parts(&mut parts, &()).await.unwrap();
710 assert_eq!(path.as_slice().len(), 1);
711 assert_eq!(path.as_slice()[0].to_string(), uuid_str);
712 }
713
714 #[cfg(feature = "axum")]
715 #[tokio::test]
716 async fn org_path_axum_extractor_missing_header() {
717 use axum::extract::FromRequestParts;
718 use axum::http::Request;
719
720 let req = Request::builder().body(()).unwrap();
721 let (mut parts, ()) = req.into_parts();
722 let result = OrgPath::from_request_parts(&mut parts, &()).await;
723 assert!(result.is_err());
724 let err = result.unwrap_err();
725 assert_eq!(err.status, 400);
726 }
727
728 #[cfg(feature = "axum")]
729 #[tokio::test]
730 async fn org_path_axum_extractor_invalid_uuid() {
731 use axum::extract::FromRequestParts;
732 use axum::http::Request;
733
734 let req = Request::builder()
735 .header("x-org-path", "not-a-uuid")
736 .body(())
737 .unwrap();
738 let (mut parts, ()) = req.into_parts();
739 let result = OrgPath::from_request_parts(&mut parts, &()).await;
740 assert!(result.is_err());
741 let err = result.unwrap_err();
742 assert_eq!(err.status, 400);
743 }
744
745 #[cfg(feature = "axum")]
746 #[tokio::test]
747 async fn org_path_axum_extractor_non_utf8_returns_400() {
748 use axum::extract::FromRequestParts;
749 use axum::http::{Request, header::HeaderValue};
750
751 let mut req = Request::builder().body(()).unwrap();
752 req.headers_mut().insert(
753 "x-org-path",
754 HeaderValue::from_bytes(&[0xFF, 0xFE]).unwrap(),
755 );
756 let (mut parts, ()) = req.into_parts();
757 let result = OrgPath::from_request_parts(&mut parts, &()).await;
758 assert!(result.is_err());
759 let err = result.unwrap_err();
760 assert_eq!(err.status, 400);
761 }
762}