ea 0.1.0 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +125 -0
  3. data/Rakefile +12 -4
  4. data/TODO.next/00-publish-blocking-bugs.md +74 -0
  5. data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
  6. data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
  7. data/TODO.next/03-slim-lutaml-uml.md +79 -0
  8. data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
  9. data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
  10. data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
  11. data/TODO.next/07-transformer-registry-in-factory.md +20 -0
  12. data/TODO.next/08-connector-type-registry.md +27 -0
  13. data/TODO.next/09-element-renderer-registry.md +29 -0
  14. data/TODO.next/10-connector-renderer-lsp.md +18 -0
  15. data/TODO.next/11-consolidate-style-knowledge.md +33 -0
  16. data/TODO.next/12-data-driven-from-db-row.md +24 -0
  17. data/TODO.next/13-extract-duplicated-methods.md +17 -0
  18. data/TODO.next/14-remove-dead-code.md +10 -0
  19. data/TODO.next/15-narrow-exception-handling.md +39 -0
  20. data/TODO.next/16-repository-indexes.md +28 -0
  21. data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
  22. data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
  23. data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
  24. data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
  25. data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
  26. data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
  27. data/TODO.next/23-cleanup-idallocator.md +41 -0
  28. data/TODO.next/24-tighten-parity-specs.md +42 -0
  29. data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
  30. data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
  31. data/TODO.next/27-extract-cardinality-module.md +68 -0
  32. data/TODO.next/28-extract-xml-sanitizer.md +51 -0
  33. data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
  34. data/TODO.next/30-struct-return-for-association-end.md +37 -0
  35. data/TODO.next/31-idallocator-specs.md +27 -0
  36. data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
  37. data/TODO.next/33-normalize-lower-cleanup.md +30 -0
  38. data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
  39. data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
  40. data/TODO.next/36-wire-interface-realization.md +50 -0
  41. data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
  42. data/TODO.next/38-xmi-parser-feature-parity.md +75 -0
  43. data/TODO.next/39-ea-spa-cli-command.md +53 -0
  44. data/TODO.next/40-drop-lur-only-check-in-diagrams-extract.md +56 -0
  45. data/config/diagram_styles.yml +200 -0
  46. data/config/model_transformations.yml +266 -0
  47. data/config/qea_schema.yml +1024 -0
  48. data/docs/ea_to_uml_type_mapping.md +89 -0
  49. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  50. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  51. data/examples/lur/basic.lur +0 -0
  52. data/examples/lur/test-output.lur +0 -0
  53. data/examples/lur/test.lur +0 -0
  54. data/examples/lur_basic_usage.rb +221 -0
  55. data/examples/lur_cli_workflow.rb +263 -0
  56. data/examples/lur_statistics.rb +326 -0
  57. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  58. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  59. data/examples/qea/README_qea_parser.adoc +230 -0
  60. data/examples/qea/UmlModel_template.qea +0 -0
  61. data/examples/qea/basic.qea +0 -0
  62. data/examples/qea/simple.qea +0 -0
  63. data/examples/qea/simple_example.qea +0 -0
  64. data/examples/qea/test.qea +0 -0
  65. data/examples/qea_standalone_query.rb +73 -0
  66. data/examples/qea_to_repository.rb +51 -0
  67. data/examples/smoke_test_real_qea.rb +81 -0
  68. data/exe/ea +7 -0
  69. data/lib/ea/cli/app.rb +81 -0
  70. data/lib/ea/cli/command/base.rb +80 -0
  71. data/lib/ea/cli/command/convert.rb +62 -0
  72. data/lib/ea/cli/command/diagrams.rb +75 -0
  73. data/lib/ea/cli/command/list.rb +61 -0
  74. data/lib/ea/cli/command/parse.rb +29 -0
  75. data/lib/ea/cli/command/repository_builder.rb +64 -0
  76. data/lib/ea/cli/command/spa.rb +79 -0
  77. data/lib/ea/cli/command/stats.rb +20 -0
  78. data/lib/ea/cli/command/validate.rb +41 -0
  79. data/lib/ea/cli/command.rb +17 -0
  80. data/lib/ea/cli/error.rb +34 -0
  81. data/lib/ea/cli/output/formatter.rb +34 -0
  82. data/lib/ea/cli/output/json_formatter.rb +20 -0
  83. data/lib/ea/cli/output/table_formatter.rb +42 -0
  84. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  85. data/lib/ea/cli/output.rb +56 -0
  86. data/lib/ea/cli.rb +17 -0
  87. data/lib/ea/diagram/configuration.rb +379 -0
  88. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  89. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  90. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  91. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  92. data/lib/ea/diagram/element_renderers.rb +43 -0
  93. data/lib/ea/diagram/extractor.rb +560 -0
  94. data/lib/ea/diagram/layout_engine.rb +170 -0
  95. data/lib/ea/diagram/path_builder.rb +202 -0
  96. data/lib/ea/diagram/style_parser.rb +42 -0
  97. data/lib/ea/diagram/style_resolver.rb +276 -0
  98. data/lib/ea/diagram/svg_renderer.rb +274 -0
  99. data/lib/ea/diagram/util.rb +73 -0
  100. data/lib/ea/diagram.rb +47 -0
  101. data/lib/ea/qea/benchmark.rb +210 -0
  102. data/lib/ea/qea/database.rb +308 -0
  103. data/lib/ea/qea/factory/association_builder.rb +203 -0
  104. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  105. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  106. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  107. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  108. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  109. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  110. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  111. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  112. data/lib/ea/qea/factory/document_builder.rb +283 -0
  113. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  114. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  115. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  116. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  117. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  118. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  119. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  120. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  121. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  122. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  123. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  124. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  125. data/lib/ea/qea/factory.rb +37 -0
  126. data/lib/ea/qea/file_detector.rb +178 -0
  127. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  128. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  129. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  130. data/lib/ea/qea/infrastructure.rb +12 -0
  131. data/lib/ea/qea/models/base_model.rb +59 -0
  132. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  133. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  134. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  135. data/lib/ea/qea/models/ea_connector.rb +160 -0
  136. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  137. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  138. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  139. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  140. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  141. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  142. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  143. data/lib/ea/qea/models/ea_document.rb +63 -0
  144. data/lib/ea/qea/models/ea_object.rb +223 -0
  145. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  146. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  147. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  148. data/lib/ea/qea/models/ea_operation.rb +127 -0
  149. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  150. data/lib/ea/qea/models/ea_package.rb +78 -0
  151. data/lib/ea/qea/models/ea_script.rb +62 -0
  152. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  153. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  154. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  155. data/lib/ea/qea/models/ea_xref.rb +165 -0
  156. data/lib/ea/qea/models.rb +35 -0
  157. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  158. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  159. data/lib/ea/qea/repositories.rb +10 -0
  160. data/lib/ea/qea/services/configuration.rb +211 -0
  161. data/lib/ea/qea/services/database_loader.rb +191 -0
  162. data/lib/ea/qea/services.rb +10 -0
  163. data/lib/ea/qea/validation/association_validator.rb +73 -0
  164. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  165. data/lib/ea/qea/validation/base_validator.rb +331 -0
  166. data/lib/ea/qea/validation/class_validator.rb +121 -0
  167. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  168. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  169. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  170. data/lib/ea/qea/validation/database.rb +16 -0
  171. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  172. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  173. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  174. data/lib/ea/qea/validation/formatters.rb +12 -0
  175. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  176. data/lib/ea/qea/validation/package_validator.rb +111 -0
  177. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  178. data/lib/ea/qea/validation/validation_message.rb +144 -0
  179. data/lib/ea/qea/validation/validation_result.rb +210 -0
  180. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  181. data/lib/ea/qea/validation.rb +28 -0
  182. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  183. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  184. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  185. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  186. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  187. data/lib/ea/qea/verification.rb +14 -0
  188. data/lib/ea/qea.rb +185 -0
  189. data/lib/ea/transformations/configuration.rb +333 -0
  190. data/lib/ea/transformations/format_registry.rb +366 -0
  191. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  192. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  193. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  194. data/lib/ea/transformations/transformation_engine.rb +390 -0
  195. data/lib/ea/transformations.rb +85 -0
  196. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  197. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  198. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  199. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  200. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  201. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  202. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  203. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  204. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  205. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  206. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  207. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  208. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  209. data/lib/ea/transformers.rb +34 -0
  210. data/lib/ea/version.rb +1 -1
  211. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  212. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  213. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  214. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  215. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  216. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  217. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  218. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  219. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  220. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  221. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  222. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  223. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  224. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  225. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  226. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  227. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  228. data/lib/ea/xmi/lookup_service.rb +89 -0
  229. data/lib/ea/xmi/parser.rb +1015 -0
  230. data/lib/ea/xmi.rb +35 -0
  231. data/lib/ea.rb +10 -1
  232. metadata +387 -9
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # End-to-end smoke test: parse a real Sparx EA .qea file end-to-end through
5
+ # both the standalone QEA loader and the UML bridge.
6
+ #
7
+ # Run: bundle exec ruby examples/smoke_test_real_qea.rb
8
+ #
9
+ # Demonstrates:
10
+ # 1. Ea::Qea.database_info — quick table-count stats (no full load)
11
+ # 2. Ea::Qea.load — standalone load → Ea::Qea::Database
12
+ # 3. Ea::Qea.to_uml — bridge → Lutaml::Uml::Document
13
+ #
14
+ # Requires a real .qea file. Defaults to the plateau model fixture; override
15
+ # with the QEA_PATH environment variable.
16
+
17
+ require "bundler/setup"
18
+ require "lutaml/uml"
19
+ require "ea"
20
+
21
+ QEA_PATH = ENV.fetch(
22
+ "QEA_PATH",
23
+ "/Users/mulgogi/src/mn/plateau-model/20251010_current_plateau_v5.1.qea",
24
+ )
25
+
26
+ abort "QEA not found: #{QEA_PATH}" unless File.exist?(QEA_PATH)
27
+
28
+ puts "QEA: #{QEA_PATH}"
29
+ puts "Size: #{(File.size(QEA_PATH) / 1_048_576.0).round(2)} MB"
30
+ puts
31
+
32
+ # 1. Quick stats — opens the SQLite DB and counts rows per table, no model load
33
+ puts "== database_info (quick stats, top 12 tables) =="
34
+ Ea::Qea.database_info(QEA_PATH)
35
+ .sort_by { |_, count| -count.to_i }
36
+ .first(12)
37
+ .each { |table, count| puts " #{table}: #{count}" }
38
+ puts
39
+
40
+ # 2. Standalone load — pure QEA parsing, no lutaml-uml needed
41
+ puts "== Ea::Qea.load (standalone) =="
42
+ database = Ea::Qea.load(QEA_PATH)
43
+ puts " loaded: #{database.class}"
44
+ puts " total_records: #{database.total_records}"
45
+ puts
46
+
47
+ puts "== EA objects by type =="
48
+ database.objects.group_by(:object_type)
49
+ .sort_by { |_, objs| -objs.size }
50
+ .each { |type, objs| puts " #{type}: #{objs.size}" }
51
+ puts
52
+
53
+ # 3. UML bridge — compose with lutaml-uml to get a Lutaml::Uml::Document
54
+ puts "== Ea::Qea.to_uml (bridge) =="
55
+ document = Ea::Qea.to_uml(database)
56
+ puts " document: #{document.class}"
57
+ puts
58
+
59
+ # 4. Walk the package tree — classes live inside packages, not at document root
60
+ def walk(pkgs, counters)
61
+ Array(pkgs).each do |pkg|
62
+ counters[:packages] += 1
63
+ counters[:classes] += Array(pkg.classes).size
64
+ counters[:enums] += Array(pkg.enums).size
65
+ counters[:data_types] += Array(pkg.data_types).size
66
+ counters[:instances] += Array(pkg.instances).size
67
+ walk(pkg.packages, counters)
68
+ end
69
+ end
70
+
71
+ puts "== UML Document (incl. nested in packages) =="
72
+ counters = { packages: 0, classes: 0, enums: 0, data_types: 0, instances: 0 }
73
+ walk(document.packages, counters)
74
+ counters.each { |k, v| puts " #{k}: #{v}" }
75
+ puts " associations (document root): #{document.associations.size}"
76
+ puts " diagrams (document root): #{document.diagrams.size}"
77
+ puts " orphan classes (document root): #{document.classes.size}"
78
+ puts " ^ classes whose EA package_id has no t_package row"
79
+ puts
80
+
81
+ puts "SMOKE TEST PASSED"
data/exe/ea ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "ea/cli"
7
+ Ea::Cli::App.start(ARGV)
data/lib/ea/cli/app.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Ea
6
+ module Cli
7
+ class App < Thor
8
+ class << self
9
+ def exit_on_failure?
10
+ true
11
+ end
12
+ end
13
+
14
+ desc "version", "Show ea gem version"
15
+ def version
16
+ puts Ea::VERSION
17
+ end
18
+
19
+ desc "list FILE", "List model elements (auto-detects QEA or XMI)"
20
+ option :type, type: :string,
21
+ desc: "Filter: class | interface | package | diagram | connector | enum"
22
+ option :format, type: :string, default: "table",
23
+ desc: "Output format: table | json | yaml"
24
+ def list(file)
25
+ Command::List.new(file: file, **symbolize(options)).call
26
+ end
27
+
28
+ desc "diagrams ACTION FILE [NAME]",
29
+ "Diagram operations: list FILE | extract NAME FILE"
30
+ option :format, type: :string, default: "table"
31
+ option :output, type: :string, desc: "Output path (extract only)"
32
+ def diagrams(action, file = nil, name = nil)
33
+ Command::Diagrams
34
+ .new(action: action, file: file, name: name, **symbolize(options))
35
+ .call
36
+ end
37
+
38
+ desc "validate FILE", "Validate EA model"
39
+ option :format, type: :string, default: "table"
40
+ def validate(file)
41
+ Command::Validate.new(file: file, **symbolize(options)).call
42
+ end
43
+
44
+ desc "stats FILE", "Show collection counts (standalone — no lutaml-uml)"
45
+ option :format, type: :string, default: "table"
46
+ def stats(file)
47
+ Command::Stats.new(file: file, **symbolize(options)).call
48
+ end
49
+
50
+ desc "parse FILE", "Parse to Lutaml::Uml::Document (requires lutaml-uml)"
51
+ option :format, type: :string, default: "yaml",
52
+ desc: "Output: json | yaml"
53
+ def parse(file)
54
+ Command::Parse.new(file: file, **symbolize(options)).call
55
+ end
56
+
57
+ desc "convert FILE", "Convert between EA formats (e.g. QEA → XMI)"
58
+ option :to, type: :string, required: true, desc: "Target format: xmi"
59
+ option :output, type: :string, desc: "Output path"
60
+ option :format, type: :string, default: "table"
61
+ def convert(file)
62
+ Command::Convert.new(file: file, **symbolize(options)).call
63
+ end
64
+
65
+ desc "spa FILE", "Generate single-page app (SPA) from QEA/XMI/LUR"
66
+ option :output, type: :string,
67
+ desc: "Output path (default: <basename>.html)"
68
+ option :mode, type: :string, default: "single_file",
69
+ desc: "Output mode: single_file | multi_file"
70
+ def spa(file)
71
+ Command::Spa.new(file: file, **symbolize(options)).call
72
+ end
73
+
74
+ private
75
+
76
+ def symbolize(opts)
77
+ opts.transform_keys(&:to_sym)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # Shared base for all CLI commands.
7
+ #
8
+ # Provides:
9
+ # - Options Hash access (read-only)
10
+ # - Output formatter resolution from `:format` option
11
+ # - Standalone QEA database loading (no lutaml-uml required)
12
+ # - UML document parsing (lazy-loads lutaml-uml)
13
+ # - File existence validation
14
+ #
15
+ # Subclasses implement {#call} and may use the protected helpers.
16
+ class Base
17
+ def initialize(options = {})
18
+ @options = options
19
+ end
20
+
21
+ # Template method — subclasses implement.
22
+ # @raise [NotImplementedError] if not overridden
23
+ def call
24
+ raise NotImplementedError, "#{self.class}#call not implemented"
25
+ end
26
+
27
+ protected
28
+
29
+ attr_reader :options
30
+
31
+ def formatter
32
+ Ea::Cli::Output.instance_for(options[:format] || :table)
33
+ end
34
+
35
+ def file_path
36
+ options[:file] or raise Ea::Cli::Error, "missing required --file"
37
+ end
38
+
39
+ # Load a QEA database. Works standalone — no lutaml-uml dependency.
40
+ # @return [Ea::Qea::Database]
41
+ def load_database(path = file_path)
42
+ validate_file!(path)
43
+ Ea::Qea.load(path)
44
+ rescue Ea::Cli::Error
45
+ raise
46
+ rescue Ea::Error => e
47
+ raise Ea::Cli::Error, "Failed to load #{path}: #{e.message}"
48
+ end
49
+
50
+ # Parse any EA file to a Lutaml::Uml::Document.
51
+ # Lazy-loads lutaml-uml via Ea::Qea's require_uml! guard.
52
+ # @return [Lutaml::Uml::Document]
53
+ def parse_to_uml(path = file_path, **parse_options)
54
+ validate_file!(path)
55
+ Ea::Qea.parse(path, parse_options)
56
+ rescue Ea::Cli::Error
57
+ raise
58
+ rescue Ea::Error => e
59
+ if e.message.include?("lutaml-uml")
60
+ raise Ea::Cli::MissingUmlDependency
61
+ end
62
+
63
+ raise Ea::Cli::Error, "Failed to parse #{path}: #{e.message}"
64
+ end
65
+
66
+ def validate_file!(path)
67
+ raise Ea::Cli::FileNotFound, path unless File.exist?(path)
68
+ end
69
+
70
+ # Write content to a path; honors the `:output` option.
71
+ # @return [String] the path written to
72
+ def write_output(content, default_name:)
73
+ path = options[:output] || default_name
74
+ File.write(path, content)
75
+ path
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # `ea convert FILE --to xmi [--output PATH]`
7
+ #
8
+ # Converts an EA file to another format. Currently supports:
9
+ # --to xmi .qea → Sparx XMI (direct, full Sparx fidelity)
10
+ #
11
+ # Routing is by input file extension:
12
+ # .qea → Ea::Transformers.qea_to_xmi (direct path, no intermediate
13
+ # UML model — preserves all Sparx-specific concepts)
14
+ # any other extension → UnsupportedFormat
15
+ #
16
+ # Future formats (e.g. parsing a Lutaml::LML .lur file) are added by
17
+ # extending the case statement in {#convert_to_xmi}.
18
+ class Convert < Base
19
+ SUPPORTED_TARGETS = %w[xmi].freeze
20
+ SUPPORTED_INPUT_FORMATS = %i[qea].freeze
21
+
22
+ def call
23
+ target = options[:to] or raise Ea::Cli::Error, "missing required --to"
24
+ case target.to_s
25
+ when "xmi" then convert_to_xmi
26
+ else
27
+ raise Ea::Cli::Error,
28
+ "Unsupported target '#{target}'. " \
29
+ "Valid: #{SUPPORTED_TARGETS.join(', ')}"
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def convert_to_xmi
36
+ xml = case input_format
37
+ when :qea then qea_to_xmi_xml
38
+ else
39
+ raise Ea::Cli::UnsupportedFormat.new(
40
+ file_path,
41
+ "supported inputs: " \
42
+ "#{SUPPORTED_INPUT_FORMATS.join(', ')}",
43
+ )
44
+ end
45
+ path = write_output(xml, default_name: "#{file_path}.xmi")
46
+ formatter.render([[path]], columns: [:written_to])
47
+ end
48
+
49
+ def qea_to_xmi_xml
50
+ database = load_database(file_path)
51
+ Ea::Transformers.qea_to_xmi(database)
52
+ ensure
53
+ database&.close_connection
54
+ end
55
+
56
+ def input_format
57
+ File.extname(file_path).downcase[1..].to_sym
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # `ea diagrams ACTION FILE [NAME]`
7
+ #
8
+ # Actions:
9
+ # list FILE — list diagrams in a QEA/XMI/LUR file (standalone)
10
+ # extract FILE NAME — render the named diagram to SVG
11
+ #
12
+ # `list` reads the EA database directly (no lutaml-uml required
13
+ # for QEA; XMI parsing via xmi gem only).
14
+ #
15
+ # `extract` accepts QEA, XMI, or LUR. The Repository is built
16
+ # from the input file via `RepositoryBuilder` (single source of
17
+ # truth — QEA/XMI are parsed via `Ea::Transformations`, LUR is
18
+ # loaded natively via `Repository.from_file`).
19
+ class Diagrams < Base
20
+ include RepositoryBuilder
21
+
22
+ ACTIONS = %w[list extract].freeze
23
+
24
+ def call
25
+ case action
26
+ when "list" then list
27
+ when "extract" then extract
28
+ else
29
+ raise Ea::Cli::UnknownAction.new(action, valid: ACTIONS)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def action
36
+ options[:action] or raise Ea::Cli::Error, "missing required ACTION"
37
+ end
38
+
39
+ def name
40
+ options[:name] or raise Ea::Cli::Error, "missing required NAME"
41
+ end
42
+
43
+ def list
44
+ db = load_database
45
+ rows = db.diagrams.map { |d| [d.name, diagram_type_label(d), d.ea_guid] }
46
+ formatter.render(rows, columns: %i[name type guid])
47
+ end
48
+
49
+ def extract
50
+ repository = RepositoryBuilder.build_repository(file_path)
51
+ result = extractor.extract_one(repository, name, extract_options)
52
+ raise Ea::Cli::Error, result[:message] unless result[:success]
53
+
54
+ path = result[:path] || write_output(result.fetch(:svg_content),
55
+ default_name: "#{name}.svg")
56
+ formatter.render([[path]], columns: [:written_to])
57
+ end
58
+
59
+ def extractor
60
+ Ea::Diagram::Extractor.new
61
+ end
62
+
63
+ def extract_options
64
+ opts = {}
65
+ opts[:output] = options[:output] if options[:output]
66
+ opts
67
+ end
68
+
69
+ def diagram_type_label(diagram)
70
+ diagram.diagram_type || "Logical"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # `ea list FILE [--type TYPE]`
7
+ #
8
+ # Lists model elements from a QEA file. With `--type`, returns elements
9
+ # of one kind (class/interface/package/diagram/connector/enum). Without
10
+ # `--type`, returns a summary of counts per kind.
11
+ #
12
+ # Operates standalone — does not require lutaml-uml.
13
+ class List < Base
14
+ def call
15
+ rows =
16
+ if options[:type]
17
+ list_by_type(options[:type])
18
+ else
19
+ list_summary
20
+ end
21
+ formatter.render(rows, columns: columns_for_current_mode)
22
+ end
23
+
24
+ private
25
+
26
+ def list_summary
27
+ db = load_database
28
+ [
29
+ [:classes, db.objects.find_by_type("Class").size],
30
+ [:interfaces, db.objects.find_by_type("Interface").size],
31
+ [:packages, db.packages.size],
32
+ [:enums, db.objects.find_by_type("Enumeration").size],
33
+ [:datatypes, db.objects.find_by_type("DataType").size],
34
+ [:diagrams, db.diagrams.size],
35
+ [:connectors, db.connectors.size],
36
+ ]
37
+ end
38
+
39
+ def list_by_type(type)
40
+ db = load_database
41
+ case type.to_s
42
+ when "class" then db.objects.find_by_type("Class").map { |o| [o.name, o.ea_guid] }
43
+ when "interface" then db.objects.find_by_type("Interface").map { |o| [o.name, o.ea_guid] }
44
+ when "enum" then db.objects.find_by_type("Enumeration").map { |o| [o.name, o.ea_guid] }
45
+ when "package" then db.packages.map { |p| [p.name, p.ea_guid] }
46
+ when "diagram" then db.diagrams.map { |d| [d.name, d.ea_guid] }
47
+ when "connector" then db.connectors.map { |c| [c.name, c.ea_guid] }
48
+ else
49
+ raise Ea::Cli::Error,
50
+ "Unknown type '#{type}'. " \
51
+ "Valid: class, interface, package, diagram, connector, enum"
52
+ end
53
+ end
54
+
55
+ def columns_for_current_mode
56
+ options[:type] ? %i[name guid] : %i[kind count]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # `ea parse FILE [--format json|yaml]`
7
+ #
8
+ # Parses the EA file (QEA or Sparx XMI) to a Lutaml::Uml::Document and
9
+ # serializes it. Requires lutaml-uml.
10
+ class Parse < Base
11
+ def call
12
+ document = parse_to_uml(file_path)
13
+ formatter_output(document)
14
+ end
15
+
16
+ private
17
+
18
+ def formatter_output(document)
19
+ case (options[:format] || :yaml).to_sym
20
+ when :json then puts document.to_json
21
+ when :yaml then puts document.to_yaml
22
+ else
23
+ raise Ea::Cli::Error, "Unknown format: #{options[:format]}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # Shared helper for building a `Lutaml::UmlRepository::Repository`
7
+ # from any file format supported by `Ea::Transformations`.
8
+ #
9
+ # Single source of truth for "QEA/XMI/LUR → Repository" wiring.
10
+ # Used by `Ea::Cli::Command::Diagrams` (extract action) and
11
+ # `Ea::Cli::Command::Spa` (SPA generation).
12
+ #
13
+ # `.lur` files take the fast path via `Repository.from_file` (the
14
+ # native LUR loader). QEA/XMI files are parsed via
15
+ # `Ea::Transformations.parse` and wrapped via
16
+ # `Repository.from_document`.
17
+ module RepositoryBuilder
18
+ LUR_EXT = ".lur".freeze
19
+
20
+ module_function
21
+
22
+ # @param path [String] file path (QEA, XMI, or LUR)
23
+ # @return [Lutaml::UmlRepository::Repository]
24
+ def build_repository(path)
25
+ require_lutaml_uml!
26
+
27
+ if lur?(path)
28
+ require_lutaml_uml_repository!
29
+ return Lutaml::UmlRepository::Repository.from_file(path)
30
+ end
31
+
32
+ document = Ea::Transformations.parse(path)
33
+ require_lutaml_uml_repository!
34
+ Lutaml::UmlRepository::Repository.from_document(document)
35
+ end
36
+
37
+ def lur?(path)
38
+ File.extname(path).downcase == LUR_EXT
39
+ end
40
+
41
+ # Loads the bridge gem. The lutaml-uml gem is an optional
42
+ # dependency; if missing, raise the CLI's MissingUmlDependency
43
+ # so the user gets a clear message about installing it.
44
+ def require_lutaml_uml!
45
+ require "lutaml/uml"
46
+ rescue LoadError => e
47
+ raise Ea::Cli::MissingUmlDependency,
48
+ "spa and diagrams-extract commands require the " \
49
+ "`lutaml-uml` gem. Install it via `gem install lutaml-uml` " \
50
+ "or add it to your Gemfile. (#{e.message})"
51
+ end
52
+
53
+ def require_lutaml_uml_repository!
54
+ require "lutaml/uml_repository"
55
+ rescue LoadError => e
56
+ raise Ea::Cli::MissingUmlDependency,
57
+ "spa and diagrams-extract commands require the " \
58
+ "`lutaml-uml` gem. Install it via `gem install lutaml-uml` " \
59
+ "or add it to your Gemfile. (#{e.message})"
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # `ea spa FILE [--output=PATH] [--mode=MODE]`
7
+ #
8
+ # Generates a static single-page application (SPA) from a QEA,
9
+ # XMI, or LUR file. The SPA shows the package hierarchy, class
10
+ # details, diagram list, and member navigation in a browser.
11
+ #
12
+ # Pipeline:
13
+ # 1. Ea::Transformations.parse(file) → Lutaml::Uml::Document
14
+ # 2. Lutaml::UmlRepository::Repository.from_document(document)
15
+ # 3. Lutaml::UmlRepository::StaticSite::Generator.new(repo, ...).generate
16
+ #
17
+ # Output format defaults to single-file Vue IIFE HTML.
18
+ class Spa < Base
19
+ DEFAULT_MODE = :single_file
20
+
21
+ def call
22
+ repository = RepositoryBuilder.build_repository(file_path)
23
+
24
+ require_lutaml_uml_static_site!
25
+ strategy = resolve_output_strategy
26
+
27
+ output_path = resolve_output_path
28
+ options = {
29
+ output: output_path,
30
+ mode: mode,
31
+ output_strategy: strategy,
32
+ }.compact
33
+
34
+ Lutaml::UmlRepository::StaticSite::Generator
35
+ .new(repository, options)
36
+ .generate
37
+
38
+ formatter.render([[output_path]], columns: [:written_to])
39
+ end
40
+
41
+ private
42
+
43
+ def mode
44
+ (options[:mode] || DEFAULT_MODE).to_sym
45
+ end
46
+
47
+ def resolve_output_path
48
+ options[:output] || default_output_path
49
+ end
50
+
51
+ def default_output_path
52
+ base = File.basename(file_path, ".*")
53
+ dir = File.dirname(file_path)
54
+ File.join(dir, "#{base}.html")
55
+ end
56
+
57
+ def resolve_output_strategy
58
+ case mode
59
+ when :single_file, "single_file"
60
+ Lutaml::UmlRepository::StaticSite::Output::VueInlinedStrategy
61
+ when :multi_file, "multi_file"
62
+ Lutaml::UmlRepository::StaticSite::Output::MultiFileStrategy
63
+ else
64
+ raise Ea::Cli::UnsupportedFormat,
65
+ "Unknown spa mode '#{mode}': use single_file or multi_file"
66
+ end
67
+ end
68
+
69
+ def require_lutaml_uml_static_site!
70
+ require "lutaml/uml_repository/static_site"
71
+ rescue LoadError => e
72
+ raise Ea::Cli::MissingUmlDependency,
73
+ "spa command requires the `lutaml-uml` gem. " \
74
+ "Install it via `gem install lutaml-uml`. (#{e.message})"
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # `ea stats FILE`
7
+ #
8
+ # Standalone — does not require lutaml-uml. Reads the QEA database
9
+ # directly and prints per-collection counts.
10
+ class Stats < Base
11
+ def call
12
+ db = load_database
13
+ stats = db.stats
14
+ rows = stats.map { |k, v| [k, v] }
15
+ formatter.render(rows, columns: %i[collection count])
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ # `ea validate FILE`
7
+ #
8
+ # Runs Ea::Qea::Validation::ValidationEngine and reports messages.
9
+ # Exits non-zero if any errors were found.
10
+ #
11
+ # Requires lutaml-uml (validation runs against Lutaml::Uml::Document).
12
+ class Validate < Base
13
+ COLUMNS = %i[severity entity_type entity_name message].freeze
14
+
15
+ def call
16
+ result = parse_with_validation(file_path)
17
+ rows = result.messages.map do |m|
18
+ [m.severity, m.entity_type, m.entity_name, m.message]
19
+ end
20
+ formatter.render(rows, columns: COLUMNS)
21
+ exit(1) if result.errors.any?
22
+ end
23
+
24
+ private
25
+
26
+ def parse_with_validation(path)
27
+ validate_file!(path)
28
+ Ea::Qea.parse(path, validate: true)[:validation_result]
29
+ rescue Ea::Cli::Error
30
+ raise
31
+ rescue Ea::Error => e
32
+ if e.message.include?("lutaml-uml")
33
+ raise Ea::Cli::MissingUmlDependency
34
+ end
35
+
36
+ raise Ea::Cli::Error, "Failed to validate #{path}: #{e.message}"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end