feat: generate typed frontend API contracts
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
export class ApiError extends Error {
|
||||
constructor(status, message) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
function isRecord(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
export function isRigSnapshot(value) {
|
||||
if (!isRecord(value) || !isRecord(value.info) || !isRecord(value.status)) {
|
||||
return false;
|
||||
}
|
||||
const { info, status } = value;
|
||||
return typeof value.initialized === "boolean" && typeof info.manufacturer === "string" && typeof info.model === "string" && isRecord(status.freq) && typeof status.freq.hz === "number" && (typeof status.mode === "string" || isRecord(status.mode)) && typeof status.tx_en === "boolean";
|
||||
}
|
||||
export function isRigListResponse(value) {
|
||||
return isRecord(value) && (value.active_remote === null || typeof value.active_remote === "string") && Array.isArray(value.rigs) && value.rigs.every(
|
||||
(rig) => isRecord(rig) && typeof rig.remote === "string" && typeof rig.manufacturer === "string" && typeof rig.model === "string" && Array.isArray(rig.supported_modes) && typeof rig.tx === "boolean" && typeof rig.filter_controls === "boolean" && typeof rig.initialized === "boolean"
|
||||
);
|
||||
}
|
||||
export function isDecoderRegistry(value) {
|
||||
return Array.isArray(value) && value.every(
|
||||
(decoder) => isRecord(decoder) && typeof decoder.id === "string" && typeof decoder.label === "string" && (decoder.activation === "mode_bound" || decoder.activation === "toggle") && Array.isArray(decoder.active_modes) && decoder.active_modes.every((mode) => typeof mode === "string") && typeof decoder.background_decode === "boolean" && typeof decoder.bookmark_selectable === "boolean"
|
||||
);
|
||||
}
|
||||
export class TrxApi {
|
||||
constructor(baseUrl = "") {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
async get(path, validate) {
|
||||
return this.request(path, { cache: "no-store" }, validate);
|
||||
}
|
||||
async post(path, body, validate) {
|
||||
return this.request(
|
||||
path,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
},
|
||||
validate
|
||||
);
|
||||
}
|
||||
async request(path, init, validate) {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, init);
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new ApiError(response.status, detail || response.statusText);
|
||||
}
|
||||
const value = await response.json();
|
||||
if (!validate(value)) {
|
||||
throw new ApiError(response.status, `Malformed response from ${path}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
export function decodeServerEvent(event, validate) {
|
||||
let value;
|
||||
try {
|
||||
value = JSON.parse(event.data);
|
||||
} catch (error) {
|
||||
throw new TypeError("Server event is not valid JSON", { cause: error });
|
||||
}
|
||||
if (!validate(value)) {
|
||||
throw new TypeError("Server event has an unexpected shape");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user