#![allow(dead_code)]
use server_less::openapi;
#[derive(Clone)]
struct StandaloneService;
#[openapi(prefix = "/api")]
impl StandaloneService {
pub fn get_status(&self) -> String {
"ok".to_string()
}
pub fn list_items(&self) -> Vec<String> {
vec![]
}
pub fn create_item(&self, name: String) -> String {
name
}
}
#[test]
fn test_openapi_standalone_generates_spec() {
let spec = StandaloneService::openapi_spec();
assert_eq!(spec["openapi"], "3.0.0");
assert_eq!(spec["info"]["title"], "StandaloneService");
}
#[test]
fn test_openapi_standalone_has_paths() {
let spec = StandaloneService::openapi_spec();
let paths = &spec["paths"];
assert!(paths.is_object(), "Should have paths object");
assert!(
paths["/api/status"]["get"].is_object(),
"Should have GET /api/status. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
assert!(
paths["/api/items"]["get"].is_object(),
"Should have GET /api/items"
);
assert!(
paths["/api/item"]["post"].is_object() || paths["/api/items"]["post"].is_object(),
"Should have POST /api/item or /api/items. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
}
use server_less::{http, jsonrpc};
#[derive(Clone)]
struct ProtocolAwareService;
#[openapi]
#[http(prefix = "/api", openapi = false)]
#[jsonrpc(path = "/rpc")]
impl ProtocolAwareService {
pub fn get_status(&self) -> String {
"ok".to_string()
}
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
}
#[test]
fn test_openapi_protocol_aware_detects_http() {
let spec = ProtocolAwareService::openapi_spec();
let paths = &spec["paths"];
assert!(
paths["/api/status"]["get"].is_object(),
"Should have HTTP endpoint. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
}
#[test]
fn test_openapi_protocol_aware_detects_jsonrpc() {
let spec = ProtocolAwareService::openapi_spec();
let paths = &spec["paths"];
assert!(
paths["/rpc"]["post"].is_object(),
"Should have JSON-RPC endpoint. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
}
#[test]
fn test_openapi_protocol_aware_combined_spec() {
let spec = ProtocolAwareService::openapi_spec();
assert_eq!(spec["openapi"], "3.0.0");
assert_eq!(spec["info"]["title"], "ProtocolAwareService");
let paths = &spec["paths"];
let path_count = paths.as_object().map(|o| o.len()).unwrap_or(0);
assert!(
path_count >= 2,
"Should have at least 2 paths (HTTP + JSON-RPC). Got: {}",
path_count
);
}
use server_less::ws;
#[derive(Clone)]
struct HttpWsService;
#[openapi]
#[http(prefix = "/api", openapi = false)]
#[ws(path = "/ws")]
impl HttpWsService {
pub fn get_info(&self) -> String {
"info".to_string()
}
pub fn echo(&self, msg: String) -> String {
msg
}
}
#[test]
fn test_openapi_protocol_aware_with_ws() {
let spec = HttpWsService::openapi_spec();
let paths = &spec["paths"];
assert!(
paths["/api/infos"]["get"].is_object(),
"Should have HTTP endpoint. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
assert!(
paths["/ws"]["get"].is_object(),
"Should have WebSocket endpoint. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
}
use server_less::graphql;
#[derive(Clone)]
struct HttpGraphqlService;
#[openapi]
#[http(prefix = "/api", openapi = false)]
#[graphql]
impl HttpGraphqlService {
pub fn get_status(&self) -> String {
"ok".to_string()
}
}
#[test]
fn test_openapi_protocol_aware_with_graphql() {
let spec = HttpGraphqlService::openapi_spec();
let paths = &spec["paths"];
assert!(
paths["/api/status"]["get"].is_object(),
"Should have HTTP endpoint"
);
assert!(
paths["/graphql"]["post"].is_object(),
"Should have GraphQL POST endpoint. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
}
#[derive(Clone)]
struct HttpOnlyWithOpenapi;
#[openapi]
#[http(openapi = false)]
impl HttpOnlyWithOpenapi {
pub fn list_things(&self) -> Vec<String> {
vec![]
}
}
#[test]
fn test_openapi_detects_single_protocol() {
let spec = HttpOnlyWithOpenapi::openapi_spec();
let paths = &spec["paths"];
assert!(
paths["/things"]["get"].is_object(),
"Should have HTTP endpoint from detected #[http]. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
}
#[allow(unused_imports)]
use server_less::response;
#[allow(unused_imports)]
use server_less::route;
#[derive(Clone)]
struct EnhancedAttrsService;
#[openapi(prefix = "/api")]
impl EnhancedAttrsService {
#[route(tags = "users,public")]
pub fn get_user(&self, id: String) -> String {
id
}
#[route(tags = "users", deprecated)]
#[response(description = "User created successfully")]
pub fn create_user(&self, name: String) -> String {
name
}
#[route(hidden)]
pub fn internal_method(&self) -> String {
"secret".to_string()
}
}
#[test]
fn test_openapi_tags_attribute() {
let spec = EnhancedAttrsService::openapi_spec();
let paths = &spec["paths"];
let get_user = &paths["/api/users/{id}"]["get"];
assert!(get_user.is_object(), "Should have get_user endpoint");
let tags = get_user["tags"].as_array();
assert!(tags.is_some(), "Should have tags array");
let tags = tags.unwrap();
assert!(
tags.iter().any(|t| t.as_str() == Some("users")),
"Should have 'users' tag. Tags: {:?}",
tags
);
assert!(
tags.iter().any(|t| t.as_str() == Some("public")),
"Should have 'public' tag. Tags: {:?}",
tags
);
}
#[test]
fn test_openapi_deprecated_attribute() {
let spec = EnhancedAttrsService::openapi_spec();
let paths = &spec["paths"];
let create_user = &paths["/api/users"]["post"];
assert!(create_user.is_object(), "Should have create_user endpoint");
assert_eq!(
create_user["deprecated"], true,
"Should be marked as deprecated"
);
}
#[test]
fn test_openapi_description_from_doc_comment() {
let spec = EnhancedAttrsService::openapi_spec();
let paths = &spec["paths"];
let get_user = &paths["/api/users/{id}"]["get"];
assert!(get_user.is_object(), "Should have get_user endpoint");
assert_eq!(
get_user["summary"].as_str(),
Some("Get user by ID"),
"Summary should be first line of doc comment"
);
let description = get_user["description"].as_str();
assert!(
description.is_some(),
"Should have description from doc comment"
);
assert!(
description
.unwrap()
.contains("Fetch a user by their unique ID"),
"Description should contain full doc text. Got: {:?}",
description
);
}
#[test]
fn test_openapi_response_description_attribute() {
let spec = EnhancedAttrsService::openapi_spec();
let paths = &spec["paths"];
let create_user = &paths["/api/users"]["post"];
assert!(create_user.is_object(), "Should have create_user endpoint");
let response_200 = &create_user["responses"]["200"];
assert_eq!(
response_200["description"].as_str(),
Some("User created successfully"),
"Should have custom response description"
);
}
#[test]
fn test_openapi_hidden_excludes_from_spec() {
let spec = EnhancedAttrsService::openapi_spec();
let paths = &spec["paths"];
let internal = &paths["/api/internal-methods"]["get"];
assert!(
internal.is_null(),
"Hidden endpoint should not appear in spec. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
}
#[derive(Clone)]
struct ParamHelpStandaloneService;
#[allow(unused_variables)]
#[openapi(prefix = "/api")]
impl ParamHelpStandaloneService {
pub fn find_users(
&self,
#[param(help = "Substring to match against user names")]
name: String,
) -> Vec<String> {
vec![name]
}
pub fn get_item(
&self,
id: String,
#[param(help = "Maximum number of results")]
limit: Option<u32>,
) -> String {
id
}
}
#[test]
fn test_param_help_in_standalone_openapi_query_param() {
let spec = ParamHelpStandaloneService::openapi_spec();
let paths = &spec["paths"];
let find_users = &paths["/api/users"]["get"];
assert!(
find_users.is_object(),
"Should have GET /api/users. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
let parameters = find_users["parameters"]
.as_array()
.expect("Should have parameters array");
let name_param = parameters
.iter()
.find(|p| p["name"].as_str() == Some("name"))
.expect("'name' parameter should appear in find_users OpenAPI spec");
assert_eq!(
name_param["description"].as_str(),
Some("Substring to match against user names"),
"#[param(help = \"...\")] should populate the OpenAPI parameter description in standalone mode"
);
}
#[test]
fn test_param_help_in_standalone_openapi_optional_query_param() {
let spec = ParamHelpStandaloneService::openapi_spec();
let paths = &spec["paths"];
let get_item = &paths["/api/items/{id}"]["get"];
assert!(
get_item.is_object(),
"Should have GET /api/items/{{id}}. Paths: {}",
serde_json::to_string_pretty(paths).unwrap()
);
let parameters = get_item["parameters"]
.as_array()
.expect("Should have parameters array");
let limit_param = parameters
.iter()
.find(|p| p["name"].as_str() == Some("limit"))
.expect("'limit' parameter should appear in get_item OpenAPI spec");
assert_eq!(
limit_param["description"].as_str(),
Some("Maximum number of results"),
"#[param(help = \"...\")] should populate the description for optional query params in standalone mode"
);
}