1use crate::kinds::Kind;
4use crate::xeto::spec::{Slot, Spec};
5
6pub fn export_spec(spec: &Spec) -> String {
8 let mut out = String::new();
9
10 if !spec.doc.is_empty() {
12 for line in spec.doc.lines() {
13 out.push_str(&format!("// {}\n", line));
14 }
15 }
16
17 out.push_str(&spec.name);
19 if let Some(ref base) = spec.base {
20 out.push_str(": ");
21 let base_short = base.split("::").last().unwrap_or(base);
23 out.push_str(base_short);
24 } else {
25 out.push_str(": Obj");
26 }
27
28 let meta_tags: Vec<String> = spec
30 .meta
31 .iter()
32 .filter(|(k, _)| k.as_str() != "abstract")
33 .map(|(k, v)| format_meta_tag(k, v))
34 .collect();
35 if spec.is_abstract || !meta_tags.is_empty() {
36 out.push_str(" <");
37 let mut parts = Vec::new();
38 if spec.is_abstract {
39 parts.push("abstract".to_string());
40 }
41 parts.extend(meta_tags);
42 out.push_str(&parts.join(", "));
43 out.push('>');
44 }
45
46 if spec.slots.is_empty() {
48 out.push('\n');
49 } else {
50 out.push_str(" {\n");
51 for slot in &spec.slots {
52 export_slot(&mut out, slot, 2);
53 }
54 out.push_str("}\n");
55 }
56
57 out
58}
59
60pub fn export_lib(
62 _lib_name: &str,
63 version: &str,
64 doc: &str,
65 depends: &[String],
66 specs: &[&Spec],
67) -> String {
68 let mut out = String::new();
69
70 out.push_str("pragma: Lib <\n");
72 if !doc.is_empty() {
73 out.push_str(&format!(" doc: \"{}\"\n", escape_xeto_str(doc)));
74 }
75 out.push_str(&format!(" version: \"{}\"\n", escape_xeto_str(version)));
76 if !depends.is_empty() {
77 out.push_str(" depends: {\n");
78 for dep in depends {
79 out.push_str(&format!(" {{ lib: \"{}\" }}\n", escape_xeto_str(dep)));
80 }
81 out.push_str(" }\n");
82 }
83 out.push_str(">\n\n");
84
85 for spec in specs {
87 out.push_str(&export_spec(spec));
88 out.push('\n');
89 }
90
91 out
92}
93
94fn export_slot(out: &mut String, slot: &Slot, indent: usize) {
95 let pad = " ".repeat(indent);
96 out.push_str(&pad);
97
98 if slot.is_marker {
99 out.push_str(&slot.name);
101 if slot.is_maybe() {
102 out.push('?');
103 }
104 } else if slot.is_query {
105 out.push_str(&slot.name);
107 out.push_str(": Query");
108 let mut query_parts = Vec::new();
109 if let Some(Kind::Str(of)) = slot.meta.get("of") {
110 query_parts.push(format!("of:{}", of));
111 }
112 if let Some(Kind::Str(via)) = slot.meta.get("via") {
113 query_parts.push(format!("via:\"{}\"", via));
114 }
115 if let Some(Kind::Str(inv)) = slot.meta.get("inverse") {
116 query_parts.push(format!("inverse:\"{}\"", inv));
117 }
118 if !query_parts.is_empty() {
119 out.push('<');
120 out.push_str(&query_parts.join(", "));
121 out.push('>');
122 }
123 } else {
124 out.push_str(&slot.name);
126 out.push_str(": ");
127 if let Some(ref t) = slot.type_ref {
128 let short = t.split("::").last().unwrap_or(t);
129 out.push_str(short);
130 } else {
131 out.push_str("Obj");
132 }
133 if slot.is_maybe() {
134 out.push('?');
135 }
136
137 let slot_meta: Vec<String> = slot
139 .meta
140 .iter()
141 .filter(|(k, _)| {
142 let k = k.as_str();
143 k != "maybe" && k != "of" && k != "via" && k != "inverse"
144 })
145 .map(|(k, v)| format_meta_tag(k, v))
146 .collect();
147 if !slot_meta.is_empty() {
148 out.push_str(" <");
149 out.push_str(&slot_meta.join(", "));
150 out.push('>');
151 }
152 }
153
154 if let Some(ref val) = slot.default {
156 match val {
157 Kind::Str(s) => out.push_str(&format!(" \"{}\"", escape_xeto_str(s))),
158 Kind::Number(n) => out.push_str(&format!(" {}", n)),
159 _ => out.push_str(&format!(" {}", val)),
160 }
161 }
162
163 out.push('\n');
164
165 if !slot.children.is_empty() {
167 out.push_str(&format!("{} {{\n", pad));
168 for child in &slot.children {
169 export_slot(out, child, indent + 4);
170 }
171 out.push_str(&format!("{} }}\n", pad));
172 }
173}
174
175fn escape_xeto_str(s: &str) -> String {
176 s.replace('\\', "\\\\")
177 .replace('"', "\\\"")
178 .replace('\n', "\\n")
179 .replace('\t', "\\t")
180}
181
182fn format_meta_tag(key: &str, val: &Kind) -> String {
183 match val {
184 Kind::Marker => key.to_string(),
185 Kind::Str(s) => format!("{}: \"{}\"", key, escape_xeto_str(s)),
186 Kind::Number(n) => format!("{}: {}", key, n),
187 Kind::Bool(b) => format!("{}: {}", key, b),
188 _ => format!("{}: {}", key, val),
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use std::collections::HashMap;
196
197 #[test]
198 fn export_simple_spec() {
199 let mut spec = Spec::new("test::Foo", "test", "Foo");
200 spec.slots.push(Slot {
201 name: "active".into(),
202 type_ref: None,
203 meta: HashMap::new(),
204 default: None,
205 is_marker: true,
206 is_query: false,
207 children: vec![],
208 });
209 let output = export_spec(&spec);
210 assert!(output.contains("Foo: Obj"));
211 assert!(output.contains("active"));
212 }
213
214 #[test]
215 fn export_abstract_spec() {
216 let mut spec = Spec::new("test::Base", "test", "Base");
217 spec.is_abstract = true;
218 let output = export_spec(&spec);
219 assert!(output.contains("<abstract>"));
220 }
221
222 #[test]
223 fn export_spec_with_typed_slots() {
224 let mut spec = Spec::new("test::Site", "test", "Site");
225 spec.slots.push(Slot {
226 name: "dis".into(),
227 type_ref: Some("Str".into()),
228 meta: HashMap::new(),
229 default: None,
230 is_marker: false,
231 is_query: false,
232 children: vec![],
233 });
234 let output = export_spec(&spec);
235 assert!(output.contains("dis: Str"));
236 }
237
238 #[test]
239 fn export_spec_with_query_slot() {
240 let mut meta = HashMap::new();
241 meta.insert("of".into(), Kind::Str("Point".into()));
242 meta.insert("via".into(), Kind::Str("equipRef+".into()));
243 let mut spec = Spec::new("test::Equip", "test", "Equip");
244 spec.slots.push(Slot {
245 name: "points".into(),
246 type_ref: None,
247 meta,
248 default: None,
249 is_marker: false,
250 is_query: true,
251 children: vec![],
252 });
253 let output = export_spec(&spec);
254 assert!(output.contains("Query"));
255 assert!(output.contains("of:Point"));
256 assert!(output.contains("via:\"equipRef+\""));
257 }
258
259 #[test]
260 fn export_lib_with_pragma() {
261 let spec = Spec::new("mylib::Thing", "mylib", "Thing");
262 let output = export_lib("mylib", "2.0.0", "My library", &["sys".into()], &[&spec]);
263 assert!(output.contains("pragma: Lib"));
264 assert!(output.contains("version: \"2.0.0\""));
265 assert!(output.contains("doc: \"My library\""));
266 assert!(output.contains("lib: \"sys\""));
267 assert!(output.contains("Thing: Obj"));
268 }
269
270 #[test]
271 fn export_roundtrip() {
272 use crate::xeto::parser::parse_xeto;
273
274 let source = "Foo: Obj {\n active\n dis: Str\n}\n";
275 let xf = parse_xeto(source).unwrap();
276 let spec = crate::xeto::spec::spec_from_def(&xf.specs[0], "test");
277 let exported = export_spec(&spec);
278 let xf2 = parse_xeto(&exported).unwrap();
280 assert_eq!(xf2.specs[0].name, "Foo");
281 assert_eq!(xf2.specs[0].slots.len(), 2);
282 }
283
284 #[test]
285 fn export_spec_with_doc() {
286 let mut spec = Spec::new("test::Foo", "test", "Foo");
287 spec.doc = "A foo thing\nWith multiple lines".into();
288 let output = export_spec(&spec);
289 assert!(output.contains("// A foo thing"));
290 assert!(output.contains("// With multiple lines"));
291 }
292
293 #[test]
294 fn export_maybe_slot() {
295 let mut meta = HashMap::new();
296 meta.insert("maybe".into(), Kind::Marker);
297 let mut spec = Spec::new("test::Foo", "test", "Foo");
298 spec.slots.push(Slot {
299 name: "optional".into(),
300 type_ref: None,
301 meta: meta.clone(),
302 default: None,
303 is_marker: true,
304 is_query: false,
305 children: vec![],
306 });
307 spec.slots.push(Slot {
308 name: "optStr".into(),
309 type_ref: Some("Str".into()),
310 meta,
311 default: None,
312 is_marker: false,
313 is_query: false,
314 children: vec![],
315 });
316 let output = export_spec(&spec);
317 assert!(output.contains("optional?"));
318 assert!(output.contains("optStr: Str?"));
319 }
320
321 #[test]
322 fn format_meta_tag_escapes_strings() {
323 let result = format_meta_tag(
324 "doc",
325 &Kind::Str("has \"quotes\" and \\backslash".to_string()),
326 );
327 assert_eq!(result, r#"doc: "has \"quotes\" and \\backslash""#);
328 }
329
330 #[test]
331 fn format_meta_tag_escapes_newlines_and_tabs() {
332 let result = format_meta_tag("note", &Kind::Str("line1\nline2\there".to_string()));
333 assert_eq!(result, r#"note: "line1\nline2\there""#);
334 }
335
336 #[test]
337 fn export_slot_default_value_escapes_strings() {
338 let mut spec = Spec::new("test::Esc", "test", "Esc");
339 spec.slots.push(Slot {
340 name: "greeting".into(),
341 type_ref: Some("Str".into()),
342 meta: HashMap::new(),
343 default: Some(Kind::Str("say \"hello\"".to_string())),
344 is_marker: false,
345 is_query: false,
346 children: vec![],
347 });
348 let output = export_spec(&spec);
349 assert!(
350 output.contains(r#""say \"hello\"""#),
351 "default value string should be escaped, got: {}",
352 output
353 );
354 }
355}