use super::types::{Admin, AdminEntry, AdminField, FieldType};
pub(crate) fn build_typescript(admin: &Admin) -> String {
let app_name = &admin.branding().app_name;
let mut out = String::with_capacity(1024);
out.push_str(&format!(
"// Auto-generated by rustio-admin for {app_name}.\n\
// Edit at your peril — re-run `curl /admin/apis/sdk.ts` to refresh.\n\
//\n\
// One `export interface` per registered project model.\n\
// Wrap your own fetch around the JSON envelopes documented at\n\
// /admin/apis/openapi.json.\n\n"
));
let mut emitted = 0usize;
for entry in admin.entries() {
if entry.core {
continue;
}
emit_interface(&mut out, entry);
emitted += 1;
}
if emitted == 0 {
out.push_str("// No project models registered.\n");
}
out
}
fn emit_interface(out: &mut String, entry: &AdminEntry) {
out.push_str(&format!(
"/** {} (`{}`). */\n",
entry.singular_name, entry.admin_name
));
out.push_str(&format!("export interface {} {{\n", entry.singular_name));
for f in entry.fields {
out.push_str(&format!(" {}: {};\n", f.name, ts_type_for(f)));
}
out.push_str("}\n\n");
}
fn ts_type_for(field: &AdminField) -> String {
let base: &str = match field.field_type {
FieldType::Bool => "boolean",
FieldType::I32
| FieldType::I64
| FieldType::OptionalI64
| FieldType::String
| FieldType::OptionalString
| FieldType::DateTime
| FieldType::OptionalDateTime
| FieldType::FilePath
| FieldType::OptionalFilePath => match field.field_type {
FieldType::I32 | FieldType::I64 | FieldType::OptionalI64 => "number",
_ => "string",
},
};
if field.field_type.nullable() {
format!("{base} | null")
} else {
base.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn f(name: &'static str, field_type: FieldType) -> AdminField {
AdminField {
name,
label: name,
field_type,
editable: true,
relation: None,
choices: None,
}
}
#[test]
fn ts_type_for_maps_each_field_type() {
assert_eq!(ts_type_for(&f("active", FieldType::Bool)), "boolean");
assert_eq!(ts_type_for(&f("n", FieldType::I32)), "number");
assert_eq!(ts_type_for(&f("n", FieldType::I64)), "number");
assert_eq!(
ts_type_for(&f("n", FieldType::OptionalI64)),
"number | null"
);
assert_eq!(ts_type_for(&f("s", FieldType::String)), "string");
assert_eq!(
ts_type_for(&f("s", FieldType::OptionalString)),
"string | null"
);
assert_eq!(ts_type_for(&f("d", FieldType::DateTime)), "string");
assert_eq!(
ts_type_for(&f("d", FieldType::OptionalDateTime)),
"string | null"
);
assert_eq!(ts_type_for(&f("p", FieldType::FilePath)), "string");
assert_eq!(
ts_type_for(&f("p", FieldType::OptionalFilePath)),
"string | null"
);
}
#[test]
fn empty_admin_emits_header_and_no_interfaces() {
let ts = build_typescript(&Admin::new());
assert!(
ts.starts_with("// Auto-generated by rustio-admin for"),
"missing header: {ts}"
);
assert!(
!ts.contains("export interface User"),
"core User must not leak into the SDK"
);
assert!(
ts.contains("// No project models registered.\n"),
"expected empty-marker line: {ts}"
);
}
#[test]
fn header_carries_branded_app_name() {
let admin = Admin::new().app_name("Acme Clinic");
let ts = build_typescript(&admin);
assert!(
ts.contains("rustio-admin for Acme Clinic"),
"branded app name must appear in the header"
);
}
}