1use std::collections::{HashMap, HashSet};
9use std::fs::File;
10use std::io::BufReader;
11use std::path::{Path, PathBuf};
12
13use crate::io::{DocumentType, PcbDoc, PrjPcb, SchDoc};
14use crate::ops::output::*;
15use crate::records::sch::SchRecord;
16use crate::tree::RecordTree;
17
18fn open_prjpcb(path: &Path) -> Result<PrjPcb, Box<dyn std::error::Error>> {
19 Ok(PrjPcb::open_file(path)?)
20}
21
22fn project_dir(path: &Path) -> PathBuf {
24 path.parent().unwrap_or(Path::new(".")).to_path_buf()
25}
26
27fn resolve_document_path(project_path: &Path, doc_path: &str) -> PathBuf {
29 let project_dir = project_dir(project_path);
30 project_dir.join(doc_path)
31}
32
33fn open_schdoc(project_path: &Path, doc_path: &str) -> Result<SchDoc, Box<dyn std::error::Error>> {
35 let full_path = resolve_document_path(project_path, doc_path);
36 let file = File::open(&full_path)?;
37 Ok(SchDoc::open(BufReader::new(file))?)
38}
39
40fn open_pcbdoc(project_path: &Path, doc_path: &str) -> Result<PcbDoc, Box<dyn std::error::Error>> {
42 let full_path = resolve_document_path(project_path, doc_path);
43 let file = File::open(&full_path)?;
44 Ok(PcbDoc::open(BufReader::new(file))?)
45}
46
47#[derive(Debug, Clone, Default)]
53struct SchematicComponent {
54 designator: String,
55 lib_reference: String,
56 description: String,
57 footprint: String,
58 value: String,
59 sheet: String,
60 parameters: HashMap<String, String>,
61}
62
63#[derive(Debug, Clone, Default)]
65struct SchematicNet {
66 name: String,
67 pins: Vec<NetPin>,
68}
69
70#[derive(Debug, Clone, Default)]
72struct NetPin {
73 component: String,
74 pin: String,
75}
76
77fn extract_components(
79 prj: &PrjPcb,
80 project_path: &Path,
81) -> Result<Vec<SchematicComponent>, Box<dyn std::error::Error>> {
82 let mut components = Vec::new();
83
84 for doc in prj.schematics() {
85 let schdoc = match open_schdoc(project_path, &doc.path) {
86 Ok(s) => s,
87 Err(e) => {
88 log::warn!("Failed to open schematic document {}: {}", doc.path, e);
89 continue;
90 }
91 };
92
93 let tree = RecordTree::from_records(schdoc.primitives.clone());
95
96 for (id, record) in tree.iter() {
98 if let SchRecord::Component(comp) = record {
99 let mut designator = String::new();
101 for (_child_id, child) in tree.children(id) {
102 if let SchRecord::Designator(d) = child {
103 designator = d.param.label.text.clone();
104 break;
105 }
106 }
107
108 let sch_comp = SchematicComponent {
109 designator,
110 lib_reference: comp.lib_reference.clone(),
111 description: comp.component_description.clone(),
112 sheet: doc.path.clone(),
113 ..Default::default()
114 };
115
116 components.push(sch_comp);
117 }
118 }
119 }
120
121 Ok(components)
122}
123
124fn extract_nets(
126 prj: &PrjPcb,
127 project_path: &Path,
128) -> Result<Vec<SchematicNet>, Box<dyn std::error::Error>> {
129 let mut net_map: HashMap<String, SchematicNet> = HashMap::new();
130
131 for doc in prj.schematics() {
132 let schdoc = match open_schdoc(project_path, &doc.path) {
133 Ok(s) => s,
134 Err(e) => {
135 log::warn!("Failed to open schematic document {}: {}", doc.path, e);
136 continue;
137 }
138 };
139
140 for record in &schdoc.primitives {
142 match record {
143 SchRecord::NetLabel(label) => {
144 let net_name = label.label.text.clone();
145 let net = net_map
146 .entry(net_name.clone())
147 .or_insert_with(|| SchematicNet {
148 name: net_name,
149 pins: Vec::new(),
150 });
151 let _ = net; }
154 SchRecord::PowerObject(power) => {
155 let net_name = power.text.clone();
156 let net = net_map
157 .entry(net_name.clone())
158 .or_insert_with(|| SchematicNet {
159 name: net_name,
160 pins: Vec::new(),
161 });
162 let _ = net;
163 }
164 _ => {}
165 }
166 }
167 }
168
169 Ok(net_map.into_values().collect())
170}
171
172pub fn cmd_overview(path: &Path) -> Result<PrjPcbOverview, Box<dyn std::error::Error>> {
177 let prj = open_prjpcb(path)?;
178
179 let schematics: Vec<_> = prj
180 .documents
181 .iter()
182 .filter(|d| d.doc_type == DocumentType::Schematic)
183 .map(|d| DocumentInfo {
184 path: d.path.clone(),
185 doc_type: d.doc_type.display_name().to_string(),
186 exists: resolve_document_path(path, &d.path).exists(),
187 })
188 .collect();
189
190 let pcb_documents: Vec<_> = prj
191 .documents
192 .iter()
193 .filter(|d| d.doc_type == DocumentType::Pcb)
194 .map(|d| DocumentInfo {
195 path: d.path.clone(),
196 doc_type: d.doc_type.display_name().to_string(),
197 exists: resolve_document_path(path, &d.path).exists(),
198 })
199 .collect();
200
201 let libraries: Vec<_> = prj
202 .documents
203 .iter()
204 .filter(|d| {
205 d.doc_type == DocumentType::SchLib
206 || d.doc_type == DocumentType::PcbLib
207 || d.doc_type == DocumentType::IntLib
208 })
209 .map(|d| DocumentInfo {
210 path: d.path.clone(),
211 doc_type: d.doc_type.display_name().to_string(),
212 exists: resolve_document_path(path, &d.path).exists(),
213 })
214 .collect();
215
216 let other: Vec<_> = prj
217 .documents
218 .iter()
219 .filter(|d| d.doc_type == DocumentType::Other || d.doc_type == DocumentType::OutputJob)
220 .map(|d| DocumentInfo {
221 path: d.path.clone(),
222 doc_type: d.doc_type.display_name().to_string(),
223 exists: resolve_document_path(path, &d.path).exists(),
224 })
225 .collect();
226
227 let document_summary = DocumentSummary {
228 total_documents: prj.documents.len(),
229 schematics,
230 pcb_documents,
231 libraries,
232 other,
233 };
234
235 let component_summary = if !prj.schematics().is_empty() {
237 extract_components(&prj, path).ok().and_then(|components| {
238 if components.is_empty() {
239 None
240 } else {
241 let mut by_prefix: HashMap<String, usize> = HashMap::new();
243 for comp in &components {
244 let prefix: String = comp
245 .designator
246 .chars()
247 .take_while(|c| c.is_alphabetic())
248 .collect();
249 *by_prefix.entry(prefix).or_default() += 1;
250 }
251
252 let mut prefixes: Vec<_> = by_prefix
253 .into_iter()
254 .map(|(prefix, count)| {
255 let display_name = match prefix.as_str() {
256 "R" => "Resistors".to_string(),
257 "C" => "Capacitors".to_string(),
258 "L" => "Inductors".to_string(),
259 "U" => "ICs".to_string(),
260 "Q" => "Transistors".to_string(),
261 "D" => "Diodes".to_string(),
262 "J" | "P" => "Connectors".to_string(),
263 "SW" | "S" => "Switches".to_string(),
264 "F" => "Fuses".to_string(),
265 "Y" => "Crystals".to_string(),
266 _ => prefix.clone(),
267 };
268 (prefix, display_name, count)
269 })
270 .collect();
271 prefixes.sort_by(|a, b| b.2.cmp(&a.2));
272
273 Some(ComponentSummaryStats {
274 total_components: components.len(),
275 by_prefix: prefixes,
276 })
277 }
278 })
279 } else {
280 None
281 };
282
283 Ok(PrjPcbOverview {
284 path: path.display().to_string(),
285 name: prj.name(),
286 version: prj.version.clone(),
287 hierarchy_mode: if prj.hierarchy_mode == 0 {
288 "Flat".to_string()
289 } else {
290 "Hierarchical".to_string()
291 },
292 document_summary,
293 parameters: prj.parameters.clone(),
294 component_summary,
295 })
296}
297
298pub fn cmd_info(path: &Path) -> Result<PrjPcbInfo, Box<dyn std::error::Error>> {
299 let prj = open_prjpcb(path)?;
300
301 let mut by_type: HashMap<&str, usize> = HashMap::new();
302 for doc in &prj.documents {
303 *by_type.entry(doc.doc_type.display_name()).or_default() += 1;
304 }
305
306 let mut document_counts: Vec<_> = by_type
307 .into_iter()
308 .map(|(k, v)| (k.to_string(), v))
309 .collect();
310 document_counts.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
311
312 Ok(PrjPcbInfo {
313 path: path.display().to_string(),
314 name: prj.name(),
315 version: prj.version.clone(),
316 hierarchy_mode: if prj.hierarchy_mode == 0 {
317 "Flat".to_string()
318 } else {
319 "Hierarchical".to_string()
320 },
321 output_path: if prj.output_path.is_empty() {
322 "(default)".to_string()
323 } else {
324 prj.output_path.clone()
325 },
326 annotation_start: prj.annotation_start_value,
327 document_counts,
328 parameter_count: prj.parameters.len(),
329 erc_matrix_rows: prj.erc_matrix.rows.len(),
330 })
331}
332
333pub fn cmd_documents(
334 path: &Path,
335 doc_type: Option<String>,
336) -> Result<PrjPcbDocumentList, Box<dyn std::error::Error>> {
337 let prj = open_prjpcb(path)?;
338
339 let filter_type = doc_type.as_ref().map(|t| t.to_lowercase());
340
341 let documents: Vec<_> = prj
342 .documents
343 .iter()
344 .filter(|d| {
345 if let Some(ref filter) = filter_type {
346 d.doc_type.display_name().to_lowercase().contains(filter)
347 } else {
348 true
349 }
350 })
351 .map(|d| DocumentDetailInfo {
352 path: d.path.clone(),
353 doc_type: d.doc_type.display_name().to_string(),
354 exists: resolve_document_path(path, &d.path).exists(),
355 annotation_enabled: d.annotation_enabled,
356 library_update: d.do_library_update,
357 })
358 .collect();
359
360 Ok(PrjPcbDocumentList {
361 path: path.display().to_string(),
362 filter: doc_type,
363 total_documents: documents.len(),
364 documents,
365 })
366}
367
368const BLANK_PRJPCB_TEMPLATE: &[u8] = include_bytes!("../../data/Project1.PrjPcb");
374
375pub fn cmd_create(
376 path: &Path,
377 name: Option<String>,
378 template: Option<PathBuf>,
379) -> Result<String, Box<dyn std::error::Error>> {
380 if path.exists() {
381 return Err(format!("File already exists: {}", path.display()).into());
382 }
383
384 let message = match template {
385 Some(template_path) => {
386 std::fs::copy(&template_path, path)?;
387 format!(
388 "Created project from template: {}\n Template: {}",
389 path.display(),
390 template_path.display()
391 )
392 }
393 None => {
394 std::fs::write(path, BLANK_PRJPCB_TEMPLATE)?;
396
397 if let Some(ref project_name) = name {
399 let mut prj = open_prjpcb(path)?;
400 prj.set_name(project_name);
401 prj.save_to_file(path)?;
402 }
403
404 format!("Created new project: {}", path.display())
405 }
406 };
407
408 let prj = open_prjpcb(path)?;
410 Ok(format!(
411 "{}\n Name: {}\n Documents: {}",
412 message,
413 prj.name(),
414 prj.documents.len()
415 ))
416}
417
418pub fn cmd_add_document(path: &Path, document: &str) -> Result<String, Box<dyn std::error::Error>> {
423 let mut prj = open_prjpcb(path)?;
424
425 if prj.get_document(document).is_some() {
427 return Err(format!("Document '{}' already in project", document).into());
428 }
429
430 prj.add_document(document);
431 prj.save_to_file(path)?;
432
433 let doc_type = DocumentType::from_path(document);
434 Ok(format!(
435 "Added {} to project: {}\n Type: {}\n Total documents: {}",
436 document,
437 path.display(),
438 doc_type,
439 prj.documents.len()
440 ))
441}
442
443pub fn cmd_remove_document(
444 path: &Path,
445 document: &str,
446) -> Result<String, Box<dyn std::error::Error>> {
447 let mut prj = open_prjpcb(path)?;
448
449 if !prj.remove_document(document) {
450 return Err(format!("Document '{}' not found in project", document).into());
451 }
452
453 prj.save_to_file(path)?;
454
455 Ok(format!(
456 "Removed {} from project: {}\n Remaining documents: {}",
457 document,
458 path.display(),
459 prj.documents.len()
460 ))
461}
462
463pub fn cmd_parameters(path: &Path) -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
468 let prj = open_prjpcb(path)?;
469 Ok(prj.parameters.clone())
470}
471
472pub fn cmd_set_parameter(
473 path: &Path,
474 name: &str,
475 value: &str,
476) -> Result<String, Box<dyn std::error::Error>> {
477 let mut prj = open_prjpcb(path)?;
478
479 let was_existing = prj.parameters.contains_key(name);
480 prj.set_parameter(name, value);
481 prj.save_to_file(path)?;
482
483 if was_existing {
484 Ok(format!(
485 "Updated parameter '{}' = '{}' in {}",
486 name,
487 value,
488 path.display()
489 ))
490 } else {
491 Ok(format!(
492 "Added parameter '{}' = '{}' to {}",
493 name,
494 value,
495 path.display()
496 ))
497 }
498}
499
500pub fn cmd_remove_parameter(path: &Path, name: &str) -> Result<String, Box<dyn std::error::Error>> {
501 let mut prj = open_prjpcb(path)?;
502
503 if prj.remove_parameter(name).is_none() {
504 return Err(format!("Parameter '{}' not found", name).into());
505 }
506
507 prj.save_to_file(path)?;
508 Ok(format!(
509 "Removed parameter '{}' from {}",
510 name,
511 path.display()
512 ))
513}
514
515pub fn cmd_netlist(path: &Path) -> Result<PrjPcbNetlist, Box<dyn std::error::Error>> {
520 let prj = open_prjpcb(path)?;
521 let nets = extract_nets(&prj, path)?;
522
523 let net_infos: Vec<NetInfo> = nets
524 .into_iter()
525 .map(|net| NetInfo {
526 name: net.name,
527 pins: net
528 .pins
529 .into_iter()
530 .map(|pin| NetPinConnection {
531 component: pin.component,
532 pin: pin.pin,
533 })
534 .collect(),
535 })
536 .collect();
537
538 Ok(PrjPcbNetlist {
539 path: path.display().to_string(),
540 total_nets: net_infos.len(),
541 nets: net_infos,
542 })
543}
544
545pub fn cmd_components(path: &Path) -> Result<PrjPcbComponentList, Box<dyn std::error::Error>> {
546 let prj = open_prjpcb(path)?;
547 let components = extract_components(&prj, path)?;
548
549 let component_infos: Vec<SchematicComponentInfo> = components
550 .into_iter()
551 .map(|comp| SchematicComponentInfo {
552 designator: comp.designator,
553 lib_reference: comp.lib_reference,
554 description: comp.description,
555 footprint: comp.footprint,
556 value: comp.value,
557 sheet: comp.sheet,
558 parameters: comp.parameters,
559 })
560 .collect();
561
562 Ok(PrjPcbComponentList {
563 path: path.display().to_string(),
564 total_components: component_infos.len(),
565 components: component_infos,
566 })
567}
568
569pub fn cmd_bom(path: &Path, grouped: bool) -> Result<PrjPcbBom, Box<dyn std::error::Error>> {
570 let prj = open_prjpcb(path)?;
571 let components = extract_components(&prj, path)?;
572
573 let items = if grouped {
574 let mut groups: HashMap<String, Vec<&SchematicComponent>> = HashMap::new();
576 for comp in &components {
577 groups
578 .entry(comp.lib_reference.clone())
579 .or_default()
580 .push(comp);
581 }
582
583 let mut group_items: Vec<_> = groups
584 .into_iter()
585 .map(|(lib_ref, comps)| BomGroupItem {
586 lib_reference: lib_ref,
587 quantity: comps.len(),
588 designators: comps.iter().map(|c| c.designator.clone()).collect(),
589 })
590 .collect();
591 group_items.sort_by(|a, b| b.quantity.cmp(&a.quantity));
592
593 BomItems::Grouped(group_items)
594 } else {
595 let component_infos: Vec<SchematicComponentInfo> = components
596 .into_iter()
597 .map(|comp| SchematicComponentInfo {
598 designator: comp.designator,
599 lib_reference: comp.lib_reference,
600 description: comp.description,
601 footprint: comp.footprint,
602 value: comp.value,
603 sheet: comp.sheet,
604 parameters: comp.parameters,
605 })
606 .collect();
607
608 BomItems::Individual(component_infos)
609 };
610
611 let (total_components, unique_parts) = match &items {
612 BomItems::Grouped(groups) => {
613 let total = groups.iter().map(|g| g.quantity).sum();
614 (total, Some(groups.len()))
615 }
616 BomItems::Individual(comps) => (comps.len(), None),
617 };
618
619 Ok(PrjPcbBom {
620 path: path.display().to_string(),
621 total_components,
622 unique_parts,
623 items,
624 })
625}
626
627pub fn cmd_import_to_pcb(
632 path: &Path,
633 pcb: Option<String>,
634 dry_run: bool,
635) -> Result<String, Box<dyn std::error::Error>> {
636 let prj = open_prjpcb(path)?;
637
638 let pcb_doc = if let Some(ref pcb_path) = pcb {
640 prj.get_document(pcb_path)
641 .ok_or_else(|| format!("PCB document '{}' not found in project", pcb_path))?
642 } else {
643 prj.primary_pcb()
644 .ok_or_else(|| "No PCB document found in project".to_string())?
645 };
646
647 let pcb_path_str = pcb_doc.path.clone();
648
649 let components = extract_components(&prj, path)?;
651 let nets = extract_nets(&prj, path)?;
652
653 if dry_run {
654 let mut message = format!("Import to PCB: {}\n", path.display());
655 message.push_str(&format!("Target PCB: {}\n", pcb_path_str));
656 message.push_str(&format!("Components to import: {}\n", components.len()));
657 message.push_str(&format!("Nets to import: {}\n\n", nets.len()));
658 message.push_str("[DRY RUN - No changes will be made]\n\n");
659 message.push_str("Components that would be added:\n");
660 for comp in components.iter().take(20) {
661 message.push_str(&format!(" {} - {}\n", comp.designator, comp.lib_reference));
662 }
663 if components.len() > 20 {
664 message.push_str(&format!(" ... and {} more\n", components.len() - 20));
665 }
666 message.push_str("\nNets that would be added:\n");
667 for net in nets.iter().take(20) {
668 message.push_str(&format!(" {}\n", net.name));
669 }
670 if nets.len() > 20 {
671 message.push_str(&format!(" ... and {} more", nets.len() - 20));
672 }
673 Ok(message)
674 } else {
675 let full_pcb_path = resolve_document_path(path, &pcb_path_str);
677 let mut pcbdoc = open_pcbdoc(path, &pcb_path_str)?;
678
679 let existing_designators: HashSet<_> = pcbdoc
681 .components
682 .iter()
683 .map(|c| c.designator.clone())
684 .collect();
685 let existing_nets: HashSet<_> = pcbdoc.nets.iter().cloned().collect();
686
687 let mut added_components = 0;
688 let mut added_nets = 0;
689
690 for comp in &components {
692 if !existing_designators.contains(&comp.designator) {
693 added_components += 1;
695 }
696 }
697
698 for net in &nets {
700 if !existing_nets.contains(&net.name) {
701 pcbdoc.nets.push(net.name.clone());
702 added_nets += 1;
703 }
704 }
705
706 if added_components > 0 || added_nets > 0 {
708 pcbdoc.save_to_file(&full_pcb_path)?;
709 Ok(format!(
710 "Import complete:\n Added {} nets\n Components: {} (component placement not yet implemented)",
711 added_nets, added_components
712 ))
713 } else {
714 Ok("No changes needed - PCB already up to date".to_string())
715 }
716 }
717}
718
719pub fn cmd_sync_to_pcb(
720 path: &Path,
721 pcb: Option<String>,
722 dry_run: bool,
723) -> Result<String, Box<dyn std::error::Error>> {
724 cmd_import_to_pcb(path, pcb, dry_run)
726}
727
728pub fn cmd_diff_sch_pcb(
729 path: &Path,
730 pcb: Option<String>,
731) -> Result<PrjPcbSchPcbDiff, Box<dyn std::error::Error>> {
732 let prj = open_prjpcb(path)?;
733
734 let pcb_doc = if let Some(ref pcb_path) = pcb {
736 prj.get_document(pcb_path)
737 .ok_or_else(|| format!("PCB document '{}' not found in project", pcb_path))?
738 } else {
739 prj.primary_pcb()
740 .ok_or_else(|| "No PCB document found in project".to_string())?
741 };
742
743 let pcb_path_str = pcb_doc.path.clone();
744
745 let sch_components = extract_components(&prj, path)?;
747 let sch_nets = extract_nets(&prj, path)?;
748
749 let pcbdoc = open_pcbdoc(path, &pcb_path_str)?;
751
752 let sch_designators: HashSet<_> = sch_components.iter().map(|c| &c.designator).collect();
753 let pcb_designators: HashSet<_> = pcbdoc.components.iter().map(|c| &c.designator).collect();
754
755 let sch_net_names: HashSet<_> = sch_nets.iter().map(|n| &n.name).collect();
756 let pcb_net_names: HashSet<_> = pcbdoc.nets.iter().collect();
757
758 let only_in_schematic: Vec<String> = sch_designators
760 .difference(&pcb_designators)
761 .map(|s| s.to_string())
762 .collect();
763 let only_in_pcb: Vec<String> = pcb_designators
765 .difference(&sch_designators)
766 .map(|s| s.to_string())
767 .collect();
768
769 let nets_only_in_schematic: Vec<String> = sch_net_names
771 .difference(&pcb_net_names)
772 .map(|s| s.to_string())
773 .collect();
774 let nets_only_in_pcb: Vec<String> = pcb_net_names
776 .difference(&sch_net_names)
777 .map(|s| s.to_string())
778 .collect();
779
780 Ok(PrjPcbSchPcbDiff {
781 path: path.display().to_string(),
782 pcb_document: pcb_path_str,
783 schematic_components: sch_components.len(),
784 pcb_components: pcbdoc.components.len(),
785 only_in_schematic,
786 only_in_pcb,
787 schematic_nets: sch_nets.len(),
788 pcb_nets: pcbdoc.nets.len(),
789 nets_only_in_schematic,
790 nets_only_in_pcb,
791 })
792}
793
794pub fn cmd_validate(
799 path: &Path,
800 check_files: bool,
801) -> Result<PrjPcbValidation, Box<dyn std::error::Error>> {
802 let prj = open_prjpcb(path)?;
803
804 let mut errors = Vec::new();
805 let mut warnings = Vec::new();
806
807 if prj.name().is_empty() || prj.name() == "Unnamed" {
809 warnings.push("Project has no name defined".to_string());
810 }
811
812 if prj.documents.is_empty() {
813 warnings.push("Project has no documents".to_string());
814 }
815
816 if prj.schematics().is_empty() {
818 warnings.push("Project has no schematic documents".to_string());
819 }
820 if prj.pcb_documents().is_empty() {
821 warnings.push("Project has no PCB documents".to_string());
822 }
823
824 let mut seen_paths = HashSet::new();
826 for doc in &prj.documents {
827 if !seen_paths.insert(&doc.path) {
828 errors.push(format!("Duplicate document path: {}", doc.path));
829 }
830 }
831
832 if check_files {
834 for doc in &prj.documents {
835 let full_path = resolve_document_path(path, &doc.path);
836 if !full_path.exists() {
837 errors.push(format!("Missing document: {}", doc.path));
838 }
839 }
840 }
841
842 Ok(PrjPcbValidation {
843 path: path.display().to_string(),
844 errors,
845 warnings,
846 })
847}
848
849pub fn cmd_json(
854 path: &Path,
855 full: bool,
856 pretty: bool,
857) -> Result<String, Box<dyn std::error::Error>> {
858 use serde::Serialize;
859
860 let prj = open_prjpcb(path)?;
861
862 #[derive(Serialize)]
863 struct DocumentJson {
864 path: String,
865 doc_type: String,
866 annotation_enabled: bool,
867 }
868
869 #[derive(Serialize)]
870 struct ProjectJson {
871 name: String,
872 version: String,
873 hierarchy_mode: i32,
874 output_path: String,
875 documents: Vec<DocumentJson>,
876 #[serde(skip_serializing_if = "HashMap::is_empty")]
877 parameters: HashMap<String, String>,
878 }
879
880 let documents: Vec<_> = prj
881 .documents
882 .iter()
883 .map(|d| DocumentJson {
884 path: d.path.clone(),
885 doc_type: d.doc_type.display_name().to_string(),
886 annotation_enabled: d.annotation_enabled,
887 })
888 .collect();
889
890 let output = ProjectJson {
891 name: prj.name(),
892 version: prj.version.clone(),
893 hierarchy_mode: prj.hierarchy_mode,
894 output_path: prj.output_path.clone(),
895 documents,
896 parameters: if full {
897 prj.parameters.clone()
898 } else {
899 HashMap::new()
900 },
901 };
902
903 let json = if pretty {
904 serde_json::to_string_pretty(&output)?
905 } else {
906 serde_json::to_string(&output)?
907 };
908
909 Ok(json)
910}