#![cfg(feature = "openapi-schemars")]
#![allow(dead_code)]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use ullun::openapi::{OpenApiInfo, Operation};
use ullun::prelude::*;
use ullun::testing::TestClient;
#[derive(Deserialize, JsonSchema)]
struct NewUser {
name: String,
age: Option<u8>,
role: Role,
}
#[derive(Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
enum Role {
Admin,
Member,
}
#[derive(Serialize, JsonSchema)]
struct User {
id: u64,
name: String,
role: Role,
tags: Vec<String>,
}
#[derive(Deserialize, JsonSchema)]
struct Paging {
limit: Option<u32>,
after: u64,
}
async fn doc(op: Operation) -> Value {
let app = App::new()
.post("/users", |_req: Request| async { Ok(Response::text("ok")) })
.doc("POST", "/users", op)
.openapi(OpenApiInfo::new("Users", "1.0.0"));
TestClient::new(app)
.get("/openapi.json")
.send()
.await
.json::<Value>()
}
#[tokio::test]
async fn request_and_response_types_become_component_schemas() {
let doc = doc(Operation::new("Create a user")
.request_type::<NewUser>()
.response_type::<User>(201, "Created"))
.await;
let body =
&doc["paths"]["/users"]["post"]["requestBody"]["content"]["application/json"]["schema"];
assert_eq!(body["$ref"], "#/components/schemas/NewUser");
let reply = &doc["paths"]["/users"]["post"]["responses"]["201"]["content"]["application/json"]
["schema"];
assert_eq!(reply["$ref"], "#/components/schemas/User");
let schemas = &doc["components"]["schemas"];
let new_user = &schemas["NewUser"];
assert_eq!(new_user["type"], "object");
assert_eq!(
new_user["properties"]["name"]["description"],
"Display name"
);
let required: Vec<&str> = new_user["required"]
.as_array()
.unwrap()
.iter()
.filter_map(Value::as_str)
.collect();
assert!(required.contains(&"name") && required.contains(&"role"));
assert!(!required.contains(&"age"));
assert_eq!(
new_user["properties"]["role"]["$ref"],
"#/components/schemas/Role"
);
assert_eq!(
schemas["Role"]["enum"],
serde_json::json!(["admin", "member"])
);
assert_eq!(schemas["User"]["properties"]["tags"]["type"], "array");
assert_eq!(schemas["Error"]["type"], "object");
}
#[tokio::test]
async fn no_json_schema_dollar_defs_leak_into_the_document() {
let doc = doc(Operation::new("x").request_type::<NewUser>()).await;
let text = doc.to_string();
assert!(!text.contains("$defs"), "{text}");
assert!(!text.contains("\"$schema\""), "{text}");
}
#[tokio::test]
async fn query_type_lists_one_parameter_per_field() {
let doc = doc(Operation::new("List").query_type::<Paging>()).await;
let params = doc["paths"]["/users"]["post"]["parameters"]
.as_array()
.unwrap();
let find = |name: &str| params.iter().find(|p| p["name"] == name).unwrap();
assert_eq!(find("limit")["in"], "query");
assert_eq!(find("limit")["required"], false);
assert_eq!(find("limit")["description"], "Page size");
assert_eq!(find("after")["required"], true);
assert_eq!(params.len(), 2);
}
#[tokio::test]
async fn form_bodies_and_shared_types_work_together() {
let doc = doc(Operation::new("Both")
.request_form_type::<Paging>()
.response_type::<User>(200, "ok")
.response_type::<Vec<User>>(207, "many"))
.await;
let form = &doc["paths"]["/users"]["post"]["requestBody"]["content"]
["application/x-www-form-urlencoded"]["schema"];
assert_eq!(form["$ref"], "#/components/schemas/Paging");
let many = &doc["paths"]["/users"]["post"]["responses"]["207"]["content"]["application/json"]
["schema"];
assert_eq!(many["type"], "array");
assert_eq!(many["items"]["$ref"], "#/components/schemas/User");
assert!(doc["components"]["schemas"]["Paging"].is_object());
assert!(doc["components"]["schemas"]["Role"].is_object());
}