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(
47        "// Auto-generated from RuVector GraphSchema (ADR-252 P6). Do not edit by hand.\n\n",
48    );
49
50    for n in schema.node_schemas_sorted() {
51        out.push_str(&format!("export interface {} {{\n", n.label));
52        for p in &n.properties {
53            out.push_str(&ts_property(p));
54        }
55        out.push_str("}\n\n");
56    }
57
58    for e in schema.edge_schemas_sorted() {
59        out.push_str(&format!(
60            "/** Edge {0}: {1} -> {2} */\nexport interface {0} {{\n  from: string;\n  to: string;\n",
61            e.edge_type, e.from_label, e.to_label
62        ));
63        for p in &e.properties {
64            out.push_str(&ts_property(p));
65        }
66        out.push_str("}\n\n");
67    }
68
69    out.push_str("export const VectorTypes = {\n");
70    for v in schema.vector_schemas_sorted() {
71        out.push_str(&format!(
72            "  {}: {{ label: \"{}\", property: \"{}\", dimensions: {}, metric: \"{}\" }},\n",
73            v.name,
74            v.label,
75            v.property,
76            v.dimensions,
77            metric_name(v.metric)
78        ));
79    }
80    out.push_str("} as const;\n\nexport type VectorTypeName = keyof typeof VectorTypes;\n");
81    out
82}
83
84// ---- Python ----------------------------------------------------------------
85
86fn py_type(t: PropertyType) -> &'static str {
87    match t {
88        PropertyType::Boolean => "bool",
89        PropertyType::Integer => "int",
90        PropertyType::Float => "float",
91        PropertyType::String => "str",
92        PropertyType::Vector => "list[float]",
93        PropertyType::Array => "list",
94        PropertyType::Map => "dict",
95        PropertyType::Any => "Any",
96    }
97}
98
99fn py_property(p: &PropertySchema) -> String {
100    let ty = py_type(p.ptype);
101    if p.required {
102        format!("    {}: {}\n", p.name, ty)
103    } else {
104        format!("    {}: NotRequired[{}]\n", p.name, ty)
105    }
106}
107
108/// Generate Python `TypedDict` classes + a vector-type manifest from the schema.
109pub fn generate_python(schema: &GraphSchema) -> String {
110    let mut out = String::new();
111    out.push_str("# Auto-generated from RuVector GraphSchema (ADR-252 P6). Do not edit by hand.\n");
112    out.push_str("from __future__ import annotations\n");
113    out.push_str("from typing import Any, NotRequired, TypedDict\n\n");
114
115    for n in schema.node_schemas_sorted() {
116        out.push_str(&format!("class {}(TypedDict):\n", n.label));
117        if n.properties.is_empty() {
118            out.push_str("    pass\n\n");
119            continue;
120        }
121        for p in &n.properties {
122            out.push_str(&py_property(p));
123        }
124        out.push('\n');
125    }
126
127    for e in schema.edge_schemas_sorted() {
128        out.push_str(&format!("class {}(TypedDict):\n", e.edge_type));
129        out.push_str(&format!("    # {} -> {}\n", e.from_label, e.to_label));
130        out.push_str("    from_: str\n    to: str\n");
131        for p in &e.properties {
132            out.push_str(&py_property(p));
133        }
134        out.push('\n');
135    }
136
137    out.push_str("VECTOR_TYPES = {\n");
138    for v in schema.vector_schemas_sorted() {
139        out.push_str(&format!(
140            "    \"{}\": {{\"label\": \"{}\", \"property\": \"{}\", \"dimensions\": {}, \"metric\": \"{}\"}},\n",
141            v.name, v.label, v.property, v.dimensions, metric_name(v.metric)
142        ));
143    }
144    out.push_str("}\n");
145    out
146}
147
148// ---- Rust ------------------------------------------------------------------
149
150fn rust_type(t: PropertyType) -> &'static str {
151    match t {
152        PropertyType::Boolean => "bool",
153        PropertyType::Integer => "i64",
154        PropertyType::Float => "f64",
155        PropertyType::String => "String",
156        PropertyType::Vector => "Vec<f32>",
157        PropertyType::Array => "Vec<serde_json::Value>",
158        PropertyType::Map => "std::collections::HashMap<String, serde_json::Value>",
159        PropertyType::Any => "serde_json::Value",
160    }
161}
162
163fn rust_field(p: &PropertySchema) -> String {
164    let ty = rust_type(p.ptype);
165    if p.required {
166        format!("    pub {}: {},\n", p.name, ty)
167    } else {
168        format!("    pub {}: Option<{}>,\n", p.name, ty)
169    }
170}
171
172/// Generate Rust structs from the schema (serde-ready).
173pub fn generate_rust(schema: &GraphSchema) -> String {
174    let mut out = String::new();
175    out.push_str(
176        "// Auto-generated from RuVector GraphSchema (ADR-252 P6). Do not edit by hand.\n",
177    );
178    out.push_str("use serde::{Deserialize, Serialize};\n\n");
179
180    for n in schema.node_schemas_sorted() {
181        out.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
182        out.push_str(&format!("pub struct {} {{\n", n.label));
183        for p in &n.properties {
184            out.push_str(&rust_field(p));
185        }
186        out.push_str("}\n\n");
187    }
188
189    for e in schema.edge_schemas_sorted() {
190        out.push_str(&format!(
191            "/// Edge {}: {} -> {}\n",
192            e.edge_type, e.from_label, e.to_label
193        ));
194        out.push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\n");
195        out.push_str(&format!(
196            "pub struct {} {{\n    pub from: String,\n    pub to: String,\n",
197            e.edge_type
198        ));
199        for p in &e.properties {
200            out.push_str(&rust_field(p));
201        }
202        out.push_str("}\n\n");
203    }
204    out
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::schema::{EdgeSchema, NodeSchema, VectorSchema};
211
212    fn schema() -> GraphSchema {
213        let mut s = GraphSchema::new();
214        s.add_node(
215            NodeSchema::new("Person")
216                .property(
217                    PropertySchema::new("name", PropertyType::String)
218                        .required()
219                        .indexed(),
220                )
221                .property(PropertySchema::new("age", PropertyType::Integer))
222                .property(PropertySchema::new("embedding", PropertyType::Vector)),
223        );
224        s.add_node(NodeSchema::new("Company"));
225        s.add_edge(EdgeSchema::new("WORKS_AT", "Person", "Company"));
226        s.add_vector(VectorSchema::new(
227            "PersonEmb",
228            "Person",
229            "embedding",
230            384,
231            DistanceMetric::Cosine,
232        ));
233        s
234    }
235
236    #[test]
237    fn typescript_has_typed_interfaces_and_manifest() {
238        let ts = generate_typescript(&schema());
239        assert!(ts.contains("export interface Person {"));
240        assert!(ts.contains("name: string;")); // required
241        assert!(ts.contains("age?: number;")); // optional
242        assert!(ts.contains("embedding?: number[];")); // vector
243        assert!(ts.contains("@indexed"));
244        assert!(ts.contains("export interface WORKS_AT {"));
245        assert!(ts.contains("Person -> Company"));
246        assert!(ts.contains("PersonEmb: { label: \"Person\""));
247        assert!(ts.contains("dimensions: 384"));
248        assert!(ts.contains("export type VectorTypeName"));
249    }
250
251    #[test]
252    fn python_has_typeddicts_and_manifest() {
253        let py = generate_python(&schema());
254        assert!(py.contains("class Person(TypedDict):"));
255        assert!(py.contains("    name: str"));
256        assert!(py.contains("    age: NotRequired[int]"));
257        assert!(py.contains("class Company(TypedDict):"));
258        assert!(py.contains("    pass")); // empty node
259        assert!(py.contains("\"PersonEmb\": {\"label\": \"Person\""));
260    }
261
262    #[test]
263    fn rust_has_structs() {
264        let rs = generate_rust(&schema());
265        assert!(rs.contains("pub struct Person {"));
266        assert!(rs.contains("pub name: String,"));
267        assert!(rs.contains("pub age: Option<i64>,"));
268        assert!(rs.contains("pub embedding: Option<Vec<f32>>,"));
269        assert!(rs.contains("pub struct WORKS_AT {"));
270    }
271
272    #[test]
273    fn output_is_deterministic() {
274        let s = schema();
275        assert_eq!(generate_typescript(&s), generate_typescript(&s));
276        assert_eq!(generate_python(&s), generate_python(&s));
277        assert_eq!(generate_rust(&s), generate_rust(&s));
278    }
279}