Skip to content

Commit 34c1f20

Browse files
committed
Implement AST struct generation from config.yml
The build script now reads the config.yml file and generates corresponding Rust struct definitions for all RBS AST nodes. Implementation details: - Parse config.yml using serde to extract node definitions - Generate proper Rust module hierarchy from :: namespace separators - Apply Rust naming conventions: - Modules use snake_case - Structs remain PascalCase - Handle Rust reserved keywords (Use -> UseDirective, Self -> SelfType) - Smart PascalCase to snake_case conversion that correctly handles acronyms (e.g., 'AST' -> 'ast', not 'a_s_t') The generated bindings create empty struct definitions organized in the correct module hierarchy, laying the foundation for the safe Rust API that will wrap the ruby-rbs-sys FFI bindings.
1 parent 9f267ef commit 34c1f20

4 files changed

Lines changed: 200 additions & 14 deletions

File tree

rust/Cargo.lock

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/ruby-rbs/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ edition = "2024"
55

66
[dependencies]
77
ruby-rbs-sys = { path = "../ruby-rbs-sys" }
8+
9+
[build-dependencies]
10+
serde = { version = "1.0", features = ["derive"] }
11+
serde_yaml = "0.9"

rust/ruby-rbs/build.rs

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,132 @@
1-
use std::env;
2-
use std::fs::File;
3-
use std::io::Write;
4-
use std::path::Path;
1+
use serde::Deserialize;
2+
use std::{env, error::Error, fs::File, io::Write, path::Path};
53

6-
fn main() {
7-
println!("cargo:warning=Build script is running!");
4+
#[derive(Debug, Deserialize)]
5+
struct Config {
6+
nodes: Vec<Node>,
7+
}
8+
9+
#[derive(Debug, Deserialize)]
10+
struct Node {
11+
name: String,
12+
}
13+
14+
fn main() -> Result<(), Box<dyn Error>> {
15+
let config_path = Path::new(env!("CARGO_MANIFEST_DIR"))
16+
.join("../../config.yml")
17+
.canonicalize()?;
18+
19+
println!("cargo:rerun-if-changed={}", config_path.display());
20+
21+
let config_file = File::open(&config_path)?;
22+
let mut config: Config = serde_yaml::from_reader(config_file)?;
23+
24+
config.nodes.sort_by(|a, b| a.name.cmp(&b.name));
25+
generate(&config)?;
26+
27+
Ok(())
28+
}
29+
30+
fn replace_reserved_keyword(name: &str) -> &str {
31+
match name {
32+
"Use" => "UseDirective",
33+
"Self" => "SelfType",
34+
_ => name,
35+
}
36+
}
37+
38+
fn safe_module_name(name: &str) -> String {
39+
let name = replace_reserved_keyword(name);
840

9-
if let Err(err) = generate() {
10-
panic!("build.rs failed: {err}");
41+
let chars: Vec<char> = name.chars().collect();
42+
let mut result = String::new();
43+
44+
for (i, &ch) in chars.iter().enumerate() {
45+
// Insert underscore before uppercase if:
46+
// - Not at the start
47+
// - Previous char was lowercase OR
48+
// - Previous was uppercase but next is lowercase
49+
// e.g., "RBSTypes" -> "RBS_Types" -> "rbs_types"
50+
if i > 0 && ch.is_uppercase() {
51+
let prev_was_lower = chars[i - 1].is_lowercase();
52+
let next_is_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase());
53+
54+
if prev_was_lower || (chars[i - 1].is_uppercase() && next_is_lower) {
55+
result.push('_');
56+
}
57+
}
58+
result.push(ch);
1159
}
60+
61+
result.to_lowercase()
1262
}
1363

14-
fn generate() -> Result<(), Box<dyn std::error::Error>> {
15-
let out_dir = env::var("OUT_DIR").unwrap();
64+
fn generate(config: &Config) -> Result<(), Box<dyn Error>> {
65+
let out_dir = env::var("OUT_DIR")?;
1666
let dest_path = Path::new(&out_dir).join("bindings.rs");
1767

1868
let mut file = File::create(&dest_path)?;
1969

20-
writeln!(file, "// Generated by build.rs")?;
21-
writeln!(file, "// Do not edit this file directly")?;
22-
writeln!(file, "")?;
70+
writeln!(file, "// Generated by build.rs from config.yml")?;
71+
writeln!(file, "// Nodes to generate: {}", config.nodes.len())?;
72+
writeln!(file)?;
73+
74+
let mut current_path: Vec<String> = Vec::new();
75+
let mut first_in_module = true;
76+
77+
for node in &config.nodes {
78+
// Parse node path (skip "RBS" prefix)
79+
let parts: Vec<_> = node.name.split("::").skip(1).collect();
80+
let (modules, struct_name) = parts.split_at(parts.len() - 1);
81+
82+
// Transform module and struct names
83+
let modules: Vec<String> = modules.iter().map(|s| safe_module_name(s)).collect();
84+
let struct_name = {
85+
let name = struct_name[0];
86+
replace_reserved_keyword(name).to_string()
87+
};
88+
89+
// Find where paths diverge
90+
let common_len = current_path
91+
.iter()
92+
.zip(&modules)
93+
.take_while(|(a, b)| a == b)
94+
.count();
95+
96+
// Close old modules
97+
for depth in (common_len..current_path.len()).rev() {
98+
writeln!(file, "{}}}", " ".repeat(depth))?;
99+
first_in_module = false;
100+
}
101+
102+
// Open new modules
103+
for (depth, module) in modules.iter().enumerate().skip(common_len) {
104+
if !first_in_module {
105+
writeln!(file)?;
106+
}
107+
writeln!(file, "{}pub mod {} {{", " ".repeat(depth), module)?;
108+
first_in_module = true;
109+
}
110+
111+
// Write struct (with spacing if not first in module)
112+
if !first_in_module {
113+
writeln!(file)?;
114+
}
115+
writeln!(
116+
file,
117+
"{}pub struct {} {{}}",
118+
" ".repeat(modules.len()),
119+
struct_name
120+
)?;
121+
first_in_module = false;
122+
123+
current_path = modules;
124+
}
125+
126+
// Close remaining modules
127+
for depth in (0..current_path.len()).rev() {
128+
writeln!(file, "{}}}", " ".repeat(depth))?;
129+
}
23130

24131
Ok(())
25132
}

rust/ruby-rbs/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

0 commit comments

Comments
 (0)