use forge_core::schema::RustType;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Position {
Arg,
Return,
}
pub fn ts_type(rust_type: &RustType, pos: Position) -> String {
match rust_type {
RustType::String
| RustType::Uuid
| RustType::DateTime
| RustType::Date
| RustType::Instant
| RustType::LocalDate
| RustType::LocalTime => "string".into(),
RustType::I32 | RustType::I64 | RustType::F32 | RustType::F64 => "number".into(),
RustType::Bool => "boolean".into(),
RustType::Json => "unknown".into(),
RustType::Upload => "File | Blob".into(),
RustType::Bytes => match pos {
Position::Return => "Blob".into(),
Position::Arg => "Uint8Array".into(),
},
RustType::Option(inner) => format!("{} | null", ts_type(inner, pos)),
RustType::Vec(inner) => format!("{}[]", ts_type(inner, pos)),
RustType::Custom(name) => ts_custom(name, pos),
}
}
fn ts_custom(name: &str, pos: Position) -> String {
match name {
"()" => "void".into(),
"Upload" => "File | Blob".into(),
"Bytes" => match pos {
Position::Return => "Blob".into(),
Position::Arg => "Uint8Array".into(),
},
_ if name.starts_with("Vec<") => {
let inner = name
.strip_prefix("Vec<")
.and_then(|s| s.strip_suffix('>'))
.unwrap_or("unknown");
format!("{}[]", ts_type(&RustType::Custom(inner.to_string()), pos))
}
_ if name.starts_with("HashMap<") || name.starts_with("std::collections::HashMap<") => {
ts_hashmap(name)
}
_ => name.to_string(),
}
}
fn ts_hashmap(name: &str) -> String {
let inner = name
.strip_prefix("HashMap<")
.or_else(|| name.strip_prefix("std::collections::HashMap<"))
.and_then(|s| s.strip_suffix('>'));
let Some(inner) = inner else {
return "Record<string, unknown>".into();
};
let mut parts = inner.splitn(2, ',').map(|s| s.trim());
let _key_type = parts.next();
if let Some(value) = parts.next() {
let value_type = match value {
"String" | "&str" | "str" => "string",
"i32" | "i64" | "u32" | "u64" | "f32" | "f64" => "number",
"bool" => "boolean",
other => other,
};
format!("Record<string, {}>", value_type)
} else {
"Record<string, unknown>".into()
}
}
pub fn dioxus_type(rust_type: &RustType) -> String {
match rust_type {
RustType::String | RustType::Uuid => "String".into(),
RustType::I32 => "i32".into(),
RustType::I64 => "i64".into(),
RustType::F32 => "f32".into(),
RustType::F64 => "f64".into(),
RustType::Bool => "bool".into(),
RustType::DateTime
| RustType::Date
| RustType::Instant
| RustType::LocalDate
| RustType::LocalTime => "String".into(),
RustType::Upload => "ForgeUpload".into(),
RustType::Json => "JsonValue".into(),
RustType::Bytes => "Vec<u8>".into(),
RustType::Option(inner) => format!("Option<{}>", dioxus_type(inner)),
RustType::Vec(inner) => format!("Vec<{}>", dioxus_type(inner)),
RustType::Custom(name) => dioxus_custom(name),
}
}
fn dioxus_custom(name: &str) -> String {
match name {
"Uuid" | "uuid::Uuid" => "String".into(),
"DateTime<Utc>" | "NaiveDate" | "NaiveDateTime" | "Instant" | "LocalDate" | "LocalTime"
| "Timestamp" => "String".into(),
"Value" | "serde_json::Value" => "JsonValue".into(),
"Bytes" => "Vec<u8>".into(),
"Upload" => "ForgeUpload".into(),
other => other.to_string(),
}
}
fn walk_type(rust_type: &RustType, predicate: &dyn Fn(&RustType) -> bool) -> bool {
if predicate(rust_type) {
return true;
}
match rust_type {
RustType::Option(inner) | RustType::Vec(inner) => walk_type(inner, predicate),
_ => false,
}
}
pub fn contains_upload(rust_type: &RustType) -> bool {
walk_type(rust_type, &|t| {
matches!(t, RustType::Upload) || matches!(t, RustType::Custom(n) if n == "Upload")
})
}
pub fn contains_json(rust_type: &RustType) -> bool {
walk_type(rust_type, &|t| {
matches!(t, RustType::Json)
|| matches!(t, RustType::Custom(n) if n == "Value" || n == "serde_json::Value")
})
}
pub fn collect_type_imports(rust_type: &RustType, imports: &mut Vec<String>) {
match rust_type {
RustType::Custom(name) if is_importable_type(name) => {
imports.push(name.clone());
}
RustType::Option(inner) | RustType::Vec(inner) => collect_type_imports(inner, imports),
_ => {}
}
}
fn is_importable_type(name: &str) -> bool {
!matches!(
name,
"()" | "Upload" | "Bytes" | "Instant" | "LocalDate" | "LocalTime"
) && !name.starts_with("Vec<")
&& !name.starts_with("HashMap<")
&& !name.starts_with("std::collections::")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ts_primitives() {
assert_eq!(ts_type(&RustType::String, Position::Arg), "string");
assert_eq!(ts_type(&RustType::I32, Position::Arg), "number");
assert_eq!(ts_type(&RustType::Bool, Position::Arg), "boolean");
assert_eq!(ts_type(&RustType::Uuid, Position::Arg), "string");
assert_eq!(ts_type(&RustType::Json, Position::Arg), "unknown");
}
#[test]
fn ts_temporal_types() {
for ty in [
RustType::DateTime,
RustType::Date,
RustType::Instant,
RustType::LocalDate,
RustType::LocalTime,
] {
assert_eq!(ts_type(&ty, Position::Arg), "string");
}
}
#[test]
fn ts_bytes_depends_on_position() {
assert_eq!(ts_type(&RustType::Bytes, Position::Arg), "Uint8Array");
assert_eq!(ts_type(&RustType::Bytes, Position::Return), "Blob");
}
#[test]
fn ts_option_and_vec() {
assert_eq!(
ts_type(&RustType::Option(Box::new(RustType::String)), Position::Arg),
"string | null"
);
assert_eq!(
ts_type(&RustType::Vec(Box::new(RustType::I32)), Position::Arg),
"number[]"
);
}
#[test]
fn ts_custom_types() {
assert_eq!(
ts_type(&RustType::Custom("()".into()), Position::Arg),
"void"
);
assert_eq!(
ts_type(&RustType::Custom("User".into()), Position::Arg),
"User"
);
assert_eq!(
ts_type(&RustType::Custom("Upload".into()), Position::Arg),
"File | Blob"
);
}
#[test]
fn ts_hashmap() {
assert_eq!(
ts_type(
&RustType::Custom("HashMap<String, i32>".into()),
Position::Arg
),
"Record<string, number>"
);
assert_eq!(
ts_type(
&RustType::Custom("HashMap<String, User>".into()),
Position::Arg
),
"Record<string, User>"
);
}
#[test]
fn dioxus_primitives() {
assert_eq!(dioxus_type(&RustType::String), "String");
assert_eq!(dioxus_type(&RustType::I32), "i32");
assert_eq!(dioxus_type(&RustType::Bool), "bool");
assert_eq!(dioxus_type(&RustType::Upload), "ForgeUpload");
assert_eq!(dioxus_type(&RustType::Json), "JsonValue");
assert_eq!(dioxus_type(&RustType::Bytes), "Vec<u8>");
}
#[test]
fn dioxus_custom_known_aliases() {
assert_eq!(dioxus_type(&RustType::Custom("Timestamp".into())), "String");
assert_eq!(dioxus_type(&RustType::Custom("Value".into())), "JsonValue");
assert_eq!(
dioxus_type(&RustType::Custom("Upload".into())),
"ForgeUpload"
);
}
#[test]
fn upload_detection() {
assert!(contains_upload(&RustType::Upload));
assert!(contains_upload(&RustType::Option(Box::new(
RustType::Upload
))));
assert!(contains_upload(&RustType::Vec(Box::new(RustType::Upload))));
assert!(contains_upload(&RustType::Custom("Upload".into())));
assert!(!contains_upload(&RustType::String));
assert!(!contains_upload(&RustType::Custom("User".into())));
}
#[test]
fn json_detection() {
assert!(contains_json(&RustType::Json));
assert!(contains_json(&RustType::Option(Box::new(RustType::Json))));
assert!(contains_json(&RustType::Custom("Value".into())));
assert!(!contains_json(&RustType::String));
}
#[test]
fn type_imports() {
let mut imports = Vec::new();
collect_type_imports(&RustType::Custom("User".into()), &mut imports);
collect_type_imports(&RustType::Custom("()".into()), &mut imports);
collect_type_imports(&RustType::Custom("Upload".into()), &mut imports);
collect_type_imports(
&RustType::Vec(Box::new(RustType::Custom("Project".into()))),
&mut imports,
);
assert_eq!(imports, vec!["User", "Project"]);
}
}