TS_RS Rust and TypeScript types should now be in parity

This commit is contained in:
Natty 2023-11-03 21:14:17 +01:00
parent c8627a996c
commit a5ab2acca0
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
16 changed files with 116 additions and 92 deletions

View File

@ -1,12 +1,16 @@
import {FrontendApiEndpoint, FrontendApiEndpoints} from "./fe-api"; import { GetNoteById } from "./types/endpoints/GetNoteById";
import {GetNoteById} from "./types/endpoints/GetNoteById";
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
export interface BackendApiEndpoint<M extends Method, T, R> { export interface BackendApiEndpoint<
M extends Method,
PP extends string[],
T,
R
> {
method: M; method: M;
endpoint: string; endpoint: string;
pathParams: [string], pathParams: PP;
request?: T; request?: T;
response?: R; response?: R;
} }
@ -18,21 +22,22 @@ function nestedUrlSearchParams(data: any, topLevel: boolean = true): string {
case "boolean": case "boolean":
case "number": case "number":
case "symbol": case "symbol":
if (topLevel) if (topLevel) return encodeURIComponent(data.toString()) + "=";
return encodeURIComponent(data.toString()) + "=";
return data.toString(); return data.toString();
case "object": case "object":
if (data === null) if (data === null) return "null";
return "null";
if (Array.isArray(data)) if (Array.isArray(data))
return data.map(d => nestedUrlSearchParams(d, true)) return data
.map((d) => nestedUrlSearchParams(d, true))
.map(encodeURIComponent) .map(encodeURIComponent)
.join("&"); .join("&");
const inner = Object.entries(data) const inner = Object.entries(data).map(([k, v]) => [
.map(([k, v]) => [k, nestedUrlSearchParams(v, false)]); k,
nestedUrlSearchParams(v, false),
]);
return new URLSearchParams(inner).toString(); return new URLSearchParams(inner).toString();
@ -45,8 +50,8 @@ type MagApiErrorCode = "Client:GenericApiError" | string;
export interface MagApiError { export interface MagApiError {
status: number; status: number;
code: MagApiErrorCode, code: MagApiErrorCode;
message: string, message: string;
} }
export class MagApiClient { export class MagApiClient {
@ -56,11 +61,21 @@ export class MagApiClient {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
} }
async call<M extends Method, T extends BackendApiEndpoint<M, T["request"], T["response"]>>( async call<
endpoint: T["endpoint"], T extends BackendApiEndpoint<
method: M, T["method"] & Method,
T["pathParams"] & string[],
T["request"],
T["response"]
>
>(
{ endpoint, method }: T,
data: T["request"], data: T["request"],
pathParams: Record<string, string>, pathParams: {
[K in keyof T["pathParams"] as T["pathParams"][K] & string]:
| string
| number;
},
token?: string | null | undefined token?: string | null | undefined
): Promise<T["response"]> { ): Promise<T["response"]> {
type Response = T["response"]; type Response = T["response"];
@ -70,6 +85,10 @@ export class MagApiClient {
? `Bearer ${authorizationToken}` ? `Bearer ${authorizationToken}`
: undefined; : undefined;
for (const name in pathParams) {
endpoint = endpoint.replace(`:${name}`, `${pathParams[name]}`);
}
let url = `${this.baseUrl}/${endpoint}`; let url = `${this.baseUrl}/${endpoint}`;
if (method === "GET") { if (method === "GET") {
@ -84,7 +103,7 @@ export class MagApiClient {
body: method !== "GET" ? JSON.stringify(data) : undefined, body: method !== "GET" ? JSON.stringify(data) : undefined,
credentials: "omit", credentials: "omit",
cache: "no-cache", cache: "no-cache",
headers: authorization ? {authorization} : {}, headers: authorization ? { authorization } : {},
}) })
.then(async (res) => { .then(async (res) => {
const body = res.status === 204 ? null : await res.json(); const body = res.status === 204 ? null : await res.json();
@ -98,15 +117,14 @@ export class MagApiClient {
} }
}) })
.catch((e) => { .catch((e) => {
throw ({ throw {
status: -1, status: -1,
code: "Client:GenericApiError", code: "Client:GenericApiError",
message: e message: e,
}) as MagApiError; } as MagApiError;
}); });
} }
} }
const a = new MagApiClient("https://aaa"); const a = new MagApiClient("https://aaa");
a.call<"GET", GetNoteById>("", "",{}, {}) const result = await a.call(GetNoteById, { attachments: true }, { id: "aaaa" });

View File

@ -1,4 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NoteListFilter } from "./NoteListFilter"; import type { NoteListFilter } from "./NoteListFilter";
export interface GetTimelineReq { limit: bigint, filter: NoteListFilter | null, } export interface GetTimelineReq { limit?: bigint, filter?: NoteListFilter, }

View File

@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface NoteByIdReq { context: boolean, attachments: boolean, } export interface NoteByIdReq { context?: boolean, attachments?: boolean, }

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackNoteMaybeAttachments } from "./packed/PackNoteMaybeAttachments";
export interface NoteDetailExt { parent_note: PackNoteMaybeAttachments | null, renoted_note: PackNoteMaybeAttachments | null, }

View File

@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface UserByIdReq { profile: boolean, pins: boolean, detail: boolean, relation: boolean, auth: boolean, } export interface UserByIdReq { profile?: boolean, pins?: boolean, detail?: boolean, relation?: boolean, auth?: boolean, }

View File

@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface UserSelfReq { profile: boolean, pins: boolean, detail: boolean, secrets: boolean, } export interface UserSelfReq { profile?: boolean, pins?: boolean, detail?: boolean, secrets?: boolean, }

View File

@ -2,10 +2,11 @@
import type { NoteByIdReq } from "../NoteByIdReq"; import type { NoteByIdReq } from "../NoteByIdReq";
import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull"; import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull";
interface GetNoteById { export const GetNoteById = {
endpoint: "/notes/:id", endpoint: "/notes/:id",
pathParams: ["id"], pathParams: ["id"] as ["id"],
method: "GET", method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: NoteByIdReq, request: undefined as unknown as NoteByIdReq,
response: PackNoteMaybeFull response: undefined as unknown as PackNoteMaybeFull
} }

View File

@ -2,11 +2,10 @@
import type { GetTimelineReq } from "../GetTimelineReq"; import type { GetTimelineReq } from "../GetTimelineReq";
import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull"; import type { PackNoteMaybeFull } from "../packed/PackNoteMaybeFull";
export interface GetTimeline { export const GetTimeline = {
endpoint: "/timeline"; endpoint: "/timeline",
pathParams: []; pathParams: [] as [],
method: "GET"; method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: GetTimelineReq; request: undefined as unknown as GetTimelineReq,
response: Array<PackNoteMaybeFull>; response: undefined as unknown as Array<PackNoteMaybeFull>,
} };

View File

@ -2,11 +2,11 @@
import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll"; import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
import type { UserByIdReq } from "../UserByIdReq"; import type { UserByIdReq } from "../UserByIdReq";
export interface GetUserByAcct { export const GetUserByAcct = {
endpoint: "/users/by-acct/:user_id"; endpoint: "/users/by-acct/:user_id",
pathParams: ["user_id"]; pathParams: ["user_id"] as ["user_id"],
method: "GET"; method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: UserByIdReq; request: undefined as unknown as UserByIdReq,
response: PackUserMaybeAll; response: undefined as unknown as PackUserMaybeAll
} }

View File

@ -2,11 +2,11 @@
import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll"; import type { PackUserMaybeAll } from "../packed/PackUserMaybeAll";
import type { UserByIdReq } from "../UserByIdReq"; import type { UserByIdReq } from "../UserByIdReq";
export interface GetUserById { export const GetUserById = {
endpoint: "/users/:user_id"; endpoint: "/users/:user_id",
pathParams: ["user_id"]; pathParams: ["user_id"] as ["user_id"],
method: "GET"; method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: UserByIdReq; request: undefined as unknown as UserByIdReq,
response: PackUserMaybeAll; response: undefined as unknown as PackUserMaybeAll
} }

View File

@ -2,11 +2,11 @@
import type { PackUserSelfMaybeAll } from "../packed/PackUserSelfMaybeAll"; import type { PackUserSelfMaybeAll } from "../packed/PackUserSelfMaybeAll";
import type { UserSelfReq } from "../UserSelfReq"; import type { UserSelfReq } from "../UserSelfReq";
export interface GetUserSelf { export const GetUserSelf = {
endpoint: "/users/@self"; endpoint: "/users/@self",
pathParams: []; pathParams: [] as [],
method: "GET"; method: "GET" as "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
request: UserSelfReq; request: undefined as unknown as UserSelfReq,
response: PackUserSelfMaybeAll; response: undefined as unknown as PackUserSelfMaybeAll
} }

View File

@ -356,17 +356,18 @@ pub fn derive_endpoint(item: TokenStream) -> TokenStream {
fn decl() -> String { fn decl() -> String {
format!( format!(
"interface {} {{\n \ "const {} = {{\n \
endpoint: \"{}\";\n \ endpoint: \"{}\",\n \
pathParams: {};\n \ pathParams: {} as {},\n \
method: \"{}\";\n \ method: \"{}\" as \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\",\n \
request: {};\n \ request: undefined as unknown as {},\n \
response: {};\n\ response: undefined as unknown as {}\n\
}} }}
", ",
Self::name(), Self::name(),
Self::ENDPOINT, Self::ENDPOINT,
#path_params, #path_params,
#path_params,
Self::METHOD, Self::METHOD,
#call_name_req, #call_name_req,
#call_name_res #call_name_res

View File

@ -9,10 +9,10 @@ use ts_rs::TS;
#[derive(Serialize, Deserialize, TS)] #[derive(Serialize, Deserialize, TS)]
#[ts(export)] #[ts(export)]
pub struct NoteByIdReq { pub struct NoteByIdReq {
#[serde(default)] #[ts(optional)]
pub context: bool, pub context: Option<bool>,
#[serde(default)] #[ts(optional)]
pub attachments: bool, pub attachments: Option<bool>,
} }
#[derive(Endpoint)] #[derive(Endpoint)]

View File

@ -10,13 +10,14 @@ use ts_rs::TS;
#[derive(Serialize, Deserialize, TS)] #[derive(Serialize, Deserialize, TS)]
#[ts(export)] #[ts(export)]
pub struct GetTimelineReq { pub struct GetTimelineReq {
#[serde(default = "default_timeline_limit")] #[ts(optional)]
pub limit: U64Range<1, 100>, pub limit: Option<U64Range<1, 100>>,
#[ts(optional)]
pub filter: Option<NoteListFilter>, pub filter: Option<NoteListFilter>,
} }
fn default_timeline_limit<const MIN: u64, const MAX: u64>() -> U64Range<MIN, MAX> { pub fn default_timeline_limit<const MIN: u64, const MAX: u64>() -> U64Range<MIN, MAX> {
15.try_into().unwrap() 30.try_into().unwrap()
} }
#[derive(Endpoint)] #[derive(Endpoint)]

View File

@ -10,14 +10,14 @@ use crate::types::user::{PackUserMaybeAll, PackUserSelfMaybeAll};
#[derive(Serialize, Deserialize, TS)] #[derive(Serialize, Deserialize, TS)]
#[ts(export)] #[ts(export)]
pub struct UserSelfReq { pub struct UserSelfReq {
#[serde(default)] #[ts(optional)]
pub profile: bool, pub profile: Option<bool>,
#[serde(default)] #[ts(optional)]
pub pins: bool, pub pins: Option<bool>,
#[serde(default)] #[ts(optional)]
pub detail: bool, pub detail: Option<bool>,
#[serde(default)] #[ts(optional)]
pub secrets: bool, pub secrets: Option<bool>,
} }
#[derive(Endpoint)] #[derive(Endpoint)]
@ -33,16 +33,16 @@ pub struct GetUserSelf;
#[derive(Serialize, Deserialize, TS)] #[derive(Serialize, Deserialize, TS)]
#[ts(export)] #[ts(export)]
pub struct UserByIdReq { pub struct UserByIdReq {
#[serde(default)] #[ts(optional)]
pub profile: bool, pub profile: Option<bool>,
#[serde(default)] #[ts(optional)]
pub pins: bool, pub pins: Option<bool>,
#[serde(default)] #[ts(optional)]
pub detail: bool, pub detail: Option<bool>,
#[serde(default)] #[ts(optional)]
pub relation: bool, pub relation: Option<bool>,
#[serde(default)] #[ts(optional)]
pub auth: bool, pub auth: Option<bool>,
} }
#[derive(Endpoint)] #[derive(Endpoint)]

View File

@ -22,8 +22,8 @@ pub async fn handle_note(
) -> Result<Json<Res<GetNoteById>>, ApiError> { ) -> Result<Json<Res<GetNoteById>>, ApiError> {
let ctx = PackingContext::new(service, self_user.clone()).await?; let ctx = PackingContext::new(service, self_user.clone()).await?;
let note = NoteModel { let note = NoteModel {
attachments, attachments: attachments.unwrap_or_default(),
with_context: context, with_context: context.unwrap_or_default(),
} }
.fetch_single(&ctx, self_user.as_deref(), &id) .fetch_single(&ctx, self_user.as_deref(), &id)
.await? .await?