openapi_mocker/openapi/
spec.rs1use std::collections::HashMap;
2
3use actix_web::HttpRequest;
4use oas3::spec::{Example, MediaTypeExamples, ObjectOrReference, Operation, PathItem, Response};
5
6pub type SpecResult<T> = Result<T, Box<dyn std::error::Error>>;
7
8pub struct Spec {
9 spec: oas3::OpenApiV3Spec,
10}
11
12impl Spec {
13 pub fn from_path(path: &str) -> SpecResult<Self> {
28 let spec = load_spec(path).ok_or("Failed to load spec")?;
29 Ok(Self { spec })
30 }
31
32 pub fn get_example(&self, req: &HttpRequest) -> Option<serde_json::Value> {
68 let path = req.uri().path();
69 let method = req.method().as_str().to_lowercase();
70 let media_type = "application/json";
71
72 Some(&self.spec)
73 .and_then(load_path(path))
74 .and_then(load_method(&method))
75 .and_then(load_responses())
76 .and_then(load_examples(&self.spec, media_type))
77 .and_then(find_example_match(req))
78 .and_then(|example| example.resolve(&self.spec).ok())
79 .and_then(|example| example.value)
80 }
81}
82
83fn load_spec(path: &str) -> Option<oas3::OpenApiV3Spec> {
84 match oas3::from_path(path) {
85 Ok(spec) => Some(spec),
86 Err(_) => None,
87 }
88}
89
90fn load_path<'a>(path: &'a str) -> impl Fn(&oas3::OpenApiV3Spec) -> Option<PathItem> + 'a {
91 move |spec: &oas3::OpenApiV3Spec| {
92 spec.paths
93 .iter()
94 .find(|(key, _)| match_url(path, &[*key]))
95 .map(|(_, value)| value.clone())
96 }
97}
98
99fn match_url(url: &str, routes: &[&str]) -> bool {
100 let url_parts: Vec<&str> = url.split('/').filter(|s| !s.is_empty()).collect();
101
102 for route in routes {
103 let route_parts: Vec<&str> = route.split('/').filter(|s| !s.is_empty()).collect();
104 if url_parts.len() == route_parts.len()
105 && route_parts
106 .iter()
107 .zip(url_parts.iter())
108 .all(|(r, u)| r.starts_with('{') && r.ends_with('}') || r == u)
109 {
110 return true;
111 }
112 }
113 false
114}
115
116fn load_method<'a>(method: &'a str) -> impl Fn(PathItem) -> Option<Operation> + 'a {
117 move |path: PathItem| match method {
118 "get" => path.get.clone(),
119 "put" => path.put.clone(),
120 "post" => path.post.clone(),
121 "delete" => path.delete.clone(),
122 "options" => path.options.clone(),
123 "head" => path.head.clone(),
124 "patch" => path.patch.clone(),
125 "trace" => path.trace.clone(),
126 _ => None,
127 }
128}
129
130fn load_responses<'a>() -> impl Fn(Operation) -> Option<Vec<ObjectOrReference<Response>>> + 'a {
131 move |op: Operation| {
132 let mut responses = Vec::new();
133 for (_, response) in op.responses.iter() {
134 responses.push(response.clone());
135 }
136 Some(responses)
137 }
138}
139
140fn load_examples<'a>(
141 spec: &'a oas3::OpenApiV3Spec,
142 media_type: &'a str,
143) -> impl Fn(Vec<ObjectOrReference<Response>>) -> Option<Vec<MediaTypeExamples>> + 'a {
144 move |responses: Vec<ObjectOrReference<Response>>| {
145 let mut examples = Vec::new();
146 for response in responses {
147 extract_response(response, spec)
148 .as_ref()
149 .and_then(|r| r.content.get(media_type))
150 .and_then(|content| content.examples.as_ref())
151 .map(|media_type| examples.push(media_type.clone()));
152 }
153 Some(examples)
154 }
155}
156
157fn extract_response(
158 response: ObjectOrReference<Response>,
159 spec: &oas3::OpenApiV3Spec,
160) -> Option<Response> {
161 match response {
162 ObjectOrReference::Object(response) => Some(response),
163 ObjectOrReference::Ref { ref_path } => {
164 let components = &spec.components;
165 components
166 .as_ref()
167 .and_then(|components| components.responses.get(&ref_path).cloned())
168 .and_then(|resp| extract_response(resp, spec))
169 }
170 }
171}
172
173fn find_example_match<'a>(
191 req: &'a HttpRequest,
192) -> impl Fn(Vec<MediaTypeExamples>) -> Option<ObjectOrReference<Example>> {
193 let path = req.uri().path().to_string();
194 let query = QueryMatcher::from_request(req);
195 let headers = HeaderMatcher::from_request(req);
196
197 move |examples: Vec<MediaTypeExamples>| {
198 let mut default: Option<ObjectOrReference<Example>> = None;
199 for example in examples {
200 match example {
201 MediaTypeExamples::Examples { examples } => {
202 for (example_name, e) in examples.iter() {
203 if example_name == &path {
205 return Some(e.clone());
206 }
207
208 if query.match_example(&example_name) {
210 return Some(e.clone());
211 }
212
213 if headers.match_example(&example_name) {
215 return Some(e.clone());
216 }
217
218 if example_name == "default" {
220 default = Some(e.clone());
221 }
222 }
223 }
224 _ => {}
225 }
226 }
227 default
228 }
229}
230
231struct QueryMatcher {
232 params: HashMap<String, String>,
233}
234
235impl QueryMatcher {
236 fn from_request(req: &HttpRequest) -> Self {
237 let mut params = HashMap::new();
238 for (key, value) in req.query_string().split('&').map(|pair| {
239 let mut split = pair.split('=');
240 (split.next().unwrap(), split.next().unwrap_or(""))
241 }) {
242 params.insert(key.to_string(), value.to_string());
243 }
244 Self { params }
245 }
246
247 fn match_example(&self, example_name: &str) -> bool {
248 if example_name.starts_with("query:") {
249 let query = example_name.trim_start_matches("query:");
250 let mut query_params = HashMap::new();
251 for pair in query.split('&').map(|pair| {
252 let mut split = pair.split('=');
253 (split.next().unwrap(), split.next().unwrap_or(""))
254 }) {
255 query_params.insert(pair.0.to_string(), pair.1.to_string());
256 }
257 query_params
258 .iter()
259 .all(|(key, value)| self.params.get(key).map_or(false, |v| v == value))
260 } else {
261 false
262 }
263 }
264}
265
266struct HeaderMatcher {
267 headers: HashMap<String, String>,
268}
269
270impl HeaderMatcher {
271 fn from_request(req: &HttpRequest) -> Self {
272 let headers = req
273 .headers()
274 .iter()
275 .map(|(key, value)| {
276 (
277 key.as_str().to_string(),
278 value.to_str().unwrap_or("").to_string(),
279 )
280 })
281 .collect();
282 Self { headers }
283 }
284
285 fn match_example(&self, example_name: &str) -> bool {
286 if example_name.starts_with("header:") {
287 let header = example_name.trim_start_matches("header:");
288 let mut header_params = HashMap::new();
289 for pair in header.split('&').map(|pair| {
290 let mut split = pair.split('=');
291 (split.next().unwrap(), split.next().unwrap_or(""))
292 }) {
293 header_params.insert(pair.0.to_string(), pair.1.to_string());
294 }
295 header_params
296 .iter()
297 .all(|(key, value)| self.headers.get(key).map_or(false, |v| v == value))
298 } else {
299 false
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use actix_web::test::TestRequest;
308
309 #[test]
310 fn test_load_spec() {
311 let spec = load_spec("tests/testdata/petstore.yaml");
312 assert_eq!(spec.unwrap().openapi, "3.0.0");
313 }
314
315 #[test]
316 fn test_load_path() {
317 let path = load_spec("tests/testdata/petstore.yaml")
318 .as_ref()
319 .and_then(load_path("/pets"));
320 assert!(path.is_some());
321 }
322
323 #[test]
324 fn test_load_path_not_found() {
325 let path = load_spec("tests/testdata/petstore.yaml")
326 .as_ref()
327 .and_then(load_path("/notfound"));
328 assert!(path.is_none());
329 }
330
331 #[test]
332 fn test_load_path_with_params() {
333 let path = load_spec("tests/testdata/petstore.yaml")
334 .as_ref()
335 .and_then(load_path("/pets/{petId}"));
336 assert!(path.is_some());
337 }
338
339 #[test]
340 fn test_load_path_with_dynamic_params() {
341 let path = load_spec("tests/testdata/petstore.yaml")
342 .as_ref()
343 .and_then(load_path("/pets/123"));
344 assert!(path.is_some());
345 }
346
347 #[test]
348 fn test_load_method() {
349 let method = load_spec("tests/testdata/petstore.yaml")
350 .as_ref()
351 .and_then(load_path("/pets"))
352 .and_then(load_method("get"));
353 assert!(method.is_some());
354 }
355
356 #[test]
357 fn test_load_method_not_found() {
358 let method = load_spec("tests/testdata/petstore.yaml")
359 .as_ref()
360 .and_then(load_path("/pets"))
361 .and_then(load_method("notfound"));
362 assert!(method.is_none());
363 }
364
365 #[test]
366 fn test_load_examples() {
367 let spec = load_spec("tests/testdata/petstore.yaml").unwrap();
368
369 let example = Some(&spec)
370 .and_then(load_path("/pets"))
371 .and_then(load_method("get"))
372 .and_then(load_responses())
373 .and_then(load_examples(&spec, "application/json"));
374 assert!(example.is_some());
375 }
376
377 #[test]
378 fn test_spec() {
379 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
380 let req = TestRequest::with_uri("/pets").to_http_request();
381 let example = spec.get_example(&req);
382 assert!(example.is_some());
383 }
384
385 #[test]
386 fn test_spec_with_path_params() {
387 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
388 let req = TestRequest::with_uri("/pets/123").to_http_request();
389 let example = spec.get_example(&req);
390 assert!(example.is_some());
391 }
392
393 #[test]
394 fn test_spec_with_params_custom_example() {
395 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
396 let req = TestRequest::with_uri("/pets/2").to_http_request();
397 let example = spec.get_example(&req).unwrap();
398
399 assert_eq!(
400 example["id"],
401 serde_json::Value::Number(serde_json::Number::from(2))
402 );
403 }
404
405 #[test]
406 fn test_spec_match_query_params() {
407 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
408 let req = TestRequest::with_uri("/pets?page=1").to_http_request();
409 let res = spec.get_example(&req).unwrap();
410
411 let example = res.as_array().unwrap().get(0).unwrap();
412 assert_eq!(
413 example["id"],
414 serde_json::Value::Number(serde_json::Number::from(1))
415 );
416 }
417
418 #[test]
419 fn test_spec_match_query_params_with_multiple_params() {
420 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
421 let req = TestRequest::with_uri("/pets?page=1&limit=1").to_http_request();
422 let res = spec.get_example(&req).unwrap();
423
424 let examples = res.as_array().unwrap();
425 assert_eq!(examples.len(), 1,);
426
427 let example = examples.get(0).unwrap();
428 assert_eq!(
429 example["id"],
430 serde_json::Value::Number(serde_json::Number::from(1))
431 );
432 }
433
434 #[test]
435 fn test_spec_prefer_path_over_query_params() {
436 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
437 let req = TestRequest::with_uri("/pets/2?term=dog").to_http_request();
438 let example = spec.get_example(&req).unwrap();
439 assert_eq!(
440 example["id"],
441 serde_json::Value::Number(serde_json::Number::from(2))
442 );
443 }
444
445 #[test]
446 fn test_spec_match_headers() {
447 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
448 let req = TestRequest::with_uri("/pets/4")
449 .insert_header(("x-api-key", "123"))
450 .to_http_request();
451 let example = spec.get_example(&req).unwrap();
452 assert_eq!(
453 example["id"],
454 serde_json::Value::Number(serde_json::Number::from(4))
455 );
456 }
457
458 #[test]
459 fn test_spec_match_headers_with_multiple_headers() {
460 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
461 let req = TestRequest::with_uri("/pets/4")
462 .insert_header(("x-api-key", "123"))
463 .insert_header(("x-tenant-id", "1"))
464 .to_http_request();
465 let example = spec.get_example(&req).unwrap();
466 assert_eq!(
467 example["id"],
468 serde_json::Value::Number(serde_json::Number::from(4))
469 );
470 }
471
472 #[test]
473 fn test_match_401_response() {
474 let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
475 let req = TestRequest::with_uri("/pets/5").to_http_request();
476 let example = spec.get_example(&req).unwrap();
477 assert_eq!(
478 example["code"],
479 serde_json::Value::Number(serde_json::Number::from(401))
480 );
481 }
482}