use std::{fmt, net::Ipv4Addr, num::ParseIntError, str::FromStr};
use kynos::{
extract::{
FromRequestParts,
connection::{ConnectInfo, MatchedPath},
describe::Describe,
params::{cookie::Cookies, header::Headers},
},
http::Parts,
openapi::{Parameter, ParameterIn},
prelude::*,
router::operation::OperationCx,
schema::{ParamValue, registry::Registry},
server::Server,
};
use serde::{Deserialize, Serialize};
#[derive(Schema, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
}
struct UserId(u64);
impl Schema for UserId {
fn schema(registry: &mut Registry) -> kynos::openapi::Schema {
u64::schema(registry)
}
}
impl FromStr for UserId {
type Err = ParseIntError;
fn from_str(raw: &str) -> Result<Self, Self::Err> {
raw.parse().map(Self)
}
}
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl ParamValue for UserId {}
#[allow(dead_code)]
#[derive(Schema, PathParams)]
struct UserPath {
id: UserId,
}
#[allow(dead_code)]
#[derive(Schema, QueryParams)]
struct Page {
after: Option<u64>,
#[param(rename = "per_page")]
per: u32,
}
#[allow(dead_code)]
#[derive(HeaderParams)]
struct Conditional {
#[header(rename = "If-None-Match")]
if_none_match: Option<String>,
#[header(rename = "If-Modified-Since")]
if_modified_since: Option<String>,
}
#[allow(dead_code)]
#[derive(kynos::CookieParams)]
struct Preferences {
#[cookie(rename = "ui_theme")]
theme: String,
}
struct ApiVersion(u32);
impl<C: Sync> FromRequestParts<C> for ApiVersion {
type Rejection = kynos::error::rejection::HeaderRejection;
async fn from_request_parts(parts: &mut Parts, context: &C) -> Result<Self, Self::Rejection> {
let _ = context;
let invalid = |detail: &str| kynos::error::rejection::HeaderRejection::Invalid {
name: "X-Api-Version".to_owned(),
detail: detail.to_owned(),
};
let value = parts
.headers
.get("X-Api-Version")
.ok_or_else(|| invalid("the header is required"))?;
value
.to_str()
.map_err(|_| invalid("the value is not ASCII"))?
.parse()
.map(Self)
.map_err(|_| invalid("the value is not a version number"))
}
}
impl Describe for ApiVersion {
fn describe(operation: &mut OperationCx<'_>) {
let schema = <u32 as kynos::schema::Schema>::schema(operation.registry());
let mut parameter = Parameter::new("X-Api-Version", ParameterIn::Header, schema);
parameter.description = Some("The API version this client was written against".to_owned());
parameter.required = Some(true);
operation.add_parameter(parameter);
}
}
#[kynos::get("/users/{id}")]
#[allow(clippy::too_many_arguments)]
async fn get_user(
Path(path): Path<UserPath>,
Query(page): Query<Page>,
Headers(conditional): Headers<Conditional>,
Cookies(preferences): Cookies<Preferences>,
version: ApiVersion,
matched: MatchedPath,
peer: ConnectInfo,
) -> Json<User> {
println!("v{} from {} for {}", version.0, peer.0, matched.0);
let _ = (page, conditional, preferences);
Json(User {
id: path.id.0,
name: "Ada Lovelace".to_owned(),
})
}
#[tokio::main]
async fn main() -> kynos::Result<()> {
let router = Router::<()>::new().mount(kynos::routes![get_user]);
let document = router.openapi()?;
println!("{}", document.to_json()?);
Server::new(router.build(())?)
.bind((Ipv4Addr::UNSPECIFIED, 3000))
.serve()
.await
}