use convert_case::{Case, Casing};
use crate::builtin::serde::{TypeCategory, get_foreign_types, split_top_level_union};
use crate::ts_syn::abi::DecoratorIR;
use crate::ts_syn::abi::ir::type_registry::{
ResolvedTypeRef, TypeDefinitionIR, TypeRegistry, TypeRegistryEntry,
};
#[derive(Default, Clone)]
pub struct CompareFieldOptions {
pub skip: bool,
}
impl CompareFieldOptions {
pub fn from_decorators(decorators: &[DecoratorIR], attr_name: &str) -> Self {
let mut opts = Self::default();
for decorator in decorators {
if !decorator.name.eq_ignore_ascii_case(attr_name) {
continue;
}
let args = decorator.args_src.trim();
if has_flag(args, "skip") {
opts.skip = true;
}
}
opts
}
}
#[derive(Default, Clone)]
pub struct DefaultFieldOptions {
pub value: Option<String>,
pub has_default: bool,
}
impl DefaultFieldOptions {
pub fn from_decorators(decorators: &[DecoratorIR]) -> Self {
let mut opts = Self::default();
for decorator in decorators {
if !decorator.name.eq_ignore_ascii_case("default") {
continue;
}
opts.has_default = true;
let args = decorator.args_src.trim();
if let Some(value) = extract_default_value(args) {
opts.value = Some(value);
} else if !args.is_empty() {
opts.value = Some(args.to_string());
}
}
opts
}
}
fn extract_default_value(args: &str) -> Option<String> {
if let Some(value) = extract_named_string(args, "value") {
return Some(value);
}
if let Some(value) = parse_string_literal(args) {
return Some(format!("\"{}\"", value));
}
None
}
pub fn is_primitive_type(ts_type: &str) -> bool {
matches!(
ts_type.trim(),
"string" | "number" | "boolean" | "bigint" | "null" | "undefined"
)
}
pub fn is_numeric_type(ts_type: &str) -> bool {
matches!(ts_type.trim(), "number" | "bigint")
}
pub fn is_nullable_type(ts_type: &str) -> bool {
let normalized = ts_type.replace(' ', "");
normalized.contains("|null") || normalized.contains("|undefined")
}
pub fn is_generic_type(type_name: &str) -> bool {
type_name.contains('<') && type_name.contains('>')
}
pub fn parse_generic_type(type_name: &str) -> Option<(&str, &str)> {
let open = type_name.find('<')?;
let close = type_name.rfind('>')?;
if open < close {
let base = &type_name[..open];
let args = &type_name[open + 1..close];
Some((base.trim(), args.trim()))
} else {
None
}
}
pub fn has_known_default(_ts_type: &str) -> bool {
true
}
pub fn get_type_default(ts_type: &str) -> String {
let t = ts_type.trim();
let foreign_types = get_foreign_types();
let ft_match = TypeCategory::match_foreign_type(t, &foreign_types);
if let Some(ft) = ft_match.config
&& let Some(ref default_expr) = ft.default_expr
{
let rewritten = crate::builtin::serde::rewrite_expression_namespaces(default_expr);
return format!("({})()", rewritten);
}
if is_nullable_type(t) {
return "null".to_string();
}
if t.starts_with('{') {
return "{}".to_string();
}
if let Some(parts) = split_top_level_union(t) {
for part in &parts {
if is_primitive_type(part) {
return get_type_default(part);
}
}
for part in &parts {
let p = part.trim();
if (p.starts_with('"') && p.ends_with('"'))
|| (p.starts_with('\'') && p.ends_with('\''))
|| (p.starts_with('`') && p.ends_with('`'))
|| p.parse::<f64>().is_ok()
|| matches!(p, "true" | "false")
{
return get_type_default(p);
}
}
return get_type_default(parts[0]);
}
match t {
"string" => r#""""#.to_string(),
"number" => "0".to_string(),
"boolean" => "false".to_string(),
"bigint" => "0n".to_string(),
t if t.ends_with("[]") => "[]".to_string(),
t if t.starts_with("Array<") => "[]".to_string(),
t if t.starts_with("Map<") => "new Map()".to_string(),
t if t.starts_with("Set<") => "new Set()".to_string(),
"Date" => "new Date()".to_string(),
t if is_generic_type(t) => {
if let Some((base, args)) = parse_generic_type(t) {
format!("{}DefaultValue<{}>()", base.to_case(Case::Camel), args)
} else {
format_default_call(t)
}
}
t if (t.starts_with('"') && t.ends_with('"'))
|| (t.starts_with('\'') && t.ends_with('\''))
|| (t.starts_with('`') && t.ends_with('`')) =>
{
t.to_string()
}
t if t.parse::<f64>().is_ok() => t.to_string(),
"true" | "false" => t.to_string(),
type_name => format_default_call(type_name),
}
}
fn format_default_call(type_name: &str) -> String {
format!("{}DefaultValue()", type_name.to_case(Case::Camel))
}
pub fn has_flag(args: &str, flag: &str) -> bool {
if flag_explicit_false(args, flag) {
return false;
}
args.split(|c: char| !c.is_alphanumeric() && c != '_')
.any(|token| token.eq_ignore_ascii_case(flag))
}
fn flag_explicit_false(args: &str, flag: &str) -> bool {
let lower = args.to_ascii_lowercase();
let condensed: String = lower.chars().filter(|c| !c.is_whitespace()).collect();
condensed.contains(&format!("{flag}:false")) || condensed.contains(&format!("{flag}=false"))
}
pub fn extract_named_string(args: &str, name: &str) -> Option<String> {
let lower = args.to_ascii_lowercase();
let idx = lower.find(name)?;
let remainder = &args[idx + name.len()..];
let remainder = remainder.trim_start();
if remainder.starts_with(':') || remainder.starts_with('=') {
let value = remainder[1..].trim_start();
return parse_string_literal(value);
}
if remainder.starts_with('(')
&& let Some(close) = remainder.rfind(')')
{
let inner = remainder[1..close].trim();
return parse_string_literal(inner);
}
None
}
fn parse_string_literal(input: &str) -> Option<String> {
let trimmed = input.trim();
let mut chars = trimmed.chars();
let quote = chars.next()?;
if quote != '"' && quote != '\'' {
return None;
}
let mut escaped = false;
let mut buf = String::new();
for c in chars {
if escaped {
buf.push(c);
escaped = false;
continue;
}
if c == '\\' {
escaped = true;
continue;
}
if c == quote {
return Some(buf);
}
buf.push(c);
}
None
}
pub fn type_has_derive(registry: &TypeRegistry, type_name: &str, derive_name: &str) -> bool {
let has_derive = |entry: &TypeRegistryEntry| {
let decorators = match &entry.definition {
TypeDefinitionIR::Class(c) => &c.decorators,
TypeDefinitionIR::Interface(i) => &i.decorators,
TypeDefinitionIR::Enum(e) => &e.decorators,
TypeDefinitionIR::TypeAlias(t) => &t.decorators,
};
decorators.iter().any(|d| {
d.name.eq_ignore_ascii_case("derive")
&& d.args_src
.split(',')
.any(|arg| arg.trim().eq_ignore_ascii_case(derive_name))
})
};
if let Some(entry) = registry.get(type_name)
&& has_derive(entry)
{
return true;
}
if registry.ambiguous_names.iter().any(|n| n == type_name) {
return registry
.qualified_types
.values()
.any(|entry| entry.name == type_name && has_derive(entry));
}
false
}
#[allow(dead_code)]
pub fn resolved_type_has_derive(
registry: &TypeRegistry,
resolved: &ResolvedTypeRef,
derive_name: &str,
) -> bool {
type_has_derive(registry, &resolved.base_type_name, derive_name)
}
pub fn collection_element_type(resolved: &ResolvedTypeRef) -> Option<&ResolvedTypeRef> {
if !resolved.is_collection || resolved.type_args.is_empty() {
return None;
}
match resolved.base_type_name.as_str() {
"Map" if resolved.type_args.len() >= 2 => Some(&resolved.type_args[1]),
_ => Some(&resolved.type_args[0]), }
}
#[allow(dead_code)]
pub fn map_key_type(resolved: &ResolvedTypeRef) -> Option<&ResolvedTypeRef> {
if resolved.base_type_name == "Map" && resolved.type_args.len() >= 2 {
Some(&resolved.type_args[0])
} else {
None
}
}
pub fn standalone_fn_name(type_name: &str, suffix: &str) -> String {
format!("{}{}", type_name.to_case(Case::Camel), suffix)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ts_syn::abi::SpanIR;
fn span() -> SpanIR {
SpanIR::new(0, 0)
}
fn make_decorator(name: &str, args: &str) -> DecoratorIR {
DecoratorIR {
name: name.into(),
args_src: args.into(),
span: span(),
node: None,
}
}
#[test]
fn test_compare_field_skip() {
let decorator = make_decorator("partialEq", "skip");
let opts = CompareFieldOptions::from_decorators(&[decorator], "partialEq");
assert!(opts.skip);
}
#[test]
fn test_compare_field_no_skip() {
let decorator = make_decorator("partialEq", "");
let opts = CompareFieldOptions::from_decorators(&[decorator], "partialEq");
assert!(!opts.skip);
}
#[test]
fn test_compare_field_skip_false() {
let decorator = make_decorator("hash", "skip: false");
let opts = CompareFieldOptions::from_decorators(&[decorator], "hash");
assert!(!opts.skip);
}
#[test]
fn test_default_field_with_string_value() {
let decorator = make_decorator("default", r#""hello""#);
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some(r#""hello""#));
}
#[test]
fn test_default_field_with_number_value() {
let decorator = make_decorator("default", "42");
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some("42"));
}
#[test]
fn test_default_field_with_array_value() {
let decorator = make_decorator("default", "[]");
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some("[]"));
}
#[test]
fn test_default_field_with_named_value() {
let decorator = make_decorator("default", r#"{ value: "test" }"#);
let opts = DefaultFieldOptions::from_decorators(&[decorator]);
assert!(opts.has_default);
assert_eq!(opts.value.as_deref(), Some("test"));
}
#[test]
fn test_is_primitive_type() {
assert!(is_primitive_type("string"));
assert!(is_primitive_type("number"));
assert!(is_primitive_type("boolean"));
assert!(is_primitive_type("bigint"));
assert!(!is_primitive_type("Date"));
assert!(!is_primitive_type("User"));
assert!(!is_primitive_type("string[]"));
}
#[test]
fn test_is_numeric_type() {
assert!(is_numeric_type("number"));
assert!(is_numeric_type("bigint"));
assert!(!is_numeric_type("string"));
assert!(!is_numeric_type("boolean"));
}
#[test]
fn test_get_type_default() {
assert_eq!(get_type_default("string"), r#""""#);
assert_eq!(get_type_default("number"), "0");
assert_eq!(get_type_default("boolean"), "false");
assert_eq!(get_type_default("bigint"), "0n");
assert_eq!(get_type_default("string[]"), "[]");
assert_eq!(get_type_default("Array<number>"), "[]");
assert_eq!(get_type_default("Map<string, number>"), "new Map()");
assert_eq!(get_type_default("Set<string>"), "new Set()");
assert_eq!(get_type_default("Date"), "new Date()");
assert_eq!(get_type_default("User"), "userDefaultValue()");
assert_eq!(
get_type_default("RecordLink<Service>"),
"recordLinkDefaultValue<Service>()"
);
assert_eq!(
get_type_default("Result<User, Error>"),
"resultDefaultValue<User, Error>()"
);
assert_eq!(get_type_default("{ [key: string]: number }"), "{}");
assert_eq!(get_type_default("{ foo: string; bar: number }"), "{}");
assert_eq!(get_type_default("{ [K in keyof T]: V }"), "{}");
}
#[test]
fn test_get_type_default_object_literal_before_union_split() {
assert_eq!(get_type_default("{ a: string | number }"), "{}");
assert_eq!(
get_type_default("{ status: \"active\" | \"inactive\" }"),
"{}"
);
}
#[test]
fn test_get_type_default_union_with_primitive() {
assert_eq!(get_type_default("string | Account"), r#""""#);
assert_eq!(get_type_default("string | Employee"), r#""""#);
assert_eq!(get_type_default("string | Appointment"), r#""""#);
assert_eq!(get_type_default("string | Site"), r#""""#);
assert_eq!(get_type_default("number | Custom"), "0");
assert_eq!(get_type_default("boolean | Foo"), "false");
assert_eq!(get_type_default("bigint | Bar"), "0n");
assert_eq!(get_type_default("Account | string"), r#""""#);
}
#[test]
fn test_get_type_default_union_with_literal() {
assert_eq!(
get_type_default(r#""Estimate" | "Invoice""#),
r#""Estimate""#
);
assert_eq!(
get_type_default(r#""active" | "pending" | "completed""#),
r#""active""#
);
}
#[test]
fn test_get_type_default_union_custom_types() {
assert_eq!(
get_type_default("Account | Employee"),
"accountDefaultValue()"
);
}
#[test]
fn test_get_type_default_nullable_union() {
assert_eq!(get_type_default("string | null"), "null");
assert_eq!(get_type_default("Account | undefined"), "null");
assert_eq!(get_type_default("string | Account | null"), "null");
}
#[test]
fn test_is_generic_type() {
assert!(is_generic_type("RecordLink<Service>"));
assert!(is_generic_type("Map<string, number>"));
assert!(is_generic_type("Array<User>"));
assert!(is_generic_type("Result<T, E>"));
assert!(!is_generic_type("User"));
assert!(!is_generic_type("string"));
assert!(!is_generic_type("number[]")); }
#[test]
fn test_parse_generic_type() {
assert_eq!(
parse_generic_type("RecordLink<Service>"),
Some(("RecordLink", "Service"))
);
assert_eq!(
parse_generic_type("Map<string, number>"),
Some(("Map", "string, number"))
);
assert_eq!(
parse_generic_type("Result<Array<User>, Error>"),
Some(("Result", "Array<User>, Error"))
);
assert_eq!(parse_generic_type("User"), None);
assert_eq!(parse_generic_type("string"), None);
assert_eq!(parse_generic_type("Array<User"), None);
}
use crate::ts_syn::abi::ir::type_registry::{
TypeDefinitionIR, TypeRegistry, TypeRegistryEntry,
};
use crate::ts_syn::abi::{ClassIR, InterfaceIR};
fn zero_span() -> SpanIR {
SpanIR::new(0, 0)
}
fn make_registry_with_derives() -> TypeRegistry {
let mut registry = TypeRegistry::new();
let user_entry = TypeRegistryEntry {
name: "User".to_string(),
file_path: "/project/src/user.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Class(ClassIR {
name: "User".to_string(),
span: zero_span(),
body_span: zero_span(),
is_abstract: false,
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator(
"derive",
"Clone, Hash, PartialEq, Debug, Default",
)],
decorators_ast: vec![],
fields: vec![],
methods: vec![],
members: vec![],
}),
file_imports: vec![],
};
registry.insert(user_entry, "/project");
let order_entry = TypeRegistryEntry {
name: "Order".to_string(),
file_path: "/project/src/order.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(InterfaceIR {
name: "Order".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator("derive", "Clone")],
fields: vec![],
methods: vec![],
}),
file_imports: vec![],
};
registry.insert(order_entry, "/project");
let product_entry = TypeRegistryEntry {
name: "Product".to_string(),
file_path: "/project/src/product.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Class(ClassIR {
name: "Product".to_string(),
span: zero_span(),
body_span: zero_span(),
is_abstract: false,
type_params: vec![],
heritage: vec![],
decorators: vec![],
decorators_ast: vec![],
fields: vec![],
methods: vec![],
members: vec![],
}),
file_imports: vec![],
};
registry.insert(product_entry, "/project");
registry
}
#[test]
fn test_type_has_derive() {
let registry = make_registry_with_derives();
assert!(type_has_derive(®istry, "User", "Clone"));
assert!(type_has_derive(®istry, "User", "Hash"));
assert!(type_has_derive(®istry, "User", "PartialEq"));
assert!(type_has_derive(®istry, "User", "Debug"));
assert!(type_has_derive(®istry, "User", "Default"));
assert!(!type_has_derive(®istry, "User", "Ord"));
assert!(type_has_derive(®istry, "Order", "Clone"));
assert!(!type_has_derive(®istry, "Order", "Hash"));
assert!(!type_has_derive(®istry, "Product", "Clone"));
assert!(!type_has_derive(®istry, "Unknown", "Clone"));
}
#[test]
fn test_type_has_derive_ambiguous_name() {
let mut registry = TypeRegistry::new();
let phone_entry = TypeRegistryEntry {
name: "PhoneNumber".to_string(),
file_path: "/project/src/types/phone-number.svelte.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(InterfaceIR {
name: "PhoneNumber".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator(
"derive",
"Default, Serialize, Deserialize, Gigaform",
)],
fields: vec![],
methods: vec![],
}),
file_imports: vec![],
};
registry.insert(phone_entry, "/project");
let barrel_entry = TypeRegistryEntry {
name: "PhoneNumber".to_string(),
file_path: "/project/src/types/all-types.svelte.ts".to_string(),
is_exported: true,
definition: TypeDefinitionIR::Interface(InterfaceIR {
name: "PhoneNumber".to_string(),
span: zero_span(),
body_span: zero_span(),
type_params: vec![],
heritage: vec![],
decorators: vec![make_decorator(
"derive",
"Default, Serialize, Deserialize, Gigaform",
)],
fields: vec![],
methods: vec![],
}),
file_imports: vec![],
};
registry.insert(barrel_entry, "/project");
assert!(
registry
.ambiguous_names
.contains(&"PhoneNumber".to_string())
);
assert!(type_has_derive(®istry, "PhoneNumber", "Gigaform"));
assert!(type_has_derive(®istry, "PhoneNumber", "Default"));
assert!(type_has_derive(®istry, "PhoneNumber", "Serialize"));
assert!(type_has_derive(®istry, "PhoneNumber", "Deserialize"));
}
#[test]
fn test_type_has_derive_case_insensitive() {
let registry = make_registry_with_derives();
assert!(type_has_derive(®istry, "User", "clone"));
assert!(type_has_derive(®istry, "User", "CLONE"));
}
#[test]
fn test_resolved_type_has_derive() {
let registry = make_registry_with_derives();
let resolved = ResolvedTypeRef {
raw_type: "User".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: false,
is_optional: false,
type_args: vec![],
};
assert!(resolved_type_has_derive(®istry, &resolved, "Clone"));
assert!(!resolved_type_has_derive(®istry, &resolved, "Ord"));
}
#[test]
fn test_collection_element_type() {
let user_ref = ResolvedTypeRef {
raw_type: "User".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: false,
is_optional: false,
type_args: vec![],
};
let array_ref = ResolvedTypeRef {
raw_type: "User[]".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: true,
is_optional: false,
type_args: vec![user_ref.clone()],
};
let elem = collection_element_type(&array_ref);
assert!(elem.is_some());
assert_eq!(elem.unwrap().base_type_name, "User");
let string_ref = ResolvedTypeRef {
raw_type: "string".to_string(),
base_type_name: "string".to_string(),
registry_key: None,
is_collection: false,
is_optional: false,
type_args: vec![],
};
let map_ref = ResolvedTypeRef {
raw_type: "Map<string, User>".to_string(),
base_type_name: "Map".to_string(),
registry_key: None,
is_collection: true,
is_optional: false,
type_args: vec![string_ref.clone(), user_ref.clone()],
};
let elem = collection_element_type(&map_ref);
assert!(elem.is_some());
assert_eq!(elem.unwrap().base_type_name, "User");
assert!(collection_element_type(&user_ref).is_none());
}
#[test]
fn test_map_key_type() {
let string_ref = ResolvedTypeRef {
raw_type: "string".to_string(),
base_type_name: "string".to_string(),
registry_key: None,
is_collection: false,
is_optional: false,
type_args: vec![],
};
let user_ref = ResolvedTypeRef {
raw_type: "User".to_string(),
base_type_name: "User".to_string(),
registry_key: Some("src/user.ts::User".to_string()),
is_collection: false,
is_optional: false,
type_args: vec![],
};
let map_ref = ResolvedTypeRef {
raw_type: "Map<string, User>".to_string(),
base_type_name: "Map".to_string(),
registry_key: None,
is_collection: true,
is_optional: false,
type_args: vec![string_ref.clone(), user_ref.clone()],
};
let key = map_key_type(&map_ref);
assert!(key.is_some());
assert_eq!(key.unwrap().base_type_name, "string");
assert!(map_key_type(&user_ref).is_none());
}
#[test]
fn test_standalone_fn_name() {
assert_eq!(standalone_fn_name("User", "Clone"), "userClone");
assert_eq!(standalone_fn_name("User", "HashCode"), "userHashCode");
assert_eq!(standalone_fn_name("User", "Equals"), "userEquals");
assert_eq!(standalone_fn_name("Order", "ToString"), "orderToString");
assert_eq!(
standalone_fn_name("MyLongType", "PartialCompare"),
"myLongTypePartialCompare"
);
}
}