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(
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
84fn 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
108pub 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
148fn 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
172pub 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;")); assert!(ts.contains("age?: number;")); assert!(ts.contains("embedding?: number[];")); 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")); 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}