#[derive(Debug, Clone)]
pub struct StateMethod {
pub name: String,
pub takes_payload: bool,
pub return_type: Option<String>,
}
pub fn extract_state_methods(script: &str) -> Vec<StateMethod> {
let mut out: Vec<StateMethod> = Vec::new();
let lines: Vec<&str> = script.lines().collect();
let mut i = 0usize;
while i < lines.len() {
let trimmed = lines[i].trim_start();
if let Some(rest) = strip_prefix_ignore(trimmed, "pub fn ") {
let name = parse_ident(rest);
if let Some(name) = name {
let mut sig = rest.to_string();
let mut j = i;
while !sig_has_closing_paren(&sig) && j + 1 < lines.len() {
j += 1;
sig.push('\n');
sig.push_str(lines[j]);
}
if sig.contains("&self") {
let takes_payload = signature_takes_payload(&sig);
let return_type = signature_return_type(&sig);
out.push(StateMethod {
name,
takes_payload,
return_type,
});
}
i = j; }
}
i += 1;
}
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropField {
pub name: String,
pub ty: String,
}
pub fn extract_state_fields(script: &str) -> Vec<String> {
extract_struct_fields(script, "State")
.into_iter()
.map(|(name, _)| name)
.collect()
}
pub fn extract_props_fields(script: &str) -> Vec<PropField> {
extract_struct_fields(script, "Props")
.into_iter()
.map(|(name, ty)| PropField { name, ty })
.collect()
}
fn extract_struct_fields(script: &str, name: &str) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
let mut in_struct = false;
let mut depth = 0usize;
for line in script.lines() {
let trimmed = line.trim();
if !in_struct {
if let Some(rest) = strip_prefix_ignore(trimmed, "pub struct ")
.or_else(|| strip_prefix_ignore(trimmed, "struct "))
&& rest.starts_with(name)
&& rest[name.len()..]
.chars()
.next()
.is_none_or(|c| !is_ident_char(c))
{
in_struct = true;
depth = count_char(trimmed, '{');
if trimmed.contains(";") && !trimmed.contains('{') {
in_struct = false;
}
}
continue;
}
depth += count_char(trimmed, '{');
if depth == 0 {
in_struct = false;
continue;
}
depth = depth.saturating_sub(count_char(trimmed, '}'));
let stripped = trimmed
.strip_prefix("pub")
.map(str::trim_start)
.unwrap_or(trimmed);
if let Some(colon) = stripped.find(':') {
let field = stripped[..colon].trim();
let ty = stripped[colon + 1..].trim();
if is_ident(field) {
let ty = strip_type_comment(ty);
let ty = ty.strip_suffix(',').unwrap_or(ty).trim_end();
out.push((field.to_string(), ty.to_string()));
}
}
if depth == 0 {
in_struct = false;
}
}
out
}
fn strip_type_comment(ty: &str) -> &str {
match ty.find("//") {
Some(at) => ty[..at].trim_end(),
None => ty,
}
}
pub fn resolve_method_name(methods: &[String], key: &str) -> String {
if methods.iter().any(|m| m == key) {
return key.to_string();
}
for prefix in ["get_", "is_", "has_"] {
let candidate = format!("{prefix}{key}");
if methods.iter().any(|m| m == &candidate) {
return candidate;
}
}
key.to_string()
}
pub fn method_names(methods: &[StateMethod]) -> Vec<String> {
methods.iter().map(|m| m.name.clone()).collect()
}
#[derive(Debug, Clone)]
pub struct TreeHandler {
pub name: String,
pub owner: Option<String>,
pub takes_payload: bool,
}
fn strip_prefix_ignore<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let p = s.find(prefix)?;
Some(&s[p + prefix.len()..])
}
fn parse_ident(s: &str) -> Option<String> {
let mut name = String::new();
for ch in s.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
name.push(ch);
} else {
break;
}
}
if name.is_empty() { None } else { Some(name) }
}
fn sig_has_closing_paren(sig: &str) -> bool {
if let Some(open) = sig.find('(') {
return sig[open + 1..].contains(')');
}
false
}
fn signature_takes_payload(sig: &str) -> bool {
let Some(open) = sig.find('(') else {
return false;
};
let Some(close) = sig[open + 1..].find(')') else {
return false;
};
let params = &sig[open + 1..open + 1 + close];
let params = params.trim();
if params.is_empty() {
return false;
}
params
.split_once(',')
.map(|(_, rest)| !rest.trim().is_empty())
.unwrap_or(false)
}
fn signature_return_type(sig: &str) -> Option<String> {
let open = sig.find('(')?;
let close = open + 1 + sig[open + 1..].find(')')?;
let after = &sig[close + 1..];
let head_end = after.find('{').unwrap_or(after.len());
let head = &after[..head_end];
let arrow = head.find("->")?;
let ty = head[arrow + 2..].trim();
if ty.is_empty() {
None
} else {
Some(ty.to_string())
}
}
fn count_char(s: &str, c: char) -> usize {
s.chars().filter(|&ch| ch == c).count()
}
fn is_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn is_ident_char(ch: char) -> bool {
ch.is_ascii_alphanumeric() || ch == '_'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_methods_with_payload_flag() {
let script = r#"
pub struct State {
pub count: Cell<i32>,
}
impl State {
pub fn new() -> Self {
Self { count: Cell::new(0) }
}
pub fn title(&self) -> String { String::from("Velox App") }
pub fn count(&self) -> i32 { self.count.get() }
pub fn positive(&self) -> bool { self.count.get() > 0 }
pub fn increment(&self) { self.count.set(self.count.get() + 1); }
pub fn on_input(&self, payload: &str) { self.new_todo.set(payload.to_string()); }
}
"#;
let methods = extract_state_methods(script);
let names: Vec<&str> = methods.iter().map(|m| m.name.as_str()).collect();
assert!(names.contains(&"title"));
assert!(names.contains(&"count"));
assert!(names.contains(&"positive"));
assert!(names.contains(&"increment"));
assert!(names.contains(&"on_input"));
assert!(!names.contains(&"new"));
let on_input = methods.iter().find(|m| m.name == "on_input").unwrap();
assert!(on_input.takes_payload);
let increment = methods.iter().find(|m| m.name == "increment").unwrap();
assert!(!increment.takes_payload);
}
#[test]
fn extracts_return_types_from_signatures() {
let script = r#"
impl State {
pub fn title(&self) -> String { String::from("Velox App") }
pub fn count(&self) -> i32 { 0 }
pub fn positive(&self) -> bool { true }
pub fn label(&self) -> &'static str { "x" }
pub fn reset(&self) { self.count.set(0); }
pub fn items(&self) -> Vec<String> { Vec::new() }
pub fn maybe(
&self,
fallback: String,
) -> String {
fallback
}
}
"#;
let methods = extract_state_methods(script);
let return_type = |name: &str| {
methods
.iter()
.find(|m| m.name == name)
.unwrap()
.return_type
.as_deref()
};
assert_eq!(return_type("title"), Some("String"));
assert_eq!(return_type("count"), Some("i32"));
assert_eq!(return_type("positive"), Some("bool"));
assert_eq!(return_type("label"), Some("&'static str"));
assert_eq!(return_type("items"), Some("Vec<String>"));
assert_eq!(return_type("maybe"), Some("String"));
assert_eq!(return_type("reset"), None);
}
#[test]
fn extracts_fields_from_state_struct() {
let script = r#"
pub struct State {
pub count: Cell<i32>,
pub todos: Rc<Signal<Vec<Todo>>>,
}
impl State { ... }
"#;
let fields = extract_state_fields(script);
assert_eq!(fields, vec!["count", "todos"]);
}
#[test]
fn resolves_prefixed_getters() {
let methods = vec![
"get_title".to_string(),
"get_counter".to_string(),
"is_positive".to_string(),
"increment".to_string(),
];
assert_eq!(resolve_method_name(&methods, "title"), "get_title");
assert_eq!(resolve_method_name(&methods, "counter"), "get_counter");
assert_eq!(resolve_method_name(&methods, "positive"), "is_positive");
assert_eq!(resolve_method_name(&methods, "increment"), "increment");
assert_eq!(resolve_method_name(&methods, "missing"), "missing");
}
}