xmi 0.5.4 → 0.5.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +29 -47
  3. data/CLAUDE.md +166 -0
  4. data/README.adoc +9 -27
  5. data/TODO.perf/01-eliminate-duplicate-nokogiri-parse.md +33 -0
  6. data/TODO.perf/02-read-only-fast-mode.md +38 -0
  7. data/TODO.perf/03-register-fallback-idempotency.md +20 -0
  8. data/TODO.perf/04-pipeline-hash-mutation.md +31 -0
  9. data/TODO.perf/05-ea-root-single-parse.md +29 -0
  10. data/docs/migration.md +6 -6
  11. data/docs/versioning.md +1 -1
  12. data/lib/tasks/benchmark_runner.rb +1 -1
  13. data/lib/xmi/custom_profile/abstract.rb +16 -0
  14. data/lib/xmi/custom_profile/basic_doc.rb +16 -0
  15. data/lib/xmi/custom_profile/bibliography.rb +16 -0
  16. data/lib/xmi/custom_profile/edition.rb +18 -0
  17. data/lib/xmi/custom_profile/enumeration.rb +16 -0
  18. data/lib/xmi/custom_profile/informative.rb +16 -0
  19. data/lib/xmi/custom_profile/invariant.rb +16 -0
  20. data/lib/xmi/custom_profile/number.rb +18 -0
  21. data/lib/xmi/custom_profile/ocl.rb +16 -0
  22. data/lib/xmi/custom_profile/persistence.rb +20 -0
  23. data/lib/xmi/custom_profile/publication_date.rb +18 -0
  24. data/lib/xmi/custom_profile/year_version.rb +18 -0
  25. data/lib/xmi/custom_profile.rb +12 -143
  26. data/lib/xmi/ea_root/code_generation.rb +140 -0
  27. data/lib/xmi/ea_root/extension_lifecycle.rb +52 -0
  28. data/lib/xmi/ea_root/namespace_handling.rb +39 -0
  29. data/lib/xmi/ea_root/xml_parsing.rb +51 -0
  30. data/lib/xmi/ea_root.rb +14 -407
  31. data/lib/xmi/namespace_detector.rb +35 -3
  32. data/lib/xmi/parser_pipeline.rb +9 -9
  33. data/lib/xmi/sparx/index.rb +2 -2
  34. data/lib/xmi/sparx/mappings/base_mapping.rb +4 -4
  35. data/lib/xmi/sparx/mappings.rb +1 -1
  36. data/lib/xmi/sparx/root.rb +3 -3
  37. data/lib/xmi/sparx.rb +2 -2
  38. data/lib/xmi/version.rb +1 -1
  39. data/lib/xmi/version_registry.rb +11 -8
  40. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ffa11bdf4fbb9c95493ef25cb5662107d72e1e708d526b7a6240ed3ffcd4f99
4
- data.tar.gz: ba939950443a99f5e4c402ec004f4446c9b520486faa0e90aeeaa4bfb4bc679b
3
+ metadata.gz: 62e6b5232df619d790a5ca7b69454d3ed6f2762bb457808436be572162867f16
4
+ data.tar.gz: 4adbc6c45fd9e75a2fb6612f60cb35ef47a91afe44cca8c366f8c9b17237aeab
5
5
  SHA512:
6
- metadata.gz: a23e73e4194971d9e3f8b83e3b746195ad227dd812e3777c5e3d33b59498e85448a4dd4ef3177f5e297693e7efb9a8e6a7de0ed59d9c0d6e33a1fea7b58a7290
7
- data.tar.gz: 3ec9666b1313e1f5d0cb426739c7add6fea48253e8d51bf72e04323e2a7213b07877af7867e267f73c70d5c6b44e8b898375d11d137a9ac01868feb2c7cd2a22
6
+ metadata.gz: fad7e8f844a110c38395e3d6b90f5506832cf4d971830b3dee23294d7268c6183b5a07ffafaa0832aba65b13ae1a2ad7dae7176b93d333c1607ef5607079f7af
7
+ data.tar.gz: e81076d61606ae1742f08fffa61aa7b0efd999c29dcbffd1c80ee5a34b81adaeb79bb988021f33a07cb51c8c16d023a09cdf5b884905dfb80b4070634731dbdd
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-04-20 09:41:00 UTC using RuboCop version 1.86.1.
3
+ # on 2026-04-22 09:21:04 UTC using RuboCop version 1.86.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -11,46 +11,13 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'xmi.gemspec'
13
13
 
14
- # Offense count: 2
15
- # This cop supports safe autocorrection (--autocorrect).
16
- # Configuration parameters: EnforcedStyle, IndentationWidth.
17
- # SupportedStyles: with_first_argument, with_fixed_indentation
18
- Layout/ArgumentAlignment:
19
- Exclude:
20
- - 'lib/xmi/parser_pipeline.rb'
21
- - 'lib/xmi/sparx/index.rb'
22
-
23
- # Offense count: 1
24
- # This cop supports safe autocorrection (--autocorrect).
25
- Layout/EmptyLinesAfterModuleInclusion:
26
- Exclude:
27
- - 'lib/xmi/sparx/gml/shared_attributes.rb'
28
-
29
- # Offense count: 1
30
- # This cop supports safe autocorrection (--autocorrect).
31
- # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
32
- # SupportedHashRocketStyles: key, separator, table
33
- # SupportedColonStyles: key, separator, table
34
- # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
35
- Layout/HashAlignment:
36
- Exclude:
37
- - 'lib/xmi/sparx/mappings/base_mapping.rb'
38
-
39
- # Offense count: 76
14
+ # Offense count: 77
40
15
  # This cop supports safe autocorrection (--autocorrect).
41
16
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
42
17
  # URISchemes: http, https
43
18
  Layout/LineLength:
44
19
  Enabled: false
45
20
 
46
- # Offense count: 2
47
- # This cop supports safe autocorrection (--autocorrect).
48
- # Configuration parameters: AllowInHeredoc.
49
- Layout/TrailingWhitespace:
50
- Exclude:
51
- - 'lib/xmi/parser_pipeline.rb'
52
- - 'lib/xmi/sparx/index.rb'
53
-
54
21
  # Offense count: 6
55
22
  # Configuration parameters: AllowedMethods.
56
23
  # AllowedMethods: enums
@@ -59,14 +26,14 @@ Lint/ConstantDefinitionInBlock:
59
26
  - 'lib/xmi/sparx/mappings/base_mapping.rb'
60
27
  - 'spec/performance/xmi_parsing_spec.rb'
61
28
 
62
- # Offense count: 15
29
+ # Offense count: 16
63
30
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
64
31
  Metrics/AbcSize:
65
32
  Exclude:
66
33
  - 'lib/tasks/benchmark_runner.rb'
67
34
  - 'lib/tasks/performance_comparator.rb'
68
35
  - 'lib/tasks/performance_helpers.rb'
69
- - 'lib/xmi/ea_root.rb'
36
+ - 'lib/xmi/ea_root/code_generation.rb'
70
37
  - 'lib/xmi/sparx/index.rb'
71
38
  - 'lib/xmi/version_registry.rb'
72
39
  - 'spec/performance/xmi_parsing_spec.rb'
@@ -77,23 +44,25 @@ Metrics/AbcSize:
77
44
  Metrics/BlockLength:
78
45
  Max: 98
79
46
 
80
- # Offense count: 4
47
+ # Offense count: 5
81
48
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
82
49
  Metrics/CyclomaticComplexity:
83
50
  Exclude:
84
51
  - 'lib/tasks/performance_helpers.rb'
52
+ - 'lib/xmi/ea_root/code_generation.rb'
85
53
  - 'lib/xmi/sparx/index.rb'
86
54
 
87
- # Offense count: 29
55
+ # Offense count: 30
88
56
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
89
57
  Metrics/MethodLength:
90
- Max: 33
58
+ Max: 55
91
59
 
92
- # Offense count: 4
60
+ # Offense count: 5
93
61
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
94
62
  Metrics/PerceivedComplexity:
95
63
  Exclude:
96
64
  - 'lib/tasks/performance_helpers.rb'
65
+ - 'lib/xmi/ea_root/code_generation.rb'
97
66
  - 'lib/xmi/sparx/index.rb'
98
67
  - 'lib/xmi/version_registry.rb'
99
68
 
@@ -135,7 +104,7 @@ RSpec/DescribeClass:
135
104
  - 'spec/xmi/namespace_aliases_spec.rb'
136
105
  - 'spec/xmi/versioning_spec.rb'
137
106
 
138
- # Offense count: 26
107
+ # Offense count: 30
139
108
  # Configuration parameters: CountAsOne.
140
109
  RSpec/ExampleLength:
141
110
  Max: 33
@@ -145,7 +114,12 @@ RSpec/LeakyConstantDeclaration:
145
114
  Exclude:
146
115
  - 'spec/performance/xmi_parsing_spec.rb'
147
116
 
148
- # Offense count: 36
117
+ # Offense count: 1
118
+ RSpec/MultipleDescribes:
119
+ Exclude:
120
+ - 'spec/xmi/sparx/gml/shared_attributes_spec.rb'
121
+
122
+ # Offense count: 40
149
123
  RSpec/MultipleExpectations:
150
124
  Max: 8
151
125
 
@@ -159,11 +133,19 @@ RSpec/MultipleMemoizedHelpers:
159
133
  RSpec/NestedGroups:
160
134
  Max: 4
161
135
 
162
- # Offense count: 1
163
- # This cop supports safe autocorrection (--autocorrect).
164
- Style/MultilineIfModifier:
136
+ # Offense count: 8
137
+ # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
138
+ # SupportedInflectors: default, active_support
139
+ RSpec/SpecFilePathFormat:
165
140
  Exclude:
166
- - 'lib/xmi/sparx/index.rb'
141
+ - 'spec/xmi/ea_root/extension_loading_spec.rb'
142
+ - 'spec/xmi/sparx/sparx_root_citygml_rel_ns_spec.rb'
143
+ - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
144
+ - 'spec/xmi/sparx/sparx_root_eauml_spec.rb'
145
+ - 'spec/xmi/sparx/sparx_root_gml_spec.rb'
146
+ - 'spec/xmi/sparx/sparx_root_mdg_spec.rb'
147
+ - 'spec/xmi/sparx/sparx_root_xmi2013_uml2013_spec.rb'
148
+ - 'spec/xmi/sparx/sparx_root_xmi_parsing_spec.rb'
167
149
 
168
150
  # Offense count: 4
169
151
  # Configuration parameters: AllowedClasses.
data/CLAUDE.md ADDED
@@ -0,0 +1,166 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Build, Test, and Development Commands
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ bundle install
10
+
11
+ # Run all tests
12
+ bundle exec rake spec
13
+ bundle exec rspec
14
+
15
+ # Run a single test file
16
+ bundle exec rspec spec/xmi/sparx/sparx_root_xmi2013_uml2013_spec.rb
17
+
18
+ # Run a specific test by line number
19
+ bundle exec rspec spec/xmi/sparx/sparx_root_xmi2013_uml2016_spec.rb:610
20
+
21
+ # Run linter with auto-correct
22
+ bundle exec rubocop -A --auto-gen-config
23
+
24
+ # Run both tests and linting (default rake task)
25
+ bundle exec rake
26
+
27
+ # Interactive console for experimentation
28
+ bin/console
29
+ ```
30
+
31
+ ## Architecture Overview
32
+
33
+ This gem converts XMI (XML Metadata Interchange) files into Ruby objects, specifically designed for Enterprise Architect generated XMI files.
34
+
35
+ ### Core Dependencies
36
+
37
+ - **lutaml-model**: All serializable models inherit from `Lutaml::Model::Serializable`
38
+ - **nokogiri**: XML parsing backend
39
+
40
+ ### Main Entry Point
41
+
42
+ `Xmi::Sparx::Root.parse_xml(xml_content)` is the primary method to parse XMI files. It performs preprocessing before parsing:
43
+
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)
48
+
49
+ ### OMG Namespace Version Normalization
50
+
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.
52
+
53
+ ### Enterprise Architect's Misuse of the `xmlns` Attribute
54
+
55
+ **This is a critical quirk to understand when working with EA-generated XMI.**
56
+
57
+ 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
+
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:
70
+
71
+ ```ruby
72
+ class ApplicationSchema < Lutaml::Model::Serializable
73
+ attribute :altered_xmlns, :string # renamed from xmlns
74
+ end
75
+ ```
76
+
77
+ ### Namespace Architecture
78
+
79
+ **All XMI/UML namespace versions are normalized to 20131001 before parsing** (see `Root.replace_xmlns`).
80
+
81
+ Namespace classes are defined in:
82
+ - `lib/xmi/namespace/omg.rb` - OMG namespaces (XMI, UML, UmlDi, UmlDc)
83
+ - `lib/xmi/namespace/sparx.rb` - Sparx-specific profiles (SysPhS, GML, EaUml, CustomProfile, CityGML)
84
+
85
+ Use version-agnostic alias classes that inherit from 20131001 versions:
86
+ ```ruby
87
+ ::Xmi::Namespace::Omg::Xmi # => inherits from Xmi20131001
88
+ ::Xmi::Namespace::Omg::Uml # => inherits from Uml20131001
89
+ ```
90
+
91
+ ### Custom Types with XML Namespace
92
+
93
+ Custom types in `lib/xmi/type.rb` declare their XML namespace using the `xml do ... end` block:
94
+
95
+ ```ruby
96
+ class XmiId < Lutaml::Model::Type::String
97
+ xml do
98
+ namespace ::Xmi::Namespace::Omg::Xmi
99
+ end
100
+ end
101
+ ```
102
+
103
+ ### Model Definition Pattern
104
+
105
+ Models use lutaml-model syntax with explicit namespace declarations:
106
+ ```ruby
107
+ class MyModel < Lutaml::Model::Serializable
108
+ attribute :id, ::Xmi::Type::XmiId
109
+
110
+ xml do
111
+ root "Model"
112
+ namespace ::Xmi::Namespace::Omg::Uml
113
+ namespace_scope [::Xmi::Namespace::Omg::Xmi, ::Xmi::Namespace::Omg::Uml]
114
+
115
+ # Attributes with XMI namespace require explicit declaration
116
+ map_attribute "id", to: :id,
117
+ namespace: "http://www.omg.org/spec/XMI/20131001",
118
+ prefix: "xmi"
119
+ end
120
+ end
121
+ ```
122
+
123
+ ### Dynamic Extension Loading
124
+
125
+ `Xmi::EaRoot.load_extension(xml_path)` dynamically generates Ruby classes from EA MDG extension XML files. This creates stereotype classes under `Xmi::EaRoot::{ModuleName}::{ClassName}` and updates `Root` mappings.
126
+
127
+ Extensions use `NamespaceRegistry` to look up or create namespace classes dynamically:
128
+ - Existing namespace URIs resolve to predefined classes
129
+ - New URIs create dynamic classes under `Xmi::Namespace::Dynamic::{ModuleName}`
130
+
131
+ ### Key Files
132
+
133
+ | File | Purpose |
134
+ |------|---------|
135
+ | `lib/xmi.rb` | Main entry point, loads dependencies and configures XML adapter |
136
+ | `lib/xmi/sparx.rb` | Module with autoload declarations for Sparx components |
137
+ | `lib/xmi/sparx/root.rb` | Main `Root` class with parsing and namespace normalization |
138
+ | `lib/xmi/root.rb` | Base `Root` class with common XMI attributes |
139
+ | `lib/xmi/uml.rb` | UML model classes (UmlModel, PackagedElement, etc.) |
140
+ | `lib/xmi/ea_root.rb` | Dynamic extension loading from MDG XML |
141
+ | `lib/xmi/type.rb` | Custom types with namespace declarations (XmiId, XmiType, etc.) |
142
+ | `lib/xmi/namespace/omg.rb` | OMG namespace classes (XMI, UML, UmlDi, UmlDc) |
143
+ | `lib/xmi/namespace/sparx.rb` | Sparx-specific profile namespaces |
144
+ | `lib/xmi/namespace_registry.rb` | URI-to-class mapping for namespace lookup |
145
+
146
+ ### Collection Value Maps
147
+
148
+ When mapping collection elements, use the standard `VALUE_MAP` pattern to handle nil/empty values:
149
+
150
+ ```ruby
151
+ map_element "Element", to: :elements,
152
+ value_map: {
153
+ from: { nil: :empty, empty: :empty, omitted: :empty },
154
+ to: { nil: :empty, empty: :empty, omitted: :empty }
155
+ }
156
+ ```
157
+
158
+ A shared constant is available at `Xmi::Sparx::VALUE_MAP`.
159
+
160
+ ## Known Issues
161
+
162
+ One serialization test fails due to a lutaml-model bug where `to_xml` does not respect namespace declarations on custom types. See `LUTAML_MODEL_BUG_REPORT.md` for details. Parsing works correctly; only serialization is affected.
163
+
164
+ ## Limitations
165
+
166
+ 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.
data/README.adoc CHANGED
@@ -66,21 +66,6 @@ xmi_root_model = Xmi::Sparx::Root.parse_xml(xml_content)
66
66
  Ruby classes and modules defined in XML file dynamically.
67
67
  Then, you can generate Ruby objects by `Xmi::Sparx::Root.parse_xml`.
68
68
 
69
- === Output Classes and Modules Generated from Extension into Ruby Files
70
-
71
- You can also generate Ruby files directly from the XMI content:
72
-
73
- [source,ruby]
74
- ----
75
- Xmi::EaRoot.load_extension(
76
- input_xml_path: 'path/to/your/custom_extension.xml',
77
- module_name: 'CustomModule'
78
- )
79
- Xmi::EaRoot.output_rb_file('path/to/output_file.rb')
80
- ----
81
-
82
- This approach allows you to save the dynamically generated Ruby code to a file for further use.
83
-
84
69
  === Create Extension XML File
85
70
 
86
71
  If you would like to create an extension, which allows to be loaded later, you
@@ -172,7 +157,7 @@ classes.
172
157
 
173
158
  === Namespace Normalization
174
159
 
175
- The normalization is performed by [`SparxRoot.replace_xmlns`](lib/xmi/sparx.rb:1158) which rewrites
160
+ The normalization is performed by `Xmi::ParserPipeline` which rewrites
176
161
  namespace URIs in the input XML:
177
162
 
178
163
  .Example of namespace normalization
@@ -195,7 +180,7 @@ namespace URIs in the input XML:
195
180
 
196
181
  === Namespace Classes
197
182
 
198
- Namespace classes are defined in [`lib/xmi/namespace/omg.rb`](lib/xmi/namespace/omg.rb:1):
183
+ Namespace classes are defined in `lib/xmi/namespace/omg.rb`:
199
184
 
200
185
  * **Version-specific classes**: `Xmi20110701`, `Uml20131001`, etc.
201
186
  * **Version-agnostic aliases**: `Xmi`, `Uml`, `UmlDi`, `UmlDc`
@@ -292,7 +277,7 @@ namespace prefixes will parse as `nil`.
292
277
 
293
278
  === Sparx Systems Namespaces
294
279
 
295
- Sparx-specific namespaces are defined in [`lib/xmi/namespace/sparx.rb`](lib/xmi/namespace/sparx.rb:1):
280
+ Sparx-specific namespaces are defined in `lib/xmi/namespace/sparx.rb`:
296
281
 
297
282
  * **SysPhS** - System Physical Systems profile
298
283
  * **GML** - Geography Markup Language profile
@@ -316,7 +301,7 @@ end
316
301
 
317
302
  === Extension Namespaces
318
303
 
319
- Dynamically loaded extensions (via [`EaRoot.load_extension`](lib/xmi/ea_root.rb:54)) also use
304
+ Dynamically loaded extensions (via `EaRoot.load_extension`) also use
320
305
  namespace-qualified mappings:
321
306
 
322
307
  .Extension with namespace mapping
@@ -399,17 +384,14 @@ The old API used `SparxRoot.parse_xml` with automatic namespace normalization:
399
384
  [source,ruby]
400
385
  ----
401
386
  # Old API (still works, but version-aware API is recommended)
402
- doc = Xmi::Sparx::SparxRoot.parse_xml(xml_content)
387
+ doc = Xmi::Sparx::Root.parse_xml(xml_content)
403
388
  ----
404
389
 
405
390
  The new version-aware API:
406
391
 
407
392
  [source,ruby]
408
393
  ----
409
- # New API - explicit about version handling
410
- doc = Xmi::Sparx::SparxRoot.parse_xml_with_versioning(xml_content)
411
-
412
- # Or use the module-level API
394
+ # New API - module-level convenience method
413
395
  doc = Xmi.parse(xml_content)
414
396
  ----
415
397
 
@@ -480,13 +462,13 @@ define an `altered_xmlns` attribute to receive this value.
480
462
 
481
463
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
482
464
 
483
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
465
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to https://rubygems.org[rubygems.org].
484
466
 
485
467
 
486
468
  == Contributing
487
469
 
488
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/xmi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/xmi/blob/master/CODE_OF_CONDUCT.md).
470
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lutaml/xmi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the https://github.com/lutaml/xmi/blob/master/CODE_OF_CONDUCT.md[code of conduct].
489
471
 
490
472
  == Code of Conduct
491
473
 
492
- Everyone interacting in the Xmi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/xmi/blob/master/CODE_OF_CONDUCT.md).
474
+ Everyone interacting in the Xmi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the https://github.com/lutaml/xmi/blob/master/CODE_OF_CONDUCT.md[code of conduct].
@@ -0,0 +1,33 @@
1
+ # 01: Eliminate Duplicate Nokogiri Parse
2
+
3
+ ## Impact: HIGH (~30-50% parse time for large files)
4
+
5
+ ## Problem
6
+
7
+ `ParserPipeline` parses XML twice:
8
+ 1. `NamespaceDetector.detect_versions` → `Nokogiri::XML(xml_content)` + `collect_namespaces` — parses entire doc just to read root namespace URIs
9
+ 2. `from_xml` → adapter.parse(xml_content) — parses again via lutaml-model
10
+
11
+ For the 3.5MB large fixture, this is the dominant cost.
12
+
13
+ ## Fix
14
+
15
+ Extract namespace URIs from the first ~4KB of the XML string using regex, avoiding Nokogiri entirely for version detection.
16
+
17
+ ```ruby
18
+ # Instead of:
19
+ doc = Nokogiri::XML(xml_content)
20
+ doc.collect_namespaces
21
+
22
+ # Use:
23
+ NS_REGEX = /xmlns:?(\\w*)=["']([^"']+)["']/
24
+ xml_content[0, 4096].scan(NS_REGEX).to_h { |prefix, uri| [prefix, uri] }
25
+ ```
26
+
27
+ The namespace declarations are always on or near the root element, so scanning the first 4KB is sufficient.
28
+
29
+ ## Files
30
+
31
+ - `lib/xmi/namespace_detector.rb` — replace `extract_namespace_uris` Nokogiri parse with regex
32
+ - `lib/xmi/namespace_detector.rb` — verify `detect_versions` still works
33
+ - `spec/xmi/parser_pipeline_spec.rb` — verify tests pass
@@ -0,0 +1,38 @@
1
+ # 02: Read-Only Fast Mode (Skip Namespace Declaration Plan)
2
+
3
+ ## Impact: HIGH (eliminates full tree walk before mapping starts)
4
+
5
+ ## Problem
6
+
7
+ In lutaml-model's `ModelTransform#data_to_model` (lines 71-76), `build_input_declaration_plan` calls `collect_element_namespaces` which recursively walks **every element** in the parsed document — solely for round-trip namespace fidelity (preserving xmlns declarations for `to_xml`).
8
+
9
+ Most XMI consumers only parse (read) and never call `to_xml`. This full tree walk is wasted work.
10
+
11
+ ## Fix
12
+
13
+ ### In lutaml-model
14
+
15
+ Add a `parse_only: true` option to `from_xml` that skips namespace declaration plan collection:
16
+
17
+ ```ruby
18
+ # model_transform.rb line 71
19
+ unless options.key?(:lutaml_parent) || options[:parse_only]
20
+ input_declaration_plan = build_input_declaration_plan(root_element)
21
+ # ...
22
+ end
23
+ ```
24
+
25
+ ### In xmi gem
26
+
27
+ Pass `parse_only: true` in `ParserPipeline::ParseXml` step:
28
+
29
+ ```ruby
30
+ model_class.from_xml(ctx[:xml], register: register, parse_only: true)
31
+ ```
32
+
33
+ If the user later calls `to_xml`, it works but without preserved input namespace declarations (acceptable for read-only use). If they need full round-trip, they opt out of `parse_only`.
34
+
35
+ ## Files
36
+
37
+ - `lutaml-model/lib/lutaml/xml/model_transform.rb` — gate `build_input_declaration_plan` behind `parse_only` option
38
+ - `lib/xmi/parser_pipeline.rb` — pass `parse_only: true` in ParseXml step
@@ -0,0 +1,20 @@
1
+ # 03: Register Fallback Idempotency Guard
2
+
3
+ ## Impact: MEDIUM (prevents cache invalidation on every parse)
4
+
5
+ ## Problem
6
+
7
+ `extend_fallback_for_mixed_namespaces` is called on every `parse_xml` for mixed-namespace documents. It calls `primary_register.add_fallback(reg.id)` which invalidates internal caches even when the fallback was already added on a previous parse.
8
+
9
+ ## Fix
10
+
11
+ Add a guard before calling `add_fallback`:
12
+
13
+ ```ruby
14
+ next if primary_register.fallback.include?(reg.id)
15
+ primary_register.add_fallback(reg.id)
16
+ ```
17
+
18
+ ## Files
19
+
20
+ - `lib/xmi/namespace_detector.rb` or `lib/xmi/version_registry.rb` — find where `extend_fallback_for_mixed_namespaces` is called and add the guard
@@ -0,0 +1,31 @@
1
+ # 04: Pipeline Hash Mutation
2
+
3
+ ## Impact: LOW (saves 4 intermediate Hash allocations per parse)
4
+
5
+ ## Problem
6
+
7
+ Each pipeline step returns `ctx.merge(key: value)` creating intermediate Hash objects.
8
+
9
+ ## Fix
10
+
11
+ Use `ctx[:key] = value` mutation instead:
12
+
13
+ ```ruby
14
+ # Before
15
+ def self.call(ctx)
16
+ xml = ctx[:xml]
17
+ # ...process...
18
+ ctx.merge(xml: fixed_xml)
19
+
20
+ # After
21
+ def self.call(ctx)
22
+ xml = ctx[:xml]
23
+ # ...process...
24
+ ctx[:xml] = fixed_xml
25
+ ctx
26
+ end
27
+ ```
28
+
29
+ ## Files
30
+
31
+ - `lib/xmi/parser_pipeline.rb` — all 4 step modules
@@ -0,0 +1,29 @@
1
+ # 05: EaRoot Single Parse for Extension Loading
2
+
3
+ ## Impact: LOW (only affects load_extension, not parse_xml hot path)
4
+
5
+ ## Problem
6
+
7
+ `EaRoot.load_extension(xml_path)` reads and parses the XML file twice:
8
+ 1. `derive_module_name` in `ea_root.rb:54-56` — `Nokogiri::XML(File.read(xml_path))`
9
+ 2. `build_extension` in `code_generation.rb:8` — `Nokogiri::XML(File.read(xml_path))`
10
+
11
+ ## Fix
12
+
13
+ Parse once in `load_extension`, pass the Nokogiri doc to both:
14
+
15
+ ```ruby
16
+ def load_extension(xml_path)
17
+ xmi_doc = Nokogiri::XML(File.read(xml_path))
18
+ extension_id = get_module_name(xmi_doc)
19
+ # ...guard...
20
+ build_extension_from_doc(xmi_doc)
21
+ update_mappings(extension_id)
22
+ loaded_extensions[extension_id] = xml_path
23
+ end
24
+ ```
25
+
26
+ ## Files
27
+
28
+ - `lib/xmi/ea_root.rb` — parse once, pass doc
29
+ - `lib/xmi/ea_root/code_generation.rb` — accept doc parameter instead of file path
data/docs/migration.md CHANGED
@@ -12,7 +12,7 @@ The old API used `SparxRoot.parse_xml` with automatic namespace normalization:
12
12
  require 'xmi'
13
13
 
14
14
  # Old approach - normalizes all namespaces to 20131001
15
- doc = Xmi::Sparx::SparxRoot.parse_xml(xml_content)
15
+ doc = Xmi::Sparx::Root.parse_xml(xml_content)
16
16
  ```
17
17
 
18
18
  This approach:
@@ -31,7 +31,7 @@ require 'xmi'
31
31
  doc = Xmi.parse(xml_content)
32
32
 
33
33
  # Or for Sparx EA files
34
- doc = Xmi::Sparx::SparxRoot.parse_xml_with_versioning(xml_content)
34
+ doc = Xmi::Sparx::Root.parse_xml_with_versioning(xml_content)
35
35
  ```
36
36
 
37
37
  This approach:
@@ -45,10 +45,10 @@ This approach:
45
45
 
46
46
  ```ruby
47
47
  # Before
48
- doc = Xmi::Sparx::SparxRoot.parse_xml(xml_content)
48
+ doc = Xmi::Sparx::Root.parse_xml(xml_content)
49
49
 
50
50
  # After
51
- doc = Xmi::Sparx::SparxRoot.parse_xml_with_versioning(xml_content)
51
+ doc = Xmi::Sparx::Root.parse_xml_with_versioning(xml_content)
52
52
 
53
53
  # Or
54
54
  doc = Xmi.parse(xml_content)
@@ -86,7 +86,7 @@ doc = Xmi.parse_with_version(xml_content, "20131001")
86
86
 
87
87
  | Use Case | Recommended API |
88
88
  |----------|----------------|
89
- | Sparx EA files | `Xmi::Sparx::SparxRoot.parse_xml_with_versioning` |
89
+ | Sparx EA files | `Xmi::Sparx::Root.parse_xml_with_versioning` |
90
90
  | General XMI files | `Xmi.parse` |
91
91
  | Version detection | `Xmi::Parsing.detect_version` |
92
92
  | Known version | `Xmi.parse_with_version(xml, version)` |
@@ -97,7 +97,7 @@ The old `parse_xml` method still works but normalizes namespaces:
97
97
 
98
98
  ```ruby
99
99
  # Old API - still supported
100
- doc = Xmi::Sparx::SparxRoot.parse_xml(xml_content)
100
+ doc = Xmi::Sparx::Root.parse_xml(xml_content)
101
101
 
102
102
  # This internally normalizes to 20131001 namespace
103
103
  ```
data/docs/versioning.md CHANGED
@@ -64,7 +64,7 @@ Xmi::Parsing.version_supported?("20131001") # => true
64
64
 
65
65
  ```ruby
66
66
  # For Enterprise Architect generated XMI files
67
- doc = Xmi::Sparx::SparxRoot.parse_xml_with_versioning(xml_content)
67
+ doc = Xmi::Sparx::Root.parse_xml_with_versioning(xml_content)
68
68
  ```
69
69
 
70
70
  ## Version Modules
@@ -194,7 +194,7 @@ class BenchmarkRunner
194
194
 
195
195
  case method
196
196
  when :xmi_parse_242_small, :xmi_parse_242_medium, :xmi_parse_242_large, :xmi_parse_251
197
- measure_time { Xmi::Sparx::SparxRoot.parse_xml(xml_content) }
197
+ measure_time { Xmi::Sparx::Root.parse_xml(xml_content) }
198
198
  else
199
199
  raise "Unknown benchmark: #{method}"
200
200
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xmi
4
+ module CustomProfile
5
+ class Abstract < Lutaml::Model::Serializable
6
+ attribute :base_class, :string
7
+
8
+ xml do
9
+ element "Abstract"
10
+ namespace ::Xmi::Namespace::Sparx::CustomProfile
11
+
12
+ map_attribute "base_Class", to: :base_class
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Xmi
4
+ module CustomProfile
5
+ class BasicDoc < Lutaml::Model::Serializable
6
+ attribute :base_class, :string
7
+
8
+ xml do
9
+ element "BasicDoc"
10
+ namespace ::Xmi::Namespace::Sparx::CustomProfile
11
+
12
+ map_attribute "base_Class", to: :base_class
13
+ end
14
+ end
15
+ end
16
+ end