1use serde::Serialize;
7use std::path::{Path, PathBuf};
8use syster::hir::{Severity, check_file};
9use syster::ide::AnalysisHost;
10use walkdir::WalkDir;
11
12#[derive(Debug, Serialize)]
14pub struct AnalysisResult {
15 pub file_count: usize,
17 pub symbol_count: usize,
19 pub error_count: usize,
21 pub warning_count: usize,
23 pub diagnostics: Vec<DiagnosticInfo>,
25}
26
27#[derive(Debug, Clone, Serialize)]
29pub struct DiagnosticInfo {
30 pub file: String,
32 pub line: u32,
34 pub col: u32,
36 pub end_line: u32,
38 pub end_col: u32,
40 pub message: String,
42 #[serde(serialize_with = "serialize_severity")]
44 pub severity: Severity,
45 pub code: Option<String>,
47}
48
49fn serialize_severity<S>(severity: &Severity, serializer: S) -> Result<S::Ok, S::Error>
51where
52 S: serde::Serializer,
53{
54 let s = match severity {
55 Severity::Error => "error",
56 Severity::Warning => "warning",
57 Severity::Info => "info",
58 Severity::Hint => "hint",
59 };
60 serializer.serialize_str(s)
61}
62
63pub fn run_analysis(
74 input: &Path,
75 verbose: bool,
76 load_stdlib: bool,
77 stdlib_path: Option<&Path>,
78) -> Result<AnalysisResult, String> {
79 let mut host = AnalysisHost::new();
80
81 if load_stdlib {
83 load_stdlib_files(&mut host, stdlib_path, verbose)?;
84 }
85
86 load_input(&mut host, input, verbose)?;
88
89 let _analysis = host.analysis();
91
92 let diagnostics = collect_diagnostics(&host);
94
95 let error_count = diagnostics
97 .iter()
98 .filter(|d| matches!(d.severity, Severity::Error))
99 .count();
100 let warning_count = diagnostics
101 .iter()
102 .filter(|d| matches!(d.severity, Severity::Warning))
103 .count();
104
105 Ok(AnalysisResult {
106 file_count: host.file_count(),
107 symbol_count: host.symbol_index().all_symbols().count(),
108 error_count,
109 warning_count,
110 diagnostics,
111 })
112}
113
114pub fn load_input(host: &mut AnalysisHost, input: &Path, verbose: bool) -> Result<(), String> {
116 if input.is_file() {
117 load_file(host, input, verbose)
118 } else if input.is_dir() {
119 load_directory(host, input, verbose)
120 } else {
121 Err(format!("Path does not exist: {}", input.display()))
122 }
123}
124
125fn load_file(host: &mut AnalysisHost, path: &Path, verbose: bool) -> Result<(), String> {
127 if verbose {
128 println!(" Loading: {}", path.display());
129 }
130
131 let content = std::fs::read_to_string(path)
132 .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
133
134 let path_str = path.to_string_lossy();
135 let parse_errors = host.set_file_content(&path_str, &content);
136
137 for err in parse_errors {
139 eprintln!(
140 "parse error: {}:{}:{}: {}",
141 path.display(),
142 err.position.line,
143 err.position.column,
144 err.message
145 );
146 }
147
148 Ok(())
149}
150
151fn load_directory(host: &mut AnalysisHost, dir: &Path, verbose: bool) -> Result<(), String> {
153 if verbose {
154 println!("Scanning directory: {}", dir.display());
155 }
156
157 for entry in WalkDir::new(dir).follow_links(true) {
158 let entry = entry.map_err(|e| format!("Walk error: {}", e))?;
159 let path = entry.path();
160
161 if is_sysml_file(path) {
162 load_file(host, path, verbose)?;
163 }
164 }
165
166 Ok(())
167}
168
169fn is_sysml_file(path: &Path) -> bool {
171 path.is_file()
172 && matches!(
173 path.extension().and_then(|e| e.to_str()),
174 Some("sysml") | Some("kerml")
175 )
176}
177
178pub fn load_stdlib_files(
180 host: &mut AnalysisHost,
181 custom_path: Option<&Path>,
182 verbose: bool,
183) -> Result<(), String> {
184 if verbose {
185 println!("Loading standard library...");
186 }
187
188 if let Some(path) = custom_path {
190 if path.exists() {
191 return load_directory(host, path, verbose);
192 } else {
193 return Err(format!("Stdlib path does not exist: {}", path.display()));
194 }
195 }
196
197 let default_paths = [
199 PathBuf::from("sysml.library"),
200 PathBuf::from("../sysml.library"),
201 PathBuf::from("../base/sysml.library"),
202 ];
203
204 for path in &default_paths {
205 if path.exists() {
206 return load_directory(host, path, verbose);
207 }
208 }
209
210 if verbose {
211 println!(" Warning: Standard library not found");
212 }
213
214 Ok(())
215}
216
217fn collect_diagnostics(host: &AnalysisHost) -> Vec<DiagnosticInfo> {
219 let mut all_diagnostics = Vec::new();
220
221 for path in host.files().keys() {
222 if let Some(file_id) = host.get_file_id_for_path(path) {
223 let file_path = path.to_string_lossy().to_string();
224 let diagnostics = check_file(host.symbol_index(), file_id);
225
226 for diag in diagnostics {
227 all_diagnostics.push(DiagnosticInfo {
228 file: file_path.clone(),
229 line: diag.start_line + 1, col: diag.start_col + 1,
231 end_line: diag.end_line + 1,
232 end_col: diag.end_col + 1,
233 message: diag.message.to_string(),
234 severity: diag.severity,
235 code: diag.code.map(|c| c.to_string()),
236 });
237 }
238 }
239 }
240
241 all_diagnostics.sort_by(|a, b| (&a.file, a.line, a.col).cmp(&(&b.file, b.line, b.col)));
243
244 all_diagnostics
245}
246
247#[derive(Debug, Serialize)]
253pub struct ExportSymbol {
254 pub name: String,
255 pub qualified_name: String,
256 pub kind: String,
257 pub file: String,
258 pub start_line: u32,
259 pub start_col: u32,
260 pub end_line: u32,
261 pub end_col: u32,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub doc: Option<String>,
264 #[serde(skip_serializing_if = "Vec::is_empty")]
265 pub supertypes: Vec<String>,
266}
267
268#[derive(Debug, Serialize)]
270pub struct AstExport {
271 pub files: Vec<FileAst>,
272}
273
274#[derive(Debug, Serialize)]
276pub struct FileAst {
277 pub path: String,
278 pub symbols: Vec<ExportSymbol>,
279}
280
281pub fn export_ast(
283 input: &Path,
284 verbose: bool,
285 load_stdlib: bool,
286 stdlib_path: Option<&Path>,
287) -> Result<String, String> {
288 let mut host = AnalysisHost::new();
289
290 if load_stdlib {
291 load_stdlib_files(&mut host, stdlib_path, verbose)?;
292 }
293
294 load_input(&mut host, input, verbose)?;
295 let _analysis = host.analysis();
296
297 let mut files = Vec::new();
298
299 for path in host.files().keys() {
301 let path_str = path.to_string_lossy().to_string();
302
303 if path_str.contains("sysml.library") {
305 continue;
306 }
307
308 if let Some(file_id) = host.get_file_id_for_path(path) {
309 let symbols: Vec<ExportSymbol> = host
310 .symbol_index()
311 .symbols_in_file(file_id)
312 .into_iter()
313 .map(|sym| ExportSymbol {
314 name: sym.name.to_string(),
315 qualified_name: sym.qualified_name.to_string(),
316 kind: format!("{:?}", sym.kind),
317 file: path_str.clone(),
318 start_line: sym.start_line + 1,
319 start_col: sym.start_col + 1,
320 end_line: sym.end_line + 1,
321 end_col: sym.end_col + 1,
322 doc: sym.doc.as_ref().map(|d| d.to_string()),
323 supertypes: sym.supertypes.iter().map(|s| s.to_string()).collect(),
324 })
325 .collect();
326
327 files.push(FileAst {
328 path: path_str,
329 symbols,
330 });
331 }
332 }
333
334 files.sort_by(|a, b| a.path.cmp(&b.path));
336
337 let export = AstExport { files };
338 serde_json::to_string_pretty(&export).map_err(|e| format!("Failed to serialize AST: {}", e))
339}
340
341pub fn export_json(result: &AnalysisResult) -> Result<String, String> {
343 serde_json::to_string_pretty(result).map_err(|e| format!("Failed to serialize result: {}", e))
344}
345
346#[cfg(feature = "interchange")]
367pub fn export_model(
368 input: &Path,
369 format: &str,
370 verbose: bool,
371 load_stdlib: bool,
372 stdlib_path: Option<&Path>,
373 self_contained: bool,
374) -> Result<Vec<u8>, String> {
375 use syster::interchange::{
376 JsonLd, Kpar, ModelFormat, Xmi, Yaml, model_from_symbols, restore_ids_from_symbols,
377 };
378
379 let mut host = AnalysisHost::new();
380
381 if load_stdlib {
383 load_stdlib_files(&mut host, stdlib_path, verbose)?;
384 }
385
386 load_input(&mut host, input, verbose)?;
388
389 #[cfg(feature = "interchange")]
391 {
392 use syster::project::WorkspaceLoader;
393 let loader = WorkspaceLoader::new();
394
395 if input.is_file() {
397 let parent_dir = input.parent().unwrap_or(input);
398 if let Err(e) = loader.load_metadata_from_directory(parent_dir, &mut host) {
399 if verbose {
400 eprintln!("Note: Could not load metadata: {}", e);
401 }
402 } else if verbose {
403 println!("Loaded metadata from {}", parent_dir.display());
404 }
405 } else if input.is_dir() {
406 if let Err(e) = loader.load_metadata_from_directory(input, &mut host) {
408 if verbose {
409 eprintln!("Note: Could not load metadata: {}", e);
410 }
411 } else if verbose {
412 println!("Loaded metadata from {}", input.display());
413 }
414 }
415 }
416
417 let analysis = host.analysis();
419
420 let symbols: Vec<_> = if self_contained {
422 analysis.symbol_index().all_symbols().cloned().collect()
424 } else {
425 analysis
427 .symbol_index()
428 .all_symbols()
429 .filter(|sym| {
430 if let Some(path) = analysis.get_file_path(sym.file) {
432 !path.contains("sysml.library")
433 } else {
434 true }
436 })
437 .cloned()
438 .collect()
439 };
440
441 if verbose {
442 println!(
443 "Collecting {} symbols (self_contained={})",
444 symbols.len(),
445 self_contained
446 );
447 }
448
449 let mut model = model_from_symbols(&symbols);
451
452 model = restore_ids_from_symbols(model, analysis.symbol_index());
454 if verbose {
455 println!("Restored element IDs from symbol database");
456 }
457
458 if verbose {
459 println!(
460 "Exported model: {} elements, {} relationships",
461 model.elements.len(),
462 model.relationship_count()
463 );
464 }
465
466 match format.to_lowercase().as_str() {
468 "xmi" => Xmi.write(&model).map_err(|e| e.to_string()),
469 "kpar" => Kpar.write(&model).map_err(|e| e.to_string()),
470 "jsonld" | "json-ld" => JsonLd.write(&model).map_err(|e| e.to_string()),
471 "yaml" | "yml" => Yaml.write(&model).map_err(|e| e.to_string()),
472 _ => Err(format!(
473 "Unsupported format: {}. Use xmi, kpar, jsonld, or yaml.",
474 format
475 )),
476 }
477}
478
479#[cfg(feature = "interchange")]
487pub fn export_from_host(
488 host: &mut AnalysisHost,
489 format: &str,
490 verbose: bool,
491 self_contained: bool,
492) -> Result<Vec<u8>, String> {
493 use syster::interchange::{
494 JsonLd, Kpar, ModelFormat, Xmi, Yaml, model_from_symbols, restore_ids_from_symbols,
495 };
496
497 let analysis = host.analysis();
498 let symbols: Vec<_> = if self_contained {
499 analysis.symbol_index().all_symbols().cloned().collect()
500 } else {
501 analysis
502 .symbol_index()
503 .all_symbols()
504 .filter(|sym| {
505 if let Some(path) = analysis.get_file_path(sym.file) {
506 !path.contains("sysml.library")
507 } else {
508 true
509 }
510 })
511 .cloned()
512 .collect()
513 };
514
515 if verbose {
516 println!(
517 "Collecting {} symbols (self_contained={})",
518 symbols.len(),
519 self_contained
520 );
521 }
522
523 let mut model = model_from_symbols(&symbols);
524 model = restore_ids_from_symbols(model, analysis.symbol_index());
525
526 if verbose {
527 println!(
528 "Exported model: {} elements, {} relationships",
529 model.elements.len(),
530 model.relationship_count()
531 );
532 }
533
534 match format.to_lowercase().as_str() {
535 "xmi" => Xmi.write(&model).map_err(|e| e.to_string()),
536 "kpar" => Kpar.write(&model).map_err(|e| e.to_string()),
537 "jsonld" | "json-ld" => JsonLd.write(&model).map_err(|e| e.to_string()),
538 "yaml" | "yml" => Yaml.write(&model).map_err(|e| e.to_string()),
539 _ => Err(format!(
540 "Unsupported format: {}. Use xmi, kpar, jsonld, or yaml.",
541 format
542 )),
543 }
544}
545
546#[cfg(feature = "interchange")]
548#[derive(Debug)]
549pub struct ImportResult {
550 pub element_count: usize,
552 pub relationship_count: usize,
554 pub error_count: usize,
556 pub warning_count: usize,
558 pub messages: Vec<String>,
560}
561
562#[cfg(feature = "interchange")]
580pub fn import_model_into_host(
581 host: &mut AnalysisHost,
582 input: &Path,
583 format: Option<&str>,
584 verbose: bool,
585) -> Result<ImportResult, String> {
586 use syster::interchange::{JsonLd, Kpar, ModelFormat, Xmi, detect_format};
587
588 let bytes =
590 std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
591
592 let format_str = format.map(String::from).unwrap_or_else(|| {
594 input
595 .extension()
596 .and_then(|e| e.to_str())
597 .unwrap_or("xmi")
598 .to_string()
599 });
600
601 if verbose {
602 println!(
603 "Importing {} as {} into workspace",
604 input.display(),
605 format_str
606 );
607 }
608
609 let model = match format_str.to_lowercase().as_str() {
611 "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
612 "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
613 "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
614 _ => {
615 if let Some(format_impl) = detect_format(input) {
617 format_impl.read(&bytes).map_err(|e| e.to_string())?
618 } else {
619 return Err(format!(
620 "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
621 format_str
622 ));
623 }
624 }
625 };
626
627 let element_count = model.elements.len();
628 let relationship_count = model.relationship_count();
629
630 if verbose {
631 println!(
632 "Parsed {} elements and {} relationships",
633 element_count, relationship_count
634 );
635 }
636
637 let virtual_path = input.to_string_lossy().to_string() + ".sysml";
640 let errors = host.add_model(&model, &virtual_path);
641
642 if verbose {
643 if errors.is_empty() {
644 println!("Loaded model into workspace with preserved element IDs");
645 } else {
646 println!("Loaded model with {} parse warnings", errors.len());
647 }
648 }
649
650 Ok(ImportResult {
651 element_count,
652 relationship_count,
653 error_count: errors.len(),
654 warning_count: 0,
655 messages: vec![format!("Successfully imported {} elements", element_count)],
656 })
657}
658
659#[cfg(feature = "interchange")]
672pub fn import_model(
673 input: &Path,
674 format: Option<&str>,
675 verbose: bool,
676) -> Result<ImportResult, String> {
677 use syster::interchange::{JsonLd, Kpar, ModelFormat, Xmi, detect_format};
678
679 let bytes =
681 std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
682
683 let format_str = format.map(String::from).unwrap_or_else(|| {
685 input
686 .extension()
687 .and_then(|e| e.to_str())
688 .unwrap_or("xmi")
689 .to_string()
690 });
691
692 if verbose {
693 println!("Importing {} as {}", input.display(), format_str);
694 }
695
696 let model = match format_str.to_lowercase().as_str() {
698 "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
699 "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
700 "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
701 _ => {
702 if let Some(format_impl) = detect_format(input) {
704 format_impl.read(&bytes).map_err(|e| e.to_string())?
705 } else {
706 return Err(format!(
707 "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
708 format_str
709 ));
710 }
711 }
712 };
713
714 let mut messages = Vec::new();
716 let mut error_count = 0;
717 let mut warning_count = 0;
718
719 for rel in model.iter_relationship_elements() {
725 if let Some(rd) = &rel.relationship {
726 if let Some(src) = rd.source.first() {
727 if model.elements.get(src).is_none() {
728 messages.push(format!("Error: Relationship source '{}' not found", src));
729 error_count += 1;
730 }
731 }
732 if let Some(tgt) = rd.target.first() {
733 let target_el = model.elements.get(tgt);
734 let is_missing = target_el.is_none();
735 let is_external_stub = tgt.as_str().starts_with("_ext_");
739 if is_missing || is_external_stub {
740 let name = target_el
741 .and_then(|el| el.name.as_deref())
742 .unwrap_or(tgt.as_str());
743 messages.push(format!(
744 "Warning: Relationship target '{}' not found (may be a stdlib type)",
745 name
746 ));
747 warning_count += 1;
748 }
749 }
750 }
751 }
752
753 if verbose {
754 println!(
755 "Imported: {} elements, {} relationships, {} errors, {} warnings",
756 model.elements.len(),
757 model.relationship_count(),
758 error_count,
759 warning_count
760 );
761 for msg in &messages {
762 println!(" {}", msg);
763 }
764 }
765
766 Ok(ImportResult {
767 element_count: model.elements.len(),
768 relationship_count: model.relationship_count(),
769 error_count,
770 warning_count,
771 messages,
772 })
773}
774
775#[cfg(feature = "interchange")]
781#[derive(Debug, Serialize)]
782pub struct QueryResult {
783 pub match_count: usize,
785 pub elements: Vec<ElementInfo>,
787}
788
789#[cfg(feature = "interchange")]
791#[derive(Debug, Clone, Serialize)]
792pub struct ElementInfo {
793 pub id: String,
795 pub name: Option<String>,
797 pub qualified_name: Option<String>,
799 pub kind: String,
801 pub is_abstract: bool,
803 pub owner: Option<String>,
805 pub owned_member_count: usize,
807 pub typed_by: Vec<String>,
809 pub supertypes: Vec<String>,
811 pub documentation: Option<String>,
813}
814
815#[cfg(feature = "interchange")]
817#[derive(Debug, Serialize)]
818pub struct InspectResult {
819 pub element: ElementInfo,
821 pub children: Vec<ElementInfo>,
823 pub relationships_from: Vec<RelationshipInfo>,
825 pub relationships_to: Vec<RelationshipInfo>,
827}
828
829#[cfg(feature = "interchange")]
831#[derive(Debug, Clone, Serialize)]
832pub struct RelationshipInfo {
833 pub kind: String,
835 pub source: String,
837 pub target: String,
839}
840
841#[cfg(feature = "interchange")]
843#[derive(Debug)]
844pub struct RenameResult {
845 pub old_name: String,
847 pub new_name: String,
849 pub rendered_text: String,
851 pub metadata_json: Option<String>,
853}
854
855#[cfg(feature = "interchange")]
863fn load_model_from_file(
864 input: &Path,
865 verbose: bool,
866) -> Result<(syster::ide::AnalysisHost, String), String> {
867 let source = std::fs::read_to_string(input)
868 .map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
869
870 let path_str = input.to_string_lossy().to_string();
871 let mut host = syster::ide::AnalysisHost::new();
872 let errors = host.set_file_content(&path_str, &source);
873
874 if !errors.is_empty() && verbose {
875 eprintln!("Parse warnings: {}", errors.len());
876 }
877
878 let model = host.take_model().unwrap();
881 let model = load_companion_metadata(input, model, verbose);
882 host.set_model_cache(model);
883
884 if verbose {
885 eprintln!(
886 "Loaded {} elements, {} relationships",
887 host.model().element_count(),
888 host.model().relationship_count()
889 );
890 }
891
892 Ok((host, source))
893}
894
895#[cfg(feature = "interchange")]
901fn load_companion_metadata(
902 input: &Path,
903 model: syster::interchange::model::Model,
904 verbose: bool,
905) -> syster::interchange::model::Model {
906 use syster::interchange::metadata::ImportMetadata;
907 use syster::interchange::recompile::restore_element_ids;
908
909 let metadata_path = input.with_extension("metadata.json");
910 if metadata_path.exists() {
911 match ImportMetadata::read_from_file(&metadata_path) {
912 Ok(metadata) => {
913 if verbose {
914 eprintln!(
915 "Loaded metadata from {} ({} elements)",
916 metadata_path.display(),
917 metadata.elements.len()
918 );
919 }
920 restore_element_ids(model, &metadata)
921 }
922 Err(e) => {
923 if verbose {
924 eprintln!(
925 "Note: Could not load metadata from {}: {}",
926 metadata_path.display(),
927 e
928 );
929 }
930 model
931 }
932 }
933 } else {
934 model
935 }
936}
937
938#[cfg(feature = "interchange")]
943fn build_updated_metadata(
944 model: &syster::interchange::model::Model,
945 source_path: &Path,
946) -> Option<String> {
947 use syster::interchange::metadata::{ElementMeta, ImportMetadata, SourceInfo};
948
949 let mut metadata = ImportMetadata::new().with_source(SourceInfo::from_path(
950 source_path.to_string_lossy().to_string(),
951 ));
952
953 for element in model.elements.values() {
954 if let Some(qn) = &element.qualified_name {
955 let meta = ElementMeta::with_id(element.id.as_str());
956 metadata.add_element(qn.as_ref(), meta);
957 }
958 }
959
960 serde_json::to_string_pretty(&metadata).ok()
961}
962
963#[cfg(feature = "interchange")]
976pub fn query_model(
977 input: &Path,
978 name: Option<&str>,
979 kind: Option<&str>,
980 qualified_name: Option<&str>,
981 verbose: bool,
982) -> Result<QueryResult, String> {
983 let (mut host, _source) = load_model_from_file(input, verbose)?;
984
985 let model = host.model();
987
988 let views: Vec<_> = if let Some(qn) = qualified_name {
990 model.find_by_qualified_name(qn).into_iter().collect()
992 } else {
993 model
995 .elements
996 .values()
997 .filter_map(|el| {
998 let view = model.view(&el.id)?;
999
1000 if let Some(n) = name {
1002 let matches_name = view.name().map(|vn| vn.contains(n)).unwrap_or(false);
1003 if !matches_name {
1004 return None;
1005 }
1006 }
1007
1008 if let Some(k) = kind {
1010 let kind_str = format!("{:?}", view.kind());
1011 if !kind_str.eq_ignore_ascii_case(k) {
1012 return None;
1013 }
1014 }
1015
1016 if kind.is_none() && view.kind().is_relationship() {
1018 return None;
1019 }
1020
1021 Some(view)
1022 })
1023 .collect()
1024 };
1025
1026 let elements: Vec<ElementInfo> = views.iter().map(|v| element_info(v)).collect();
1027
1028 Ok(QueryResult {
1029 match_count: elements.len(),
1030 elements,
1031 })
1032}
1033
1034#[cfg(feature = "interchange")]
1042pub fn inspect_element(input: &Path, target: &str, verbose: bool) -> Result<InspectResult, String> {
1043 let (mut host, _source) = load_model_from_file(input, verbose)?;
1044
1045 let model = host.model();
1046
1047 let view = model
1049 .find_by_qualified_name(target)
1050 .or_else(|| model.find_by_name(target).into_iter().next())
1051 .ok_or_else(|| format!("Element '{}' not found", target))?;
1052
1053 if verbose {
1054 eprintln!("Found: {:?} '{}'", view.kind(), view.name().unwrap_or("?"));
1055 }
1056
1057 let children: Vec<ElementInfo> = view
1058 .owned_members()
1059 .iter()
1060 .map(|c| element_info(c))
1061 .collect();
1062
1063 let relationships_from: Vec<RelationshipInfo> = view
1064 .relationships_from()
1065 .iter()
1066 .map(|r| {
1067 let source_name = r
1068 .source()
1069 .and_then(|sid| model.view(sid))
1070 .and_then(|v| v.name().map(String::from))
1071 .unwrap_or_else(|| {
1072 r.source()
1073 .map(|s| s.as_str().to_string())
1074 .unwrap_or_default()
1075 });
1076 let target_name = r
1077 .target()
1078 .and_then(|tid| model.view(tid))
1079 .and_then(|v| v.name().map(String::from))
1080 .unwrap_or_else(|| {
1081 r.target()
1082 .map(|t| t.as_str().to_string())
1083 .unwrap_or_default()
1084 });
1085 RelationshipInfo {
1086 kind: format!("{:?}", r.kind),
1087 source: source_name,
1088 target: target_name,
1089 }
1090 })
1091 .collect();
1092
1093 let relationships_to: Vec<RelationshipInfo> = view
1094 .relationships_to()
1095 .iter()
1096 .map(|r| {
1097 let source_name = r
1098 .source()
1099 .and_then(|sid| model.view(sid))
1100 .and_then(|v| v.name().map(String::from))
1101 .unwrap_or_else(|| {
1102 r.source()
1103 .map(|s| s.as_str().to_string())
1104 .unwrap_or_default()
1105 });
1106 let target_name = r
1107 .target()
1108 .and_then(|tid| model.view(tid))
1109 .and_then(|v| v.name().map(String::from))
1110 .unwrap_or_else(|| {
1111 r.target()
1112 .map(|t| t.as_str().to_string())
1113 .unwrap_or_default()
1114 });
1115 RelationshipInfo {
1116 kind: format!("{:?}", r.kind),
1117 source: source_name,
1118 target: target_name,
1119 }
1120 })
1121 .collect();
1122
1123 Ok(InspectResult {
1124 element: element_info(&view),
1125 children,
1126 relationships_from,
1127 relationships_to,
1128 })
1129}
1130
1131#[cfg(feature = "interchange")]
1143pub fn rename_element(
1144 input: &Path,
1145 target: &str,
1146 new_name: &str,
1147 verbose: bool,
1148) -> Result<RenameResult, String> {
1149 let (mut host, _source) = load_model_from_file(input, verbose)?;
1150 let path_str = input.to_string_lossy().to_string();
1151
1152 let (old_name, element_id) = {
1154 let model = host.model();
1155 let view = model
1156 .find_by_qualified_name(target)
1157 .or_else(|| model.find_by_name(target).into_iter().next())
1158 .ok_or_else(|| format!("Element '{}' not found", target))?;
1159 let old = view.name().unwrap_or("(anonymous)").to_string();
1160 let id = view.id().clone();
1161 if verbose {
1162 eprintln!("Renaming {:?} '{}' -> '{}'", view.kind(), old, new_name);
1163 }
1164 (old, id)
1165 };
1166
1167 let result = host.apply_model_edit(&path_str, |model, tracker| {
1169 tracker.rename(model, &element_id, new_name);
1170 });
1171
1172 if verbose {
1173 eprintln!("Renamed '{}' -> '{}'", old_name, new_name);
1174 }
1175
1176 let metadata_json = build_updated_metadata(host.model(), input);
1178
1179 Ok(RenameResult {
1180 old_name,
1181 new_name: new_name.to_string(),
1182 rendered_text: result.rendered_text,
1183 metadata_json,
1184 })
1185}
1186
1187#[cfg(feature = "interchange")]
1189#[derive(Debug)]
1190pub struct AddMemberResult {
1191 pub parent_name: String,
1193 pub member_name: String,
1195 pub member_kind: String,
1197 pub member_id: String,
1199 pub rendered_text: String,
1201 pub metadata_json: Option<String>,
1203}
1204
1205#[cfg(feature = "interchange")]
1207#[derive(Debug)]
1208pub struct RemoveMemberResult {
1209 pub removed_name: String,
1211 pub rendered_text: String,
1213 pub metadata_json: Option<String>,
1215}
1216
1217#[cfg(feature = "interchange")]
1231pub fn add_member(
1232 input: &Path,
1233 parent: &str,
1234 member_name: &str,
1235 member_kind: &str,
1236 type_name: Option<&str>,
1237 verbose: bool,
1238) -> Result<AddMemberResult, String> {
1239 use syster::interchange::model::{Element, ElementId, ElementKind as EK};
1240
1241 let (mut host, _source) = load_model_from_file(input, verbose)?;
1242 let path_str = input.to_string_lossy().to_string();
1243
1244 let (parent_id, parent_display_name, new_qn) = {
1246 let model = host.model();
1247 let parent_view = model
1248 .find_by_qualified_name(parent)
1249 .or_else(|| model.find_by_name(parent).into_iter().next())
1250 .ok_or_else(|| format!("Parent element '{}' not found", parent))?;
1251
1252 let pid = parent_view.id().clone();
1253 let pname = parent_view.name().unwrap_or("(anonymous)").to_string();
1254 let parent_qn = parent_view.qualified_name().unwrap_or(pname.as_str());
1255 let qn = format!("{}::{}", parent_qn, member_name);
1256
1257 if verbose {
1258 eprintln!(
1259 "Adding {:?} '{}' to {:?} '{}'",
1260 parse_element_kind(member_kind)
1261 .unwrap_or(syster::interchange::ElementKind::Package),
1262 member_name,
1263 parent_view.kind(),
1264 pname
1265 );
1266 }
1267
1268 (pid, pname, qn)
1269 };
1270
1271 let kind = parse_element_kind(member_kind)?;
1273
1274 let type_id = if let Some(tn) = type_name {
1276 let model = host.model();
1277 Some(
1278 model
1279 .find_by_qualified_name(tn)
1280 .or_else(|| model.find_by_name(tn).into_iter().next())
1281 .map(|v| v.id().clone())
1282 .ok_or_else(|| format!("Type '{}' not found", tn))?,
1283 )
1284 } else {
1285 None
1286 };
1287
1288 let new_id = ElementId::generate();
1290 let new_id_for_closure = new_id.clone();
1291
1292 let result = host.apply_model_edit(&path_str, move |model, tracker| {
1294 let element = Element::new(new_id_for_closure.clone(), kind)
1295 .with_name(member_name)
1296 .with_qualified_name(new_qn);
1297 tracker.add_element(model, element, Some(&parent_id));
1298
1299 if let Some(tid) = type_id {
1301 let rel_id = ElementId::generate();
1302 tracker.add_relationship(model, rel_id, EK::FeatureTyping, new_id_for_closure, tid);
1303 }
1304 });
1305
1306 if verbose {
1307 eprintln!(
1308 "Added {} '{}' to '{}'",
1309 member_kind, member_name, parent_display_name
1310 );
1311 }
1312
1313 let metadata_json = build_updated_metadata(host.model(), input);
1314
1315 Ok(AddMemberResult {
1316 parent_name: parent_display_name,
1317 member_name: member_name.to_string(),
1318 member_kind: member_kind.to_string(),
1319 member_id: new_id.as_str().to_string(),
1320 rendered_text: result.rendered_text,
1321 metadata_json,
1322 })
1323}
1324
1325#[cfg(feature = "interchange")]
1336pub fn remove_member(
1337 input: &Path,
1338 target: &str,
1339 verbose: bool,
1340) -> Result<RemoveMemberResult, String> {
1341 let (mut host, _source) = load_model_from_file(input, verbose)?;
1342 let path_str = input.to_string_lossy().to_string();
1343
1344 let (element_id, removed_name) = {
1346 let model = host.model();
1347 let view = model
1348 .find_by_qualified_name(target)
1349 .or_else(|| model.find_by_name(target).into_iter().next())
1350 .ok_or_else(|| format!("Element '{}' not found", target))?;
1351
1352 let id = view.id().clone();
1353 let name = view.name().unwrap_or("(anonymous)").to_string();
1354 if verbose {
1355 eprintln!("Removing {:?} '{}'", view.kind(), name);
1356 }
1357 (id, name)
1358 };
1359
1360 let result = host.apply_model_edit(&path_str, |model, tracker| {
1362 tracker.remove_element(model, &element_id);
1363 });
1364
1365 if verbose {
1366 eprintln!("Removed '{}'", removed_name);
1367 }
1368
1369 let metadata_json = build_updated_metadata(host.model(), input);
1370
1371 Ok(RemoveMemberResult {
1372 removed_name,
1373 rendered_text: result.rendered_text,
1374 metadata_json,
1375 })
1376}
1377
1378#[cfg(feature = "interchange")]
1380fn parse_element_kind(kind_str: &str) -> Result<syster::interchange::model::ElementKind, String> {
1381 use syster::interchange::model::ElementKind;
1382
1383 match kind_str {
1384 "Namespace" | "namespace" => Ok(ElementKind::Namespace),
1386 "Package" | "package" => Ok(ElementKind::Package),
1387 "LibraryPackage" | "library package" => Ok(ElementKind::LibraryPackage),
1388
1389 "Class" | "class" => Ok(ElementKind::Class),
1391 "DataType" | "datatype" => Ok(ElementKind::DataType),
1392 "Structure" | "struct" => Ok(ElementKind::Structure),
1393 "Association" | "assoc" => Ok(ElementKind::Association),
1394 "AssociationStructure" | "assoc struct" => Ok(ElementKind::AssociationStructure),
1395 "Interaction" | "interaction" => Ok(ElementKind::Interaction),
1396 "Behavior" | "behavior" => Ok(ElementKind::Behavior),
1397 "Function" | "function" => Ok(ElementKind::Function),
1398 "Predicate" | "predicate" => Ok(ElementKind::Predicate),
1399
1400 "PartDefinition" | "part def" => Ok(ElementKind::PartDefinition),
1402 "ItemDefinition" | "item def" => Ok(ElementKind::ItemDefinition),
1403 "ActionDefinition" | "action def" => Ok(ElementKind::ActionDefinition),
1404 "PortDefinition" | "port def" => Ok(ElementKind::PortDefinition),
1405 "AttributeDefinition" | "attribute def" => Ok(ElementKind::AttributeDefinition),
1406 "ConnectionDefinition" | "connection def" => Ok(ElementKind::ConnectionDefinition),
1407 "InterfaceDefinition" | "interface def" => Ok(ElementKind::InterfaceDefinition),
1408 "AllocationDefinition" | "allocation def" => Ok(ElementKind::AllocationDefinition),
1409 "RequirementDefinition" | "requirement def" => Ok(ElementKind::RequirementDefinition),
1410 "ConstraintDefinition" | "constraint def" => Ok(ElementKind::ConstraintDefinition),
1411 "StateDefinition" | "state def" => Ok(ElementKind::StateDefinition),
1412 "CalculationDefinition" | "calc def" => Ok(ElementKind::CalculationDefinition),
1413 "UseCaseDefinition" | "use case def" => Ok(ElementKind::UseCaseDefinition),
1414 "AnalysisCaseDefinition" | "analysis case def" => Ok(ElementKind::AnalysisCaseDefinition),
1415 "ConcernDefinition" | "concern def" => Ok(ElementKind::ConcernDefinition),
1416 "ViewDefinition" | "view def" => Ok(ElementKind::ViewDefinition),
1417 "ViewpointDefinition" | "viewpoint def" => Ok(ElementKind::ViewpointDefinition),
1418 "RenderingDefinition" | "rendering def" => Ok(ElementKind::RenderingDefinition),
1419 "EnumerationDefinition" | "enum def" => Ok(ElementKind::EnumerationDefinition),
1420 "MetadataDefinition" | "metadata def" => Ok(ElementKind::MetadataDefinition),
1421
1422 "PartUsage" | "part" => Ok(ElementKind::PartUsage),
1424 "ItemUsage" | "item" => Ok(ElementKind::ItemUsage),
1425 "ActionUsage" | "action" => Ok(ElementKind::ActionUsage),
1426 "PortUsage" | "port" => Ok(ElementKind::PortUsage),
1427 "AttributeUsage" | "attribute" => Ok(ElementKind::AttributeUsage),
1428 "ConnectionUsage" | "connection" => Ok(ElementKind::ConnectionUsage),
1429 "InterfaceUsage" | "interface" => Ok(ElementKind::InterfaceUsage),
1430 "AllocationUsage" | "allocation" => Ok(ElementKind::AllocationUsage),
1431 "RequirementUsage" | "requirement" => Ok(ElementKind::RequirementUsage),
1432 "ConstraintUsage" | "constraint" => Ok(ElementKind::ConstraintUsage),
1433 "StateUsage" | "state" => Ok(ElementKind::StateUsage),
1434 "TransitionUsage" | "transition" => Ok(ElementKind::TransitionUsage),
1435 "CalculationUsage" | "calc" => Ok(ElementKind::CalculationUsage),
1436 "ReferenceUsage" | "ref" => Ok(ElementKind::ReferenceUsage),
1437 "OccurrenceUsage" | "occurrence" => Ok(ElementKind::OccurrenceUsage),
1438 "FlowConnectionUsage" | "flow" => Ok(ElementKind::FlowConnectionUsage),
1439 "SuccessionFlowConnectionUsage" | "succession flow" => {
1440 Ok(ElementKind::SuccessionFlowConnectionUsage)
1441 }
1442 "MetadataUsage" | "metadata" => Ok(ElementKind::MetadataUsage),
1443
1444 "Feature" | "feature" => Ok(ElementKind::Feature),
1446 "Step" | "step" => Ok(ElementKind::Step),
1447 "Connector" | "connector" => Ok(ElementKind::Connector),
1448 "BindingConnector" | "binding" => Ok(ElementKind::BindingConnector),
1449 "Succession" | "succession" => Ok(ElementKind::Succession),
1450
1451 "Comment" | "comment" => Ok(ElementKind::Comment),
1453 "Documentation" | "doc" => Ok(ElementKind::Documentation),
1454
1455 _ => Err(format!(
1456 "Unknown element kind: '{}'. Examples: part, part def, attribute, connection def, etc.",
1457 kind_str
1458 )),
1459 }
1460}
1461
1462#[cfg(feature = "interchange")]
1464fn element_info(view: &syster::interchange::views::ElementView<'_>) -> ElementInfo {
1465 ElementInfo {
1466 id: view.id().as_str().to_string(),
1467 name: view.name().map(String::from),
1468 qualified_name: view.qualified_name().map(String::from),
1469 kind: format!("{:?}", view.kind()),
1470 is_abstract: view.is_abstract(),
1471 owner: view.owner().and_then(|o| o.name().map(String::from)),
1472 owned_member_count: view.owned_members().len(),
1473 typed_by: view
1474 .typing()
1475 .iter()
1476 .filter_map(|t| t.name().map(String::from))
1477 .collect(),
1478 supertypes: view
1479 .supertypes()
1480 .iter()
1481 .filter_map(|s| s.name().map(String::from))
1482 .collect(),
1483 documentation: view.documentation().map(String::from),
1484 }
1485}
1486
1487#[cfg(feature = "interchange")]
1489#[derive(Debug)]
1490pub struct DecompileResult {
1491 pub sysml_text: String,
1493 pub metadata_json: String,
1495 pub element_count: usize,
1497 pub source_path: String,
1499}
1500
1501#[cfg(feature = "interchange")]
1515pub fn decompile_model(
1516 input: &Path,
1517 format: Option<&str>,
1518 verbose: bool,
1519) -> Result<DecompileResult, String> {
1520 use syster::interchange::{
1521 JsonLd, Kpar, ModelFormat, SourceInfo, Xmi, decompile_with_source, detect_format,
1522 };
1523
1524 let bytes =
1526 std::fs::read(input).map_err(|e| format!("Failed to read {}: {}", input.display(), e))?;
1527
1528 let format_str = format.map(String::from).unwrap_or_else(|| {
1530 input
1531 .extension()
1532 .and_then(|e| e.to_str())
1533 .unwrap_or("xmi")
1534 .to_string()
1535 });
1536
1537 if verbose {
1538 println!("Decompiling {} as {}", input.display(), format_str);
1539 }
1540
1541 let model = match format_str.to_lowercase().as_str() {
1543 "xmi" | "sysmlx" | "kermlx" => Xmi.read(&bytes).map_err(|e| e.to_string())?,
1544 "kpar" => Kpar.read(&bytes).map_err(|e| e.to_string())?,
1545 "jsonld" | "json-ld" | "json" => JsonLd.read(&bytes).map_err(|e| e.to_string())?,
1546 _ => {
1547 if let Some(format_impl) = detect_format(input) {
1548 format_impl.read(&bytes).map_err(|e| e.to_string())?
1549 } else {
1550 return Err(format!(
1551 "Unknown format: {}. Use xmi, sysmlx, kermlx, kpar, or jsonld.",
1552 format_str
1553 ));
1554 }
1555 }
1556 };
1557
1558 let element_count = model.elements.len();
1559
1560 let source = SourceInfo::from_path(input.to_string_lossy()).with_format(&format_str);
1562
1563 let result = decompile_with_source(&model, source);
1565
1566 if verbose {
1567 println!(
1568 "Decompiled: {} elements -> {} chars of SysML, {} metadata entries",
1569 element_count,
1570 result.text.len(),
1571 result.metadata.elements.len()
1572 );
1573 }
1574
1575 let metadata_json = serde_json::to_string_pretty(&result.metadata)
1577 .map_err(|e| format!("Failed to serialize metadata: {}", e))?;
1578
1579 Ok(DecompileResult {
1580 sysml_text: result.text,
1581 metadata_json,
1582 element_count,
1583 source_path: input.to_string_lossy().to_string(),
1584 })
1585}