oxidite_core/
versioning.rs1use std::collections::HashMap;
4use crate::{Router, OxiditeRequest, OxiditeResponse};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum ApiVersion {
9 V1,
10 V2,
11 V3,
12 Custom(u8),
13}
14
15impl ApiVersion {
16 pub fn from_str(s: &str) -> Option<Self> {
17 match s {
18 "v1" | "1" => Some(ApiVersion::V1),
19 "v2" | "2" => Some(ApiVersion::V2),
20 "v3" | "3" => Some(ApiVersion::V3),
21 _ => s.trim_start_matches('v').parse::<u8>().ok().map(ApiVersion::Custom),
22 }
23 }
24
25 pub fn as_str(&self) -> &'static str {
26 match self {
27 ApiVersion::V1 => "v1",
28 ApiVersion::V2 => "v2",
29 ApiVersion::V3 => "v3",
30 ApiVersion::Custom(_) => "custom",
31 }
32 }
33}
34
35pub struct VersionedRouter {
37 routers: HashMap<ApiVersion, Router>,
38 default_version: ApiVersion,
39}
40
41impl VersionedRouter {
42 pub fn new(default_version: ApiVersion) -> Self {
43 Self {
44 routers: HashMap::new(),
45 default_version,
46 }
47 }
48
49 pub fn version(&mut self, version: ApiVersion, router: Router) {
51 self.routers.insert(version, router);
52 }
53
54 pub fn extract_version(&self, req: &OxiditeRequest) -> ApiVersion {
60 if let Some(path) = req.uri().path().split('/').find(|s| s.starts_with('v')) {
62 if let Some(version) = ApiVersion::from_str(path) {
63 return version;
64 }
65 }
66
67 if let Some(accept) = req.headers().get("accept") {
69 if let Ok(accept_str) = accept.to_str() {
70 if let Some(version_part) = accept_str.split(";version=").nth(1) {
71 if let Some(version) = ApiVersion::from_str(version_part.split(',').next().unwrap_or("")) {
72 return version;
73 }
74 }
75 }
76 }
77
78 if let Some(query) = req.uri().query() {
80 for pair in query.split('&') {
81 if let Some((key, value)) = pair.split_once('=') {
82 if key == "version" {
83 if let Some(version) = ApiVersion::from_str(value) {
84 return version;
85 }
86 }
87 }
88 }
89 }
90
91 self.default_version
93 }
94
95 pub fn get_router(&self, version: ApiVersion) -> Option<&Router> {
97 self.routers.get(&version)
98 }
99}
100
101pub struct DeprecationMiddleware {
103 deprecated_versions: Vec<ApiVersion>,
104 sunset_date: Option<String>,
105}
106
107impl DeprecationMiddleware {
108 pub fn new(deprecated_versions: Vec<ApiVersion>) -> Self {
109 Self {
110 deprecated_versions,
111 sunset_date: None,
112 }
113 }
114
115 pub fn with_sunset_date(mut self, date: String) -> Self {
116 self.sunset_date = Some(date);
117 self
118 }
119
120 pub fn add_headers(&self, version: ApiVersion, response: &mut OxiditeResponse) {
122 if self.deprecated_versions.contains(&version) {
123 response.headers_mut().insert(
124 "Deprecation",
125 "true".parse().unwrap()
126 );
127
128 if let Some(date) = &self.sunset_date {
129 response.headers_mut().insert(
130 "Sunset",
131 date.parse().unwrap()
132 );
133 }
134
135 response.headers_mut().insert(
136 "Link",
137 format!("</api/docs>; rel=\"deprecation\"").parse().unwrap()
138 );
139 }
140 }
141}