From ec0fff3a7350ba7bafee7ca0a3be99e04b0a78d5 Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Wed, 1 May 2024 16:56:55 -0400 Subject: [PATCH 01/10] Included support to load multiple examples. Refactored spec parser --- src/lib.rs | 1 + src/openapi/mod.rs | 1 + src/openapi/spec.rs | 193 +++++++++++++++++++++++++++++++++++ tests/testdata/petstore.yaml | 3 + 4 files changed, 198 insertions(+) create mode 100644 src/openapi/mod.rs create mode 100644 src/openapi/spec.rs diff --git a/src/lib.rs b/src/lib.rs index 20a0042..d648161 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,6 +34,7 @@ //! use clap::Parser; use std::path::PathBuf; +pub mod openapi; pub mod server; pub mod spec; diff --git a/src/openapi/mod.rs b/src/openapi/mod.rs new file mode 100644 index 0000000..cc05df3 --- /dev/null +++ b/src/openapi/mod.rs @@ -0,0 +1 @@ +pub mod spec; diff --git a/src/openapi/spec.rs b/src/openapi/spec.rs new file mode 100644 index 0000000..74e5a3b --- /dev/null +++ b/src/openapi/spec.rs @@ -0,0 +1,193 @@ +use actix_web::HttpRequest; +use oas3::spec::{MediaTypeExamples, ObjectOrReference, Operation, PathItem, Response}; + +pub type SpecResult = Result>; + +pub struct Spec { + spec: oas3::OpenApiV3Spec, +} + +impl Spec { + pub fn from_path(path: &str) -> SpecResult { + let spec = load_spec(path).ok_or("Failed to load spec")?; + Ok(Self { spec }) + } + + pub fn get_example(&self, req: &HttpRequest) -> Option { + let path = req.uri().path(); + let method = req.method().as_str().to_lowercase(); + let media_type = "application/json"; + let example_name = "default"; + + Some(&self.spec) + .and_then(load_path(path)) + .and_then(load_method(&method)) + .and_then(load_responses()) + .and_then(load_examples(&self.spec, media_type)) + .and_then(|examples| examples.into_iter().next()) + .and_then(get_example(example_name, &self.spec)) + } +} + +fn load_spec(path: &str) -> Option { + match oas3::from_path(path) { + Ok(spec) => Some(spec), + Err(_) => None, + } +} + +fn load_path<'a>(path: &'a str) -> impl Fn(&oas3::OpenApiV3Spec) -> Option + 'a { + move |spec: &oas3::OpenApiV3Spec| spec.paths.clone().get(path).cloned() +} + +fn load_method<'a>(method: &'a str) -> impl Fn(PathItem) -> Option + 'a { + move |path: PathItem| match method { + "get" => path.get.clone(), + "put" => path.put.clone(), + "post" => path.post.clone(), + "delete" => path.delete.clone(), + "options" => path.options.clone(), + "head" => path.head.clone(), + "patch" => path.patch.clone(), + "trace" => path.trace.clone(), + _ => None, + } +} + +fn load_responses<'a>() -> impl Fn(Operation) -> Option>> + 'a { + move |op: Operation| { + let mut responses = Vec::new(); + for (_, response) in op.responses.iter() { + responses.push(response.clone()); + } + Some(responses) + } +} + +fn load_examples<'a>( + spec: &'a oas3::OpenApiV3Spec, + media_type: &'a str, +) -> impl Fn(Vec>) -> Option> + 'a { + move |responses: Vec>| { + let mut examples = Vec::new(); + for response in responses { + extract_response(response, spec) + .as_ref() + .and_then(|r| r.content.get(media_type)) + .and_then(|content| content.examples.as_ref()) + .map(|media_type| examples.push(media_type.clone())); + } + Some(examples) + } +} + +fn extract_response( + response: ObjectOrReference, + spec: &oas3::OpenApiV3Spec, +) -> Option { + match response { + ObjectOrReference::Object(response) => Some(response), + ObjectOrReference::Ref { ref_path } => { + let components = &spec.components; + components + .as_ref() + .and_then(|components| components.responses.get(&ref_path).cloned()) + .and_then(|resp| extract_response(resp, spec)) + } + } +} + +fn get_example<'a>( + example_name: &'a str, + spec: &'a oas3::OpenApiV3Spec, +) -> impl Fn(MediaTypeExamples) -> Option + 'a { + move |examples: MediaTypeExamples| match examples { + MediaTypeExamples::Examples { examples } => examples + .get(example_name) + .map(|example| example.resolve(spec)) + .map(|example| match example { + Ok(example) => example.value, + Err(_) => None, + }) + .flatten(), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use actix_web::test::TestRequest; + + use super::*; + + #[test] + fn test_load_spec() { + let spec = load_spec("tests/testdata/petstore.yaml"); + assert_eq!(spec.unwrap().openapi, "3.0.0"); + } + + #[test] + fn test_load_path() { + let path = load_spec("tests/testdata/petstore.yaml") + .as_ref() + .and_then(load_path("/pets")); + assert!(path.is_some()); + } + + #[test] + fn test_load_path_not_found() { + let path = load_spec("tests/testdata/petstore.yaml") + .as_ref() + .and_then(load_path("/notfound")); + assert!(path.is_none()); + } + + #[test] + fn test_load_method() { + let method = load_spec("tests/testdata/petstore.yaml") + .as_ref() + .and_then(load_path("/pets")) + .and_then(load_method("get")); + assert!(method.is_some()); + } + + #[test] + fn test_load_method_not_found() { + let method = load_spec("tests/testdata/petstore.yaml") + .as_ref() + .and_then(load_path("/pets")) + .and_then(load_method("notfound")); + assert!(method.is_none()); + } + + #[test] + fn test_load_examples() { + let spec = load_spec("tests/testdata/petstore.yaml").unwrap(); + + let example = Some(&spec) + .and_then(load_path("/pets")) + .and_then(load_method("get")) + .and_then(load_responses()) + .and_then(load_examples(&spec, "application/json")); + assert!(example.is_some()); + } + + #[test] + fn test_spec() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets").to_http_request(); + let example = spec.get_example(&req); + assert!(example.is_some()); + } + + #[test] + fn test_load_responses() { + let responses = load_spec("tests/testdata/petstore.yaml") + .as_ref() + .and_then(load_path("/pets")) + .and_then(load_method("get")) + .and_then(load_responses()); + assert!(responses.is_some()); + assert_eq!(responses.unwrap().len(), 2); + } +} diff --git a/tests/testdata/petstore.yaml b/tests/testdata/petstore.yaml index bbd0a0e..f1a5fc5 100644 --- a/tests/testdata/petstore.yaml +++ b/tests/testdata/petstore.yaml @@ -33,6 +33,9 @@ paths: application/json: schema: $ref: "#/components/schemas/Pets" + examples: + default: + value: [] text/plain: schema: type: string From 2f535ce84263bd5bcae1ffeb7780349af117a47c Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Wed, 1 May 2024 19:49:37 -0400 Subject: [PATCH 02/10] Included support to query examples by path or query params --- src/openapi/spec.rs | 194 ++++++++++++++++++++++++++++++++--- tests/testdata/petstore.yaml | 24 +++++ 2 files changed, 203 insertions(+), 15 deletions(-) diff --git a/src/openapi/spec.rs b/src/openapi/spec.rs index 74e5a3b..95c66a8 100644 --- a/src/openapi/spec.rs +++ b/src/openapi/spec.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; + use actix_web::HttpRequest; -use oas3::spec::{MediaTypeExamples, ObjectOrReference, Operation, PathItem, Response}; +use oas3::spec::{Example, MediaTypeExamples, ObjectOrReference, Operation, PathItem, Response}; pub type SpecResult = Result>; @@ -17,15 +19,15 @@ impl Spec { let path = req.uri().path(); let method = req.method().as_str().to_lowercase(); let media_type = "application/json"; - let example_name = "default"; Some(&self.spec) .and_then(load_path(path)) .and_then(load_method(&method)) .and_then(load_responses()) .and_then(load_examples(&self.spec, media_type)) - .and_then(|examples| examples.into_iter().next()) - .and_then(get_example(example_name, &self.spec)) + .and_then(find_example_match(req)) + .and_then(|example| example.resolve(&self.spec).ok()) + .and_then(|example| example.value) } } @@ -37,7 +39,29 @@ fn load_spec(path: &str) -> Option { } fn load_path<'a>(path: &'a str) -> impl Fn(&oas3::OpenApiV3Spec) -> Option + 'a { - move |spec: &oas3::OpenApiV3Spec| spec.paths.clone().get(path).cloned() + move |spec: &oas3::OpenApiV3Spec| { + spec.paths + .iter() + .find(|(key, _)| match_url(path, &[*key])) + .map(|(_, value)| value.clone()) + } +} + +fn match_url(url: &str, routes: &[&str]) -> bool { + let url_parts: Vec<&str> = url.split('/').filter(|s| !s.is_empty()).collect(); + + for route in routes { + let route_parts: Vec<&str> = route.split('/').filter(|s| !s.is_empty()).collect(); + if url_parts.len() == route_parts.len() + && route_parts + .iter() + .zip(url_parts.iter()) + .all(|(r, u)| r.starts_with('{') && r.ends_with('}') || r == u) + { + return true; + } + } + false } fn load_method<'a>(method: &'a str) -> impl Fn(PathItem) -> Option + 'a { @@ -97,6 +121,94 @@ fn extract_response( } } +/// Find the example that matches the request. +/// +/// It matches the examples by comparing the request path, query, +/// and headers with the example name. +/// If the example name matches the request path, it returns the example. +/// If the example name does not match the request path, it returns None. +/// +/// # Matching exact route +/// If the example name is the same as the request path, it returns the example. +/// Example: +/// - Example name: `/pets` +/// - Request path: `/pets` +/// - Returns the example +/// +/// - Example name: `/pets` +/// - Request path: `/pets/123` +/// - Returns None +fn find_example_match<'a>( + req: &'a HttpRequest, +) -> impl Fn(Vec) -> Option> { + let path = req.uri().path().to_string(); + + let query = QueryString::from_request(req); + + move |examples: Vec| { + let mut default: Option> = None; + for example in examples { + match example { + MediaTypeExamples::Examples { examples } => { + for (example_name, e) in examples.iter() { + // Match exact path + if example_name == &path { + return Some(e.clone()); + } + + // Match query parameters + if query.match_example(&example_name) { + return Some(e.clone()); + } + + // Match default example + if example_name == "default" { + default = Some(e.clone()); + } + } + } + _ => {} + } + } + default + } +} + +struct QueryString { + params: HashMap, +} + +impl QueryString { + fn from_request(req: &HttpRequest) -> Self { + let mut params = HashMap::new(); + for (key, value) in req.query_string().split('&').map(|pair| { + let mut split = pair.split('='); + (split.next().unwrap(), split.next().unwrap_or("")) + }) { + params.insert(key.to_string(), value.to_string()); + } + Self { params } + } + + fn match_example(&self, example_name: &str) -> bool { + if example_name.starts_with("query:") { + let query = example_name.trim_start_matches("query:"); + let mut query_params = HashMap::new(); + for pair in query.split('&').map(|pair| { + let mut split = pair.split('='); + (split.next().unwrap(), split.next().unwrap_or("")) + }) { + query_params.insert(pair.0.to_string(), pair.1.to_string()); + } + self.params + .iter() + .all(|(key, value)| query_params.get(key).map_or(false, |v| v == value)) + } else { + false + } + } +} + fn get_example<'a>( example_name: &'a str, spec: &'a oas3::OpenApiV3Spec, @@ -116,9 +228,8 @@ fn get_example<'a>( #[cfg(test)] mod tests { - use actix_web::test::TestRequest; - use super::*; + use actix_web::test::TestRequest; #[test] fn test_load_spec() { @@ -142,6 +253,22 @@ mod tests { assert!(path.is_none()); } + #[test] + fn test_load_path_with_params() { + let path = load_spec("tests/testdata/petstore.yaml") + .as_ref() + .and_then(load_path("/pets/{petId}")); + assert!(path.is_some()); + } + + #[test] + fn test_load_path_with_dynamic_params() { + let path = load_spec("tests/testdata/petstore.yaml") + .as_ref() + .and_then(load_path("/pets/123")); + assert!(path.is_some()); + } + #[test] fn test_load_method() { let method = load_spec("tests/testdata/petstore.yaml") @@ -181,13 +308,50 @@ mod tests { } #[test] - fn test_load_responses() { - let responses = load_spec("tests/testdata/petstore.yaml") - .as_ref() - .and_then(load_path("/pets")) - .and_then(load_method("get")) - .and_then(load_responses()); - assert!(responses.is_some()); - assert_eq!(responses.unwrap().len(), 2); + fn test_spec_with_path_params() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets/123").to_http_request(); + let example = spec.get_example(&req); + assert!(example.is_some()); + } + + #[test] + fn test_spec_with_params_custom_example() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets/2").to_http_request(); + let example = spec.get_example(&req).unwrap(); + + assert_eq!( + example["id"], + serde_json::Value::Number(serde_json::Number::from(2)) + ); + } + + #[test] + fn test_spec_match_query_params() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets?page=1").to_http_request(); + let res = spec.get_example(&req).unwrap(); + + let example = res.as_array().unwrap().get(0).unwrap(); + assert_eq!( + example["id"], + serde_json::Value::Number(serde_json::Number::from(1)) + ); + } + + #[test] + fn test_spec_match_query_params_with_multiple_params() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets?page=1&limit=1").to_http_request(); + let res = spec.get_example(&req).unwrap(); + + let examples = res.as_array().unwrap(); + assert_eq!(examples.len(), 1,); + let example = examples.get(0).unwrap(); + assert_eq!( + example["id"], + serde_json::Value::Number(serde_json::Number::from(1)) + ); } } diff --git a/tests/testdata/petstore.yaml b/tests/testdata/petstore.yaml index f1a5fc5..efcc9f2 100644 --- a/tests/testdata/petstore.yaml +++ b/tests/testdata/petstore.yaml @@ -36,6 +36,19 @@ paths: examples: default: value: [] + "query:page=1": + value: + - id: 1 + name: doggie + tag: dog + - id: 2 + name: kitty + tag: cat + "query:page=1&limit=1": + value: + - id: 1 + name: doggie + tag: dog text/plain: schema: type: string @@ -80,6 +93,17 @@ paths: application/json: schema: $ref: "#/components/schemas/Pets" + examples: + default: + value: + id: 1 + name: doggie + tag: dog + "/pets/2": + value: + id: 2 + name: kitty + tag: cat default: description: unexpected error content: From 5b700efcc02a753bdc79969fa1abdec632f3f843 Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Wed, 1 May 2024 21:07:55 -0400 Subject: [PATCH 03/10] Included test for query parameter and path --- src/openapi/spec.rs | 11 +++++++++++ tests/testdata/petstore.yaml | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/src/openapi/spec.rs b/src/openapi/spec.rs index 95c66a8..18767a1 100644 --- a/src/openapi/spec.rs +++ b/src/openapi/spec.rs @@ -354,4 +354,15 @@ mod tests { serde_json::Value::Number(serde_json::Number::from(1)) ); } + + #[test] + fn test_spec_prefer_path_over_query_params() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets/2?term=dog").to_http_request(); + let example = spec.get_example(&req).unwrap(); + assert_eq!( + example["id"], + serde_json::Value::Number(serde_json::Number::from(2)) + ); + } } diff --git a/tests/testdata/petstore.yaml b/tests/testdata/petstore.yaml index efcc9f2..985521b 100644 --- a/tests/testdata/petstore.yaml +++ b/tests/testdata/petstore.yaml @@ -104,6 +104,11 @@ paths: id: 2 name: kitty tag: cat + "query:term=dog": + value: + id: 1 + name: doggie + tag: dog default: description: unexpected error content: From 82ed8daf0f1edbaae28595f4e8c4605cfb28c9de Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Thu, 2 May 2024 08:12:48 -0400 Subject: [PATCH 04/10] Included support to match headers --- src/openapi/spec.rs | 83 +++++++++++++++++++++++++++++++++--- tests/testdata/petstore.yaml | 7 ++- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/src/openapi/spec.rs b/src/openapi/spec.rs index 18767a1..ba7967e 100644 --- a/src/openapi/spec.rs +++ b/src/openapi/spec.rs @@ -142,8 +142,8 @@ fn find_example_match<'a>( req: &'a HttpRequest, ) -> impl Fn(Vec) -> Option> { let path = req.uri().path().to_string(); - - let query = QueryString::from_request(req); + let query = QueryMatcher::from_request(req); + let headers = HeaderMatcher::from_request(req); move |examples: Vec| { let mut default: Option> = None; @@ -161,6 +161,11 @@ fn find_example_match<'a>( return Some(e.clone()); } + // Match headers + if headers.match_example(&example_name) { + return Some(e.clone()); + } + // Match default example if example_name == "default" { default = Some(e.clone()); @@ -174,11 +179,11 @@ fn find_example_match<'a>( } } -struct QueryString { +struct QueryMatcher { params: HashMap, } -impl QueryString { +impl QueryMatcher { fn from_request(req: &HttpRequest) -> Self { let mut params = HashMap::new(); for (key, value) in req.query_string().split('&').map(|pair| { @@ -200,9 +205,47 @@ impl QueryString { }) { query_params.insert(pair.0.to_string(), pair.1.to_string()); } - self.params + query_params + .iter() + .all(|(key, value)| self.params.get(key).map_or(false, |v| v == value)) + } else { + false + } + } +} + +struct HeaderMatcher { + headers: HashMap, +} + +impl HeaderMatcher { + fn from_request(req: &HttpRequest) -> Self { + let headers = req + .headers() + .iter() + .map(|(key, value)| { + ( + key.as_str().to_string(), + value.to_str().unwrap_or("").to_string(), + ) + }) + .collect(); + Self { headers } + } + + fn match_example(&self, example_name: &str) -> bool { + if example_name.starts_with("header:") { + let header = example_name.trim_start_matches("header:"); + let mut header_params = HashMap::new(); + for pair in header.split('&').map(|pair| { + let mut split = pair.split('='); + (split.next().unwrap(), split.next().unwrap_or("")) + }) { + header_params.insert(pair.0.to_string(), pair.1.to_string()); + } + header_params .iter() - .all(|(key, value)| query_params.get(key).map_or(false, |v| v == value)) + .all(|(key, value)| self.headers.get(key).map_or(false, |v| v == value)) } else { false } @@ -348,6 +391,7 @@ mod tests { let examples = res.as_array().unwrap(); assert_eq!(examples.len(), 1,); + let example = examples.get(0).unwrap(); assert_eq!( example["id"], @@ -365,4 +409,31 @@ mod tests { serde_json::Value::Number(serde_json::Number::from(2)) ); } + + #[test] + fn test_spec_match_headers() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets/4") + .insert_header(("x-api-key", "123")) + .to_http_request(); + let example = spec.get_example(&req).unwrap(); + assert_eq!( + example["id"], + serde_json::Value::Number(serde_json::Number::from(4)) + ); + } + + #[test] + fn test_spec_match_headers_with_multiple_headers() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets/4") + .insert_header(("x-api-key", "123")) + .insert_header(("x-tenant-id", "1")) + .to_http_request(); + let example = spec.get_example(&req).unwrap(); + assert_eq!( + example["id"], + serde_json::Value::Number(serde_json::Number::from(4)) + ); + } } diff --git a/tests/testdata/petstore.yaml b/tests/testdata/petstore.yaml index 985521b..a1a321d 100644 --- a/tests/testdata/petstore.yaml +++ b/tests/testdata/petstore.yaml @@ -44,7 +44,7 @@ paths: - id: 2 name: kitty tag: cat - "query:page=1&limit=1": + "query:limit=1&page=1": value: - id: 1 name: doggie @@ -109,6 +109,11 @@ paths: id: 1 name: doggie tag: dog + "header:x-api-key=123": + value: + id: 4 + name: batty + tag: bat default: description: unexpected error content: From 3304d971f217ce0bb66f40ee9fa394bcabc7d5cc Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Thu, 2 May 2024 13:36:27 -0400 Subject: [PATCH 05/10] replaced old spec implementation --- src/main.rs | 3 +- src/openapi/spec.rs | 17 --------- src/server.rs | 91 ++++++++++++++------------------------------- 3 files changed, 30 insertions(+), 81 deletions(-) diff --git a/src/main.rs b/src/main.rs index c41706a..b436e30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use actix_web::{web, App, HttpServer}; use clap::Parser; use openapi_mocker::{ + openapi::spec::Spec, server::{get_scope, AppState}, Args, }; @@ -11,7 +12,7 @@ async fn main() -> std::io::Result<()> { println!("Starting server with spec: {}", args.spec.display()); let port = args.port.unwrap_or(8080); - let spec = oas3::from_path(&args.spec).expect("failed to load spec"); + let spec = Spec::from_path(args.spec.to_str().unwrap_or("")).expect("Failed to load spec"); let data = web::Data::new(AppState { spec }); let server = HttpServer::new(move || App::new().app_data(data.clone()).service(get_scope())) diff --git a/src/openapi/spec.rs b/src/openapi/spec.rs index ba7967e..28ecbb4 100644 --- a/src/openapi/spec.rs +++ b/src/openapi/spec.rs @@ -252,23 +252,6 @@ impl HeaderMatcher { } } -fn get_example<'a>( - example_name: &'a str, - spec: &'a oas3::OpenApiV3Spec, -) -> impl Fn(MediaTypeExamples) -> Option + 'a { - move |examples: MediaTypeExamples| match examples { - MediaTypeExamples::Examples { examples } => examples - .get(example_name) - .map(|example| example.resolve(spec)) - .map(|example| match example { - Ok(example) => example.value, - Err(_) => None, - }) - .flatten(), - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/server.rs b/src/server.rs index 5c9ce9f..9cb7c53 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,78 +1,26 @@ +use crate::openapi::spec::Spec; use actix_web::{ web::{self, get}, HttpRequest, HttpResponse, Scope, }; -use crate::spec::{load_endpoint, load_example, load_response, Method}; - /// Application state for the Actix Web server. pub struct AppState { - pub spec: oas3::OpenApiV3Spec, + pub spec: Spec, } /// Returns a new Actix Web scope with all the routes for the server. pub fn get_scope() -> Scope { - web::scope("") - .route("/{status}/{tail:.*}", get().to(handle_all)) - .route("/{tail:.*}", get().to(handle_all)) + web::scope("").default_service(get().to(handle_all)) } async fn handle_all(req: HttpRequest, data: web::Data) -> HttpResponse { let spec = &data.spec; - let method = Method::from(req.method().as_str()); - let status = req - .match_info() - .get("status") - .and_then(|v| v.parse().ok()) - .unwrap_or(200); - - let path = format!("/{}", req.match_info().get("tail").unwrap_or("")); - - let content_type = req - .headers() - .get("Content-Type") - .and_then(|v| v.to_str().ok()) - .unwrap_or("application/json"); + let example = spec.get_example(&req); - match build_response(spec, &path, method, status, content_type) { - Ok(resp) => resp, - Err(e) => { - eprintln!("Error: {:?}", e); - HttpResponse::NotFound().finish() - } - } -} - -fn build_response( - spec: &oas3::OpenApiV3Spec, - path: &str, - method: Method, - status: u16, - content_type: &str, -) -> Result> { - let op = load_endpoint(spec, path, method)?; - let response = load_response(spec, &op, status)?; - let result = load_example(spec, &response, content_type); - - let mut response_status = HttpResponse::build(get_status(status)); - Ok(response_status.json(result)) -} - -fn get_status(status: u16) -> actix_web::http::StatusCode { - match status { - 200 => actix_web::http::StatusCode::OK, - 201 => actix_web::http::StatusCode::CREATED, - 202 => actix_web::http::StatusCode::ACCEPTED, - 204 => actix_web::http::StatusCode::NO_CONTENT, - 400 => actix_web::http::StatusCode::BAD_REQUEST, - 401 => actix_web::http::StatusCode::UNAUTHORIZED, - 403 => actix_web::http::StatusCode::FORBIDDEN, - 404 => actix_web::http::StatusCode::NOT_FOUND, - 405 => actix_web::http::StatusCode::METHOD_NOT_ALLOWED, - 406 => actix_web::http::StatusCode::NOT_ACCEPTABLE, - 409 => actix_web::http::StatusCode::CONFLICT, - 500 => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, - _ => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + match example { + Some(example) => HttpResponse::Ok().json(example), + None => HttpResponse::NotFound().finish(), } } @@ -82,13 +30,30 @@ mod tests { use actix_web::{test, App}; #[actix_rt::test] - async fn test_request_success() { - let spec = oas3::from_path("tests/testdata/petstore.yaml").expect("failed to load spec"); + async fn test_request_default() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").expect("failed to load spec"); + let data = web::Data::new(AppState { spec }); + let app = App::new().app_data(data.clone()).service(get_scope()); + + let mut app = test::init_service(app).await; + let req = test::TestRequest::get().uri("/pets").to_request(); + let resp = test::call_service(&mut app, req).await; + println!("{:?}", resp); + assert!(resp.status().is_success()); + + let expected_res = r#"[]"#; + let body = test::read_body(resp).await; + assert_eq!(body, expected_res); + } + + #[actix_rt::test] + async fn test_request_query() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").expect("failed to load spec"); let data = web::Data::new(AppState { spec }); let app = App::new().app_data(data.clone()).service(get_scope()); let mut app = test::init_service(app).await; - let req = test::TestRequest::get().uri("/200/pets").to_request(); + let req = test::TestRequest::get().uri("/pets?page=1").to_request(); let resp = test::call_service(&mut app, req).await; println!("{:?}", resp); assert!(resp.status().is_success()); @@ -101,7 +66,7 @@ mod tests { #[actix_rt::test] async fn test_request_not_found() { - let spec = oas3::from_path("tests/testdata/petstore.yaml").expect("failed to load spec"); + let spec = Spec::from_path("tests/testdata/petstore.yaml").expect("failed to load spec"); let data = web::Data::new(AppState { spec }); let app = App::new().app_data(data.clone()).service(get_scope()); From a66a6cd13c2ac0e67dad3a026b33173b2edf9ae3 Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Thu, 2 May 2024 13:37:07 -0400 Subject: [PATCH 06/10] removed unused spec --- src/lib.rs | 1 - src/spec.rs | 273 ---------------------------------------------------- 2 files changed, 274 deletions(-) delete mode 100644 src/spec.rs diff --git a/src/lib.rs b/src/lib.rs index d648161..d647865 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,6 @@ use clap::Parser; use std::path::PathBuf; pub mod openapi; pub mod server; -pub mod spec; #[derive(Parser)] #[clap(version = "0.1.0", author = "Thiago Pacheco")] diff --git a/src/spec.rs b/src/spec.rs deleted file mode 100644 index 41b6a22..0000000 --- a/src/spec.rs +++ /dev/null @@ -1,273 +0,0 @@ -use oas3::spec::{Operation, PathItem, Response}; - -pub type SpecResult = Result>; - -/// HTTP methods -pub enum Method { - Get, - Post, - Put, - Delete, - Options, - Head, - Patch, - Trace, -} - -impl From<&str> for Method { - fn from(s: &str) -> Self { - match s.to_uppercase().as_str() { - "GET" => Method::Get, - "POST" => Method::Post, - "PUT" => Method::Put, - "DELETE" => Method::Delete, - "OPTIONS" => Method::Options, - "HEAD" => Method::Head, - "PATCH" => Method::Patch, - "TRACE" => Method::Trace, - _ => panic!("Invalid method"), - } - } -} - -/// Load an OpenAPI spec from a file -/// -/// # Arguments -/// * `path` - Path to the OpenAPI spec file -/// -/// # Returns -/// An OpenAPI spec object -/// -/// # Example -/// ``` -/// use openapi_mocker::spec::load_spec; -/// -/// let spec = load_spec("tests/testdata/petstore.yaml"); -/// assert_eq!(spec.openapi, "3.0.0"); -/// ``` -pub fn load_spec(path: &str) -> oas3::OpenApiV3Spec { - oas3::from_path(path).unwrap() -} - -/// Load an endpoint from an OpenAPI spec -/// -/// # Arguments -/// * `spec` - OpenAPI spec object -/// * `path` - Path to the endpoint -/// * `method` - HTTP method -/// -/// # Returns -/// An OpenAPI operation object -/// -/// # Example -/// ``` -/// use openapi_mocker::spec::{load_spec, load_endpoint, Method}; -/// -/// let spec = load_spec("tests/testdata/petstore.yaml"); -/// let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); -/// assert_eq!(op.operation_id, Some("listPets".to_string())); -/// ``` -pub fn load_endpoint( - spec: &oas3::OpenApiV3Spec, - path: &str, - method: Method, -) -> SpecResult { - let op = spec - .paths - .get(path) - .and_then(load_method(method)) - .ok_or("Endpoint not found")?; - Ok(op.clone()) -} - -/// Load a method from a PathItem -/// -/// # Arguments -/// * `method` - HTTP method -/// * `path_item` - PathItem object -/// -/// # Returns -/// An Option with the Operation object -fn load_method<'a>(method: Method) -> impl Fn(&PathItem) -> Option<&Operation> + 'a { - move |path_item: &PathItem| match method { - Method::Get => path_item.get.as_ref(), - Method::Post => path_item.post.as_ref(), - Method::Put => path_item.put.as_ref(), - Method::Delete => path_item.delete.as_ref(), - Method::Options => path_item.options.as_ref(), - Method::Head => path_item.head.as_ref(), - Method::Patch => path_item.patch.as_ref(), - Method::Trace => path_item.trace.as_ref(), - } -} - -/// Load a response from an OpenAPI operation -/// -/// # Arguments -/// * `spec` - OpenAPI spec object -/// * `op` - OpenAPI operation object -/// * `status` - HTTP status code -/// -/// # Returns -/// An OpenAPI response object -/// -/// # Example -/// ``` -/// use openapi_mocker::spec::{load_spec, load_endpoint, load_response, Method}; -/// -/// let spec = load_spec("tests/testdata/petstore.yaml"); -/// let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); -/// let response = load_response(&spec, &op, 200).unwrap(); -/// assert_eq!(response.description, Some("A paged array of pets".to_string())); -/// ``` -pub fn load_response( - spec: &oas3::OpenApiV3Spec, - op: &Operation, - status: u16, -) -> Result> { - let status_str = status.to_string(); - let objorref = op.responses.get(&status_str).ok_or("Response not found")?; - - match objorref.resolve(&spec) { - Ok(r) => Ok(r), - Err(_) => Err("Response not found".into()), - } -} - -/// Load an example from an OpenAPI response -/// -/// # Arguments -/// * `spec` - OpenAPI spec object -/// * `response` - OpenAPI response object -/// * `content_type` - Content type -/// -/// # Returns -/// A JSON value with the example -/// -/// # Example -/// ``` -/// use openapi_mocker::spec::{load_spec, load_endpoint, load_response, load_example, Method}; -/// use serde_json::json; -/// -/// let spec = load_spec("tests/testdata/petstore.yaml"); -/// let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); -/// let response = load_response(&spec, &op, 200).unwrap(); -/// let content_type = "application/json"; -/// let example = load_example(&spec, &response, content_type).unwrap(); -/// let expected = json!([ -/// { -/// "id": 1, -/// "name": "doggie", -/// "tag": "dog" -/// }, -/// { -/// "id": 2, -/// "name": "kitty", -/// "tag": "cat" -/// } -/// ]); -/// assert_eq!(example, expected); -/// ``` -pub fn load_example( - spec: &oas3::OpenApiV3Spec, - response: &Response, - content_type: &str, -) -> Option { - response - .content - .get(content_type) - .expect("Content not found") - .schema - .as_ref() - .expect("Schema not found") - .resolve(&spec) - .expect("Failed to resolve schema") - .example -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_load_spec() { - let spec = load_spec("tests/testdata/petstore.yaml"); - assert_eq!(spec.openapi, "3.0.0"); - } - - #[test] - fn test_load_endpoint() { - let spec = load_spec("tests/testdata/petstore.yaml"); - let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); - assert_eq!(op.operation_id, Some("listPets".to_string())); - } - - #[test] - fn test_load_endpoint_not_found() { - let spec = load_spec("tests/testdata/petstore.yaml"); - let op = load_endpoint(&spec, "/notfound", Method::Get); - assert!(op.is_err()); - } - - #[test] - fn test_load_response() { - let spec = load_spec("tests/testdata/petstore.yaml"); - let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); - let response = load_response(&spec, &op, 200).unwrap(); - assert_eq!( - response.description, - Some("A paged array of pets".to_string()) - ); - } - - #[test] - fn test_load_response_not_found() { - let spec = load_spec("tests/testdata/petstore.yaml"); - let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); - let response = load_response(&spec, &op, 404); - assert!(response.is_err()); - } - - #[test] - fn test_load_example() { - let spec = load_spec("tests/testdata/petstore.yaml"); - let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); - - let response = load_response(&spec, &op, 200).unwrap(); - let content_type = "application/json"; - let example = load_example(&spec, &response, content_type).unwrap(); - let example_json = serde_json::to_string(&example).unwrap(); - - let expected = serde_json::json!([ - { - "id": 1, - "name": "doggie", - "tag": "dog" - }, - { - "id": 2, - "name": "kitty", - "tag": "cat" - } - ]); - let expected_json = serde_json::to_string(&expected).unwrap(); - - assert_eq!(example_json, expected_json); - } - - #[test] - fn test_load_example_string() { - let spec = load_spec("tests/testdata/petstore.yaml"); - let op = load_endpoint(&spec, "/pets", Method::Get).unwrap(); - - let response = load_response(&spec, &op, 200).unwrap(); - let content_type = "text/plain"; - let example = load_example(&spec, &response, content_type).unwrap(); - let example_json = serde_json::to_string(&example).unwrap(); - - let expected = serde_json::json!("Not implemented"); - let expected_json = serde_json::to_string(&expected).unwrap(); - - assert_eq!(example_json, expected_json); - } -} From cf8a9ce9456e002ad092aa2550b5385c61909636 Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Thu, 2 May 2024 15:32:06 -0400 Subject: [PATCH 07/10] Include docs and examples --- CONTRIBUTING.md | 71 +++++++++++++ readme.md | 199 ++++++++++++++++++++++++++++++----- src/lib.rs | 10 +- src/openapi/spec.rs | 49 +++++++++ tests/testdata/petstore.yaml | 8 -- 5 files changed, 297 insertions(+), 40 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dcc198b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,71 @@ +## CONTRIBUTING.md + +### Welcome to the OpenAPI Mocker Contributing Guide! + +Thank you for your interest in contributing to the OpenAPI Mocker project! We're glad you're here. This document provides guidelines for making contributions to the project. Whether you're looking to submit a bug report, propose new features, or contribute code, your help is very much appreciated. + +### How to Contribute + +#### Reporting Bugs + +Bugs are tracked as GitHub issues. To report a bug, create an issue and include: + +- A clear title and description. +- Steps to reproduce. +- Expected behavior. +- Actual behavior. +- Screenshots if applicable. + +#### Suggesting Enhancements + +We love to receive suggestions for improvements! If you have ideas to make OpenAPI Mocker better, please submit an issue with the following: + +- A clear title and description. +- Explain why this enhancement would be useful to most OpenAPI Mocker users. +- Provide a step-by-step description of the suggested enhancement in as much detail as possible. + +#### Your First Code Contribution + +Unsure where to begin contributing? You can start by looking through the 'beginner' and 'help-wanted' issues: + +- **Beginner issues** - issues which should only require a few lines of code, and a test or two. +- **Help wanted issues** - issues which should be a bit more involved than beginner issues. + +Both issue lists are great places to start and are specifically tagged to indicate that help is needed. + +#### Pull Requests + +Here’s how to submit a pull request: + +1. Fork the repo and create your branch from `main`. +2. Clone the repository to your local machine. +3. Make your changes and ensure your code adheres to the existing style to keep the code consistent. +4. If you've added code, add tests! +5. Ensure your code passes all the tests. +6. Issue that pull request! + +Include a clear description of the reasons for your changes. It should include relevant motivations and context. List any dependencies that are required for this change. + +### Styleguides + +#### Git Commit Messages + +- Use the present tense ("Add feature" not "Added feature"). +- Use the imperative mood ("Move cursor to..." not "Moves cursor to..."). +- Limit the first line to 72 characters or less. +- Reference issues and pull requests liberally after the first line. + +#### Code Style for rust + +- Follow the [Rust style guide](https://doc.rust-lang.org/1.0.0/style/README.html) for Rust code. +- Ensure you run lint checks before submitting your pull request. + +### Community + +- If you have any questions about how to interpret the guidelines or want to discuss a substantial change/idea, don’t hesitate to post on our community channels or directly on GitHub issues. + +### Thank You! + +Every contribution counts, and by participating, you are expected to uphold this code. We appreciate your effort, and are excited to welcome you aboard and see what you can bring to the project! + +Let's create something amazing together! diff --git a/readme.md b/readme.md index 67f7db4..59b9c6e 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ cargo install openapi-mocker title: Example API version: 1.0.0 paths: - /hello: + /hello/{name}: get: responses: '200': @@ -32,8 +32,10 @@ cargo install openapi-mocker properties: message: type: string - example: - message: Hello, world! + examples: + default: + value: + message: Hello, world! '400': description: Bad Request content: @@ -43,13 +45,12 @@ cargo install openapi-mocker properties: message: type: string - example: - message: Bad request + examples: + default: + value: + message: Bad request ``` - > Note: The `example` field under the `content.schema` object is used - to generate the mock response. - 2. Run the mock server: ```bash @@ -91,30 +92,180 @@ cargo install openapi-mocker ## Performing requests -The mock server will respond to any request with a response defined in the OpenAPI specification. -If the request does not match any of the paths defined in the specification, the server will respond with a 404 Not Found. +You can use custom examples defined in the OpenAPI specification to test different responses. +Custom examples can be defined and requested in different ways. -By default, requesting an existing path defined in the specification will return a 200 response -with the example response defined in the specification. +## Requesting by path -### Request different status codes +You can define an example with the exact path you want to match. +Example: + + ```yaml + openapi: 3.0.0 + info: + title: Example API + version: 1.0.0 + paths: + /hello/{name}: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: + default: + value: + message: Hello, world! + /hello/jon_snow: + value: + message: You know nothing, Jon Snow! + ``` -To request a different status code, use the base url with the status code as a path parameter. +Request the example by the exact path: + ```bash + curl -i http://localhost:8080/hello/jon_snow + ``` + + The response should be: + + ```json + {"message":"You know nothing, Jon Snow!"} + ``` -For example, to request a 400 response example: +Request the default example: + ```bash + curl -i http://localhost:8080/hello/arya_stark + ``` + + The response should be: + + ```json + {"message":"Hello, world!"} + ``` -```bash -curl -i http://localhost:8080/400/hello -``` +## Requesting by query parameter -The response should be: +You can define an example with a query parameter you want to match. -```json -{"message":"Bad request"} -``` +Example: + + ```yaml + openapi: 3.0.0 + info: + title: Example API + version: 1.0.0 + paths: + /hello: + get: + parameters: + - name: name + in: query + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: + default: + value: + message: Hello, world! + "query:name=sansa": + value: + message: Sansa Stark + ``` + +Request the example by the query parameter: + ```bash + curl -i http://localhost:8080/hello?name=sansa + ``` + The response should be: + ```json + {"message": "Sansa Stark"} + ``` + +Request that does not match the query parameter: + ```bash + curl -i http://localhost:8080/hello?name=arya + ``` + The response should be: + ```json + {"message": "Hello, world!"} + ``` + +## Requesting by headers + +You can define an example with a header you want to match. + +Example: + + ```yaml + openapi: 3.0.0 + info: + title: Example API + version: 1.0.0 + paths: + /hello: + get: + parameters: + - name: name + in: header + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + message: + type: string + examples: + default: + value: + message: Hello, world! + "header:x-name=tyrion": + value: + message: Tyrion Lannister + ``` + +Request the example by the header: + ```bash + curl -i http://localhost:8080/hello -H "x-name: tyrion" + ``` + The response should be: + ```json + {"message": "Tyrion Lannister"} + ``` + +Request that does not match the header: + ```bash + curl -i http://localhost:8080/hello + ``` + The response should be: + ```json + {"message": "Hello, world!"} + ``` + +## Contributing -> Note: The status code must be defined in the OpenAPI specification. -> Requesting a status code that is not defined in the specification will return a 404 Not Found. +Contributions are welcome! Please see the [contributing guidelines](CONTRIBUTING.md). ## License diff --git a/src/lib.rs b/src/lib.rs index d647865..8b6d294 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,22 +23,16 @@ //! You can then make requests to the server to get example responses. //! For example, to get a list of pets: //! ```sh -//! curl http://localhost:8080/200/pets +//! curl http://localhost:8080/pets //! ``` //! This will return a list of pets from the example response in the spec. -//! You can also specify a different status code in the URL: -//! ```sh -//! curl http://localhost:8080/404/pets -//! ``` -//! This will return a 404 status code with the example response for a 404 error. -//! use clap::Parser; use std::path::PathBuf; pub mod openapi; pub mod server; #[derive(Parser)] -#[clap(version = "0.1.0", author = "Thiago Pacheco")] +#[clap(version = "0.1.2", author = "Thiago Pacheco")] pub struct Args { #[clap(index = 1)] pub spec: PathBuf, diff --git a/src/openapi/spec.rs b/src/openapi/spec.rs index 28ecbb4..d3b4313 100644 --- a/src/openapi/spec.rs +++ b/src/openapi/spec.rs @@ -10,11 +10,60 @@ pub struct Spec { } impl Spec { + /// Create a new Spec from an OpenAPI spec file. + /// # Arguments + /// * `path` - Path to the OpenAPI spec file + /// # Returns + /// A Spec instance + /// # Errors + /// Returns an error if the spec file cannot be loaded. + /// # Example + /// ```rust + /// use openapi_mocker::openapi::spec::Spec; + /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + /// ``` + /// This will create a new Spec instance from the Petstore spec. + /// You can then use the `get_example` method to get example responses. pub fn from_path(path: &str) -> SpecResult { let spec = load_spec(path).ok_or("Failed to load spec")?; Ok(Self { spec }) } + /// Get an example response for a request. + /// # Arguments + /// * `req` - The HTTP request + /// # Returns + /// An example response as a JSON value + /// # Example + /// ```rust + /// use actix_web::test::TestRequest; + /// use openapi_mocker::openapi::spec::Spec; + /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + /// let req = TestRequest::with_uri("/pets").to_http_request(); + /// let example = spec.get_example(&req); + /// ``` + /// + /// You can also load a specific example by matching the request path, query, or headers. + /// # Example with exact path match + /// ```rust + /// use actix_web::test::TestRequest; + /// use openapi_mocker::openapi::spec::Spec; + /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + /// let req = TestRequest::with_uri("/pets/2").to_http_request(); + /// let example = spec.get_example(&req).unwrap(); + /// assert_eq!(example["id"], serde_json::Value::Number(serde_json::Number::from(2))); + /// ``` + /// + /// # Example with query parameters + /// ```rust + /// use actix_web::test::TestRequest; + /// use openapi_mocker::openapi::spec::Spec; + /// let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + /// let req = TestRequest::with_uri("/pets?page=1").to_http_request(); + /// let examples = spec.get_example(&req).unwrap(); + /// let example = examples.as_array().unwrap().get(0).unwrap(); + /// assert_eq!(example["id"], serde_json::Value::Number(serde_json::Number::from(1))); + /// ``` pub fn get_example(&self, req: &HttpRequest) -> Option { let path = req.uri().path(); let method = req.method().as_str().to_lowercase(); diff --git a/tests/testdata/petstore.yaml b/tests/testdata/petstore.yaml index a1a321d..e361f90 100644 --- a/tests/testdata/petstore.yaml +++ b/tests/testdata/petstore.yaml @@ -52,7 +52,6 @@ paths: text/plain: schema: type: string - example: "Not implemented" default: description: unexpected error content: @@ -138,13 +137,6 @@ components: type: array items: $ref: "#/components/schemas/Pet" - example: - - id: 1 - name: doggie - tag: dog - - id: 2 - name: kitty - tag: cat Error: required: - code From cda719dc832e874b4eb4796eb79e5e993f0a1178 Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Thu, 2 May 2024 15:38:15 -0400 Subject: [PATCH 08/10] fix example route --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 59b9c6e..522bc2e 100644 --- a/readme.md +++ b/readme.md @@ -20,7 +20,7 @@ cargo install openapi-mocker title: Example API version: 1.0.0 paths: - /hello/{name}: + /hello: get: responses: '200': From 696f15fbe4194c8d284d4c1381faac8511813751 Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Thu, 2 May 2024 15:38:53 -0400 Subject: [PATCH 09/10] increase version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e46497..54efc57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -882,7 +882,7 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openapi-mocker" -version = "0.1.1" +version = "0.1.2" dependencies = [ "actix-rt", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index acc3cf3..99f0ac3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openapi-mocker" -version = "0.1.1" +version = "0.1.2" edition = "2021" repository = "https://github.com/pachecoio/openapi-mocker" keywords = ["openapi", "mock", "mock-server"] From 99e77bbf41d0c0db7db8d086cac80b6b0e0a99a2 Mon Sep 17 00:00:00 2001 From: Thiago Pacheco Date: Thu, 2 May 2024 15:49:38 -0400 Subject: [PATCH 10/10] Included test for matching across responses --- readme.md | 7 +++++++ src/openapi/spec.rs | 11 +++++++++++ tests/testdata/petstore.yaml | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/readme.md b/readme.md index 522bc2e..d6da5f2 100644 --- a/readme.md +++ b/readme.md @@ -263,6 +263,13 @@ Request that does not match the header: {"message": "Hello, world!"} ``` +> Note: The matches occur in the following order: path, query, headers. +> It is also important to note that the request is going to return the +> first match found in the order above. If no match is found, the default +> example is going to be returned. + +> Note: The matches are applied accross all the examples and responses in the OpenAPI specification. + ## Contributing Contributions are welcome! Please see the [contributing guidelines](CONTRIBUTING.md). diff --git a/src/openapi/spec.rs b/src/openapi/spec.rs index d3b4313..eba5c68 100644 --- a/src/openapi/spec.rs +++ b/src/openapi/spec.rs @@ -468,4 +468,15 @@ mod tests { serde_json::Value::Number(serde_json::Number::from(4)) ); } + + #[test] + fn test_match_401_response() { + let spec = Spec::from_path("tests/testdata/petstore.yaml").unwrap(); + let req = TestRequest::with_uri("/pets/5").to_http_request(); + let example = spec.get_example(&req).unwrap(); + assert_eq!( + example["code"], + serde_json::Value::Number(serde_json::Number::from(401)) + ); + } } diff --git a/tests/testdata/petstore.yaml b/tests/testdata/petstore.yaml index e361f90..9c02dcf 100644 --- a/tests/testdata/petstore.yaml +++ b/tests/testdata/petstore.yaml @@ -113,6 +113,17 @@ paths: id: 4 name: batty tag: bat + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + examples: + "/pets/5": + value: + code: 401 + message: error default: description: unexpected error content: