1use 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
23fn 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
43pub 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
82fn 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
106pub 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
146fn 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
170pub 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;")); assert!(ts.contains("age?: number;")); assert!(ts.contains("embedding?: number[];")); 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")); 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}