noapi_functions/
struct_extractor.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use regex::Regex;
use std::{fs, path::PathBuf, process::Command};

use crate::build_functions::rust_to_typescript_type;

fn cargo_doc_path() -> PathBuf {
    let output = Command::new("cargo")
        .arg("metadata")
        .arg("--format-version=1")
        .output()
        .expect("Failed to execute cargo metadata");

    let metadata = serde_json::from_slice::<serde_json::Value>(&output.stdout)
        .expect("Failed to parse cargo metadata");

    let workspace_root = metadata["workspace_root"]
        .as_str()
        .expect("Failed to get workspace root");

    PathBuf::from(workspace_root).join("target/doc")
}

pub fn extract_struct_def(
    struct_name: String,
    content: &str,
    project_name: &str,
) -> Option<String> {
    let use_statements = extract_use_statements(&content);
    let parts: Vec<&str> = struct_name.split("::").collect();
    let mut use_statement = String::new();
    let struct_name: String;

    if parts.len() > 1 {
        struct_name = parts.last().unwrap().to_string();
        let other_parts = parts[..parts.len() - 1].join("::");
        use_statements.iter().for_each(|statement| {
            if statement.contains(parts.first().unwrap()) {
                use_statement = format!("{}::{}", statement, other_parts);
            }
        });
        if use_statement.is_empty() {
            use_statement = other_parts;
        }
    } else {
        struct_name = parts.first().unwrap().to_string();
        use_statements.iter().for_each(|statement| {
            if statement.contains(parts.first().unwrap()) {
                use_statement = statement.clone();
            }
        });
    }

    let parts: Vec<&str> = use_statement.split("::").collect();
    let mut struct_path: String;
    if parts.len() > 1 {
        struct_path = parts[..parts.len() - 1].join("/");
    } else {
        struct_path = parts.join("/");
    }

    struct_path = struct_path.replace("crate", project_name);

    let doc_path = cargo_doc_path().join(format!("{}/struct.{}.html", struct_path, struct_name));

    if !doc_path.exists() {
        println!("Documentation file not found: {:?}", doc_path);
        return None;
    }

    let html_content = fs::read_to_string(&doc_path).ok()?;

    let mut ts_content = String::new();

    let struct_regex = Regex::new(r"struct\s+(\w+)\s*\{([^}]*)\}").expect("Invalid struct regex");
    for cap in struct_regex.captures_iter(&html_content) {
        let _struct_name = &cap[1];
        let fields = &cap[2];

        let ts_fields: Vec<String> = fields
            .lines()
            .filter(|line| !line.trim().is_empty())
            .map(|line| {
                let re = Regex::new(r"<[^>]*>").unwrap();
                let html_text = re.replace_all(line, "").to_string();
                let parts: Vec<&str> = html_text.trim().split(':').collect();
                if parts.len() == 2 {
                    let field_name = parts[0].trim();

                    let field_type = rust_to_typescript_type(
                        parts[1].trim().trim_end_matches(','),
                        &vec![],
                        content,
                        project_name,
                    );

                    format!("   {}: {};", field_name, field_type)
                } else {
                    String::new()
                }
            })
            .collect();

        ts_content.push_str(&format!(
            "{{\n{}\n}}",
            ts_fields.join("\n").replace("pub", "")
        ));
    }

    Some(ts_content)
}

pub fn extract_use_statements(content: &str) -> Vec<String> {
    let mut use_statements = Vec::new();

    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with("use ") {
            let statement = trimmed.strip_prefix("use ").unwrap().trim_end_matches(';');
            let expanded_statements = expand_use_statement(statement);
            use_statements.extend(expanded_statements);
        }
    }

    use_statements
}

fn expand_use_statement(statement: &str) -> Vec<String> {
    let mut results = Vec::new();

    if let Some((prefix, grouped)) = statement.split_once("::{") {
        let grouped = grouped.trim_end_matches('}');
        let imports = split_imports(grouped);
        for import in imports {
            results.push(format!("use {}::{};", prefix, import));
        }
    } else if let Some((prefix, grouped)) = statement.split_once('{') {
        let prefix = prefix.trim_end();
        let grouped = grouped.trim_end_matches('}');
        let imports = split_imports(grouped);
        for import in imports {
            results.push(format!("use {}{};", prefix, import));
        }
    } else {
        results.push(format!("use {};", statement));
    }

    results
}

fn split_imports(grouped: &str) -> Vec<String> {
    grouped.split(',').map(|s| s.trim().to_string()).collect()
}