ea 0.1.0 → 0.1.4

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 (227) 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/config/diagram_styles.yml +200 -0
  43. data/config/model_transformations.yml +266 -0
  44. data/config/qea_schema.yml +1024 -0
  45. data/docs/ea_to_uml_type_mapping.md +89 -0
  46. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  47. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  48. data/examples/lur/basic.lur +0 -0
  49. data/examples/lur/test-output.lur +0 -0
  50. data/examples/lur/test.lur +0 -0
  51. data/examples/lur_basic_usage.rb +221 -0
  52. data/examples/lur_cli_workflow.rb +263 -0
  53. data/examples/lur_statistics.rb +326 -0
  54. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  55. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  56. data/examples/qea/README_qea_parser.adoc +230 -0
  57. data/examples/qea/UmlModel_template.qea +0 -0
  58. data/examples/qea/basic.qea +0 -0
  59. data/examples/qea/simple.qea +0 -0
  60. data/examples/qea/simple_example.qea +0 -0
  61. data/examples/qea/test.qea +0 -0
  62. data/examples/qea_standalone_query.rb +73 -0
  63. data/examples/qea_to_repository.rb +51 -0
  64. data/examples/smoke_test_real_qea.rb +81 -0
  65. data/exe/ea +7 -0
  66. data/lib/ea/cli/app.rb +72 -0
  67. data/lib/ea/cli/command/base.rb +80 -0
  68. data/lib/ea/cli/command/convert.rb +62 -0
  69. data/lib/ea/cli/command/diagrams.rb +81 -0
  70. data/lib/ea/cli/command/list.rb +61 -0
  71. data/lib/ea/cli/command/parse.rb +29 -0
  72. data/lib/ea/cli/command/stats.rb +20 -0
  73. data/lib/ea/cli/command/validate.rb +41 -0
  74. data/lib/ea/cli/command.rb +15 -0
  75. data/lib/ea/cli/error.rb +34 -0
  76. data/lib/ea/cli/output/formatter.rb +34 -0
  77. data/lib/ea/cli/output/json_formatter.rb +20 -0
  78. data/lib/ea/cli/output/table_formatter.rb +42 -0
  79. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  80. data/lib/ea/cli/output.rb +56 -0
  81. data/lib/ea/cli.rb +17 -0
  82. data/lib/ea/diagram/configuration.rb +379 -0
  83. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  84. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  85. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  86. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  87. data/lib/ea/diagram/element_renderers.rb +43 -0
  88. data/lib/ea/diagram/extractor.rb +560 -0
  89. data/lib/ea/diagram/layout_engine.rb +170 -0
  90. data/lib/ea/diagram/path_builder.rb +202 -0
  91. data/lib/ea/diagram/style_parser.rb +42 -0
  92. data/lib/ea/diagram/style_resolver.rb +276 -0
  93. data/lib/ea/diagram/svg_renderer.rb +274 -0
  94. data/lib/ea/diagram/util.rb +73 -0
  95. data/lib/ea/diagram.rb +47 -0
  96. data/lib/ea/qea/benchmark.rb +210 -0
  97. data/lib/ea/qea/database.rb +308 -0
  98. data/lib/ea/qea/factory/association_builder.rb +203 -0
  99. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  100. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  101. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  102. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  103. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  104. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  105. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  106. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  107. data/lib/ea/qea/factory/document_builder.rb +283 -0
  108. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  109. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  110. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  111. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  112. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  113. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  114. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  115. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  116. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  117. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  118. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  119. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  120. data/lib/ea/qea/factory.rb +37 -0
  121. data/lib/ea/qea/file_detector.rb +178 -0
  122. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  123. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  124. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  125. data/lib/ea/qea/infrastructure.rb +12 -0
  126. data/lib/ea/qea/models/base_model.rb +59 -0
  127. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  128. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  129. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  130. data/lib/ea/qea/models/ea_connector.rb +160 -0
  131. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  132. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  133. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  134. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  135. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  136. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  137. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  138. data/lib/ea/qea/models/ea_document.rb +63 -0
  139. data/lib/ea/qea/models/ea_object.rb +223 -0
  140. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  141. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  142. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  143. data/lib/ea/qea/models/ea_operation.rb +127 -0
  144. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  145. data/lib/ea/qea/models/ea_package.rb +78 -0
  146. data/lib/ea/qea/models/ea_script.rb +62 -0
  147. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  148. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  149. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  150. data/lib/ea/qea/models/ea_xref.rb +165 -0
  151. data/lib/ea/qea/models.rb +35 -0
  152. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  153. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  154. data/lib/ea/qea/repositories.rb +10 -0
  155. data/lib/ea/qea/services/configuration.rb +211 -0
  156. data/lib/ea/qea/services/database_loader.rb +191 -0
  157. data/lib/ea/qea/services.rb +10 -0
  158. data/lib/ea/qea/validation/association_validator.rb +73 -0
  159. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  160. data/lib/ea/qea/validation/base_validator.rb +331 -0
  161. data/lib/ea/qea/validation/class_validator.rb +121 -0
  162. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  163. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  164. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  165. data/lib/ea/qea/validation/database.rb +16 -0
  166. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  167. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  168. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  169. data/lib/ea/qea/validation/formatters.rb +12 -0
  170. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  171. data/lib/ea/qea/validation/package_validator.rb +111 -0
  172. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  173. data/lib/ea/qea/validation/validation_message.rb +144 -0
  174. data/lib/ea/qea/validation/validation_result.rb +210 -0
  175. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  176. data/lib/ea/qea/validation.rb +28 -0
  177. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  178. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  179. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  180. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  181. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  182. data/lib/ea/qea/verification.rb +14 -0
  183. data/lib/ea/qea.rb +185 -0
  184. data/lib/ea/transformations/configuration.rb +333 -0
  185. data/lib/ea/transformations/format_registry.rb +366 -0
  186. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  187. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  188. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  189. data/lib/ea/transformations/transformation_engine.rb +390 -0
  190. data/lib/ea/transformations.rb +85 -0
  191. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  192. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  193. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  194. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  195. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  196. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  197. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  198. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  199. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  200. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  201. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  202. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  203. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  204. data/lib/ea/transformers.rb +34 -0
  205. data/lib/ea/version.rb +1 -1
  206. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  207. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  208. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  209. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  210. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  211. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  212. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  213. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  214. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  215. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  216. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  217. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  218. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  219. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  220. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  221. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  222. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  223. data/lib/ea/xmi/lookup_service.rb +89 -0
  224. data/lib/ea/xmi/parser.rb +919 -0
  225. data/lib/ea/xmi.rb +35 -0
  226. data/lib/ea.rb +10 -1
  227. metadata +382 -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,72 @@
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
+ private
66
+
67
+ def symbolize(opts)
68
+ opts.transform_keys(&:to_sym)
69
+ end
70
+ end
71
+ end
72
+ 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,81 @@
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 file (standalone)
10
+ # extract FILE NAME — render the named diagram from a LUR file to SVG
11
+ #
12
+ # `list` reads the EA database directly (no lutaml-uml required).
13
+ # `extract` delegates to {Ea::Diagram::Extractor}, which requires a
14
+ # `.lur` (Lutaml UML Repository) file. To render diagrams from a QEA,
15
+ # first convert it to `.lur` via the `lutaml` gem.
16
+ class Diagrams < Base
17
+ ACTIONS = %w[list extract].freeze
18
+ LUR_EXT = ".lur"
19
+
20
+ def call
21
+ case action
22
+ when "list" then list
23
+ when "extract" then extract
24
+ else
25
+ raise Ea::Cli::UnknownAction.new(action, valid: ACTIONS)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def action
32
+ options[:action] or raise Ea::Cli::Error, "missing required ACTION"
33
+ end
34
+
35
+ def name
36
+ options[:name] or raise Ea::Cli::Error, "missing required NAME"
37
+ end
38
+
39
+ def list
40
+ db = load_database
41
+ rows = db.diagrams.map { |d| [d.name, diagram_type_label(d), d.ea_guid] }
42
+ formatter.render(rows, columns: %i[name type guid])
43
+ end
44
+
45
+ def extract
46
+ validate_lur!(file_path)
47
+ result = extractor.extract_one(file_path, name, extract_options)
48
+ raise Ea::Cli::Error, result[:message] unless result[:success]
49
+
50
+ path = result[:path] || write_output(result.fetch(:svg_content),
51
+ default_name: "#{name}.svg")
52
+ formatter.render([[path]], columns: [:written_to])
53
+ end
54
+
55
+ def extractor
56
+ Ea::Diagram::Extractor.new
57
+ end
58
+
59
+ def extract_options
60
+ opts = {}
61
+ opts[:output] = options[:output] if options[:output]
62
+ opts
63
+ end
64
+
65
+ def validate_lur!(path)
66
+ return if path.end_with?(LUR_EXT)
67
+
68
+ raise Ea::Cli::UnsupportedFormat.new(
69
+ path,
70
+ "diagrams extract requires a #{LUR_EXT} file; " \
71
+ "convert from QEA via the lutaml gem first",
72
+ )
73
+ end
74
+
75
+ def diagram_type_label(diagram)
76
+ diagram.diagram_type || "Logical"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ 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,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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Command
6
+ autoload :Base, "ea/cli/command/base"
7
+ autoload :List, "ea/cli/command/list"
8
+ autoload :Diagrams, "ea/cli/command/diagrams"
9
+ autoload :Validate, "ea/cli/command/validate"
10
+ autoload :Stats, "ea/cli/command/stats"
11
+ autoload :Parse, "ea/cli/command/parse"
12
+ autoload :Convert, "ea/cli/command/convert"
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ class Error < Ea::Error; end
6
+
7
+ class FileNotFound < Error
8
+ def initialize(path)
9
+ super("File not found: #{path}")
10
+ end
11
+ end
12
+
13
+ class UnsupportedFormat < Error
14
+ def initialize(path, detail = nil)
15
+ msg = "Unsupported format for file: #{path}"
16
+ msg = "#{msg} (#{detail})" if detail
17
+ super(msg)
18
+ end
19
+ end
20
+
21
+ class MissingUmlDependency < Error
22
+ def initialize
23
+ super("lutaml-uml is required for this command. " \
24
+ "Add gem 'lutaml-uml' to your Gemfile.")
25
+ end
26
+ end
27
+
28
+ class UnknownAction < Error
29
+ def initialize(action, valid:)
30
+ super("Unknown action '#{action}'. Valid: #{valid.join(', ')}")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Output
6
+ class Formatter
7
+ def render(rows, columns: [])
8
+ raise NotImplementedError
9
+ end
10
+
11
+ protected
12
+
13
+ def normalize_row(row, columns)
14
+ return row if row.is_a?(Hash)
15
+
16
+ columns.each_with_index.to_h { |k, i| [k, row[i]] }
17
+ end
18
+
19
+ def infer_columns(rows)
20
+ first = rows.first
21
+ return columns_of(first) if first
22
+
23
+ []
24
+ end
25
+
26
+ private
27
+
28
+ def columns_of(obj)
29
+ obj.is_a?(Hash) ? obj.keys.map(&:to_sym) : []
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ea
6
+ module Cli
7
+ module Output
8
+ class JsonFormatter < Formatter
9
+ def render(rows, columns: [])
10
+ data = rows.map do |row|
11
+ normalize_row(row, columns)
12
+ end
13
+ puts JSON.pretty_generate(data)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Ea::Cli::Output.register(:json, Ea::Cli::Output::JsonFormatter)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Cli
5
+ module Output
6
+ class TableFormatter < Formatter
7
+ COLUMN_WIDTH = 24
8
+ SEPARATOR = " "
9
+
10
+ def render(rows, columns: [])
11
+ return puts("(no rows)") if rows.empty?
12
+
13
+ cols = columns.empty? ? Array(infer_columns(rows)) : columns
14
+ puts render_header(cols)
15
+ rows.each { |row| puts render_row(row, cols) }
16
+ end
17
+
18
+ private
19
+
20
+ def render_header(cols)
21
+ cols.map { |c| pad(c.to_s) }.join(SEPARATOR)
22
+ end
23
+
24
+ def render_row(row, cols)
25
+ values =
26
+ if row.is_a?(Hash)
27
+ cols.map { |c| row[c] || row[c.to_s] }
28
+ else
29
+ row
30
+ end
31
+ values.map { |v| pad(v.to_s) }.join(SEPARATOR)
32
+ end
33
+
34
+ def pad(s)
35
+ s.length >= COLUMN_WIDTH ? s : s.ljust(COLUMN_WIDTH)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ Ea::Cli::Output.register(:table, Ea::Cli::Output::TableFormatter)