openapi_mocker/openapi/
spec.rs

1use 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    /// Create a new Spec from an OpenAPI spec file.
14    /// # Arguments
15    /// * `path` - Path to the OpenAPI spec file
16    /// # Returns
17    /// A Spec instance
18    /// # Errors
19    /// Returns an error if the spec file cannot be loaded.
20    /// # Example
21    /// ```rust
22    /// use openapi_mocker::openapi::spec::Spec;
23    /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
24    /// ```
25    /// This will create a new Spec instance from the Petstore spec.
26    /// You can then use the `get_example` method to get example responses.
27    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    /// Get an example response for a request.
33    /// # Arguments
34    /// * `req` - The HTTP request
35    /// # Returns
36    /// An example response as a JSON value
37    /// # Example
38    /// ```rust
39    /// use actix_web::test::TestRequest;
40    /// use openapi_mocker::openapi::spec::Spec;
41    /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
42    /// let req = TestRequest::with_uri("/pets").to_http_request();
43    /// let example = spec.get_example(&req);
44    /// ```
45    ///
46    /// You can also load a specific example by matching the request path, query, or headers.
47    /// # Example with exact path match
48    /// ```rust
49    /// use actix_web::test::TestRequest;
50    /// use openapi_mocker::openapi::spec::Spec;
51    /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
52    /// let req = TestRequest::with_uri("/pets/2").to_http_request();
53    /// let example = spec.get_example(&req).unwrap();
54    /// assert_eq!(example["id"], serde_json::Value::Number(serde_json::Number::from(2)));
55    /// ```
56    ///
57    /// # Example with query parameters
58    /// ```rust
59    /// use actix_web::test::TestRequest;
60    /// use openapi_mocker::openapi::spec::Spec;
61    /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap();
62    /// let req = TestRequest::with_uri("/pets?page=1").to_http_request();
63    /// let examples = spec.get_example(&req).unwrap();
64    /// let example = examples.as_array().unwrap().get(0).unwrap();
65    /// assert_eq!(example["id"], serde_json::Value::Number(serde_json::Number::from(1)));
66    /// ```
67    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
173/// Find the example that matches the request.
174///
175/// It matches the examples by comparing the request path, query,
176/// and headers with the example name.
177/// If the example name matches the request path, it returns the example.
178/// If the example name does not match the request path, it returns None.
179///
180/// # Matching exact route
181/// If the example name is the same as the request path, it returns the example.
182/// Example:
183/// - Example name: `/pets`
184/// - Request path: `/pets`
185/// - Returns the example
186///
187/// - Example name: `/pets`
188/// - Request path: `/pets/123`
189/// - Returns None
190fn 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                        // Match exact path
204                        if example_name == &path {
205                            return Some(e.clone());
206                        }
207
208                        // Match query parameters
209                        if query.match_example(&example_name) {
210                            return Some(e.clone());
211                        }
212
213                        // Match headers
214                        if headers.match_example(&example_name) {
215                            return Some(e.clone());
216                        }
217
218                        // Match default example
219                        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}