xmi 0.7.3 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a22cfc924ac43626705d0adc35d932bff62d935001a2bde676ec3f0b639cf3fb
4
- data.tar.gz: 35f16dfcc225665a88cf70d3fc1a9c5720c5fe4a1c64e3f7973b6fdb47197ead
3
+ metadata.gz: 588e344ea2df1c00d5050e7377bad54f7afca96e3925b327250cff8902a5027c
4
+ data.tar.gz: 56c69d3bc8056dffc1931af4c2d112db7b351e9fa39c4061b4073428be9b756a
5
5
  SHA512:
6
- metadata.gz: c4d7b2c9e3b50521736fe7f44afe34a1f6340e201673516d83fe2067433043da4aa5e136ad6f9a9b62ba1d21d708b8e67c48bac1dda25392da20de755bdfbcaf
7
- data.tar.gz: 87aeb1af821d63b69b037d35db8018e61e4398280ed8879d73de31efcd8195d59a89f2860464272cb107443190eeeb67d52342e84de40e4f07c8cdc4bd923f12
6
+ metadata.gz: 802a4c35900a1f3ed32a7d7ee0ad9b41a74686e0d1aedb5db318a1ffaa1bf9ca81f22d94d3672599bde39b56068ef4d5481242340552298bcd148eb479ee440d
7
+ data.tar.gz: '089817e9e7b880db5756a86023541012f5ef7a6e0b0f9161bc0a2e6a140bd915f69b9040d9175aadffd1ce3d0fee3fb2df2f2ed7d86a410d3cedfe64a114caeb'
@@ -7,6 +7,9 @@ on:
7
7
  pull_request:
8
8
  workflow_dispatch:
9
9
 
10
+ permissions:
11
+ contents: read
12
+
10
13
  jobs:
11
14
  rake:
12
15
  uses: metanorma/ci/.github/workflows/generic-rake.yml@main
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+
12
+ - All parse entry points (`Xmi.parse`, `Xmi.parse_with_version`,
13
+ `Xmi::Parsing.parse`, `Sparx::Root.parse_xml`) route through
14
+ `ParserPipeline`. Encoding repair no longer depends on which entry
15
+ point you pick — four of the five previously crashed on invalid
16
+ UTF-8. The dead `Sparx::Root.fix_encoding` duplicate is removed
17
+ - The Root-level namespace scope is declared once
18
+ (`Xmi::Namespace::SCOPE`) and shared by `Xmi::Root` and
19
+ `Mappings::BaseMapping`; the two verbatim copies could drift
20
+ silently
21
+ - The version modules (`V20110701`, `V20131001`, `V20161101`) declare
22
+ their table entry via `Versioned.define_version` instead of
23
+ repeating the register_id/namespace_classes/fallback_registers
24
+ boilerplate; each file now carries only its genuine delta
25
+ - `OwnedAttribute`, `OwnedEnd`, and `OwnedParameter` inherit their
26
+ shared value-specification surface (uml_type + upper/lower/default
27
+ values + the four element mappings) from the new
28
+ `Xmi::Uml::ValueSpecs` base; serialized element order is pinned to
29
+ EA parity (type, lowerValue, upperValue)
30
+
10
31
  ### Fixed
11
32
 
12
33
  - Gemspec: restore the optimistic `~> 0.8.17` constraint on
data/CLAUDE.md CHANGED
@@ -26,8 +26,22 @@ bundle exec rake
26
26
 
27
27
  # Interactive console for experimentation
28
28
  bin/console
29
+
30
+ # Benchmarks (dev tooling under Xmi::Performance)
31
+ bundle exec rake performance:quick # fast, less accurate
32
+ bundle exec rake performance:json # JSON output
33
+ ```
34
+
35
+ ### RuboCop toolchain note
36
+
37
+ `Gemfile.lock` is **gitignored** — each machine resolves its own versions, and CI installs lock-free (typically a newer RuboCop than a stale local lock resolves). If local rubocop disagrees with CI:
38
+
39
+ ```bash
40
+ bundle update rubocop rubocop-ast rubocop-performance rubocop-rspec rubocop-rake
29
41
  ```
30
42
 
43
+ Use the `rubocop:disable-next Cop` directive form (not `disable-next-line`) — it is the form current RuboCop accepts and requires for single-statement disables.
44
+
31
45
  ## Architecture Overview
32
46
 
33
47
  This gem converts XMI (XML Metadata Interchange) files into Ruby objects, specifically designed for Enterprise Architect generated XMI files.
@@ -37,18 +51,29 @@ This gem converts XMI (XML Metadata Interchange) files into Ruby objects, specif
37
51
  - **lutaml-model**: All serializable models inherit from `Lutaml::Model::Serializable`
38
52
  - **nokogiri**: XML parsing backend
39
53
 
40
- ### Main Entry Point
54
+ ### Main Entry Point / Parse Pipeline
55
+
56
+ Every public parse door routes through one module, `Xmi::ParserPipeline`
57
+ (`lib/xmi/parser_pipeline.rb`):
41
58
 
42
- `Xmi::Sparx::Root.parse_xml(xml_content)` is the primary method to parse XMI files. It performs preprocessing before parsing:
59
+ - `Xmi.parse(xml)` auto version detection
60
+ - `Xmi.parse_with_version(xml, version)`
61
+ - `Xmi::Parsing.parse(xml, options)` — IO coercion, `:version`/`:register`/`:model_class` options
62
+ - `Xmi::Sparx::Root.parse_xml(xml)` — also builds `Sparx::Index`
43
63
 
44
- 1. `fix_encoding` - Fixes invalid UTF-8 byte sequences in the XML content
45
- 2. `normalize_omg_namespace_versions` - Normalizes OMG namespace versions (XMI, UML) to canonical 20131001
46
- 3. `resolve_relative_namespaces` - Replaces relative `xmlns` values with `targetNamespace` values
47
- 4. `rename_ea_xmlns_attribute` - Renames `xmlns` attribute to `altered_xmlns` on EA-specific elements (see below)
64
+ Pipeline steps: `FixEncoding` (repairs invalid UTF-8)
65
+ `RenameEaXmlnsAttribute` (see the EA xmlns quirk below)
66
+ `InitVersioning` `ParseXml` (detects the version register, or uses
67
+ `ctx[:register]` when a door resolved one) `BuildIndex` (Root
68
+ hierarchy only — custom `:model_class` models are skipped). Encoding
69
+ repair runs no matter which door a consumer picks;
70
+ `parser_pipeline_spec.rb` pins this for all doors.
48
71
 
49
- ### OMG Namespace Version Normalization
72
+ Adding a parse concern = adding a pipeline step, not editing doors.
50
73
 
51
- OMG publishes XMI and UML specifications with dated namespace URIs (e.g., `http://www.omg.org/spec/XMI/20110701`, `20131001`, `20161101`). This library normalizes all versions to `20131001` during parsing, allowing a single set of model classes to handle all versions.
74
+ ### OMG Namespace Version Handling
75
+
76
+ OMG publishes XMI and UML specifications with dated namespace URIs (e.g., `http://www.omg.org/spec/XMI/20110701`, `20131001`, `20161101`). Documents of any version parse via per-version registers: `NamespaceDetector` scans the namespaces, `VersionRegistry.detect_register` picks the register (`V20110701` / `V20131001` / `V20161101`), and mixed-version documents extend the primary register's fallback chain. Model classes stay version-agnostic by using alias namespace classes that inherit from the 20131001 canonical versions.
52
77
 
53
78
  ### Enterprise Architect's Misuse of the `xmlns` Attribute
54
79
 
@@ -56,17 +81,7 @@ OMG publishes XMI and UML specifications with dated namespace URIs (e.g., `http:
56
81
 
57
82
  In standard XML, `xmlns` is a reserved attribute for namespace declarations. However, Enterprise Architect incorrectly uses `xmlns` as a **regular data attribute** on certain stereotype elements (e.g., `GML:ApplicationSchema`, `CityGML:ApplicationSchema`), storing arbitrary URI values unrelated to namespace declarations.
58
83
 
59
- This violates XML conventions and creates parsing conflicts—XML libraries treat `xmlns` as reserved. The library works around this by renaming `xmlns` to `altered_xmlns` before parsing:
60
-
61
- ```xml
62
- <!-- EA-generated -->
63
- <GML:ApplicationSchema xmlns="http://some-value" ...>
64
-
65
- <!-- After preprocessing -->
66
- <GML:ApplicationSchema altered_xmlns="http://some-value" ...>
67
- ```
68
-
69
- Model classes define `altered_xmlns` attributes to receive these values:
84
+ This violates XML conventions and creates parsing conflicts—XML libraries treat `xmlns` as reserved. The workaround runs in the parse pipeline (`ParserPipeline::Steps::RenameEaXmlnsAttribute`): before parsing, `xmlns` is renamed to `altered_xmlns` on `GML:ApplicationSchema` and `CityGML:ApplicationSchema` tags only — legitimate namespace declarations are untouched. `Xmi::EaRoot` extension code generation applies the same rename when building classes from MDG XML that defines an attribute literally named `xmlns`. Pinned by `parser_pipeline_spec.rb` against the real `xmi-v2-4-2-default-with-gml.xmi` fixture.
70
85
 
71
86
  ```ruby
72
87
  class ApplicationSchema < Lutaml::Model::Serializable
@@ -74,13 +89,20 @@ class ApplicationSchema < Lutaml::Model::Serializable
74
89
  end
75
90
  ```
76
91
 
77
- ### Namespace Architecture
92
+ ### EA Writes Attribute Names Verbatim
93
+
94
+ Some EA attributes are written in non-camelCase spellings that must be
95
+ mapped literally — "correcting" them breaks parsing. Examples:
96
+ `package_name` and `associationclass` on `<extendedProperties>` (all
97
+ lowercase, no case conversion), pinned by
98
+ `spec/xmi/sparx/element/extended_properties_spec.rb`.
78
99
 
79
- **All XMI/UML namespace versions are normalized to 20131001 before parsing** (see `Root.replace_xmlns`).
100
+ ### Namespace Architecture
80
101
 
81
102
  Namespace classes are defined in:
82
103
  - `lib/xmi/namespace/omg.rb` - OMG namespaces (XMI, UML, UmlDi, UmlDc)
83
104
  - `lib/xmi/namespace/sparx.rb` - Sparx-specific profiles (SysPhS, GML, EaUml, CustomProfile, CityGML)
105
+ - `lib/xmi/namespace.rb` - `Xmi::Namespace::SCOPE`: the canonical 10-namespace scope shared by `Xmi::Root`'s xml block and `Sparx::Mappings::BaseMapping` (pinned by `spec/xmi/namespace_scope_spec.rb`)
84
106
 
85
107
  Use version-agnostic alias classes that inherit from 20131001 versions:
86
108
  ```ruby
@@ -88,6 +110,18 @@ Use version-agnostic alias classes that inherit from 20131001 versions:
88
110
  ::Xmi::Namespace::Omg::Uml # => inherits from Uml20131001
89
111
  ```
90
112
 
113
+ ### Version Registers
114
+
115
+ `V20110701` / `V20131001` / `V20161101` are table entries: each calls
116
+ `Versioned.define_version(register_id:, namespaces:, fallbacks:)` and
117
+ keeps only its real delta (version-specific `Documentation`/`Extension`
118
+ classes plus a `register_models` table). Adding an XMI version = a new
119
+ table entry, not a new boilerplate file. `Versioned` (`lib/xmi/versioned.rb`)
120
+ creates the `Lutaml::Model::Register`, binds namespaces, and runs
121
+ registration; `VersionRegistry` (`lib/xmi/version_registry.rb`) maps
122
+ version strings → registers and extends fallback chains for
123
+ mixed-namespace documents.
124
+
91
125
  ### Custom Types with XML Namespace
92
126
 
93
127
  Custom types in `lib/xmi/type.rb` declare their XML namespace using the `xml do ... end` block:
@@ -113,6 +147,14 @@ UML Diagram Interchange elements (Bounds, Waypoint, Diagram,
113
147
  OwnedElement) inherit from `Xmi::UmlDi::Base` instead — same
114
148
  pattern, different namespace.
115
149
 
150
+ `OwnedAttribute`, `OwnedEnd`, and `OwnedParameter` inherit from
151
+ `Xmi::Uml::ValueSpecs`, which owns their shared value-specification
152
+ surface: `uml_type` (`<type>` element) plus `upper_value` /
153
+ `lower_value` / `default_value` with their polymorphic mappings.
154
+ Inherited element mappings serialize before subclass mappings, so the
155
+ EA-parity order (type, lowerValue, upperValue) comes from the base —
156
+ subclasses declare only attributes.
157
+
116
158
  ```ruby
117
159
  # lib/xmi/uml/my_element.rb
118
160
  module Xmi
@@ -150,13 +192,14 @@ Two attributes use lutaml-model's polymorphic dispatch on `xmi:type`:
150
192
  - `PackagedElement.packaged_element` (and `UmlModel.packaged_element`) — dispatches to typed subclasses (`UmlClass`, `Association`, `Interface`, `InstanceSpecification`, etc.). Map: `Xmi::Uml::PACKAGED_ELEMENT_POLYMORPHIC_MAP` in `lib/xmi/uml/packaged_element.rb`.
151
193
  - `Slot.value`, `OwnedAttribute.upper_value`/`lower_value`/`default_value`, `OwnedEnd.upper_value`/`lower_value`/`default_value`, `OwnedParameter.upper_value`/`lower_value`/`default_value` — dispatch to ValueSpecification subclasses (`OpaqueExpression`, `LiteralString`, `LiteralInteger`, etc.). Map: `Xmi::Uml::VALUE_SPECIFICATION_POLYMORPHIC_MAP` in `lib/xmi/uml/value_specification.rb`.
152
194
 
153
- **Fallback contract:** unknown or missing `xmi:type` resolves to the abstract base (`PackagedElement` / `ValueSpecification`) via the class_map's Hash default value — no crash. Locked in by `spec/xmi/uml/polymorphic_map_contract_spec.rb` and `spec/xmi/uml/polymorphic_robustness_spec.rb`. Two paths land at the base: a missing discriminator short-circuits upstream (the declared attribute type is used); an unknown discriminator hits the Hash default. Residual upstream gap — modelling `xmi:type` and unprefixed `type` as separate slots on one element — is tracked in lutaml-model#744.
195
+ **Fallback contract:** unknown or missing `xmi:type` resolves to the abstract base (`PackagedElement` / `ValueSpecification`) via the class_map's Hash default value — no crash. Locked in by `spec/xmi/uml/polymorphic_map_contract_spec.rb` and `spec/xmi/uml/polymorphic_robustness_spec.rb`. Two paths land at the base: a missing discriminator short-circuits upstream (the declared attribute type is used); an unknown discriminator hits the Hash default. Residual upstream gap — modelling `xmi:type` and unprefixed `type` as separate slots on one element — is tracked in lutaml-model#758.
154
196
 
155
197
  ### Key Files
156
198
 
157
199
  | File | Purpose |
158
200
  |------|---------|
159
201
  | `lib/xmi.rb` | Main entry point, loads dependencies and configures XML adapter |
202
+ | `lib/xmi/parser_pipeline.rb` | The parse pipeline — the one module behind every public parse door |
160
203
  | `lib/xmi/sparx.rb` | Module with autoload declarations for Sparx components |
161
204
  | `lib/xmi/sparx/root.rb` | Main `Root` class with parsing and namespace normalization |
162
205
  | `lib/xmi/root.rb` | Base `Root` class with common XMI attributes |
@@ -166,11 +209,14 @@ Two attributes use lutaml-model's polymorphic dispatch on `xmi:type`:
166
209
  | `lib/xmi/uml_di/base.rb` | `Xmi::UmlDi::Base` — common boilerplate for UMLDI classes |
167
210
  | `lib/xmi/uml/packaged_element.rb` | `PackagedElement` + `PACKAGED_ELEMENT_POLYMORPHIC_MAP` |
168
211
  | `lib/xmi/uml/value_specification.rb` | `ValueSpecification` abstract base + `VALUE_SPECIFICATION_POLYMORPHIC_MAP` |
212
+ | `lib/xmi/uml/value_specs.rb` | `Xmi::Uml::ValueSpecs` — shared value surface for OwnedAttribute/OwnedEnd/OwnedParameter |
169
213
  | `lib/xmi/ea_root.rb` | Dynamic extension loading from MDG XML |
170
214
  | `lib/xmi/type.rb` | Custom types with namespace declarations (XmiId, XmiType, etc.) |
171
215
  | `lib/xmi/namespace/omg.rb` | OMG namespace classes (XMI, UML, UmlDi, UmlDc) |
172
216
  | `lib/xmi/namespace/sparx.rb` | Sparx-specific profile namespaces |
173
217
  | `lib/xmi/namespace_registry.rb` | URI-to-class mapping for namespace lookup |
218
+ | `lib/xmi/versioned.rb` | Version-register DSL: `define_version` table entries + `register_models` helper |
219
+ | `lib/xmi/version_registry.rb` | Version string → register mapping, mixed-namespace fallback chains |
174
220
 
175
221
  ### Collection Value Maps
176
222
 
@@ -188,8 +234,10 @@ A shared constant is available at `Xmi::Sparx::VALUE_MAP`.
188
234
 
189
235
  ## Known Issues
190
236
 
191
- Attribute deserialization in lutaml-model is not namespace-disjoint: same-local-name attributes in different namespaces collapse into one model slot, document-order dependent. This affects modelling `xmi:type` (the XMI discriminator) and unprefixed `type` (Sparx's classifier reference) as separate attributes on one element. Tracked in lutaml-model#744 with a repro. Serialization is not affected it is already namespace-disjoint.
237
+ Attribute deserialization in lutaml-model is not namespace-disjoint under element-level namespaces: same-local-name attributes in different namespaces collapse into one model slot, document-order dependent (last spelling wins). This affects modelling `xmi:type` (the XMI discriminator) and unprefixed `type` (Sparx's classifier reference) as separate attributes on one element — see the class docstring on `OwnedParameter`. Tracked in lutaml-model#758 with a repro; `spec/xmi/uml/owned_parameter_spec.rb` pins the current behavior in both input orders.
238
+
239
+ The `lutaml-model` gemspec constraint is itself pinned by `spec/xmi_gemspec_spec.rb` — changing the dependency range is a deliberate act that updates that spec.
192
240
 
193
241
  ## Limitations
194
242
 
195
- This gem is designed for Enterprise Architect generated XMI files and may not work with XMI from other tools. Some EA-specific elements (e.g., `GML:ApplicationSchema`) use `xmlns` as an attribute, which is renamed to `altered_xmlns` during preprocessing to avoid conflicts with Lutaml::Model internals.
243
+ This gem is designed for Enterprise Architect generated XMI files and may not work with XMI from other tools.
data/lib/xmi/namespace.rb CHANGED
@@ -5,5 +5,23 @@ module Xmi
5
5
  autoload :Omg, "xmi/namespace/omg"
6
6
  autoload :Sparx, "xmi/namespace/sparx"
7
7
  autoload :Dynamic, "xmi/namespace/dynamic"
8
+
9
+ # Canonical namespace scope for XMI document parsing — every
10
+ # namespace a Root-level mapping may resolve against, in
11
+ # resolution order (OMG core first, then Sparx profiles).
12
+ # Declared once; consumed by both Xmi::Root's xml block and
13
+ # Sparx::Mappings::BaseMapping.
14
+ SCOPE = [
15
+ Omg::Xmi,
16
+ Omg::Uml,
17
+ Omg::UmlDi,
18
+ Omg::UmlDc,
19
+ Sparx::Extension,
20
+ Sparx::Gml,
21
+ Sparx::CustomProfile,
22
+ Sparx::SysPhS,
23
+ Sparx::EaUml,
24
+ Sparx::CityGml,
25
+ ].freeze
8
26
  end
9
27
  end
@@ -26,6 +26,22 @@ module Xmi
26
26
  end
27
27
  end
28
28
 
29
+ # EA misuses the reserved xmlns attribute as a DATA attribute on
30
+ # GML:/CityGML:ApplicationSchema stereotype elements (values
31
+ # unrelated to real namespace declarations). XML parsers treat
32
+ # xmlns as a namespace declaration, so without renaming, the
33
+ # value is silently dropped. Restored from the original
34
+ # replace_ea_xmlns preprocessing (commit 8da3bfb), which was
35
+ # lost in the lutaml-model 0.8 upgrade.
36
+ module RenameEaXmlnsAttribute
37
+ EA_XMLNS_TAG = %r{(<(?:GML|CityGML):ApplicationSchema[^>]*?)xmlns="([^"]*)"}
38
+
39
+ def self.call(ctx)
40
+ ctx[:xml] = ctx[:xml].gsub(EA_XMLNS_TAG, '\1altered_xmlns="\2"')
41
+ ctx
42
+ end
43
+ end
44
+
29
45
  module InitVersioning
30
46
  def self.call(ctx)
31
47
  Xmi.init_versioning!
@@ -36,16 +52,22 @@ module Xmi
36
52
  module ParseXml
37
53
  def self.call(ctx)
38
54
  root_class = ctx[:root_class]
39
- ctx[:root] = VersionRegistry.parse_with_detected_version(
40
- ctx[:xml], root_class
41
- )
55
+ ctx[:root] = if ctx[:register]
56
+ root_class.from_xml(ctx[:xml], register: ctx[:register])
57
+ else
58
+ VersionRegistry.parse_with_detected_version(
59
+ ctx[:xml], root_class
60
+ )
61
+ end
42
62
  ctx
43
63
  end
44
64
  end
45
65
 
46
66
  module BuildIndex
47
67
  def self.call(ctx)
48
- ctx[:root].build_index
68
+ # Only the Root hierarchy carries an index; custom model
69
+ # classes parsed through the pipeline are left alone.
70
+ ctx[:root].build_index if ctx[:root].is_a?(::Xmi::Root)
49
71
  ctx
50
72
  end
51
73
  end
@@ -53,6 +75,7 @@ module Xmi
53
75
 
54
76
  DEFAULT_STEPS = [
55
77
  Steps::FixEncoding,
78
+ Steps::RenameEaXmlnsAttribute,
56
79
  Steps::InitVersioning,
57
80
  Steps::ParseXml,
58
81
  Steps::BuildIndex,
data/lib/xmi/parsing.rb CHANGED
@@ -29,17 +29,14 @@ module Xmi
29
29
  def parse(xml, options = {})
30
30
  xml_content = xml.is_a?(String) ? xml : xml.read
31
31
 
32
- Xmi.init_versioning! unless Xmi.versioning_initialized?
33
-
34
- register = determine_register(xml_content, options)
35
- model_class = options[:model_class] || Root
32
+ ctx = {
33
+ xml: xml_content,
34
+ root_class: options[:model_class] || Root,
35
+ }
36
+ register = explicit_register(options)
37
+ ctx[:register] = register if register
36
38
 
37
- if register
38
- model_class.from_xml(xml_content, register: register)
39
- else
40
- # Fallback to default parsing (existing behavior)
41
- model_class.from_xml(xml_content)
42
- end
39
+ ParserPipeline.run(ctx)[:root]
43
40
  end
44
41
 
45
42
  # @api public
@@ -88,28 +85,20 @@ module Xmi
88
85
 
89
86
  private
90
87
 
91
- # Determine the appropriate register for parsing
88
+ # Resolve an explicitly requested register from options
89
+ #
90
+ # Returns nil when neither :register nor :version is given, leaving
91
+ # version detection to the pipeline.
92
92
  #
93
- # @param xml_content [String] XML content
94
93
  # @param options [Hash] Options hash
95
94
  # @return [Lutaml::Model::Register, nil]
96
- def determine_register(xml_content, options)
97
- # Explicit register takes precedence
95
+ def explicit_register(options)
98
96
  return options[:register] if options[:register]
99
97
 
100
- # Explicit version
101
- if options[:version]
102
- reg = VersionRegistry.register_for_version(options[:version])
103
- unless reg
104
- raise ArgumentError,
105
- "Unknown version: #{options[:version]}"
106
- end
107
-
108
- return reg
109
- end
98
+ return unless options[:version]
110
99
 
111
- # Auto-detect from XML content
112
- VersionRegistry.detect_register(xml_content)
100
+ VersionRegistry.register_for_version(options[:version]) ||
101
+ raise(ArgumentError, "Unknown version: #{options[:version]}")
113
102
  end
114
103
  end
115
104
  end
data/lib/xmi/root.rb CHANGED
@@ -29,18 +29,7 @@ module Xmi
29
29
  xml do
30
30
  root "XMI"
31
31
  namespace ::Xmi::Namespace::Omg::Xmi
32
- namespace_scope [
33
- ::Xmi::Namespace::Omg::Xmi,
34
- ::Xmi::Namespace::Omg::Uml,
35
- ::Xmi::Namespace::Omg::UmlDi,
36
- ::Xmi::Namespace::Omg::UmlDc,
37
- ::Xmi::Namespace::Sparx::Extension,
38
- ::Xmi::Namespace::Sparx::Gml,
39
- ::Xmi::Namespace::Sparx::CustomProfile,
40
- ::Xmi::Namespace::Sparx::SysPhS,
41
- ::Xmi::Namespace::Sparx::EaUml,
42
- ::Xmi::Namespace::Sparx::CityGml,
43
- ]
32
+ namespace_scope ::Xmi::Namespace::SCOPE
44
33
 
45
34
  XmiIdentity.apply_xml_mappings(self)
46
35
 
@@ -17,18 +17,7 @@ module Xmi
17
17
  # Set the default namespace for element name resolution
18
18
  namespace ::Xmi::Namespace::Omg::Xmi
19
19
 
20
- namespace_scope [
21
- ::Xmi::Namespace::Omg::Xmi,
22
- ::Xmi::Namespace::Omg::Uml,
23
- ::Xmi::Namespace::Omg::UmlDi,
24
- ::Xmi::Namespace::Omg::UmlDc,
25
- ::Xmi::Namespace::Sparx::Extension,
26
- ::Xmi::Namespace::Sparx::SysPhS,
27
- ::Xmi::Namespace::Sparx::Gml,
28
- ::Xmi::Namespace::Sparx::EaUml,
29
- ::Xmi::Namespace::Sparx::CustomProfile,
30
- ::Xmi::Namespace::Sparx::CityGml,
31
- ]
20
+ namespace_scope ::Xmi::Namespace::SCOPE
32
21
 
33
22
  VM = ::Xmi::VALUE_MAP
34
23
 
@@ -37,22 +37,6 @@ module Xmi
37
37
  )
38
38
  ctx[:root]
39
39
  end
40
-
41
- # Fix invalid UTF-8 encoding in the XML content.
42
- #
43
- # Some EA-generated XMI files contain invalid UTF-8 byte sequences
44
- # that would cause parsing failures. This method replaces invalid
45
- # bytes with placeholder characters.
46
- #
47
- # @param xml_content [String] The raw XML content
48
- # @return [String] The XML content with valid UTF-8 encoding
49
- def fix_encoding(xml_content)
50
- return xml_content if xml_content.valid_encoding?
51
-
52
- xml_content
53
- .encode("UTF-16be", invalid: :replace, replace: "?")
54
- .encode("UTF-8")
55
- end
56
40
  end
57
41
 
58
42
  # Use the reusable BaseMapping class instead of eval hack
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Xmi
4
4
  module Uml
5
- class OwnedAttribute < Base
5
+ class OwnedAttribute < ValueSpecs
6
6
  attribute :association, :string
7
7
  attribute :name, :string
8
8
  attribute :visibility, :string
@@ -12,14 +12,10 @@ module Xmi
12
12
  attribute :is_unique, :boolean
13
13
  attribute :is_read_only, :boolean
14
14
  # `default` is the XMI attribute form (string shortcut);
15
- # `default_value` below is the child <defaultValue> element
16
- # (ValueSpecification). UML 2.5 Property carries both shapes.
15
+ # `default_value` (from ValueSpecs) is the child <defaultValue>
16
+ # element (ValueSpecification). UML 2.5 Property carries both shapes.
17
17
  attribute :default, :string
18
18
  attribute :aggregation, :string
19
- attribute :uml_type, Uml::Type
20
- attribute :upper_value, ValueSpecification, polymorphic: true
21
- attribute :lower_value, ValueSpecification, polymorphic: true
22
- attribute :default_value, ValueSpecification, polymorphic: true
23
19
 
24
20
  xml do
25
21
  root "ownedAttribute"
@@ -33,15 +29,6 @@ module Xmi
33
29
  map_attribute "isReadOnly", to: :is_read_only
34
30
  map_attribute "default", to: :default
35
31
  map_attribute "aggregation", to: :aggregation
36
-
37
- map_element "type", to: :uml_type
38
- # Sparx EA emits lowerValue before upperValue.
39
- map_element "lowerValue", to: :lower_value,
40
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
41
- map_element "upperValue", to: :upper_value,
42
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
43
- map_element "defaultValue", to: :default_value,
44
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
45
32
  end
46
33
  end
47
34
  end
@@ -2,16 +2,12 @@
2
2
 
3
3
  module Xmi
4
4
  module Uml
5
- class OwnedEnd < Base
5
+ class OwnedEnd < ValueSpecs
6
6
  attribute :association, :string
7
7
  attribute :name, :string
8
8
  attribute :visibility, :string
9
9
  attribute :aggregation, :string
10
- attribute :uml_type, Uml::Type
11
10
  attribute :member_end, :string
12
- attribute :upper_value, ValueSpecification, polymorphic: true
13
- attribute :lower_value, ValueSpecification, polymorphic: true
14
- attribute :default_value, ValueSpecification, polymorphic: true
15
11
  attribute :is_composite, :boolean
16
12
 
17
13
  xml do
@@ -22,15 +18,6 @@ module Xmi
22
18
  map_attribute "aggregation", to: :aggregation
23
19
  map_attribute "memberEnd", to: :member_end
24
20
  map_attribute "isComposite", to: :is_composite
25
-
26
- map_element "type", to: :uml_type
27
- # Sparx EA emits lowerValue before upperValue.
28
- map_element "lowerValue", to: :lower_value,
29
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
30
- map_element "upperValue", to: :upper_value,
31
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
32
- map_element "defaultValue", to: :default_value,
33
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
34
21
  end
35
22
  end
36
23
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Xmi
4
4
  module Uml
5
- class OwnedParameter < Base
5
+ class OwnedParameter < ValueSpecs
6
6
  attribute :name, :string
7
7
  # These are two DIFFERENT attributes that collide here:
8
8
  # xmi:type="uml:Parameter" — the XMI metaclass discriminator
@@ -27,9 +27,6 @@ module Xmi
27
27
  attribute :is_ordered, :boolean
28
28
  attribute :is_unique, :boolean
29
29
  attribute :effect, :string
30
- attribute :upper_value, ValueSpecification, polymorphic: true
31
- attribute :lower_value, ValueSpecification, polymorphic: true
32
- attribute :default_value, ValueSpecification, polymorphic: true
33
30
 
34
31
  xml do
35
32
  root "ownedParameter"
@@ -40,14 +37,6 @@ module Xmi
40
37
  map_attribute "isOrdered", to: :is_ordered
41
38
  map_attribute "isUnique", to: :is_unique
42
39
  map_attribute "effect", to: :effect
43
-
44
- # Sparx EA emits lowerValue before upperValue.
45
- map_element "lowerValue", to: :lower_value,
46
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
47
- map_element "upperValue", to: :upper_value,
48
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
49
- map_element "defaultValue", to: :default_value,
50
- polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
51
40
  end
52
41
  end
53
42
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xmi
4
+ module Uml
5
+ # Shared value-specification surface for typed structural features:
6
+ # the classifier reference (<type> element) plus the multiplicity
7
+ # bounds and default value (<lowerValue>/<upperValue>/<defaultValue>).
8
+ #
9
+ # Element order matches Sparx EA output: type first, then values,
10
+ # lowerValue before upperValue. Inherited element mappings
11
+ # serialize before subclass mappings, so subclasses (OwnedAttribute,
12
+ # OwnedEnd, OwnedParameter) declare only attributes and keep this
13
+ # output order.
14
+ class ValueSpecs < Base
15
+ attribute :uml_type, Uml::Type
16
+ attribute :upper_value, ValueSpecification, polymorphic: true
17
+ attribute :lower_value, ValueSpecification, polymorphic: true
18
+ attribute :default_value, ValueSpecification, polymorphic: true
19
+
20
+ xml do
21
+ map_element "type", to: :uml_type
22
+ map_element "lowerValue", to: :lower_value,
23
+ polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
24
+ map_element "upperValue", to: :upper_value,
25
+ polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
26
+ map_element "defaultValue", to: :default_value,
27
+ polymorphic: VALUE_SPECIFICATION_POLYMORPHIC_MAP
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/xmi/uml.rb CHANGED
@@ -10,6 +10,7 @@ module Xmi
10
10
  autoload :UpperValue, "xmi/uml/upper_value"
11
11
  autoload :LowerValue, "xmi/uml/lower_value"
12
12
  autoload :ValueSpecification, "xmi/uml/value_specification"
13
+ autoload :ValueSpecs, "xmi/uml/value_specs"
13
14
  autoload :LiteralString, "xmi/uml/literal_string"
14
15
  autoload :LiteralInteger, "xmi/uml/literal_integer"
15
16
  autoload :LiteralBoolean, "xmi/uml/literal_boolean"
data/lib/xmi/v20110701.rb CHANGED
@@ -8,29 +8,17 @@ module Xmi
8
8
  module V20110701
9
9
  extend Versioned
10
10
 
11
- # Register ID
12
- def self.register_id
13
- :xmi_20110701
14
- end
15
-
16
- # Namespace classes this version binds to
17
- def self.namespace_classes
18
- [
11
+ define_version(
12
+ register_id: :xmi_20110701,
13
+ namespaces: [
19
14
  Xmi::Namespace::Omg::Xmi20110701,
20
15
  Xmi::Namespace::Omg::Uml20110701,
21
- ]
22
- end
23
-
24
- # Fallback chain: common → default
25
- def self.fallback_registers
26
- %i[xmi_common default]
27
- end
16
+ ],
17
+ )
28
18
 
29
19
  # Register all models for this version
30
20
  def self.register_models!
31
- # Register version-specific models
32
- register.register_model(Extension, id: :extension)
33
- register.register_model(Documentation, id: :documentation)
21
+ register_models extension: Extension, documentation: Documentation
34
22
  end
35
23
 
36
24
  # XMI 2.1 Extension element
data/lib/xmi/v20131001.rb CHANGED
@@ -8,33 +8,22 @@ module Xmi
8
8
  module V20131001
9
9
  extend Versioned
10
10
 
11
- # Register ID
12
- def self.register_id
13
- :xmi_20131001
14
- end
15
-
16
- # Namespace classes this version binds to
17
- def self.namespace_classes
18
- [
11
+ # V20110701 in the chain: V20131001 reuses its Extension (same structure)
12
+ define_version(
13
+ register_id: :xmi_20131001,
14
+ namespaces: [
19
15
  Xmi::Namespace::Omg::Xmi20131001,
20
16
  Xmi::Namespace::Omg::Uml20131001,
21
17
  Xmi::Namespace::Omg::UmlDi20131001,
22
18
  Xmi::Namespace::Omg::UmlDc20131001,
23
- ]
24
- end
25
-
26
- # Fallback chain: V20110701 → common → default
27
- # This allows V20131001 to use V20110701's Extension (same structure)
28
- def self.fallback_registers
29
- %i[xmi_20110701 xmi_common default]
30
- end
19
+ ],
20
+ fallbacks: %i[xmi_20110701 xmi_common default],
21
+ )
31
22
 
32
23
  # Register all models for this version
33
24
  def self.register_models!
34
25
  # Extension is same as V20110701 - will be found via fallback
35
-
36
- # Documentation is different - register our version
37
- register.register_model(Documentation, id: :documentation)
26
+ register_models documentation: Documentation
38
27
  end
39
28
 
40
29
  # XMI 2.5.1 Documentation element
data/lib/xmi/v20161101.rb CHANGED
@@ -8,32 +8,22 @@ module Xmi
8
8
  module V20161101
9
9
  extend Versioned
10
10
 
11
- # Register ID
12
- def self.register_id
13
- :xmi_20161101
14
- end
15
-
16
- # Namespace classes this version binds to
17
- def self.namespace_classes
18
- [
11
+ define_version(
12
+ register_id: :xmi_20161101,
13
+ namespaces: [
19
14
  Xmi::Namespace::Omg::Xmi20161101,
20
15
  Xmi::Namespace::Omg::Uml20161101,
21
16
  Xmi::Namespace::Omg::UmlDi20161101,
22
17
  Xmi::Namespace::Omg::UmlDc20161101,
23
- ]
24
- end
25
-
26
- # Fallback chain: V20131001 → V20110701 → common → default
27
- def self.fallback_registers
28
- %i[xmi_20131001 xmi_20110701 xmi_common default]
29
- end
18
+ ],
19
+ fallbacks: %i[xmi_20131001 xmi_20110701 xmi_common default],
20
+ )
30
21
 
31
22
  # Register all models for this version
32
23
  def self.register_models!
33
- # Extension is different in 2.5.2 - register our version
34
- register.register_model(Extension, id: :extension)
35
-
24
+ # Extension is different in 2.5.2 - register our version.
36
25
  # Documentation is same as V20131001 - will be found via fallback
26
+ register_models extension: Extension
37
27
  end
38
28
 
39
29
  # XMI 2.5.2 Extension element
data/lib/xmi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xmi
4
- VERSION = "0.7.3"
4
+ VERSION = "0.7.4"
5
5
  end
data/lib/xmi/versioned.rb CHANGED
@@ -4,26 +4,30 @@ module Xmi
4
4
  # Base module for version-specific XMI model trees.
5
5
  #
6
6
  # Each version module (V20110701, V20131001, V20161101) extends this
7
- # and provides its own register and namespace bindings.
7
+ # and declares its table entry via define_version, keeping only its
8
+ # real delta (version-specific model classes and what to register).
8
9
  #
9
10
  # @example
10
11
  # module Xmi::V20131001
11
12
  # extend Xmi::Versioned
12
13
  #
13
- # def self.register_id
14
- # :xmi_20131001
15
- # end
16
- #
17
- # def self.namespace_classes
18
- # [Xmi::Namespace::Omg::Xmi20131001, Xmi::Namespace::Omg::Uml20131001]
19
- # end
14
+ # define_version(
15
+ # register_id: :xmi_20131001,
16
+ # namespaces: [Xmi::Namespace::Omg::Xmi20131001,
17
+ # Xmi::Namespace::Omg::Uml20131001],
18
+ # fallbacks: %i[xmi_20110701 xmi_common default],
19
+ # )
20
20
  #
21
21
  # def self.register_models!
22
- # # Register version-specific models
22
+ # register_models documentation: Documentation
23
23
  # end
24
24
  # end
25
25
  #
26
26
  module Versioned
27
+ # Fallback chain used when a version declares none: the common
28
+ # register, then the default register.
29
+ DEFAULT_FALLBACKS = %i[xmi_common default].freeze
30
+
27
31
  # @api public
28
32
  # Called when a module extends Versioned
29
33
  def self.extended(base)
@@ -33,6 +37,23 @@ module Xmi
33
37
  end
34
38
  end
35
39
 
40
+ # @api public
41
+ # Declare this version's table entry.
42
+ #
43
+ # Defines register_id, namespace_classes, and fallback_registers on
44
+ # the extending module. Versions that extend Versioned without
45
+ # calling this keep the NotImplementedError contract below.
46
+ #
47
+ # @param register_id [Symbol] Register ID for this version
48
+ # @param namespaces [Array<Class>] XmlNamespace classes to bind
49
+ # @param fallbacks [Array<Symbol>] Fallback register IDs
50
+ # @return [void]
51
+ def define_version(register_id:, namespaces:, fallbacks: DEFAULT_FALLBACKS)
52
+ define_singleton_method(:register_id) { register_id }
53
+ define_singleton_method(:namespace_classes) { namespaces }
54
+ define_singleton_method(:fallback_registers) { fallbacks }
55
+ end
56
+
36
57
  # @api public
37
58
  # Get the register for this version
38
59
  #
@@ -87,6 +108,15 @@ module Xmi
87
108
  "Each version must implement #register_models!"
88
109
  end
89
110
 
111
+ # @api public
112
+ # Bulk-register models from a table.
113
+ #
114
+ # @param models [Hash{Symbol => Class}] id → model class
115
+ # @return [void]
116
+ def register_models(models)
117
+ models.each { |id, klass| register.register_model(klass, id: id) }
118
+ end
119
+
90
120
  # @api public
91
121
  # Get the register ID for this version
92
122
  #
data/lib/xmi.rb CHANGED
@@ -101,8 +101,7 @@ module Xmi
101
101
  # @param xml_content [String] XML content
102
102
  # @return [Root] Parsed XMI document
103
103
  def parse(xml_content)
104
- init_versioning!
105
- VersionRegistry.parse_with_detected_version(xml_content, Root)
104
+ ParserPipeline.run({ xml: xml_content, root_class: Root })[:root]
106
105
  end
107
106
 
108
107
  # @api public
@@ -112,11 +111,12 @@ module Xmi
112
111
  # @param version [String] Version string (e.g., "20131001")
113
112
  # @return [Root] Parsed XMI document
114
113
  def parse_with_version(xml_content, version)
115
- init_versioning!
116
114
  register = VersionRegistry.register_for_version(version)
117
115
  raise ArgumentError, "Unknown version: #{version}" unless register
118
116
 
119
- Root.from_xml(xml_content, register: register)
117
+ ParserPipeline.run(
118
+ { xml: xml_content, root_class: Root, register: register },
119
+ )[:root]
120
120
  end
121
121
 
122
122
  # @api public
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xmi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.7.4
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-08-26 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -211,6 +211,7 @@ files:
211
211
  - lib/xmi/uml/upper_value.rb
212
212
  - lib/xmi/uml/usage.rb
213
213
  - lib/xmi/uml/value_specification.rb
214
+ - lib/xmi/uml/value_specs.rb
214
215
  - lib/xmi/uml/waypoint.rb
215
216
  - lib/xmi/uml_di.rb
216
217
  - lib/xmi/uml_di/base.rb