Skip to content

feat: QeaToXmi emits full Sparx XMI extension (elements/connectors/diagrams) - #31

Open
ronaldtse wants to merge 11 commits into
mainfrom
feat/qeatoxmi-extension-serializer
Open

feat: QeaToXmi emits full Sparx XMI extension (elements/connectors/diagrams)#31
ronaldtse wants to merge 11 commits into
mainfrom
feat/qeatoxmi-extension-serializer

Conversation

@ronaldtse

Copy link
Copy Markdown
Contributor

Summary

  • Extracts EA-specific <xmi:Extension> serialization into a new ExtensionSerializer class (OCP: new element types = new method, no existing code edited).
  • Emits proper Sparx XMI structure: <elements> with per-element blocks (each carrying <model>, <properties documentation="...">, <style>, <tags>, plus nested <attributes>/<operations> sub-blocks), full <connectors> with source/target/body, and <diagrams> with placed elements.
  • Removes banned respond_to? patterns and the structurally-wrong top-level <tags>/<documentation> sections from the old code.

Parity improvement (basic.qea vs EA reference)

type before after ref
style 0 508 478
tags 0 508 478
documentation 0 345 357

Test plan

  • bundle exec rspec — 2084 examples, 0 failures (55 pre-existing pending)
  • bundle exec rspec spec/ea/validation/xmi_parity_spec.rb — all parity thresholds met
  • bundle exec ea export xmi examples/qea/basic.qea — produces valid XMI
  • No respond_to? / send / instance_variable_get violations introduced

…agrams)

Extract extension serialization into ExtensionSerializer (OCP):
new element types = new method, no existing code edited.

- <elements>: per package/classifier with <model>, <properties
  documentation="...">, <style>, <tags>, and nested <attributes>/
  <operations> blocks (each attribute/op/parameter gets its own
  <style>, <documentation>, <tags>).
- <connectors>: full source/target/end blocks with style/docs/tags
  each, plus connector-level body (properties, appearance, style).
- <diagrams>: placed <element> children with geometry.

Parity (basic.qea): style 0→508 (ref 478), tags 0→508 (ref 478),
documentation 0→345 (ref 357). All 2084 specs pass.

Removed broken top-level <tags>/<documentation> sections (wrong
structure — Sparx nests these per-element) and banned respond_to?
patterns from the old extension code.

Bumps version to 0.5.3.
Code quality pass — removes all banned patterns from lib and closes
10 TODOs discovered during architecture audit.

Banned patterns eliminated:
- respond_to? (6 sites) → is_a? type checks or direct method calls
  (ocl_constraint_validator, json/generator, ocl/evaluator,
  plantuml/generator)
- Internal require (2 sites) → removed; already autoloaded
  (svg/parity/source, svg/parity/suite)

Latent bugs fixed (found via TODO 51 — owner_for always returned
nil, masking these):
- OclConstraintValidator: result.add → result.add_info/add_warning
- OclConstraintValidator: o.object_id → o.ea_object_id (Ruby
  Object#object_id collision — same class of bug as TODO 51 bonus)
- OclEvaluator: attribute_value rescues NoMethodError → nil
  (OCL missing-attribute semantics)

OCP + naming:
- ExtensionSerializer.uml_type_for: case/when → UML_TYPE_FOR
  registry hash
- sType keyword arg → stype (Ruby snake_case; XML attr unchanged)

Specs:
- New: spec/ea/qea/models/all_models_spec.rb (76 examples covering
  19 remaining models)
- Total: 2160 examples, 0 failures, 55 pending

TODOs closed: 51, 64, 65, 67, 68, 69 (already done), 70, 71, 72, 73
DRY: Visibility symbol mapping extracted
- 3 SVG emitter files had duplicated case/when for visibility →
  symbol mapping (+/-/#/~). Extracted to
  Ea::Svg::EaEmitter::VisibilitySymbol.for(visibility, with_space:).
- New spec: visibility_symbol_spec.rb (9 examples).
- Affected: operation_renderer, attribute_line_builder, end_label.

MECE: Database table→collection registry
- BaseValidator had 22-branch case/when mapping table names to
  collection accessors. Moved to Database::TABLE_TO_COLLECTION hash.
- New method: Database#collection_for_table(name).
- Validator delegates in 1 line. Adding a table = 1 hash entry.
- New specs: 4 examples in database_spec.rb.

Specs: ExtensionSerializer coverage
- New: extension_serializer_spec.rb (14 examples covering
  elements/connectors/diagrams sections, UML_TYPE_FOR registry,
  SKIP_OBJECT_TYPES).

Stale TODO cleanup (76)
- 40 TODOs marked "open"/"deferred" but actually fully implemented.
  All updated to "done" with evidence in STATUS.md.

Total: 2187 examples, 0 failures, 55 pending.
The user's rules apply to specs too: no double(), no instance_double,
no send to private methods, no respond_to?. This commit eliminates
63 banned-pattern sites across 18 spec files.

send to private methods (13 to 0):
- emit_images: already public, just call directly
- package_content_lines_for: promoted via public :method
- operation_text: promoted via public_class_method :method
- parse_label_boxes: promoted via public :method

double/instance_double (48 to 0):
- Factory specs: replaced with Ea::Qea::Database.new("test.qea")
- document_builder_spec: Struct-based fakes (FakePackage, FakeClass,
  FakeEnum, FakeAssociation)
- reference_resolver_spec: Struct.new(:xmi_id, :name)
- database_loader_spec: real TableDefinition.new(enabled: false)
- compartment specs: Struct.new(:attr_first_y), Struct.new(:header_first_y)

respond_to (2 to 0):
- comprehensive_equivalence_spec: is_a?(Ea::Model::Classifier)
- transformer_spec: comment only

Full suite: 2187 examples, 0 failures, 55 pending.
EA's reference XMI includes MDG stereotype definitions (uml:Stereotype
+ uml:Extension + ownedAttribute for each tagged value). Previously
our QeaToXmi emitted zero of these for any model using MDG stereotypes.

Architecture (OCP — MDGs are runtime-swappable):

Ea::Mdg::Registry is the domain-level registry. Each registered MDG
is also registered with Lutaml::Model::GlobalRegister under
ea_mdg_{name}, making it discoverable through the framework's
standard register API. MDGs can be swapped in/out at runtime.

QeaToXmi::Transformer accepts mdg_registry: kwarg. When present,
ProfileSerializer iterates the registry at call time and builds
xmi gem model objects (PackagedElement, OwnedAttribute, Type,
MemberEnd, OwnedEnd) for each stereotype definition. Fully
model-driven — no raw XML string building.

Each stereotype emits:
- <packagedElement xmi:type="uml:Stereotype" xmi:id="..." name="...">
  with one <ownedAttribute> per tagged value + base_{metaclass} link
- <packagedElement xmi:type="uml:Extension"> with ownedEnd + memberEnds

Parity improvement (simple.qea with CityGML MDG fixture):
- packagedElement: 11 → 32 (ref 38)
- ownedAttribute: 0 → 47 (ref 34)
- ownedEnd: 0 → 10 (ref 12)

Full suite: 2203 examples, 0 failures, 55 pending.
COUNT parity was hiding structural equivalence gaps. Element-by-
element xmi:id comparison revealed: 585 content mismatches across
409 common IDs (out of 830 ours / 777 ref). Fixed the root causes:

Namespace: xmi gem uses 20131001, EA uses 20110701. Added
normalize_namespaces post-processor replacing the URIs.

Attribute verbosity: omitted UML defaults that EA does not emit:
- Visibility.from_scope(0) returns nil (not "public")
- Visibility.boolean_from_flag("0") returns nil (not false)
- normalize_concurrency returns nil for "Sequential"
- normalize_direction returns nil for "in"

exporterID: added exporter_id="1624" to xmi:Documentation.

aggregation: was reading sourcecontainment/destcontainment (string,
always "Unspecified"). Now reads sourceisaggregate/destisaggregate
(integer: 0=none, 1=shared, 2=composite). Matches EA's
aggregation="shared|composite" on ownedEnds.

Result: content mismatches dropped 585 to 237 (59 percent reduction).
Remaining gaps are synthesized ID ordering differences and some
scope field discrepancies.

Updated 8 specs that asserted the old behavior. Full suite:
2203 examples, 0 failures, 55 pending.
Comment on lines +101 to +104
"http://www.omg.org/spec/UML/20110701/UMLDI")
.gsub("http://www.omg.org/spec/UML/20131001/UMLDC",
"http://www.omg.org/spec/UML/20110701/UMLDC")
.gsub("http://www.omg.org/spec/UML/20131001",
Comment on lines +101 to +102
"http://www.omg.org/spec/UML/20110701/UMLDI")
.gsub("http://www.omg.org/spec/UML/20131001/UMLDC",
Performance (TODO 80):
ExtensionSerializer was doing O(n) linear scans where the Database
already had O(1) indexes. Replaced 7 hot-path lookups:
- connector source/target objects: .find{} -> find_object()
- diagram element objects: .find{} -> find_object()
- parent/owner/diagram packages: .find{} -> find_package()
- tagged values per element: .select{} -> tagged_values_for_element()

Export output is byte-identical. For ArcGIS-scale models this is a
100x+ speedup on extension serialization.

DRY (TODO 81):
- Extracted scope predicates (public?, private?, protected?) from
  EaAttribute + EaOperation into ScopePredicate mixin. 12 method
  definitions -> 3 in one shared module.
- Removed strip_guid from ExtensionSerializer; replaced with
  GuidFormat.ea_guid_to_xmi_id (already existed). Eliminated dead
  code.

public_send audit (TODO 82):
Audited all 34 sites. All are legitimate dynamic dispatch on public
methods. Documented in TODO file with categorisation.

Full suite: 2203 examples, 0 failures, 55 pending.
DRY violations across subsystems extracted into shared top-level
modules.

Ea::XmlEscape (new):
XML entity escape was duplicated in 4 files (text_renderer,
html_reporter, shapescript/renderer, extension_serializer) with
subtle differences. The ShapeScript version was MISSING quote
escaping - a latent bug. All four now call Ea::XmlEscape.call(text).
8 new specs.

Ea::GuidFormat (promoted from Transformers::QeaToXmi):
GUID-to-XMI-ID conversion was duplicated in IdNormalizer and
ReferenceResolver with inline gsub chains. Promoted GuidFormat to
top-level Ea::GuidFormat (shared across subsystems). Added
strip_braces for callers needing just brace removal.
Transformers::QeaToXmi::GuidFormat is now an alias (backward
compatible).

Export output byte-identical (405898 bytes).
Full suite: 2211 examples, 0 failures, 55 pending.
Markers had 3 case/when blocks all dispatching on spec.shape.
Adding a new marker shape required editing 3 methods - OCP violation.

Replaced with SHAPE_REGISTRY (shape to style_key + render lambda)
and STYLE_MAP (style_key to fill/opacity). Adding a new shape = 1
hash entry. Adding a new style = 1 hash entry. No existing code
edited.

Render methods (diamond_polygon, triangle_polygon, plus_path,
arrow_path, package_anchor_path) promoted from private to public
via public :method - they are now called from SHAPE_REGISTRY
lambdas, so per user rules they must be accessible.

Full suite: 2211 examples, 0 failures, 55 pending.
frozen_string_literal: Added missing magic comment to 10 files.
One file (mdg/registry.rb) had the non-functional variant
"frozen_string: true" which Ruby does not recognize - replaced
with the correct "frozen_string_literal: true".

Rubocop: 30 auto-correctable style offenses fixed across new
files (trailing commas, hash syntax).

Spec: Added normalize_type coverage to ProfileSerializer spec
(Boolean mapping + String default).

Full suite: 2213 examples, 0 failures, 55 pending.
frozen_string_literal: Found 13 files with the non-functional
variant "# frozen_string: true" (Ruby only recognizes
"frozen_string_literal: true"). These files' string literals were
NOT frozen - silent correctness gap. Fixed all 13.

Also removed duplicate frozen comments from 9 files where TODO 85
prepended the correct variant without removing the old wrong one.

All 421 lib files now have exactly one correct
"# frozen_string_literal: true" at line 1.

Lint subsystem specs: 0 -> 13 examples (spec/ea/lint/lint_spec.rb)
Covers Offense, LintRule, Engine.

Query subsystem specs: 0 -> 11 examples (spec/ea/query/builder_spec.rb)
Covers classes, interfaces, packages, with_type, in_package,
named, name_contains, chaining, immutability, Enumerable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants