use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
crate::api::rest::handlers::create_session,
crate::api::rest::handlers::get_current_step,
crate::api::rest::handlers::advance_step,
crate::api::rest::handlers::replace_session,
crate::api::rest::handlers::update_session,
crate::api::rest::handlers::delete_session,
crate::api::rest::handlers_v2::create_simulation,
crate::api::rest::handlers_v2::get_simulation,
crate::api::rest::handlers_v2::peek_snapshot,
crate::api::rest::handlers_v2::advance_simulation,
crate::api::rest::handlers_v2::delete_simulation,
crate::api::rest::export::export_simulation,
),
components(
schemas(
crate::api::rest::responses::OptionContractResponse,
crate::api::rest::responses::OptionPriceResponse,
crate::api::rest::responses::SessionInfoResponse,
crate::api::rest::requests::CreateSessionRequest,
crate::api::rest::requests::UpdateSessionRequest,
crate::api::rest::models::SessionId,
crate::api::rest::responses::ValidationErrorResponse,
crate::api::rest::requests_v2::CreateSimulationRequest,
crate::api::rest::responses_v2::SimulationResponse,
crate::api::rest::responses_v2::SimulationParametersResponse,
crate::api::rest::responses_v2::ScheduleRuleResponse,
crate::api::rest::responses_v2::SnapshotResponse,
crate::api::rest::responses_v2::ExpiryChainResponse,
crate::api::rest::responses_v2::ContractResponse,
crate::api::rest::responses_v2::OptionQuoteResponse,
crate::api::rest::responses_v2::UnderlyingResponse,
crate::api::rest::responses_v2::CursorResponse,
)
),
tags(
(name = "Options-Simulator", description = "Options Simulator endpoints")
)
)]
pub(crate) struct ApiDoc;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use utoipa::OpenApi;
#[test]
fn test_openapi_spec_generation() {
let openapi = ApiDoc::openapi();
assert!(
!openapi.to_json().expect("REASON").is_empty(),
"OpenAPI spec should not be empty"
);
}
#[test]
fn test_openapi_paths() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().unwrap();
let parsed: Value = serde_json::from_str(&json).expect("Failed to parse OpenAPI JSON");
assert!(parsed.get("paths").is_some(), "Paths section should exist");
let paths = parsed.get("paths").unwrap();
let expected_paths = vec![
"/api/v1/chain", "/api/v1/chain/step", ];
for path in expected_paths {
assert!(
paths.get(path).is_some(),
"Path {} should be defined in OpenAPI spec",
path
);
}
}
#[test]
fn test_openapi_schemas() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().unwrap();
let parsed: Value = serde_json::from_str(&json).expect("Failed to parse OpenAPI JSON");
let components = parsed
.get("components")
.expect("Components section should exist");
let schemas = components
.get("schemas")
.expect("Schemas section should exist");
let expected_schemas = vec![
"OptionContractResponse",
"OptionPriceResponse",
"SessionInfoResponse",
"CreateSessionRequest",
"UpdateSessionRequest",
"SessionId",
];
for schema_name in expected_schemas {
assert!(
schemas.get(schema_name).is_some(),
"Schema {} should be defined in OpenAPI spec",
schema_name
);
}
}
#[test]
fn test_openapi_tags() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().unwrap();
let parsed: Value = serde_json::from_str(&json).expect("Failed to parse OpenAPI JSON");
let tags = parsed.get("tags").expect("Tags section should exist");
assert!(tags.is_array(), "Tags should be an array");
let tag_exists = tags
.as_array()
.unwrap()
.iter()
.any(|tag| tag.get("name").and_then(|n| n.as_str()) == Some("Options-Simulator"));
assert!(tag_exists, "Options-Simulator tag should be defined");
}
#[test]
fn test_openapi_json_deserializability() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().unwrap();
let result: Result<serde_json::Value, _> = serde_json::from_str(&json);
assert!(result.is_ok(), "OpenAPI JSON should be valid JSON");
}
#[test]
fn test_no_sensitive_info_in_spec() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().unwrap();
assert!(
!json.contains("localhost")
&& !json.contains("127.0.0.1")
&& !json.contains("password")
&& !json.contains("secret"),
"OpenAPI spec should not contain sensitive information"
);
}
#[test]
fn test_openapi_version() {
let openapi = ApiDoc::openapi();
let json = openapi.to_json().unwrap();
let parsed: Value = serde_json::from_str(&json).expect("Failed to parse OpenAPI JSON");
assert!(
parsed.get("openapi").is_some(),
"OpenAPI version should be specified"
);
if let Some(version) = parsed.get("openapi") {
assert!(
version.as_str().is_some_and(|v| v.starts_with("3.")),
"OpenAPI version should be 3.x"
);
}
}
#[test]
fn test_v1_openapi_paths_are_unchanged() {
let spec = match ApiDoc::openapi().to_json() {
Ok(json) => json,
Err(error) => panic!("the OpenAPI document must render: {error}"),
};
let parsed: Value = match serde_json::from_str(&spec) {
Ok(parsed) => parsed,
Err(error) => panic!("the OpenAPI document must parse: {error}"),
};
let paths = match parsed.get("paths").and_then(Value::as_object) {
Some(paths) => paths,
None => panic!("the document must carry paths"),
};
let mut v1_paths: Vec<&str> = paths
.keys()
.map(String::as_str)
.filter(|path| path.starts_with("/api/v1/"))
.collect();
v1_paths.sort_unstable();
assert_eq!(v1_paths, vec!["/api/v1/chain", "/api/v1/chain/step"]);
let mut chain_methods: Vec<&str> =
match paths.get("/api/v1/chain").and_then(Value::as_object) {
Some(operations) => operations.keys().map(String::as_str).collect(),
None => panic!("/api/v1/chain must be documented"),
};
chain_methods.sort_unstable();
assert_eq!(chain_methods, vec!["delete", "get", "patch", "post", "put"]);
let step_methods: Vec<&str> =
match paths.get("/api/v1/chain/step").and_then(Value::as_object) {
Some(operations) => operations.keys().map(String::as_str).collect(),
None => panic!("/api/v1/chain/step must be documented"),
};
assert_eq!(step_methods, vec!["post"]);
}
#[test]
fn test_v1_openapi_status_codes_are_unchanged() {
let spec = match ApiDoc::openapi().to_json() {
Ok(json) => json,
Err(error) => panic!("the OpenAPI document must render: {error}"),
};
let parsed: Value = match serde_json::from_str(&spec) {
Ok(parsed) => parsed,
Err(error) => panic!("the OpenAPI document must parse: {error}"),
};
let codes = |path: &str, method: &str| -> Vec<String> {
let responses = parsed
.get("paths")
.and_then(|paths| paths.get(path))
.and_then(|operations| operations.get(method))
.and_then(|operation| operation.get("responses"))
.and_then(Value::as_object);
match responses {
Some(responses) => {
let mut codes: Vec<String> = responses.keys().cloned().collect();
codes.sort_unstable();
codes
}
None => panic!("{method} {path} must document its responses"),
}
};
assert_eq!(
codes("/api/v1/chain/step", "post"),
vec!["200", "404", "409", "410", "412", "500"]
);
assert_eq!(
codes("/api/v1/chain", "get"),
vec!["200", "404", "410", "500"]
);
}
}