#![cfg(all(feature = "macros", feature = "json"))]
use std::process::Command;
use kynos::prelude::*;
use serde::{Deserialize, Serialize};
const BEGIN: &str = "-----BEGIN KYNOS DOCUMENT-----";
const END: &str = "-----END KYNOS DOCUMENT-----";
const PROCESSES: usize = 3;
#[derive(Schema, Serialize, Deserialize)]
struct Address {
line: String,
city: String,
}
#[derive(Schema, Serialize, Deserialize)]
struct Category {
name: String,
parent: Option<Box<Category>>,
}
#[derive(Schema, Serialize, Deserialize)]
#[serde(tag = "kind")]
enum Payment {
Card { last4: String },
Balance,
}
#[derive(Schema, Serialize, Deserialize)]
struct Customer {
id: u64,
name: String,
address: Address,
category: Category,
}
#[derive(Schema, Serialize, Deserialize)]
struct Order {
id: u64,
customer: Customer,
payment: Payment,
}
#[allow(dead_code)]
#[derive(Schema, PathParams)]
struct CustomerPath {
id: u64,
}
#[allow(dead_code)]
#[derive(Schema, QueryParams)]
struct Page {
limit: Option<u32>,
cursor: Option<String>,
}
#[derive(SecurityScheme)]
#[security(api_key(in = "header", name = "X-Api-Key"))]
struct ApiKey;
#[kynos::get("/customers")]
async fn list_customers(Query(_page): Query<Page>) -> Json<Vec<Customer>> {
Json(Vec::new())
}
#[kynos::get("/customers/{id}")]
async fn get_customer(Path(_path): Path<CustomerPath>) -> Json<Customer> {
unreachable!("this fixture is described, never served")
}
#[kynos::put("/customers/{id}")]
async fn put_customer(
Path(_path): Path<CustomerPath>,
Json(body): Json<Customer>,
) -> Json<Customer> {
Json(body)
}
#[kynos::post("/orders")]
async fn create_order(Json(order): Json<Order>) -> Created<Json<Order>> {
Created::at("/orders/1", Json(order))
}
fn document() -> String {
Router::<()>::new()
.security_scheme::<ApiKey>()
.tag::<Customers>()
.tag::<Orders>()
.mount(kynos::routes![
list_customers,
get_customer,
put_customer,
create_order
])
.openapi()
.expect("the fixture describes itself")
.to_json()
.expect("a description serializes")
}
#[derive(Tag)]
struct Customers;
#[derive(Tag)]
struct Orders;
#[test]
fn emit_fixture_description() {
println!("{BEGIN}");
println!("{}", document());
println!("{END}");
}
fn emit_in_a_fresh_process() -> String {
let executable = std::env::current_exe().expect("a test binary knows its own path");
let output = Command::new(&executable)
.args([
"--exact",
"emit_fixture_description",
"--nocapture",
"--test-threads=1",
])
.output()
.expect("the test binary re-executes");
assert!(
output.status.success(),
"the child failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("the child emits UTF-8");
let (_, after) = stdout
.split_once(BEGIN)
.expect("the child emitted an opening marker");
let (payload, _) = after
.split_once(END)
.expect("the child emitted a closing marker");
payload.trim().to_owned()
}
#[test]
fn a_description_is_the_same_bytes_in_every_process() {
let mut emissions = Vec::with_capacity(PROCESSES);
for _ in 0..PROCESSES {
emissions.push(emit_in_a_fresh_process());
}
assert!(
!emissions[0].is_empty(),
"the fixture emitted an empty document"
);
for (index, emission) in emissions.iter().enumerate().skip(1) {
if let Some(report) = first_difference(&emissions[0], emission) {
panic!("process 0 and process {index} emitted different bytes\n{report}");
}
}
}
fn first_difference(left: &str, right: &str) -> Option<String> {
for (number, (one, other)) in left.lines().zip(right.lines()).enumerate() {
if one != other {
return Some(format!(
"first differing line is {}:\n process 0: {one}\n the other: {other}",
number + 1
));
}
}
(left.lines().count() != right.lines().count()).then(|| {
format!(
"they agree line for line but differ in length: {} lines against {}",
left.lines().count(),
right.lines().count()
)
})
}
#[test]
fn a_component_is_registered_after_everything_it_refers_to() {
let document = document();
let names: Vec<&str> = ["Address", "Category", "Customer", "Payment", "Order"]
.into_iter()
.filter(|name| document.contains(&format!("\"{name}\"")))
.collect();
assert_eq!(
names.len(),
5,
"the fixture no longer registers every component this asserts over"
);
let position = |name: &str| {
document
.find(&format!("\"{name}\": {{"))
.unwrap_or_else(|| panic!("`{name}` is a declared component"))
};
assert!(
position("Address") < position("Customer"),
"a field's component is registered before the type carrying it"
);
assert!(
position("Category") < position("Customer"),
"a recursive component is registered before the type carrying it"
);
assert!(
position("Customer") < position("Order"),
"the whole chain is registered depth-first"
);
}