Skip to main content

ruvector_graph/
codegen.rs

1//! Schema-driven typed client codegen (HelixDB-inspired, ADR-252 P6).
2//!
3//! HelixDB compiles a schema into typed API endpoints + multi-language SDKs so
4//! callers get compile-time-checked node/edge/vector types. This module does the
5//! same from a [`GraphSchema`]: it emits TypeScript, Python, and Rust type
6//! definitions plus a vector-type manifest. Output is deterministic (schema
7//! elements are sorted) so it can be checked in and diffed.
8//!
9//! These are *type* stubs — the single source of truth is the schema. The
10//! generated code carries the labels, property names/types, edge `from`/`to`
11//! constraints, and vector dimensions/metrics across the language boundary.
12
13use crate::schema::{DistanceMetric, GraphSchema, PropertySchema, PropertyType};
14
15fn metric_name(m: DistanceMetric) -> &'static str {
16    match m {
17        DistanceMetric::Cosine => "Cosine",
18        DistanceMetric::DotProduct => "DotProduct",
19        DistanceMetric::Euclidean => "Euclidean",
20    }
21}
22
23// ---- TypeScript ------------------------------------------------------------
24
25fn ts_type(t: PropertyType) -> &'static str {
26    match t {
27        PropertyType::Boolean => "boolean",
28        PropertyType::Integer | PropertyType::Float => "number",
29        PropertyType::String => "string",
30        PropertyType::Vector => "number[]",
31        PropertyType::Array => "unknown[]",
32        PropertyType::Map => "Record<string, unknown>",
33        PropertyType::Any => "unknown",
34    }
35}
36
37fn ts_property(p: &PropertySchema) -> String {
38    let opt = if p.required { "" } else { "?" };
39    let indexed = if p.indexed { "  /** @indexed */\n" } else { "" };
40    format!("{indexed}  {}{}: {};\n", p.name, opt, ts_type(p.ptype))
41}
42
43/// Generate TypeScript interfaces + a vector-type manifest from the schema.
44pub fn generate_typescript(schema: &GraphSchema) -> String {
45    let mut out = String::new();
46    out.push_str("// Auto-generated from RuVector GraphSchema (ADR-252 P6). Do not edit by hand.\n\n");
47
48    for n in schema.node_schemas_sorted() {
49        out.push_str(&format!("export interface {} {{\n", n.label));
50        for p in &n.properties {
51            out.push_str(&ts_property(p));
52        }
53        out.push_str("}\n\n");
54    }
55
56    for e in schema.edge_schemas_sorted() {
57        out.push_str(&format!(
58            "/** Edge {0}: {1} -> {2} */\nexport interface {0} {{\n  from: string;\n  to: string;\n",
59            e.edge_type, e.from_label, e.to_label
60        ));
61        for p in &e.properties {
62            out.push_str(&ts_property(p));
63        }
64        out.push_str("}\n\n");
65    }
66
67    out.push_str("export const VectorTypes = {\n");
68    for v in schema.vector_schemas_sorted() {
69        out.push_str(&format!(
70            "  {}: {{ label: \"{}\", property: \"{}\", dimensions: {}, metric: \"{}\" }},\n",
71            v.name,
72            v.label,
73            v.property,
74            v.dimensions,
75            metric_name(v.metric)
76        ));
77    }
78    out.push_str("} as const;\n\nexport type VectorTypeName = keyof typeof VectorTypes;\n");
79    out
80}
81
82// ---- Python ----------------------------------------------------------------
83
84fn py_type(t: PropertyType) -> &'static str {
85    match t {
86        PropertyType::Boolean => "bool",
87        PropertyType::Integer => "int",
88        PropertyType::Float => "float",
89        PropertyType::String => "str",
90        PropertyType::Vector => "list[float]",
91        PropertyType::Array => "list",
92        PropertyType::Map => "dict",
93        PropertyType::Any => "Any",
94    }
95}
96
97fn py_property(p: &PropertySchema) -> String {
98    let ty = py_type(p.ptype);
99    if p.required {
100        format!("    {}: {}\n", p.name, ty)
101    } else {
102        format!("    {}: NotRequired[{}]\n", p.name, ty)
103    }
104}
105
106/// Generate Python `TypedDict` classes + a vector-type manifest from the schema.
107pub fn generate_python(schema: &GraphSchema) -> String {
108    let mut out = String::new();
109    out.push_str("# Auto-generated from RuVector GraphSchema (ADR-252 P6). Do not edit by hand.\n");
110    out.push_str("from __future__ import annotations\n");
111    out.push_str("from typing import Any, NotRequired, TypedDict\n\n");
112
113    for n in schema.node_schemas_sorted() {
114        out.push_str(&format!("class {}(TypedDict):\n", n.label));
115        if n.properties.is_empty() {
116            out.push_str("    pass\n\n");
117            continue;
118        }
119        for p in &n.properties {
120            out.push_str(&py_property(p));
121        }
122        out.push('\n');
123    }
124
125    for e in schema.edge_schemas_sorted() {
126        out.push_str(&format!("class {}(TypedDict):\n", e.edge_type));
127        out.push_str(&format!("    # {} -> {}\n", e.from_label, e.to_label));
128        out.push_str("    from_: str\n    to: str\n");
129        for p in &e.properties {
130            out.push_str(&py_property(p));
131        }
132        out.push('\n');
133    }
134
135    out.push_str("VECTOR_TYPES = {\n");
136    for v in schema.vector_schemas_sorted() {
137        out.push_str(&format!(
138            "    \"{}\": {{\"label\": \"{}\", \"property\": \"{}\", \"dimensions\": {}, \"metric\": \"{}\"}},\n",
139            v.name, v.label, v.property, v.dimensions, metric_name(v.metric)
140        ));
141    }
142    out.push_str("}\n");
143    out
144}
145
146// ---- Rust ------------------------------------------------------------------
147
148fn rust_type(t: PropertyType) -> &'static str {
149    match t {
150        PropertyType::Boolean => "bool",
151        PropertyType::Integer => "i64",
152        PropertyType::Float => "f64",
153        PropertyType::String => "String",
154        PropertyType::Vector => "Vec<f32>",
155        PropertyType::Array => "Vec<serde_json::Value>",
156        PropertyType::Map => "std::collections::HashMap<String, serde_json::Value>",
157        PropertyType::Any => "serde_json::Value",
158    }
159}
160
161fn rust_field(p: &PropertySchema) -> String {
162    let ty = rust_type(p.ptype);
163    if p.required {
164        format!("    pub {}: {},\n", p.name, ty)
165    } else {
166        format!("    pub {}: Option<{}>,\n", p.name, ty)
167    }
168}
169
170/// Generate Rust structs from the schema (serde-ready).
171pub fn generate_rust(schema: &GraphSchema) -> String {
172    let mut out = String::new();
173    out.push_str("// Auto-generated from RuVector GraphSchema (ADR-252 P6). Do not edit by hand.\n");
174    out.push_str("use serde::{Deserialize, Serialize};\n\n");
175
176    for n in schema.node_schemas_sorted() {
177        out.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
178        out.push_str(&format!("pub struct {} {{\n", n.label));
179        for p in &n.properties {
180            out.push_str(&rust_field(p));
181        }
182        out.push_str("}\n\n");
183    }
184
185    for e in schema.edge_schemas_sorted() {
186        out.push_str(&format!("/// Edge {}: {} -> {}\n", e.edge_type, e.from_label, e.to_label));
187        out.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
188        out.push_str(&format!("pub struct {} {{\n    pub from: String,\n    pub to: String,\n", e.edge_type));
189        for p in &e.properties {
190            out.push_str(&rust_field(p));
191        }
192        out.push_str("}\n\n");
193    }
194    out
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::schema::{EdgeSchema, NodeSchema, VectorSchema};
201
202    fn schema() -> GraphSchema {
203        let mut s = GraphSchema::new();
204        s.add_node(
205            NodeSchema::new("Person")
206                .property(PropertySchema::new("name", PropertyType::String).required().indexed())
207                .property(PropertySchema::new("age", PropertyType::Integer))
208                .property(PropertySchema::new("embedding", PropertyType::Vector)),
209        );
210        s.add_node(NodeSchema::new("Company"));
211        s.add_edge(EdgeSchema::new("WORKS_AT", "Person", "Company"));
212        s.add_vector(VectorSchema::new("PersonEmb", "Person", "embedding", 384, DistanceMetric::Cosine));
213        s
214    }
215
216    #[test]
217    fn typescript_has_typed_interfaces_and_manifest() {
218        let ts = generate_typescript(&schema());
219        assert!(ts.contains("export interface Person {"));
220        assert!(ts.contains("name: string;")); // required
221        assert!(ts.contains("age?: number;")); // optional
222        assert!(ts.contains("embedding?: number[];")); // vector
223        assert!(ts.contains("@indexed"));
224        assert!(ts.contains("export interface WORKS_AT {"));
225        assert!(ts.contains("Person -> Company"));
226        assert!(ts.contains("PersonEmb: { label: \"Person\""));
227        assert!(ts.contains("dimensions: 384"));
228        assert!(ts.contains("export type VectorTypeName"));
229    }
230
231    #[test]
232    fn python_has_typeddicts_and_manifest() {
233        let py = generate_python(&schema());
234        assert!(py.contains("class Person(TypedDict):"));
235        assert!(py.contains("    name: str"));
236        assert!(py.contains("    age: NotRequired[int]"));
237        assert!(py.contains("class Company(TypedDict):"));
238        assert!(py.contains("    pass")); // empty node
239        assert!(py.contains("\"PersonEmb\": {\"label\": \"Person\""));
240    }
241
242    #[test]
243    fn rust_has_structs() {
244        let rs = generate_rust(&schema());
245        assert!(rs.contains("pub struct Person {"));
246        assert!(rs.contains("pub name: String,"));
247        assert!(rs.contains("pub age: Option<i64>,"));
248        assert!(rs.contains("pub embedding: Option<Vec<f32>>,"));
249        assert!(rs.contains("pub struct WORKS_AT {"));
250    }
251
252    #[test]
253    fn output_is_deterministic() {
254        let s = schema();
255        assert_eq!(generate_typescript(&s), generate_typescript(&s));
256        assert_eq!(generate_python(&s), generate_python(&s));
257        assert_eq!(generate_rust(&s), generate_rust(&s));
258    }
259}