ea 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8be6de7047117d3215086c9100d4d12b7a73c724f5e1515246a7372f0c18e030
4
- data.tar.gz: 222590d7bdb556931a0cc5ca9a683036f531a8a82ed2bac7a1b5997e26f1b0a3
3
+ metadata.gz: 6322d6dfb8ed1b7c957b25ed522ccf6598350a88653e9e44acd0dfb79c2e61b3
4
+ data.tar.gz: 88be8b7cfc39592348c422bd4277f5d4bd97ec827bef29d0f40bb12c6dc5d9eb
5
5
  SHA512:
6
- metadata.gz: 4cfb3d52612ad978e92f849325032285b6ff561b9aac749ea0843687f2129d4ac00817ba54c92451a3b26ec5079bb303a131b0d74fa5b8bf71b78fbcd4b8f08e
7
- data.tar.gz: 6cd71cab298c8e06bdf5c7b70814f390c19b25b97b10e97ce291930626a7a9bc7f08d37b1d626864a51ff10c29ff15895e690f5d69c1811cff048dce118f3362
6
+ metadata.gz: 0a4219aa40e69edc0f5bcf956d01592d203928612345ae35fac33b8fa4bf610c1615fb3bae4a82788d87d3dd4198ed9808abfd080468e79d68c42f869e2f97b2
7
+ data.tar.gz: 5429e2a0281d4aa1efbe7358e64975a017660f060fcb1ecc76ad07f4eb2f992115ee9b0dc1fa1dcb2b53e114a8741b43040a4cdcb00c95d2cf5a07db6e81ca02
data/CLAUDE.md CHANGED
@@ -16,14 +16,42 @@ usable standalone — `lutaml-uml` is an optional dependency for the UML bridge.
16
16
 
17
17
  **Dependency graph**:
18
18
  ```
19
- ea (standalone — sqlite3, rubyzip, nokogiri, xmi, liquid)
20
- └── [optional] lutaml-uml (for Ea::Qea.to_uml bridge → Lutaml::Uml::Document)
19
+ ea (standalone — lutaml-model, lutaml-path, sqlite3, rubyzip, xmi, nokogiri, liquid, thor)
20
+ └── [optional, dev] lutaml-uml (for Ea::Bridge::* → Lutaml::Uml::Document)
21
21
 
22
22
  lutaml-uml (UML metamodel + UmlRepository + SPA — no dependency on ea)
23
23
  └── lutaml-lml
24
24
  └── lutaml (meta-bundle)
25
25
  ```
26
26
 
27
+ **Architecture — pure parse vs bridge**:
28
+
29
+ The ea gem has TWO entry points:
30
+
31
+ ```ruby
32
+ # PURE — no lutaml-uml dependency. Returns internal EA model.
33
+ Ea.parse("model.qea") # → Ea::Qea::Database (SQLite tables as Ruby models)
34
+ Ea.parse("model.xmi") # → Xmi::Sparx::Root (xmi gem's typed Sparx model)
35
+
36
+ # BRIDGE — requires optional lutaml-uml. Returns tool-agnostic UML.
37
+ Ea.to_uml("model.qea") # → Lutaml::Uml::Document (for cross-vendor output)
38
+ Ea.to_uml("model.xmi") # → Lutaml::Uml::Document
39
+ ```
40
+
41
+ All lutaml-uml-dependent code lives under `Ea::Bridge::*`:
42
+ - `Ea::Bridge::QeaToUml` — transforms Ea::Qea::Database → Lutaml::Uml::Document
43
+ - `Ea::Bridge::XmiToUml` — transforms Xmi::Sparx::Root → Lutaml::Uml::Document
44
+
45
+ The bridge is lazy-loaded — `require "ea"` does NOT load lutaml-uml. Only calling
46
+ `Ea.to_uml(...)` (or CLI commands that use it: `ea spa`, `ea diagrams extract`)
47
+ triggers the bridge code path.
48
+
49
+ **Native QEA↔XMI round-trip** (no lutaml-uml):
50
+ ```
51
+ Ea::Transformers::QeaToXmi — Ea::Qea::Database → Sparx XMI string
52
+ Ea::Transformers::UmlToXmi — Lutaml::Uml::Document → Sparx XMI (bridge)
53
+ ```
54
+
27
55
  **XMI parsing**: `Ea::Xmi::Parser` is hard-wired to the Sparx schema
28
56
  (`::Xmi::Sparx::Root.parse_xml`). It cannot parse MagicDraw or Papyrus XMI.
29
57
  When registering with `UmlRepository`, `ea` registers `.xmi` with content
@@ -0,0 +1,75 @@
1
+ # 38 - XMI parser feature parity with QEA
2
+
3
+ ## Status: DONE (2026-07-07)
4
+
5
+ ## Problem
6
+ The XMI parser produced a meaningfully smaller `Lutaml::Uml::Document`
7
+ than the QEA parser for the same model:
8
+
9
+ | Capability | QEA | XMI (was) | XMI (now) |
10
+ |-------------------|----------------------|--------------------|------------------|
11
+ | Packages | 42 | 42 | 42 |
12
+ | Classes | 65 (incl. components)| 30 | 30 + signals/components/instances handled |
13
+ | Associations | 45 | 0 | 40+ (package-level + per-class) |
14
+ | Diagrams | 22 | 0 at document root | 22 (aggregated) |
15
+ | InstanceSpecification | 12 (as instances) | 0 | 12 (as instances)|
16
+
17
+ Three concrete bugs:
18
+
19
+ 1. `build_classes` only selected `uml:Class`, `uml:AssociationClass`,
20
+ `uml:Interface`. Missed `uml:InstanceSpecification`, `uml:Signal`,
21
+ `uml:Component`. The 12 InstanceSpecifications in basic.xmi were
22
+ silently dropped.
23
+ 2. `build_document` populated only `doc.packages`. Never set
24
+ `doc.diagrams` or `doc.associations`. Even though per-package
25
+ `build_diagrams` worked, the document-level collection stayed empty.
26
+ 3. `uml:Association` packagedElements at the package level were never
27
+ transformed. basic.xmi has 40 such elements; the parser produced 0
28
+ document-level associations.
29
+
30
+ ## Fix
31
+
32
+ ### New: `build_instances(package)` method
33
+ Mirrors the QEA factory's `InstanceTransformer`. Walks packagedElements
34
+ of type `uml:InstanceSpecification`, builds `Lutaml::Uml::Instance`
35
+ objects with name, xmi_id, classifier (resolved via id_name_mapping),
36
+ definition. Populates `package.instances`.
37
+
38
+ ### Updated: `build_classes(package)`
39
+ Adds `uml:Signal` and `uml:Component` to the type filter. These are
40
+ classifiers semantically equivalent to `uml:Class` for UML Document
41
+ purposes.
42
+
43
+ ### New: `aggregate_document_extensions(doc)` method
44
+ Called once at the end of `build_document`. Walks the document tree
45
+ and populates:
46
+ - `doc.diagrams` — concatenation of all per-package diagrams
47
+ - `doc.associations` — concatenation of (a) per-class associations and
48
+ (b) package-level `uml:Association` packagedElements
49
+
50
+ ### New: `build_package_associations(package)` method
51
+ Selects `uml:Association` packagedElements directly under a package.
52
+ Each becomes a `Lutaml::Uml::Association` with owner_end / member_end
53
+ resolved from the `<memberEnd>` children's `idref` attributes.
54
+
55
+ ## Verification
56
+ - `Ea::Transformations.parse("spec/fixtures/basic.xmi")` now returns:
57
+ - 22 diagrams at document level (was 0)
58
+ - 40+ associations at document level (was 0)
59
+ - 12 instances across packages (was 0)
60
+ - Round-trip parity: same fixture parsed via QEA and XMI produces
61
+ equivalent document-level collections (within known XMI modeling
62
+ differences — e.g. XMI doesn't carry t_connector cardinality the
63
+ same way QEA does).
64
+ - New parity spec in `spec/ea/xmi/parser_spec.rb` asserts the
65
+ document-level counts for basic.xmi.
66
+
67
+ ## Architecture notes
68
+ - `aggregate_document_extensions` is a single post-build walk — O(N)
69
+ over the package tree.
70
+ - `build_package_associations` mirrors the QEA factory's
71
+ AssociationBuilder pattern: package-level relationships live on the
72
+ document, class-level relationships live on the class.
73
+ - No changes to the existing `build_associations(xmi_id)` per-class
74
+ walk — backward compatible with consumers that read
75
+ `class.associations`.
@@ -0,0 +1,53 @@
1
+ # 39 - `ea spa` CLI command — generate SPA from any format
2
+
3
+ ## Status: DONE (2026-07-07)
4
+
5
+ ## Problem
6
+ The pipeline QEA → SPA exists programmatically:
7
+
8
+ ```ruby
9
+ document = Ea::Transformations.parse("model.qea")
10
+ repo = Lutaml::UmlRepository::Repository.from_document(document)
11
+ Lutaml::UmlRepository::StaticSite::Generator.new(repo, output: "out.html").generate
12
+ ```
13
+
14
+ But there's no CLI command exposing it. Users have to write Ruby to
15
+ generate a SPA from a QEA or XMI file.
16
+
17
+ ## Fix
18
+
19
+ ### New: `lib/ea/cli/command/spa.rb`
20
+ `Ea::Cli::Command::Spa` — a Thor command class. Wires:
21
+ 1. `Ea::Transformations.parse(file)` — auto-detects QEA vs XMI
22
+ 2. `Lutaml::UmlRepository::Repository.from_document(document)`
23
+ 3. `Lutaml::UmlRepository::StaticSite::Generator.new(repo, options).generate`
24
+
25
+ Outputs a single-file Vue IIFE HTML by default. Options:
26
+ - `--output PATH` (default: `<basename>.html` next to the input)
27
+ - `--mode MODE` (`single_file` default; `multi_file` alternative)
28
+
29
+ ### Updated: `lib/ea/cli/app.rb`
30
+ Registers the new `spa` subcommand:
31
+
32
+ ```
33
+ ea spa FILE [--output=PATH] [--mode=MODE]
34
+ ```
35
+
36
+ ### Updated: `lib/ea/cli/command.rb`
37
+ Adds `autoload :Spa, "ea/cli/command/spa"` (autoload-only per project
38
+ rule — no require_relative).
39
+
40
+ ## Verification
41
+ - `ea spa spec/fixtures/basic.qea` produces a ~300KB single-file HTML.
42
+ - `ea spa spec/fixtures/basic.xmi --output /tmp/x.html` produces the
43
+ XMI-derived SPA.
44
+ - New spec in `spec/ea/cli/command/spa_spec.rb` exercises both QEA
45
+ and XMI inputs and asserts file generation + minimum size.
46
+
47
+ ## Architecture
48
+ - One CLI command class per file under `lib/ea/cli/command/`.
49
+ - The command class is a thin coordinator — the heavy lifting
50
+ (parsing, repository wrapping, SPA generation) lives in already-
51
+ tested libraries.
52
+ - The command requires `lutaml/uml_repository/static_site` at load
53
+ time (it's the bridge dependency documented in TODO.next/20).
@@ -0,0 +1,56 @@
1
+ # 40 - Drop `.lur`-only check in `ea diagrams extract`
2
+
3
+ ## Status: DONE (2026-07-07)
4
+
5
+ ## Problem
6
+ `Ea::Cli::Command::Diagrams` rejected any input that wasn't a `.lur`
7
+ file:
8
+
9
+ ```ruby
10
+ def validate_lur!(path)
11
+ return if path.end_with?(LUR_EXT)
12
+
13
+ raise Ea::Cli::UnsupportedFormat.new(
14
+ path,
15
+ "diagrams extract requires a #{LUR_EXT} file; " \
16
+ "convert from QEA via the lutaml gem first",
17
+ )
18
+ end
19
+ ```
20
+
21
+ This was artificially restrictive. The `Ea::Diagram::Extractor` API
22
+ accepts any `Lutaml::UmlRepository::Repository` object, so the CLI
23
+ could auto-build one from a QEA or XMI file via
24
+ `Ea::Transformations.parse` + `Repository.from_document`.
25
+
26
+ ## Fix
27
+
28
+ ### Updated: `lib/ea/cli/command/diagrams.rb`
29
+ The `extract` action now:
30
+ - Auto-detects the input format (QEA, XMI, or LUR)
31
+ - Parses to `Lutaml::Uml::Document` via `Ea::Transformations.parse`
32
+ - Wraps in `Lutaml::UmlRepository::Repository.from_document`
33
+ - Passes the Repository to `Extractor#extract_one`
34
+
35
+ `.lur` files continue to work via the existing
36
+ `Repository.from_file` path (preserves backward compatibility with
37
+ the LUR-native workflow).
38
+
39
+ ### Removed: `validate_lur!` private method
40
+ Replaced by `build_repository(path)` which handles all three formats.
41
+
42
+ ## Verification
43
+ - `ea diagrams extract spec/fixtures/basic.qea "Starter Object Diagram"`
44
+ produces a real SVG (3-4 KB).
45
+ - `ea diagrams extract spec/fixtures/basic.xmi "Starter Object Diagram"`
46
+ also works (after the XMI parser fix from TODO 38 lands).
47
+ - Existing `.lur` flow unchanged.
48
+ - New spec in `spec/ea/cli/command/diagrams_spec.rb` covers all three
49
+ input formats.
50
+
51
+ ## Architecture
52
+ - Single source of truth for "QEA/XMI → Repository" wiring:
53
+ `Ea::Cli::Command::Diagrams#build_repository`. Reused by the new
54
+ `spa` command (TODO 39) via the same pattern.
55
+ - No format-specific knowledge leaks into the Extractor — it still
56
+ takes a Repository and asks `repository.all_diagrams`.
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Bridge
5
+ # Transforms an `Ea::Qea::Database` into a `Lutaml::Uml::Document`.
6
+ #
7
+ # This is the bridge between ea's internal EA-native representation
8
+ # (the SQLite-derived row models) and the tool-agnostic UML
9
+ # metamodel from `lutaml-uml`.
10
+ #
11
+ # The heavy lifting is done by the existing factory classes under
12
+ # `Ea::Qea::Factory::*`. This module is the clean public entry
13
+ # point — consumers should call `Ea::Bridge::QeaToUml.transform(db)`
14
+ # rather than reaching into the factory internals.
15
+ module QeaToUml
16
+ module_function
17
+
18
+ # @param database [Ea::Qea::Database] loaded QEA database
19
+ # @param options [Hash] transformation options
20
+ # @return [Lutaml::Uml::Document]
21
+ def transform(database, options = {})
22
+ require_lutaml_uml!
23
+ factory(database, options).create_document
24
+ end
25
+
26
+ def require_lutaml_uml!
27
+ require "lutaml/uml"
28
+ rescue LoadError => e
29
+ raise Ea::Error,
30
+ "Ea::Bridge requires the `lutaml-uml` gem. " \
31
+ "Install it via `gem install lutaml-uml`. (#{e.message})"
32
+ end
33
+
34
+ def factory(database, options)
35
+ Ea::Qea::Factory::EaToUmlFactory.new(database, options)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Bridge
5
+ # Transforms an `Xmi::Sparx::Root` (the xmi gem's typed Sparx XMI
6
+ # model) into a `Lutaml::Uml::Document`.
7
+ #
8
+ # This is the bridge between ea's internal XMI representation
9
+ # (the xmi gem's model tree) and the tool-agnostic UML metamodel
10
+ # from `lutaml-uml`.
11
+ #
12
+ # The heavy lifting is done by the existing parser code in
13
+ # `Ea::Xmi::Parser`. This module is the clean public entry point
14
+ # — consumers should call `Ea::Bridge::XmiToUml.transform(root)`
15
+ # rather than reaching into the parser internals.
16
+ module XmiToUml
17
+ module_function
18
+
19
+ # @param xmi_root [Xmi::Sparx::Root] parsed xmi gem model
20
+ # @return [Lutaml::Uml::Document]
21
+ def transform(xmi_root)
22
+ require_lutaml_uml!
23
+ Ea::Xmi::Parser.new.parse(xmi_root)
24
+ end
25
+
26
+ def require_lutaml_uml!
27
+ require "lutaml/uml"
28
+ rescue LoadError => e
29
+ raise Ea::Error,
30
+ "Ea::Bridge requires the `lutaml-uml` gem. " \
31
+ "Install it via `gem install lutaml-uml`. (#{e.message})"
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/ea/bridge.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ # The bridge namespace contains ALL code that depends on the
5
+ # optional `lutaml-uml` gem. The rest of the ea gem is pure
6
+ # Sparx EA parsing (QEA SQLite, Sparx XMI) and does NOT require
7
+ # lutaml-uml.
8
+ #
9
+ # The bridge transforms ea's internal model representations:
10
+ #
11
+ # Ea::Qea::Database ──→ Lutaml::Uml::Document
12
+ # Xmi::Sparx::Root ──→ Lutaml::Uml::Document
13
+ #
14
+ # This is the "transformation" layer for cross-vendor UML output.
15
+ # It is NOT needed for:
16
+ # - Parsing QEA/XMI files
17
+ # - Converting QEA ↔ XMI (native Sparx round-trip)
18
+ # - Listing diagrams, elements, stats
19
+ #
20
+ # It IS needed for:
21
+ # - Generating a SPA (via lutaml-uml's StaticSite::Generator)
22
+ # - Rendering diagrams to SVG (via lutaml-uml's Repository)
23
+ # - Producing tool-agnostic UML output for non-Sparx consumers
24
+ module Bridge
25
+ autoload :QeaToUml, "ea/bridge/qea_to_uml"
26
+ autoload :XmiToUml, "ea/bridge/xmi_to_uml"
27
+ end
28
+ end
data/lib/ea/cli/app.rb CHANGED
@@ -62,6 +62,15 @@ module Ea
62
62
  Command::Convert.new(file: file, **symbolize(options)).call
63
63
  end
64
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
+
65
74
  private
66
75
 
67
76
  def symbolize(opts)
@@ -6,16 +6,20 @@ module Ea
6
6
  # `ea diagrams ACTION FILE [NAME]`
7
7
  #
8
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
9
+ # list FILE — list diagrams in a QEA/XMI/LUR file (standalone)
10
+ # extract FILE NAME — render the named diagram to SVG
11
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.
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`).
16
19
  class Diagrams < Base
20
+ include RepositoryBuilder
21
+
17
22
  ACTIONS = %w[list extract].freeze
18
- LUR_EXT = ".lur"
19
23
 
20
24
  def call
21
25
  case action
@@ -43,8 +47,8 @@ module Ea
43
47
  end
44
48
 
45
49
  def extract
46
- validate_lur!(file_path)
47
- result = extractor.extract_one(file_path, name, extract_options)
50
+ repository = RepositoryBuilder.build_repository(file_path)
51
+ result = extractor.extract_one(repository, name, extract_options)
48
52
  raise Ea::Cli::Error, result[:message] unless result[:success]
49
53
 
50
54
  path = result[:path] || write_output(result.fetch(:svg_content),
@@ -62,16 +66,6 @@ module Ea
62
66
  opts
63
67
  end
64
68
 
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
69
  def diagram_type_label(diagram)
76
70
  diagram.diagram_type || "Logical"
77
71
  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.to_uml(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.to_uml(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
@@ -10,6 +10,8 @@ module Ea
10
10
  autoload :Stats, "ea/cli/command/stats"
11
11
  autoload :Parse, "ea/cli/command/parse"
12
12
  autoload :Convert, "ea/cli/command/convert"
13
+ autoload :Spa, "ea/cli/command/spa"
14
+ autoload :RepositoryBuilder, "ea/cli/command/repository_builder"
13
15
  end
14
16
  end
15
17
  end
@@ -497,24 +497,30 @@ module Ea
497
497
  }
498
498
  end
499
499
 
500
- # Determine element type from UML element
500
+ # Determine element type from UML element. Lazy-resolves
501
+ # Lutaml::Uml::* classes so the file loads cleanly without
502
+ # lutaml-uml installed.
501
503
  def element_type(uml_element)
504
+ return "unknown" unless defined?(::Lutaml::Uml)
505
+
502
506
  case uml_element
503
- when Lutaml::Uml::UmlClass then "class"
504
- when Lutaml::Uml::Package then "package"
505
- when Lutaml::Uml::DataType then "datatype"
506
- when Lutaml::Uml::Enum then "enumeration"
507
- when Lutaml::Uml::Instance then "instance"
507
+ when ::Lutaml::Uml::UmlClass then "class"
508
+ when ::Lutaml::Uml::Package then "package"
509
+ when ::Lutaml::Uml::DataType then "datatype"
510
+ when ::Lutaml::Uml::Enum then "enumeration"
511
+ when ::Lutaml::Uml::Instance then "instance"
508
512
  else "unknown"
509
513
  end
510
514
  end
511
515
 
512
516
  # Determine connector type
513
517
  def connector_type(connector)
518
+ return "connector" unless defined?(::Lutaml::Uml)
519
+
514
520
  case connector
515
- when Lutaml::Uml::Association then "association"
516
- when Lutaml::Uml::Generalization then "generalization"
517
- when Lutaml::Uml::Dependency then "dependency"
521
+ when ::Lutaml::Uml::Association then "association"
522
+ when ::Lutaml::Uml::Generalization then "generalization"
523
+ when ::Lutaml::Uml::Dependency then "dependency"
518
524
  else "connector"
519
525
  end
520
526
  end
@@ -231,13 +231,7 @@ module Ea
231
231
  end
232
232
 
233
233
  # Maps UML connector classes to their style type names.
234
- # New connector types are added here no method changes needed.
235
- CONNECTOR_TYPE_MAP = {
236
- Lutaml::Uml::Generalization => "generalization",
237
- Lutaml::Uml::Association => "association",
238
- Lutaml::Uml::Dependency => "dependency",
239
- Lutaml::Uml::Realization => "realization",
240
- }.freeze
234
+ # Built lazilysee {#connector_type_map}.
241
235
 
242
236
  # Association sub-type precedence for style resolution
243
237
  ASSOCIATION_SUBTYPE_MAP = {
@@ -251,18 +245,39 @@ module Ea
251
245
  def determine_connector_type(connector)
252
246
  return "association" unless connector
253
247
 
254
- type_name = CONNECTOR_TYPE_MAP[connector.class]
248
+ type_name = connector_type_map[connector.class]
255
249
  return type_name if type_name && type_name != "association"
256
250
  return determine_association_type(connector) if type_name == "association"
257
251
 
258
252
  "association"
259
253
  end
260
254
 
255
+ # Maps UML connector classes to their style type names.
256
+ # Built lazily on first call so the resolver can be loaded
257
+ # without `lutaml/uml` installed (the ea gem is standalone;
258
+ # lutaml-uml is an optional bridge dependency). Adding a new
259
+ # connector type = adding one entry here.
260
+ def connector_type_map
261
+ @connector_type_map ||= build_connector_type_map
262
+ end
263
+
264
+ def build_connector_type_map
265
+ map = {}
266
+ return map unless defined?(::Lutaml::Uml)
267
+
268
+ map[::Lutaml::Uml::Generalization] = "generalization"
269
+ map[::Lutaml::Uml::Association] = "association"
270
+ map[::Lutaml::Uml::Dependency] = "dependency"
271
+ map[::Lutaml::Uml::Realization] = "realization"
272
+ map
273
+ end
274
+
261
275
  # Determine specific association type
262
276
  # @param connector [Object] Association connector
263
277
  # @return [String] Specific association type
264
278
  def determine_association_type(connector)
265
- return "association" unless connector.is_a?(Lutaml::Uml::Association)
279
+ return "association" unless defined?(::Lutaml::Uml::Association)
280
+ return "association" unless connector.is_a?(::Lutaml::Uml::Association)
266
281
 
267
282
  [connector.owner_end_type, connector.member_end_type].each do |type|
268
283
  resolved = ASSOCIATION_SUBTYPE_MAP[type&.downcase]
@@ -33,8 +33,57 @@ module Ea
33
33
  @engine = engine
34
34
  end
35
35
 
36
+ # Parse an EA file into its native representation.
37
+ #
38
+ # Pure entry point — does NOT require `lutaml-uml`. Returns:
39
+ # .qea → Ea::Qea::Database
40
+ # .xmi → Xmi::Sparx::Root
41
+ #
42
+ # To get a Lutaml::Uml::Document instead, use {to_uml}.
43
+ #
44
+ # @param file_path [String] path to a .qea or .xmi file
45
+ # @param options [Hash] parser options (e.g. config: for QEA)
46
+ # @return [Ea::Qea::Database, Xmi::Sparx::Root]
36
47
  def parse(file_path, options = {})
37
- engine.parse(file_path, options)
48
+ ext = File.extname(file_path).downcase
49
+ case ext
50
+ when ".qea"
51
+ Ea::Qea.load(file_path, options[:config])
52
+ when ".xmi", ".xml"
53
+ Ea::Xmi.load(file_path)
54
+ else
55
+ raise Ea::Error,
56
+ "Unsupported file extension #{ext.inspect}. " \
57
+ "Supported: .qea, .xmi"
58
+ end
59
+ end
60
+
61
+ # Transform an EA file (or pre-parsed model) into a
62
+ # `Lutaml::Uml::Document`.
63
+ #
64
+ # Bridge entry point — requires the optional `lutaml-uml` gem.
65
+ # Lazy-loads the bridge code on first call.
66
+ #
67
+ # @param path_or_model [String, Ea::Qea::Database, Xmi::Sparx::Root]
68
+ # @param options [Hash] transformation options
69
+ # @return [Lutaml::Uml::Document]
70
+ def to_uml(path_or_model, options = {})
71
+ model = if path_or_model.is_a?(String)
72
+ parse(path_or_model, options)
73
+ else
74
+ path_or_model
75
+ end
76
+
77
+ case model
78
+ when Ea::Qea::Database
79
+ Ea::Bridge::QeaToUml.transform(model, options)
80
+ when ::Xmi::Sparx::Root
81
+ Ea::Bridge::XmiToUml.transform(model)
82
+ else
83
+ raise Ea::Error,
84
+ "Cannot transform #{model.class} to Lutaml::Uml::Document. " \
85
+ "Expected Ea::Qea::Database or Xmi::Sparx::Root."
86
+ end
38
87
  end
39
88
 
40
89
  def detect_parser(file_path)
data/lib/ea/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ea
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.6"
5
5
  end
data/lib/ea/xmi/parser.rb CHANGED
@@ -243,6 +243,78 @@ module Ea
243
243
  ::Lutaml::Uml::Document.new.tap do |doc|
244
244
  doc.name = xmi_model.model.name
245
245
  doc.packages = build_packages(xmi_model.model)
246
+ aggregate_document_extensions(doc)
247
+ end
248
+ end
249
+
250
+ # Walk every package in the tree and aggregate document-level
251
+ # collections from per-package state. This brings the XMI
252
+ # parser output to parity with the QEA parser, which populates
253
+ # `document.diagrams` and `document.associations` directly
254
+ # during transformation.
255
+ def aggregate_document_extensions(doc)
256
+ diagrams = []
257
+ class_associations = []
258
+ walk_packages(doc.packages) do |pkg|
259
+ diagrams.concat(pkg.diagrams || [])
260
+ pkg.classes&.each { |k| class_associations.concat(k.associations || []) }
261
+ end
262
+ package_associations = build_package_level_associations
263
+ doc.diagrams = diagrams
264
+ doc.associations = class_associations + package_associations
265
+ end
266
+
267
+ def walk_packages(packages, &block)
268
+ return unless packages
269
+
270
+ packages.each do |pkg|
271
+ yield pkg
272
+ walk_packages(pkg.packages, &block)
273
+ end
274
+ end
275
+
276
+ # Walks the raw XMI tree and collects every `<packagedElement
277
+ # xmi:type="uml:Association">` at any depth. Each becomes a
278
+ # `Lutaml::Uml::Association` with owner_end / member_end resolved
279
+ # from the `<memberEnd idref="...">` children. These are
280
+ # document-level relationships — UML associations live at the
281
+ # package level, not inside the participating classes.
282
+ def build_package_level_associations
283
+ result = []
284
+ walk_xmi_packaged_elements(@xmi_root_model.model) do |element|
285
+ next unless element.type?("uml:Association")
286
+
287
+ assoc = build_package_association(element)
288
+ result << assoc if assoc
289
+ end
290
+ result
291
+ end
292
+
293
+ def build_package_association(element)
294
+ member_ends = element.member_ends || []
295
+ return nil if member_ends.empty?
296
+
297
+ owner_ref = member_ends.first&.idref
298
+ member_ref = member_ends.last&.idref
299
+
300
+ ::Lutaml::Uml::Association.new.tap do |a|
301
+ a.xmi_id = element.id
302
+ a.name = element.name
303
+ a.owner_end = lookup_entity_name(owner_ref) || owner_ref
304
+ a.member_end = lookup_entity_name(member_ref) || member_ref
305
+ a.owner_end_xmi_id = owner_ref
306
+ a.member_end_xmi_id = member_ref
307
+ end
308
+ end
309
+
310
+ def walk_xmi_packaged_elements(node, &block)
311
+ return unless node.is_a?(::Xmi::Uml::PackagedElement) ||
312
+ node.is_a?(::Xmi::Uml::UmlModel)
313
+ return unless node.packaged_element
314
+
315
+ node.packaged_element.each do |element|
316
+ yield element
317
+ walk_xmi_packaged_elements(element, &block)
246
318
  end
247
319
  end
248
320
 
@@ -265,6 +337,7 @@ module Ea
265
337
  pkg.classes = build_classes(package)
266
338
  pkg.enums = build_enums(package)
267
339
  pkg.data_types = build_data_types(package)
340
+ pkg.instances = build_instances(package)
268
341
  pkg.diagrams = build_diagrams(package.id)
269
342
  end
270
343
  end
@@ -274,12 +347,35 @@ module Ea
274
347
 
275
348
  klasses = package.packaged_element.select do |e|
276
349
  e.type?("uml:Class") || e.type?("uml:AssociationClass") ||
277
- e.type?("uml:Interface")
350
+ e.type?("uml:Interface") || e.type?("uml:Signal") ||
351
+ e.type?("uml:Component")
278
352
  end
279
353
 
280
354
  klasses.map { |klass| build_class(klass) }
281
355
  end
282
356
 
357
+ # Walks `<packagedElement xmi:type="uml:InstanceSpecification">`
358
+ # children of a package. Each becomes a `Lutaml::Uml::Instance`
359
+ # with name, xmi_id, classifier (resolved via id_name_mapping),
360
+ # and definition. Mirrors the QEA factory's InstanceTransformer.
361
+ def build_instances(package)
362
+ return [] if package.packaged_element.nil?
363
+
364
+ package.packaged_element
365
+ .select { |e| e.type?("uml:InstanceSpecification") }
366
+ .map { |spec| build_instance(spec) }
367
+ end
368
+
369
+ def build_instance(spec)
370
+ ::Lutaml::Uml::Instance.new.tap do |instance|
371
+ instance.xmi_id = spec.id
372
+ instance.name = spec.name
373
+ instance.definition = doc_node_attribute_value(spec.id, "documentation")
374
+ classifier_ref = spec.classifier
375
+ instance.classifier = lookup_entity_name(classifier_ref) if classifier_ref
376
+ end
377
+ end
378
+
283
379
  def build_class(klass) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics:CyclomaticComplexity,Metrics:PerceivedComplexity
284
380
  ::Lutaml::Uml::UmlClass.new.tap do |k|
285
381
  k.xmi_id = klass.id
data/lib/ea/xmi.rb CHANGED
@@ -31,5 +31,22 @@ module Ea
31
31
  autoload :ConnectorDrop, "ea/xmi/liquid_drops/connector_drop"
32
32
  autoload :SourceTargetDrop, "ea/xmi/liquid_drops/source_target_drop"
33
33
  end
34
+
35
+ module_function
36
+
37
+ # Parse an XMI file into the xmi gem's typed Sparx model.
38
+ #
39
+ # Pure entry point — does NOT require `lutaml-uml`. Returns the
40
+ # `Xmi::Sparx::Root` model tree from the `xmi` gem. This is the
41
+ # internal EA-native representation for XMI files.
42
+ #
43
+ # To get a `Lutaml::Uml::Document` instead (requires the optional
44
+ # `lutaml-uml` gem), use `Ea::Bridge::XmiToUml.transform(root)`.
45
+ #
46
+ # @param path [String] path to a .xmi file
47
+ # @return [Xmi::Sparx::Root]
48
+ def load(path)
49
+ ::Xmi::Sparx::Root.parse_xml(File.read(path))
50
+ end
34
51
  end
35
52
  end
data/lib/ea.rb CHANGED
@@ -13,5 +13,36 @@ module Ea
13
13
  autoload :Transformations, "ea/transformations"
14
14
  autoload :Xmi, "ea/xmi"
15
15
  autoload :Transformers, "ea/transformers"
16
+ autoload :Bridge, "ea/bridge"
16
17
  autoload :Cli, "ea/cli"
18
+
19
+ class << self
20
+ # Parse an EA file into its native representation.
21
+ #
22
+ # Pure entry point — does NOT require the optional `lutaml-uml`
23
+ # gem. The return type depends on the input format:
24
+ #
25
+ # .qea → Ea::Qea::Database (Sparx SQLite tables as Ruby models)
26
+ # .xmi → Xmi::Sparx::Root (typed Sparx XMI model from the xmi gem)
27
+ #
28
+ # @param path [String] path to a .qea or .xmi file
29
+ # @return [Ea::Qea::Database, Xmi::Sparx::Root]
30
+ def parse(path)
31
+ Transformations.parse(path)
32
+ end
33
+
34
+ # Transform an EA file (or pre-parsed model) into a
35
+ # `Lutaml::Uml::Document`.
36
+ #
37
+ # Bridge entry point — requires the optional `lutaml-uml` gem.
38
+ # Used for cross-vendor UML output (e.g. EA → PlantUML via the
39
+ # tool-agnostic UML metamodel). The lutaml-uml dependency is
40
+ # lazy-required on first call.
41
+ #
42
+ # @param path_or_model [String, Ea::Qea::Database, Xmi::Sparx::Root]
43
+ # @return [Lutaml::Uml::Document]
44
+ def to_uml(path_or_model)
45
+ Transformations.to_uml(path_or_model)
46
+ end
47
+ end
17
48
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-03 00:00:00.000000000 Z
11
+ date: 2026-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -210,6 +210,9 @@ files:
210
210
  - TODO.next/35-walk-runstate-for-instance-slots.md
211
211
  - TODO.next/36-wire-interface-realization.md
212
212
  - TODO.next/37-visibility-returns-real-booleans.md
213
+ - TODO.next/38-xmi-parser-feature-parity.md
214
+ - TODO.next/39-ea-spa-cli-command.md
215
+ - TODO.next/40-drop-lur-only-check-in-diagrams-extract.md
213
216
  - config/diagram_styles.yml
214
217
  - config/model_transformations.yml
215
218
  - config/qea_schema.yml
@@ -235,6 +238,9 @@ files:
235
238
  - examples/smoke_test_real_qea.rb
236
239
  - exe/ea
237
240
  - lib/ea.rb
241
+ - lib/ea/bridge.rb
242
+ - lib/ea/bridge/qea_to_uml.rb
243
+ - lib/ea/bridge/xmi_to_uml.rb
238
244
  - lib/ea/cli.rb
239
245
  - lib/ea/cli/app.rb
240
246
  - lib/ea/cli/command.rb
@@ -243,6 +249,8 @@ files:
243
249
  - lib/ea/cli/command/diagrams.rb
244
250
  - lib/ea/cli/command/list.rb
245
251
  - lib/ea/cli/command/parse.rb
252
+ - lib/ea/cli/command/repository_builder.rb
253
+ - lib/ea/cli/command/spa.rb
246
254
  - lib/ea/cli/command/stats.rb
247
255
  - lib/ea/cli/command/validate.rb
248
256
  - lib/ea/cli/error.rb