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.
- checksums.yaml +4 -4
- data/CLAUDE.md +125 -0
- data/Rakefile +12 -4
- data/TODO.next/00-publish-blocking-bugs.md +74 -0
- data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
- data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
- data/TODO.next/03-slim-lutaml-uml.md +79 -0
- data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
- data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
- data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
- data/TODO.next/07-transformer-registry-in-factory.md +20 -0
- data/TODO.next/08-connector-type-registry.md +27 -0
- data/TODO.next/09-element-renderer-registry.md +29 -0
- data/TODO.next/10-connector-renderer-lsp.md +18 -0
- data/TODO.next/11-consolidate-style-knowledge.md +33 -0
- data/TODO.next/12-data-driven-from-db-row.md +24 -0
- data/TODO.next/13-extract-duplicated-methods.md +17 -0
- data/TODO.next/14-remove-dead-code.md +10 -0
- data/TODO.next/15-narrow-exception-handling.md +39 -0
- data/TODO.next/16-repository-indexes.md +28 -0
- data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
- data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
- data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
- data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
- data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
- data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
- data/TODO.next/23-cleanup-idallocator.md +41 -0
- data/TODO.next/24-tighten-parity-specs.md +42 -0
- data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
- data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
- data/TODO.next/27-extract-cardinality-module.md +68 -0
- data/TODO.next/28-extract-xml-sanitizer.md +51 -0
- data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
- data/TODO.next/30-struct-return-for-association-end.md +37 -0
- data/TODO.next/31-idallocator-specs.md +27 -0
- data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
- data/TODO.next/33-normalize-lower-cleanup.md +30 -0
- data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
- data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
- data/TODO.next/36-wire-interface-realization.md +50 -0
- data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
- data/config/diagram_styles.yml +200 -0
- data/config/model_transformations.yml +266 -0
- data/config/qea_schema.yml +1024 -0
- data/docs/ea_to_uml_type_mapping.md +89 -0
- data/docs/xmi_qea_conversion_capabilities.md +99 -0
- data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
- data/examples/lur/basic.lur +0 -0
- data/examples/lur/test-output.lur +0 -0
- data/examples/lur/test.lur +0 -0
- data/examples/lur_basic_usage.rb +221 -0
- data/examples/lur_cli_workflow.rb +263 -0
- data/examples/lur_statistics.rb +326 -0
- data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
- data/examples/qea/ArcGISWorkspace_template.qea +0 -0
- data/examples/qea/README_qea_parser.adoc +230 -0
- data/examples/qea/UmlModel_template.qea +0 -0
- data/examples/qea/basic.qea +0 -0
- data/examples/qea/simple.qea +0 -0
- data/examples/qea/simple_example.qea +0 -0
- data/examples/qea/test.qea +0 -0
- data/examples/qea_standalone_query.rb +73 -0
- data/examples/qea_to_repository.rb +51 -0
- data/examples/smoke_test_real_qea.rb +81 -0
- data/exe/ea +7 -0
- data/lib/ea/cli/app.rb +72 -0
- data/lib/ea/cli/command/base.rb +80 -0
- data/lib/ea/cli/command/convert.rb +62 -0
- data/lib/ea/cli/command/diagrams.rb +81 -0
- data/lib/ea/cli/command/list.rb +61 -0
- data/lib/ea/cli/command/parse.rb +29 -0
- data/lib/ea/cli/command/stats.rb +20 -0
- data/lib/ea/cli/command/validate.rb +41 -0
- data/lib/ea/cli/command.rb +15 -0
- data/lib/ea/cli/error.rb +34 -0
- data/lib/ea/cli/output/formatter.rb +34 -0
- data/lib/ea/cli/output/json_formatter.rb +20 -0
- data/lib/ea/cli/output/table_formatter.rb +42 -0
- data/lib/ea/cli/output/yaml_formatter.rb +20 -0
- data/lib/ea/cli/output.rb +56 -0
- data/lib/ea/cli.rb +17 -0
- data/lib/ea/diagram/configuration.rb +379 -0
- data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
- data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
- data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
- data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
- data/lib/ea/diagram/element_renderers.rb +43 -0
- data/lib/ea/diagram/extractor.rb +560 -0
- data/lib/ea/diagram/layout_engine.rb +170 -0
- data/lib/ea/diagram/path_builder.rb +202 -0
- data/lib/ea/diagram/style_parser.rb +42 -0
- data/lib/ea/diagram/style_resolver.rb +276 -0
- data/lib/ea/diagram/svg_renderer.rb +274 -0
- data/lib/ea/diagram/util.rb +73 -0
- data/lib/ea/diagram.rb +47 -0
- data/lib/ea/qea/benchmark.rb +210 -0
- data/lib/ea/qea/database.rb +308 -0
- data/lib/ea/qea/factory/association_builder.rb +203 -0
- data/lib/ea/qea/factory/association_transformer.rb +91 -0
- data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
- data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
- data/lib/ea/qea/factory/base_transformer.rb +177 -0
- data/lib/ea/qea/factory/class_transformer.rb +116 -0
- data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
- data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
- data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
- data/lib/ea/qea/factory/document_builder.rb +283 -0
- data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
- data/lib/ea/qea/factory/enum_transformer.rb +74 -0
- data/lib/ea/qea/factory/generalization_builder.rb +227 -0
- data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
- data/lib/ea/qea/factory/instance_transformer.rb +68 -0
- data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
- data/lib/ea/qea/factory/operation_transformer.rb +66 -0
- data/lib/ea/qea/factory/package_transformer.rb +145 -0
- data/lib/ea/qea/factory/reference_resolver.rb +99 -0
- data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
- data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
- data/lib/ea/qea/factory/transformer_registry.rb +80 -0
- data/lib/ea/qea/factory.rb +37 -0
- data/lib/ea/qea/file_detector.rb +178 -0
- data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
- data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
- data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
- data/lib/ea/qea/infrastructure.rb +12 -0
- data/lib/ea/qea/models/base_model.rb +59 -0
- data/lib/ea/qea/models/ea_attribute.rb +109 -0
- data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
- data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
- data/lib/ea/qea/models/ea_connector.rb +160 -0
- data/lib/ea/qea/models/ea_connector_type.rb +60 -0
- data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
- data/lib/ea/qea/models/ea_datatype.rb +104 -0
- data/lib/ea/qea/models/ea_diagram.rb +115 -0
- data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
- data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
- data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
- data/lib/ea/qea/models/ea_document.rb +63 -0
- data/lib/ea/qea/models/ea_object.rb +223 -0
- data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
- data/lib/ea/qea/models/ea_object_property.rb +87 -0
- data/lib/ea/qea/models/ea_object_type.rb +73 -0
- data/lib/ea/qea/models/ea_operation.rb +127 -0
- data/lib/ea/qea/models/ea_operation_param.rb +76 -0
- data/lib/ea/qea/models/ea_package.rb +78 -0
- data/lib/ea/qea/models/ea_script.rb +62 -0
- data/lib/ea/qea/models/ea_status_type.rb +66 -0
- data/lib/ea/qea/models/ea_stereotype.rb +57 -0
- data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
- data/lib/ea/qea/models/ea_xref.rb +165 -0
- data/lib/ea/qea/models.rb +35 -0
- data/lib/ea/qea/repositories/base_repository.rb +225 -0
- data/lib/ea/qea/repositories/object_repository.rb +219 -0
- data/lib/ea/qea/repositories.rb +10 -0
- data/lib/ea/qea/services/configuration.rb +211 -0
- data/lib/ea/qea/services/database_loader.rb +191 -0
- data/lib/ea/qea/services.rb +10 -0
- data/lib/ea/qea/validation/association_validator.rb +73 -0
- data/lib/ea/qea/validation/attribute_validator.rb +91 -0
- data/lib/ea/qea/validation/base_validator.rb +331 -0
- data/lib/ea/qea/validation/class_validator.rb +121 -0
- data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
- data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
- data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
- data/lib/ea/qea/validation/database.rb +16 -0
- data/lib/ea/qea/validation/diagram_validator.rb +112 -0
- data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
- data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
- data/lib/ea/qea/validation/formatters.rb +12 -0
- data/lib/ea/qea/validation/operation_validator.rb +71 -0
- data/lib/ea/qea/validation/package_validator.rb +111 -0
- data/lib/ea/qea/validation/validation_engine.rb +387 -0
- data/lib/ea/qea/validation/validation_message.rb +144 -0
- data/lib/ea/qea/validation/validation_result.rb +210 -0
- data/lib/ea/qea/validation/validator_registry.rb +134 -0
- data/lib/ea/qea/validation.rb +28 -0
- data/lib/ea/qea/verification/comparison_result.rb +264 -0
- data/lib/ea/qea/verification/document_normalizer.rb +169 -0
- data/lib/ea/qea/verification/document_verifier.rb +322 -0
- data/lib/ea/qea/verification/element_comparator.rb +277 -0
- data/lib/ea/qea/verification/structure_matcher.rb +287 -0
- data/lib/ea/qea/verification.rb +14 -0
- data/lib/ea/qea.rb +185 -0
- data/lib/ea/transformations/configuration.rb +333 -0
- data/lib/ea/transformations/format_registry.rb +366 -0
- data/lib/ea/transformations/parsers/base_parser.rb +482 -0
- data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
- data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
- data/lib/ea/transformations/transformation_engine.rb +390 -0
- data/lib/ea/transformations.rb +85 -0
- data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
- data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
- data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
- data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
- data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
- data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
- data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
- data/lib/ea/transformers/qea_to_xmi.rb +29 -0
- data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
- data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
- data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
- data/lib/ea/transformers/uml_to_xmi.rb +16 -0
- data/lib/ea/transformers.rb +34 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
- data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
- data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
- data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
- data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
- data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
- data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
- data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
- data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
- data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
- data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
- data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
- data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
- data/lib/ea/xmi/lookup_service.rb +89 -0
- data/lib/ea/xmi/parser.rb +919 -0
- data/lib/ea/xmi.rb +35 -0
- data/lib/ea.rb +10 -1
- 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
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
|
data/lib/ea/cli/error.rb
ADDED
|
@@ -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)
|