mod openapi_impl;
pub use openapi_impl::generate_openapi_spec;
#[derive(Debug, Clone, Copy)]
pub struct OpenApiPathParam {
pub name: &'static str,
pub description: &'static str,
pub required: bool,
pub schema_type: &'static str,
pub schema_format: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct OpenApiRouteInfo {
pub path: &'static str,
pub method: &'static str,
pub summary: &'static str,
pub description: &'static str,
pub version: &'static str,
pub tags: &'static [&'static str],
pub path_params: &'static [OpenApiPathParam],
}
inventory::collect!(OpenApiRouteInfo);
#[derive(Debug, Clone, Default)]
pub struct OpenApiBuilder {
title: String,
version: String,
description: Option<String>,
}
#[cfg(test)]
inventory::submit!(OpenApiRouteInfo::new(
"/__openapi_test_marker__",
"GET",
"OpenAPI module test marker",
"Sentinel route registered by src/openapi/mod.rs tests to verify inventory collection.",
"test",
&["test"],
));
#[cfg(test)]
inventory::submit!(OpenApiRouteInfo::with_path_params(
"/__openapi_path_param_test__/{id}",
"GET",
"Path param test marker",
"Route with a u64 path param registered by src/openapi/mod.rs tests.",
"test",
&["test"],
&[OpenApiPathParam::new(
"id", "User ID", true, "integer", "uint64"
),],
));
#[cfg(test)]
mod tests {
use super::*;
use utoipa::openapi::path::HttpMethod;
#[test]
fn builder_new_yields_empty_fields() {
let builder = OpenApiBuilder::new();
assert!(builder.title.is_empty());
assert!(builder.version.is_empty());
assert!(builder.description.is_none());
}
#[test]
fn builder_chain_sets_all_fields() {
let builder = OpenApiBuilder::new()
.title("My API")
.version("9.9.9")
.description("hello");
assert_eq!(builder.title, "My API");
assert_eq!(builder.version, "9.9.9");
assert_eq!(builder.description.as_deref(), Some("hello"));
}
#[test]
fn builder_default_matches_new() {
let a = OpenApiBuilder::new();
let b = OpenApiBuilder::default();
assert_eq!(a.title, b.title);
assert_eq!(a.version, b.version);
assert_eq!(a.description, b.description);
}
#[test]
fn build_propagates_info_fields_without_description() {
let spec = OpenApiBuilder::new()
.title("Title X")
.version("0.0.1")
.build();
assert_eq!(spec.info.title, "Title X");
assert_eq!(spec.info.version, "0.0.1");
assert!(spec.info.description.is_none());
}
#[test]
fn build_propagates_description_when_set() {
let spec = OpenApiBuilder::new()
.title("T")
.version("1")
.description("desc")
.build();
assert_eq!(spec.info.description.as_deref(), Some("desc"));
}
#[test]
fn to_parameter_unknown_schema_type_falls_back_to_string() {
let param = OpenApiPathParam::new("id", "", true, "object", "");
let parameter = param.to_parameter();
assert_eq!(parameter.name, "id");
}
#[test]
fn with_path_params_constructs_route_info() {
const PARAMS: &[OpenApiPathParam] = &[OpenApiPathParam::new(
"id", "user id", true, "integer", "int64",
)];
let route = OpenApiRouteInfo::with_path_params(
"/users/{id}",
"GET",
"Get user",
"Retrieve a user by id",
"v1",
&["users"],
PARAMS,
);
assert_eq!(route.path, "/users/{id}");
assert_eq!(route.method, "GET");
assert_eq!(route.path_params.len(), 1);
assert_eq!(route.path_params[0].name, "id");
}
#[test]
fn generate_openapi_spec_uses_crate_identity() {
let spec = generate_openapi_spec();
assert_eq!(spec.info.title, "SDForge API");
assert_eq!(spec.info.version, env!("CARGO_PKG_VERSION"));
}
#[test]
fn generated_spec_contains_test_marker_route() {
let spec = generate_openapi_spec();
let paths_json = serde_json::to_value(&spec.paths).expect("paths serialize");
let paths_obj = paths_json
.as_object()
.expect("paths is a JSON object")
.keys()
.cloned()
.collect::<Vec<_>>();
assert!(
paths_obj.iter().any(|p| p == "/__openapi_test_marker__"),
"expected /__openapi_test_marker__ in paths, got {:?}",
paths_obj
);
}
#[test]
fn http_method_maps_all_canonical_variants() {
let cases = [
("GET", HttpMethod::Get),
("POST", HttpMethod::Post),
("PUT", HttpMethod::Put),
("DELETE", HttpMethod::Delete),
("PATCH", HttpMethod::Patch),
("HEAD", HttpMethod::Head),
("OPTIONS", HttpMethod::Options),
("TRACE", HttpMethod::Trace),
];
for (s, expected) in cases {
let info = OpenApiRouteInfo::new("/", s, "", "", "", &[]);
assert!(
info.http_method() == expected,
"method={} did not map to expected variant",
s
);
}
}
#[test]
fn http_method_is_case_insensitive() {
let info = OpenApiRouteInfo::new("/", "get", "", "", "", &[]);
assert!(info.http_method() == HttpMethod::Get);
}
#[test]
fn http_method_unknown_falls_back_to_get() {
let info = OpenApiRouteInfo::new("/", "CONNECT", "", "", "", &[]);
assert!(info.http_method() == HttpMethod::Get);
}
#[test]
fn route_info_new_populates_fields() {
let info = OpenApiRouteInfo::new(
"/users/{id}",
"GET",
"Fetch user",
"Fetch a user by id",
"v2",
&["users", "v2"],
);
assert_eq!(info.path, "/users/{id}");
assert_eq!(info.method, "GET");
assert_eq!(info.summary, "Fetch user");
assert_eq!(info.description, "Fetch a user by id");
assert_eq!(info.version, "v2");
assert_eq!(info.tags, &["users", "v2"]);
}
#[test]
fn route_info_is_copy() {
let info = OpenApiRouteInfo::new("/x", "GET", "s", "d", "v1", &[]);
let copied = info;
assert_eq!(info.path, copied.path);
assert_eq!(info.method, copied.method);
}
#[test]
fn build_succeeds_with_empty_inventory() {
let spec = OpenApiBuilder::new().title("T").version("0").build();
let json = serde_json::to_value(&spec.paths).expect("serialize");
assert!(json.is_object(), "paths must be a JSON object");
}
#[test]
fn builder_implements_clone_and_debug() {
let b = OpenApiBuilder::new().title("t").version("1");
let cloned = b.clone();
assert_eq!(b.title, cloned.title);
let debug = format!("{:?}", b);
assert!(debug.contains("OpenApiBuilder"));
}
#[test]
fn path_param_new_populates_fields() {
let p = OpenApiPathParam::new("id", "User ID", true, "integer", "uint64");
assert_eq!(p.name, "id");
assert_eq!(p.description, "User ID");
assert!(p.required);
assert_eq!(p.schema_type, "integer");
assert_eq!(p.schema_format, "uint64");
}
#[test]
fn path_param_to_parameter_builds_correct_parameter() {
let p = OpenApiPathParam::new("id", "User ID", true, "integer", "uint64");
let param = p.to_parameter();
assert_eq!(param.name, "id");
assert!(
matches!(param.parameter_in, utoipa::openapi::path::ParameterIn::Path),
"parameter_in must be Path"
);
assert!(
matches!(param.required, utoipa::openapi::Required::True),
"path parameter must be required"
);
let schema = param.schema.expect("schema must be present");
let json = serde_json::to_value(&schema).expect("schema serialize");
assert_eq!(json["type"], "integer", "schema type must be integer");
assert_eq!(json["format"], "uint64", "schema format must be uint64");
}
#[test]
fn path_param_to_parameter_omits_empty_format() {
let p = OpenApiPathParam::new("name", "", true, "string", "");
let param = p.to_parameter();
let schema = param.schema.expect("schema present");
let json = serde_json::to_value(&schema).expect("serialize");
assert_eq!(json["type"], "string");
assert!(
json.get("format").is_none() || json["format"].is_null(),
"format must be absent for empty schema_format"
);
}
#[test]
fn with_path_params_stores_params() {
const PARAMS: &[OpenApiPathParam] =
&[OpenApiPathParam::new("id", "", true, "integer", "uint64")];
const INFO: OpenApiRouteInfo =
OpenApiRouteInfo::with_path_params("/x/{id}", "GET", "s", "d", "v1", &[], PARAMS);
assert_eq!(INFO.path_params.len(), 1);
assert_eq!(INFO.path_params[0].name, "id");
assert_eq!(INFO.path_params[0].schema_type, "integer");
assert_eq!(INFO.path_params[0].schema_format, "uint64");
}
#[test]
fn generated_spec_contains_path_param_operation() {
let spec = generate_openapi_spec();
let paths_json = serde_json::to_value(&spec.paths).expect("paths serialize");
let paths_obj = paths_json
.as_object()
.expect("paths is a JSON object")
.clone();
let route_key = "/__openapi_path_param_test__/{id}";
let route = paths_obj
.get(route_key)
.unwrap_or_else(|| panic!("expected route {} in paths", route_key));
let get_op = route
.get("get")
.unwrap_or_else(|| panic!("expected GET operation on {}", route_key));
let params = get_op
.get("parameters")
.and_then(|p| p.as_array())
.unwrap_or_else(|| panic!("expected parameters array on {}", route_key));
assert_eq!(
params.len(),
1,
"expected exactly 1 parameter on {}",
route_key
);
let id_param = ¶ms[0];
assert_eq!(id_param["name"], "id", "parameter name must be id");
assert_eq!(id_param["in"], "path", "parameter in must be path");
assert_eq!(
id_param["required"], true,
"path parameter must be required"
);
assert_eq!(
id_param["schema"]["type"], "integer",
"schema type must be integer"
);
assert_eq!(
id_param["schema"]["format"], "uint64",
"schema format must be uint64"
);
}
}