1use crate::{datamodel::DataModel, exporters::Templates};
25use colored::Colorize;
26use convert_case::Casing;
27use serde::{Deserialize, Serialize};
28use std::{
29 collections::HashMap,
30 error::Error,
31 fs,
32 path::{Path, PathBuf},
33 str::FromStr,
34};
35
36#[derive(Debug, Serialize, Deserialize)]
38struct GenTemplate {
39 meta: Meta,
40 generate: HashMap<String, GenSpecs>,
41}
42
43impl GenTemplate {
44 pub fn prepend_root(&mut self, path: &Path) {
45 for (_, specs) in self.generate.iter_mut() {
46 specs.prepend_root(path);
47 }
48
49 self.meta.paths = self
50 .meta
51 .paths
52 .iter_mut()
53 .map(|spec| path.join(spec))
54 .collect();
55 }
56}
57
58#[derive(Debug, Serialize, Deserialize)]
60struct Meta {
61 name: Option<String>,
62 description: Option<String>,
63 paths: Vec<PathBuf>,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
68struct GenSpecs {
69 description: Option<String>,
70 out: PathBuf,
71 root: Option<String>,
72 #[serde(rename = "per-spec")]
73 per_spec: Option<bool>,
74 #[serde(flatten)]
75 #[serde(deserialize_with = "deserialize_config_map")]
76 config: HashMap<String, String>,
77 #[serde(rename = "fname-case", default)]
78 fname_case: Option<NameCase>,
79}
80
81fn deserialize_config_map<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
82where
83 D: serde::Deserializer<'de>,
84{
85 let map: HashMap<String, toml::Value> = HashMap::deserialize(deserializer)?;
86 Ok(map.into_iter().map(|(k, v)| (k, v.to_string())).collect())
87}
88
89impl GenSpecs {
90 pub fn prepend_root(&mut self, path: &Path) {
91 if path.is_file() {
92 panic!("Root to prepend is not a directory.");
93 }
94
95 self.out = path.join(&self.out);
96 }
97}
98
99#[derive(Debug)]
101enum MergeState {
102 Merge,
103 NoMerge,
104}
105
106impl From<bool> for MergeState {
107 fn from(value: bool) -> Self {
108 if value {
109 MergeState::NoMerge
110 } else {
111 MergeState::Merge
112 }
113 }
114}
115
116pub fn process_pipeline(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
126 let content = std::fs::read_to_string(path)?;
127 let mut gen_template: GenTemplate = toml::from_str(content.as_str()).unwrap();
128
129 if let Some(parent) = path.parent() {
130 gen_template.prepend_root(parent);
131 }
132
133 let paths = gen_template.meta.paths.as_slice();
134
135 for (name, mut specs) in gen_template.generate.into_iter() {
136 let template = Templates::from_str(name.as_str())?;
137 let merge_state = MergeState::from(specs.per_spec.unwrap_or(false));
138
139 match template {
140 Templates::JsonSchema => {
141 serialize_by_template(
142 &specs.out,
143 paths,
144 &merge_state,
145 &template,
146 &specs.config,
147 &specs.fname_case,
148 )?;
149 }
150 Templates::JsonSchemaAll => {
151 serialize_all_json_schemes(&specs.out, paths, &merge_state)?;
152 }
153 Templates::JsonLd => {
154 serialize_by_template(
155 &specs.out,
156 paths,
157 &merge_state,
158 &template,
159 &specs.config,
160 &specs.fname_case,
161 )?;
162 }
163 Templates::Linkml => {
164 serialize_by_template(
165 &specs.out,
166 paths,
167 &merge_state,
168 &template,
169 &specs.config,
170 &specs.fname_case,
171 )?;
172 }
173 Templates::Shex => {
174 serialize_by_template(
175 &specs.out,
176 paths,
177 &merge_state,
178 &template,
179 &specs.config,
180 &specs.fname_case,
181 )?;
182 }
183 Templates::Shacl => {
184 serialize_by_template(
185 &specs.out,
186 paths,
187 &merge_state,
188 &template,
189 &specs.config,
190 &specs.fname_case,
191 )?;
192 }
193 Templates::Markdown => {
194 serialize_by_template(
195 &specs.out,
196 paths,
197 &merge_state,
198 &template,
199 &specs.config,
200 &specs.fname_case,
201 )?;
202 }
203 Templates::Owl => {
204 serialize_by_template(
205 &specs.out,
206 paths,
207 &merge_state,
208 &template,
209 &specs.config,
210 &specs.fname_case,
211 )?;
212 }
213 Templates::CompactMarkdown => {
214 serialize_by_template(
215 &specs.out,
216 paths,
217 &merge_state,
218 &template,
219 &specs.config,
220 &specs.fname_case,
221 )?;
222 }
223 Templates::PythonDataclass => {
224 serialize_by_template(
225 &specs.out,
226 paths,
227 &merge_state,
228 &template,
229 &specs.config,
230 &specs.fname_case,
231 )?;
232 }
233 Templates::PythonPydantic => {
234 serialize_by_template(
235 &specs.out,
236 paths,
237 &merge_state,
238 &template,
239 &specs.config,
240 &specs.fname_case,
241 )?;
242 }
243 Templates::PythonPydanticXML => {
244 serialize_by_template(
245 &specs.out,
246 paths,
247 &merge_state,
248 &template,
249 &specs.config,
250 &specs.fname_case,
251 )?;
252 }
253 Templates::XmlSchema => {
254 serialize_by_template(
255 &specs.out,
256 paths,
257 &merge_state,
258 &template,
259 &specs.config,
260 &specs.fname_case,
261 )?;
262 }
263 Templates::Typescript => {
264 serialize_by_template(
265 &specs.out,
266 paths,
267 &merge_state,
268 &template,
269 &specs.config,
270 &specs.fname_case,
271 )?;
272 }
273 Templates::TypescriptZod => {
274 serialize_by_template(
275 &specs.out,
276 paths,
277 &merge_state,
278 &template,
279 &specs.config,
280 &specs.fname_case,
281 )?;
282 }
283 Templates::Rust => {
284 serialize_by_template(
285 &specs.out,
286 paths,
287 &merge_state,
288 &template,
289 &specs.config,
290 &specs.fname_case,
291 )?;
292 }
293 Templates::Golang => {
294 serialize_by_template(
295 &specs.out,
296 paths,
297 &merge_state,
298 &template,
299 &specs.config,
300 &specs.fname_case,
301 )?;
302 }
303 Templates::Julia => {
304 serialize_by_template(
305 &specs.out,
306 paths,
307 &merge_state,
308 &template,
309 &specs.config,
310 &specs.fname_case,
311 )?;
312 }
313 Templates::Protobuf => {
314 serialize_by_template(
315 &specs.out,
316 paths,
317 &merge_state,
318 &template,
319 &specs.config,
320 &specs.fname_case,
321 )?;
322 }
323 Templates::Graphql => {
324 serialize_by_template(
325 &specs.out,
326 paths,
327 &merge_state,
328 &template,
329 &specs.config,
330 &specs.fname_case,
331 )?;
332 }
333 Templates::MkDocs => {
334 if let MergeState::Merge = merge_state {
336 if !specs.config.contains_key("nav") {
337 specs.config.insert("nav".to_string(), "false".to_string());
338 }
339 }
340
341 serialize_by_template(
342 &specs.out,
343 paths,
344 &merge_state,
345 &template,
346 &specs.config,
347 &specs.fname_case,
348 )?;
349 }
350 Templates::Mermaid => {
351 serialize_by_template(
352 &specs.out,
353 paths,
354 &merge_state,
355 &template,
356 &specs.config,
357 &specs.fname_case,
358 )?;
359 }
360 Templates::Internal => {
361 let model = build_models(paths)?;
362 serialize_to_internal_schema(model, &specs.out, &merge_state)?;
363 }
364 }
365 }
366
367 Ok(())
368}
369
370fn build_models(paths: &[PathBuf]) -> Result<DataModel, Box<dyn Error>> {
380 let first_path = paths.first().unwrap();
381 path_exists(first_path)?;
382
383 let mut model = DataModel::from_markdown(first_path).map_err(|e| {
384 e.log_result();
385 format!("Error parsing markdown content: {e:#?}")
386 })?;
387
388 if paths.len() == 1 {
389 return Ok(model);
390 }
391
392 for path in paths.iter().skip(1) {
393 path_exists(path)?;
394 let new_model = DataModel::from_markdown(path)?;
395 model.merge(&new_model);
396 }
397
398 Ok(model)
399}
400
401fn path_exists(path: &PathBuf) -> Result<(), Box<dyn Error>> {
411 if !path.exists() {
412 return Err(format!("Path does not exist: {path:?}").into());
413 }
414 Ok(())
415}
416
417fn serialize_to_internal_schema(
430 model: DataModel,
431 out: &PathBuf,
432 merge_state: &MergeState,
433) -> Result<(), Box<dyn Error>> {
434 match merge_state {
435 MergeState::Merge => {
436 let schema = model.internal_schema();
437 save_to_file(out, &schema)?;
438 print_render_msg(out, &Templates::Internal);
439 Ok(())
440 }
441 MergeState::NoMerge => {
442 Err("Per spec is not supported for internal schema generation at the moment.".into())
443 }
444 }
445}
446
447fn serialize_all_json_schemes(
458 out: &PathBuf,
459 specs: &[PathBuf],
460 merge_state: &MergeState,
461) -> Result<(), Box<dyn Error>> {
462 if out.is_file() {
463 return Err("Output path is a file".into());
464 }
465 if !out.exists() {
466 fs::create_dir_all(out)?;
467 }
468
469 match merge_state {
470 MergeState::Merge => {
471 let model = build_models(specs)?;
472 model.json_schema_all(out.to_path_buf(), false)?;
473 print_render_msg(out, &Templates::JsonSchemaAll);
474 Ok(())
475 }
476 MergeState::NoMerge => {
477 for spec in specs {
478 let model = DataModel::from_markdown(spec)?;
479 let path = out.join(get_file_name(spec));
480 model.json_schema_all(path.to_path_buf(), false)?;
481 print_render_msg(&path, &Templates::JsonSchemaAll);
482 }
483 Ok(())
484 }
485 }
486}
487
488fn serialize_by_template(
501 out: &PathBuf,
502 specs: &[PathBuf],
503 merge_state: &MergeState,
504 template: &Templates,
505 config: &HashMap<String, String>,
506 case: &Option<NameCase>,
507) -> Result<(), Box<dyn Error>> {
508 match merge_state {
509 MergeState::Merge => {
510 print_render_msg(out, template);
511
512 let mut model = build_models(specs)?;
513 let content = model.convert_to(template, Some(config))?;
514
515 return save_to_file(out, content.as_str());
516 }
517 MergeState::NoMerge => {
518 if !has_wildcard_fname(out) {
519 return Err("
520 Output file name must contain a wildcard.
521 For example, a valid wildcard is 'path/to/*.json'"
522 .into());
523 }
524
525 for spec in specs {
526 if !spec.exists() {
527 return Err(format!("Path does not exist: {spec:?}").into());
528 }
529
530 let mut fname = get_file_name(spec);
531
532 if let Some(case) = case {
533 fname = casify_filename(fname, case.into());
534 }
535
536 let path = replace_wildcard(out, &fname);
537 print_render_msg(&path, template);
538
539 let mut model = DataModel::from_markdown(spec)?;
540 let content = model.convert_to(template, Some(config))?;
541
542 save_to_file(&path, content.as_str())?;
543 }
544 }
545 }
546
547 Ok(())
548}
549
550fn casify_filename(name: String, case: Option<convert_case::Case>) -> String {
561 if let Some(c) = case {
562 let (name, _) = name.split_once('.').unwrap_or((name.as_str(), ""));
563 let new_name = name.to_case(c);
564
565 new_name.to_string()
566 } else {
567 name
568 }
569}
570
571fn has_wildcard_fname(path: &Path) -> bool {
581 let path_str = path.to_str().unwrap();
582 path_str.contains("*")
583}
584
585fn replace_wildcard(path: &Path, name: &str) -> PathBuf {
596 let path_str = path.to_str().unwrap();
597 let new_path = path_str.replace('*', name);
598 PathBuf::from(new_path)
599}
600
601fn get_file_name(path: &Path) -> String {
611 let file_name = path.file_name().unwrap().to_str().unwrap();
613 let file_name = file_name.split('.').collect::<Vec<&str>>()[0];
614 file_name.to_string()
615}
616
617fn save_to_file(out: &PathBuf, content: &str) -> Result<(), Box<dyn Error>> {
628 let dir = out.parent().unwrap();
629 if !dir.exists() {
630 fs::create_dir_all(dir)?;
631 }
632
633 fs::write(out, content.trim()).map_err(|e| format!("Error writing to file: {e:#?}"))?;
634 Ok(())
635}
636
637fn print_render_msg(out: &Path, template: &Templates) {
638 println!(
639 " [{}] Writing to '{}'",
640 template.to_string().green().bold(),
641 out.to_str().unwrap().to_string().bold(),
642 );
643}
644
645#[derive(Debug, Deserialize, Serialize)]
654enum NameCase {
655 Pascal,
656 Snake,
657 Kebab,
658 Camel,
659 None,
660}
661
662impl FromStr for NameCase {
663 type Err = String;
664
665 fn from_str(s: &str) -> Result<Self, Self::Err> {
675 match s {
676 "pascal" => Ok(NameCase::Pascal),
677 "snake" => Ok(NameCase::Snake),
678 "kebab" => Ok(NameCase::Kebab),
679 "camel" => Ok(NameCase::Camel),
680 _ => Err("Invalid name case".to_string()),
681 }
682 }
683}
684
685impl<'a> From<&'a NameCase> for Option<convert_case::Case<'a>> {
686 fn from(value: &NameCase) -> Self {
696 match value {
697 NameCase::Pascal => Some(convert_case::Case::Pascal),
698 NameCase::Snake => Some(convert_case::Case::Snake),
699 NameCase::Kebab => Some(convert_case::Case::Kebab),
700 NameCase::Camel => Some(convert_case::Case::Camel),
701 NameCase::None => None,
702 }
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use std::path::PathBuf;
710
711 #[test]
712 fn test_has_wildcard_fname() {
713 let path = PathBuf::from("path/to/*.json");
714 let result = has_wildcard_fname(&path);
715 assert!(result);
716 }
717
718 #[test]
719 fn test_has_wildcard_fname_no_wildcard() {
720 let path = PathBuf::from("path/to/file.json");
721 let result = has_wildcard_fname(&path);
722 assert!(!result);
723 }
724
725 #[test]
726 fn test_build_models() {
727 let specs = vec![
728 PathBuf::from("tests/data/model.md"),
729 PathBuf::from("tests/data/model_merge.md"),
730 ];
731 let result = build_models(&specs);
732 assert!(result.is_ok());
733 }
734
735 #[test]
736 fn test_prepend_root() {
737 let mut gen_template = GenTemplate {
738 meta: Meta {
739 name: None,
740 description: None,
741 paths: vec![PathBuf::from("model.md")],
742 },
743 generate: HashMap::from_iter(vec![(
744 "json-schema".to_string(),
745 GenSpecs {
746 description: None,
747 out: PathBuf::from("schema.json"),
748 root: None,
749 per_spec: None,
750 config: HashMap::new(),
751 fname_case: None,
752 },
753 )]),
754 };
755
756 let path = PathBuf::from("tests/data");
757 gen_template.prepend_root(&path);
758
759 assert_eq!(
760 gen_template.meta.paths[0],
761 PathBuf::from("tests/data/model.md")
762 );
763 assert_eq!(
764 gen_template.generate["json-schema"].out,
765 PathBuf::from("tests/data/schema.json")
766 );
767 }
768}