1use crate::BootRequest;
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ApiVersioningStrategy {
8 Uri { prefix: String },
10 Header { header: String },
12 MediaType { key: String },
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ApiVersioning {
19 strategy: ApiVersioningStrategy,
20 default_version: Option<String>,
21}
22
23impl ApiVersioning {
24 pub fn uri() -> Self {
25 Self::uri_with_prefix("v")
26 }
27
28 pub fn uri_with_prefix(prefix: impl Into<String>) -> Self {
29 Self::new(ApiVersioningStrategy::Uri {
30 prefix: prefix.into(),
31 })
32 }
33
34 pub fn header(header: impl Into<String>) -> Self {
35 Self::new(ApiVersioningStrategy::Header {
36 header: header.into(),
37 })
38 }
39
40 pub fn media_type() -> Self {
41 Self::media_type_with_key("v")
42 }
43
44 pub fn media_type_with_key(key: impl Into<String>) -> Self {
45 Self::new(ApiVersioningStrategy::MediaType { key: key.into() })
46 }
47
48 pub fn new(strategy: ApiVersioningStrategy) -> Self {
49 Self {
50 strategy,
51 default_version: None,
52 }
53 }
54
55 pub fn with_default_version(mut self, version: impl Into<String>) -> Self {
56 self.default_version = Some(normalize_version(version));
57 self
58 }
59
60 pub fn strategy(&self) -> &ApiVersioningStrategy {
61 &self.strategy
62 }
63
64 pub fn default_version(&self) -> Option<&str> {
65 self.default_version.as_deref()
66 }
67
68 pub(crate) fn request_candidates(&self, request: &BootRequest) -> Vec<ApiVersionCandidate> {
69 match &self.strategy {
70 ApiVersioningStrategy::Uri { prefix } => uri_candidates(request.path(), prefix),
71 ApiVersioningStrategy::Header { header } => vec![ApiVersionCandidate {
72 path: request.path().to_string(),
73 version: request
74 .header(header)
75 .map(normalize_version)
76 .filter(|version| !version.is_empty()),
77 }],
78 ApiVersioningStrategy::MediaType { key } => vec![ApiVersionCandidate {
79 path: request.path().to_string(),
80 version: media_type_version(request.header_values("accept"), key),
81 }],
82 }
83 }
84
85 pub(crate) fn path_candidates(&self, path: &str) -> Vec<ApiVersionCandidate> {
86 match &self.strategy {
87 ApiVersioningStrategy::Uri { prefix } => uri_candidates(path, prefix),
88 ApiVersioningStrategy::Header { .. } | ApiVersioningStrategy::MediaType { .. } => {
89 vec![ApiVersionCandidate {
90 path: path.to_string(),
91 version: None,
92 }]
93 }
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub(crate) struct ApiVersionCandidate {
100 pub path: String,
101 pub version: Option<String>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Default)]
106pub enum RouteVersioning {
107 #[default]
108 Unspecified,
109 Versions(Vec<String>),
110 Neutral,
111}
112
113impl RouteVersioning {
114 pub fn versions<I, V>(versions: I) -> Self
115 where
116 I: IntoIterator<Item = V>,
117 V: Into<String>,
118 {
119 let mut values = Vec::new();
120 for version in versions {
121 let version = normalize_version(version);
122 if !version.is_empty() && !values.contains(&version) {
123 values.push(version);
124 }
125 }
126 Self::Versions(values)
127 }
128
129 pub fn version(version: impl Into<String>) -> Self {
130 Self::versions([version])
131 }
132
133 pub fn neutral() -> Self {
134 Self::Neutral
135 }
136
137 pub fn is_unspecified(&self) -> bool {
138 matches!(self, Self::Unspecified)
139 }
140
141 pub fn matches(&self, requested: Option<&str>, default_version: Option<&str>) -> bool {
142 match self {
143 Self::Neutral => true,
144 Self::Unspecified => match (requested, default_version) {
145 (Some(requested), Some(default)) => requested == default,
146 (Some(_), None) => false,
147 (None, _) => true,
148 },
149 Self::Versions(versions) => {
150 let requested = requested.or(default_version);
151 requested
152 .map(|requested| versions.iter().any(|version| version == requested))
153 .unwrap_or(false)
154 }
155 }
156 }
157
158 pub fn overlaps(&self, other: &Self, default_version: Option<&str>) -> bool {
159 match (self, other) {
160 (Self::Neutral, _) | (_, Self::Neutral) => true,
161 (Self::Unspecified, Self::Unspecified) => true,
162 (Self::Unspecified, Self::Versions(versions))
163 | (Self::Versions(versions), Self::Unspecified) => default_version
164 .map(|default| versions.iter().any(|version| version == default))
165 .unwrap_or(false),
166 (Self::Versions(left), Self::Versions(right)) => {
167 left.iter().any(|version| right.contains(version))
168 }
169 }
170 }
171}
172
173impl fmt::Display for RouteVersioning {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match self {
176 Self::Unspecified => f.write_str("unspecified"),
177 Self::Neutral => f.write_str("neutral"),
178 Self::Versions(versions) => f.write_str(&versions.join(",")),
179 }
180 }
181}
182
183fn uri_candidates(path: &str, prefix: &str) -> Vec<ApiVersionCandidate> {
184 let segments = path
185 .trim_start_matches('/')
186 .split('/')
187 .filter(|segment| !segment.is_empty())
188 .collect::<Vec<_>>();
189
190 for (index, segment) in segments.iter().enumerate() {
191 let Some(version) = segment.strip_prefix(prefix) else {
192 continue;
193 };
194 if version.is_empty() {
195 continue;
196 }
197
198 let mut remaining = segments.clone();
199 remaining.remove(index);
200 let path = if remaining.is_empty() {
201 "/".to_string()
202 } else {
203 format!("/{}", remaining.join("/"))
204 };
205
206 return vec![ApiVersionCandidate {
207 path,
208 version: Some(normalize_version(version)),
209 }];
210 }
211
212 vec![ApiVersionCandidate {
213 path: path.to_string(),
214 version: None,
215 }]
216}
217
218fn media_type_version(values: Vec<&str>, key: &str) -> Option<String> {
219 for value in values {
220 for media_range in value.split(',') {
221 for parameter in media_range.split(';').skip(1) {
222 let Some((name, value)) = parameter.trim().split_once('=') else {
223 continue;
224 };
225 if name.trim().eq_ignore_ascii_case(key) {
226 let version = normalize_version(value.trim().trim_matches('"'));
227 if !version.is_empty() {
228 return Some(version);
229 }
230 }
231 }
232 }
233 }
234 None
235}
236
237fn normalize_version(version: impl Into<String>) -> String {
238 let version = version.into();
239 match serde_json::from_str::<Value>(&version) {
240 Ok(Value::Number(number)) => number.to_string(),
241 _ => version.trim().to_string(),
242 }
243}