use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use umbral::migrate::ModelMeta;
use umbral_rest::{PaginationField, PaginationScalar, PaginationSchema, PaginationStyle};
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "pa_post")]
pub struct PaPost {
pub id: i64,
pub title: String,
}
fn gen_client(
style: PaginationStyle,
schema: Option<PaginationSchema>,
schemes: &[(String, Value)],
) -> umbral_openapi::client_gen::GeneratedClient {
umbral_openapi::client_gen::generate_with(
&[ModelMeta::for_::<PaPost>()],
"/api",
style,
schema,
schemes,
&[],
)
}
fn dts(style: PaginationStyle, schema: Option<PaginationSchema>) -> String {
gen_client(style, schema, &[]).dts
}
fn js_with(style: PaginationStyle, schemes: &[(String, Value)]) -> String {
gen_client(style, None, schemes).js
}
#[track_caller]
fn assert_has(haystack: &str, needle: &str) {
assert!(
haystack.contains(needle),
"expected to find:\n {needle}\nin:\n{haystack}",
);
}
#[track_caller]
fn assert_absent(haystack: &str, needle: &str) {
assert!(
!haystack.contains(needle),
"expected NOT to find:\n {needle}\nin:\n{haystack}",
);
}
fn cursor_schema() -> PaginationSchema {
PaginationSchema {
envelope: vec![
PaginationField::nullable("next_cursor", PaginationScalar::String),
PaginationField::nullable("prev_cursor", PaginationScalar::String),
PaginationField::new("has_more", PaginationScalar::Boolean),
],
params: vec![
PaginationField::new("cursor", PaginationScalar::String),
PaginationField::new("page_size", PaginationScalar::Number),
],
}
}
#[test]
fn page_number_envelope_and_builder_methods() {
let d = dts(PaginationStyle::PageNumber, None);
for field in [
"total_pages: number;",
"current_page: number;",
"page_size: number;",
"next: number | null;",
] {
assert_has(&d, field);
}
assert_has(&d, "page(v: number): this;");
assert_has(&d, "pageSize(v: number): this;");
let j = js_with(PaginationStyle::PageNumber, &[]);
assert_has(
&j,
r#"page(v) { this.params.set("page", String(v)); return this; }"#,
);
assert_has(
&j,
r#"pageSize(v) { this.params.set("page_size", String(v)); return this; }"#,
);
}
#[test]
fn limit_offset_envelope_and_builder_methods() {
let d = dts(PaginationStyle::LimitOffset, None);
assert_has(&d, " limit: number;");
assert_has(&d, " offset: number;");
assert_has(&d, "limit(v: number): this;");
assert_has(&d, "offset(v: number): this;");
let j = js_with(PaginationStyle::LimitOffset, &[]);
assert_has(
&j,
r#"limit(v) { this.params.set("limit", String(v)); return this; }"#,
);
assert_has(
&j,
r#"offset(v) { this.params.set("offset", String(v)); return this; }"#,
);
}
#[test]
fn custom_with_schema_is_fully_typed() {
let d = dts(PaginationStyle::Custom, Some(cursor_schema()));
assert_has(&d, " results: T[];");
assert_has(&d, " next_cursor: string | null;");
assert_has(&d, " prev_cursor: string | null;");
assert_has(&d, " has_more: boolean;");
assert_absent(&d, "[key: string]: unknown;");
assert_has(&d, "cursor(v: string): this;");
assert_has(&d, "pageSize(v: number): this;");
let c = gen_client(PaginationStyle::Custom, Some(cursor_schema()), &[]);
assert_has(
&c.js,
r#"cursor(v) { this.params.set("cursor", String(v)); return this; }"#,
);
assert_has(
&c.js,
r#"pageSize(v) { this.params.set("page_size", String(v)); return this; }"#,
);
}
#[test]
fn custom_without_schema_is_permissive() {
let d = dts(PaginationStyle::Custom, None);
assert_has(&d, " results?: T[];");
assert_has(&d, " [key: string]: unknown;");
assert_absent(&d, "total_pages");
assert_absent(&d, "current_page");
}
#[test]
fn generic_param_escape_hatch_is_always_present() {
for style in [
PaginationStyle::None,
PaginationStyle::PageNumber,
PaginationStyle::Custom,
] {
let c = gen_client(style, None, &[]);
assert_has(
&c.dts,
"param(key: string, value: string | number | boolean): this;",
);
assert_has(&c.js, "param(key, value) {");
}
}
fn scheme(name: &str, value: Value) -> Vec<(String, Value)> {
vec![(name.to_string(), value)]
}
#[test]
fn bearer_scheme_yields_bearer_prefix() {
let j = js_with(
PaginationStyle::None,
&scheme("bearerAuth", json!({ "type": "http", "scheme": "bearer" })),
);
assert_has(&j, r#"this.opts.tokenPrefix ?? "Bearer""#);
}
#[test]
fn token_scheme_yields_token_prefix_not_bearer() {
let j = js_with(
PaginationStyle::None,
&scheme("tokenAuth", json!({ "type": "http", "scheme": "token" })),
);
assert_has(&j, r#"this.opts.tokenPrefix ?? "Token""#);
assert_absent(&j, r#"this.opts.tokenPrefix ?? "Bearer""#);
}
#[test]
fn api_key_header_comes_from_the_scheme() {
let j = js_with(
PaginationStyle::None,
&scheme(
"apiKeyAuth",
json!({ "type": "apiKey", "in": "header", "name": "X-Umbral-Api-Key" }),
),
);
assert_has(&j, r#"this.opts.apiKeyHeader ?? "X-Umbral-Api-Key""#);
assert_absent(&j, r#"this.opts.apiKeyHeader ?? "X-API-Key""#);
}
#[test]
fn cookie_scheme_sends_credentials() {
let j = js_with(
PaginationStyle::None,
&scheme(
"sessionAuth",
json!({ "type": "apiKey", "in": "cookie", "name": "sessionid" }),
),
);
assert_has(&j, r#"this.opts.credentials ?? "include""#);
}
#[test]
fn no_scheme_falls_back_to_generic_defaults() {
let j = js_with(PaginationStyle::None, &[]);
assert_has(&j, r#"this.opts.tokenPrefix ?? "Bearer""#);
assert_has(&j, r#"this.opts.apiKeyHeader ?? "X-API-Key""#);
assert_has(&j, "this.opts.credentials ?? undefined");
}
#[test]
fn dynamic_get_auth_headers_is_always_present() {
let c = gen_client(PaginationStyle::None, None, &[]);
assert_has(
&c.dts,
"getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;",
);
assert_has(
&c.js,
"Object.assign(headers, await this.opts.getAuthHeaders());",
);
}