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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +29 -47
- data/CLAUDE.md +166 -0
- data/README.adoc +9 -27
- data/TODO.perf/01-eliminate-duplicate-nokogiri-parse.md +33 -0
- data/TODO.perf/02-read-only-fast-mode.md +38 -0
- data/TODO.perf/03-register-fallback-idempotency.md +20 -0
- data/TODO.perf/04-pipeline-hash-mutation.md +31 -0
- data/TODO.perf/05-ea-root-single-parse.md +29 -0
- data/docs/migration.md +6 -6
- data/docs/versioning.md +1 -1
- data/lib/tasks/benchmark_runner.rb +1 -1
- data/lib/xmi/custom_profile/abstract.rb +16 -0
- data/lib/xmi/custom_profile/basic_doc.rb +16 -0
- data/lib/xmi/custom_profile/bibliography.rb +16 -0
- data/lib/xmi/custom_profile/edition.rb +18 -0
- data/lib/xmi/custom_profile/enumeration.rb +16 -0
- data/lib/xmi/custom_profile/informative.rb +16 -0
- data/lib/xmi/custom_profile/invariant.rb +16 -0
- data/lib/xmi/custom_profile/number.rb +18 -0
- data/lib/xmi/custom_profile/ocl.rb +16 -0
- data/lib/xmi/custom_profile/persistence.rb +20 -0
- data/lib/xmi/custom_profile/publication_date.rb +18 -0
- data/lib/xmi/custom_profile/year_version.rb +18 -0
- data/lib/xmi/custom_profile.rb +12 -143
- data/lib/xmi/ea_root/code_generation.rb +140 -0
- data/lib/xmi/ea_root/extension_lifecycle.rb +52 -0
- data/lib/xmi/ea_root/namespace_handling.rb +39 -0
- data/lib/xmi/ea_root/xml_parsing.rb +51 -0
- data/lib/xmi/ea_root.rb +14 -407
- data/lib/xmi/namespace_detector.rb +35 -3
- data/lib/xmi/parser_pipeline.rb +9 -9
- data/lib/xmi/sparx/index.rb +2 -2
- data/lib/xmi/sparx/mappings/base_mapping.rb +4 -4
- data/lib/xmi/sparx/mappings.rb +1 -1
- data/lib/xmi/sparx/root.rb +3 -3
- data/lib/xmi/sparx.rb +2 -2
- data/lib/xmi/version.rb +1 -1
- data/lib/xmi/version_registry.rb +11 -8
- metadata +24 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62e6b5232df619d790a5ca7b69454d3ed6f2762bb457808436be572162867f16
|
|
4
|
+
data.tar.gz: 4adbc6c45fd9e75a2fb6612f60cb35ef47a91afe44cca8c366f8c9b17237aeab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
55
|
+
# Offense count: 30
|
|
88
56
|
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
89
57
|
Metrics/MethodLength:
|
|
90
|
-
Max:
|
|
58
|
+
Max: 55
|
|
91
59
|
|
|
92
|
-
# Offense count:
|
|
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:
|
|
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:
|
|
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:
|
|
163
|
-
#
|
|
164
|
-
|
|
136
|
+
# Offense count: 8
|
|
137
|
+
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
|
138
|
+
# SupportedInflectors: default, active_support
|
|
139
|
+
RSpec/SpecFilePathFormat:
|
|
165
140
|
Exclude:
|
|
166
|
-
- '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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::
|
|
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 -
|
|
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
|
|
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/
|
|
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
|
|
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::
|
|
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::
|
|
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::
|
|
48
|
+
doc = Xmi::Sparx::Root.parse_xml(xml_content)
|
|
49
49
|
|
|
50
50
|
# After
|
|
51
|
-
doc = Xmi::Sparx::
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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
|