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
data/lib/xmi/ea_root.rb
CHANGED
|
@@ -1,58 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "nokogiri"
|
|
4
|
+
require_relative "ea_root/xml_parsing"
|
|
5
|
+
require_relative "ea_root/code_generation"
|
|
6
|
+
require_relative "ea_root/extension_lifecycle"
|
|
7
|
+
require_relative "ea_root/namespace_handling"
|
|
4
8
|
|
|
5
9
|
module Xmi
|
|
6
|
-
class EaRoot
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#KLASSES#
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
TEXT
|
|
16
|
-
|
|
17
|
-
KLASS_TEMPLATE = <<~TEXT
|
|
18
|
-
class #KLASS_NAME# < #FROM_KLASS#
|
|
19
|
-
#ROOT_TAG_LINE#
|
|
20
|
-
|
|
21
|
-
#ATTRIBUTES##XML_MAPPING#
|
|
22
|
-
end
|
|
23
|
-
TEXT
|
|
10
|
+
class EaRoot
|
|
11
|
+
extend XmlParsing
|
|
12
|
+
extend CodeGeneration
|
|
13
|
+
extend ExtensionLifecycle
|
|
14
|
+
extend NamespaceHandling
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
root "#ROOT_TAG#"
|
|
28
|
-
namespace #NAMESPACE_CLASS#
|
|
29
|
-
#MAP_ATTRIBUTES#
|
|
30
|
-
end
|
|
31
|
-
TEXT
|
|
32
|
-
|
|
33
|
-
ATTRIBUTE_LINE = <<~TEXT
|
|
34
|
-
attribute :#TAG_NAME#, #ATTRIBUTE_TYPE#
|
|
35
|
-
TEXT
|
|
36
|
-
|
|
37
|
-
MAP_ATTRIBUTES = <<~TEXT
|
|
38
|
-
map_attribute "#ATTRIBUTE_NAME#", to: :#ATTRIBUTE_METHOD#
|
|
39
|
-
TEXT
|
|
40
|
-
|
|
41
|
-
MAP_ELEMENT = <<~TEXT
|
|
42
|
-
map_element "#ELEMENT_NAME#",
|
|
43
|
-
to: :#ELEMENT_METHOD#,
|
|
44
|
-
value_map: Xmi::VALUE_MAP
|
|
45
|
-
TEXT
|
|
16
|
+
private_constant :XmlParsing, :CodeGeneration, :ExtensionLifecycle,
|
|
17
|
+
:NamespaceHandling
|
|
46
18
|
|
|
47
19
|
class << self
|
|
48
|
-
# Load an EA extension from an XML file.
|
|
49
|
-
#
|
|
50
|
-
# @param xml_path [String] Path to the MDG extension XML file
|
|
51
|
-
# @return [void]
|
|
52
|
-
# @raise [ArgumentError] If the extension is already loaded
|
|
53
20
|
def load_extension(xml_path)
|
|
54
|
-
|
|
55
|
-
extension_id =
|
|
21
|
+
xmi_doc = Nokogiri::XML(File.read(xml_path))
|
|
22
|
+
extension_id = get_module_name(xmi_doc)
|
|
56
23
|
|
|
57
24
|
if loaded_extensions.key?(extension_id)
|
|
58
25
|
raise ArgumentError,
|
|
@@ -61,18 +28,11 @@ module Xmi
|
|
|
61
28
|
"Call unload_extension('#{extension_id}') first if you want to reload it."
|
|
62
29
|
end
|
|
63
30
|
|
|
64
|
-
|
|
31
|
+
build_extension(xmi_doc)
|
|
65
32
|
update_mappings(extension_id)
|
|
66
33
|
loaded_extensions[extension_id] = xml_path
|
|
67
34
|
end
|
|
68
35
|
|
|
69
|
-
# Unload an extension by removing its module and clearing tracking.
|
|
70
|
-
#
|
|
71
|
-
# This allows the extension to be loaded again, which is useful for
|
|
72
|
-
# testing scenarios that need to reload extensions.
|
|
73
|
-
#
|
|
74
|
-
# @param extension_id [String, Symbol] The extension module name (e.g., "Citygml")
|
|
75
|
-
# @return [void]
|
|
76
36
|
def unload_extension(extension_id)
|
|
77
37
|
extension_id = extension_id.to_s.capitalize
|
|
78
38
|
|
|
@@ -81,367 +41,14 @@ module Xmi
|
|
|
81
41
|
loaded_extensions.delete(extension_id)
|
|
82
42
|
end
|
|
83
43
|
|
|
84
|
-
# Check if an extension is currently loaded.
|
|
85
|
-
#
|
|
86
|
-
# @param extension_id [String, Symbol] The extension module name
|
|
87
|
-
# @return [Boolean]
|
|
88
44
|
def extension_loaded?(extension_id)
|
|
89
45
|
extension_id = extension_id.to_s.capitalize
|
|
90
46
|
loaded_extensions.key?(extension_id)
|
|
91
47
|
end
|
|
92
48
|
|
|
93
|
-
# List all currently loaded extensions.
|
|
94
|
-
#
|
|
95
|
-
# @return [Hash<String, String>] Map of extension_id => xml_path
|
|
96
49
|
def loaded_extensions
|
|
97
50
|
@loaded_extensions ||= {}
|
|
98
51
|
end
|
|
99
|
-
|
|
100
|
-
def output_rb_file(output_rb_path)
|
|
101
|
-
File.write(output_rb_path, @content)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
def update_mappings(module_name)
|
|
107
|
-
new_klasses = all_new_klasses(module_name)
|
|
108
|
-
map_elements = construct_xml_mappings(new_klasses, module_name)
|
|
109
|
-
update_model_attributes(new_klasses, Xmi::Sparx::SparxRoot, module_name)
|
|
110
|
-
update_model_xml_mappings(map_elements, Xmi::Sparx::SparxRoot)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def construct_xml_mappings(new_klasses, module_name)
|
|
114
|
-
map_elements = []
|
|
115
|
-
new_klasses.each do |klass|
|
|
116
|
-
next unless Xmi::EaRoot.const_get(module_name).const_get(klass)
|
|
117
|
-
.respond_to? :root_tag
|
|
118
|
-
|
|
119
|
-
map_elements << MAP_ELEMENT
|
|
120
|
-
.gsub("#ELEMENT_NAME#", Xmi::EaRoot.const_get(module_name).const_get(klass).root_tag)
|
|
121
|
-
.gsub("#ELEMENT_METHOD#", Lutaml::Model::Utils.snake_case(klass.to_s))
|
|
122
|
-
.gsub("#NAMESPACE#", @def_namespace[:uri])
|
|
123
|
-
.gsub("#PREFIX#", @def_namespace[:name])
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
map_elements
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def update_model_attributes(new_klasses, sparx_root, module_name)
|
|
130
|
-
new_klasses.each do |klass|
|
|
131
|
-
method_name = Lutaml::Model::Utils.snake_case(klass)
|
|
132
|
-
full_klass_name = "Xmi::EaRoot::#{module_name}::#{klass}"
|
|
133
|
-
attr_line = "#{ATTRIBUTE_LINE.rstrip}, collection: true"
|
|
134
|
-
attr_line = attr_line
|
|
135
|
-
.gsub("#TAG_NAME#", method_name)
|
|
136
|
-
.gsub("#ATTRIBUTE_TYPE#", full_klass_name)
|
|
137
|
-
|
|
138
|
-
sparx_root.class_eval(attr_line)
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Add extension element mappings to SparxRoot.
|
|
143
|
-
#
|
|
144
|
-
# Only adds NEW mappings for the extension elements. Does NOT re-evaluate
|
|
145
|
-
# the base mappings, which avoids duplicate mapping accumulation.
|
|
146
|
-
#
|
|
147
|
-
# @param map_elements [Array<String>] Array of map_element code strings
|
|
148
|
-
# @param sparx_root [Class] The SparxRoot class to update
|
|
149
|
-
def update_model_xml_mappings(map_elements, sparx_root)
|
|
150
|
-
return if map_elements.empty?
|
|
151
|
-
|
|
152
|
-
# Only add the NEW extension element mappings.
|
|
153
|
-
# Do NOT re-evaluate base mappings to avoid duplicates.
|
|
154
|
-
extension_block = proc do
|
|
155
|
-
map_elements.each { |element_code| instance_eval(element_code) }
|
|
156
|
-
end
|
|
157
|
-
sparx_root.class_eval { xml(&extension_block) }
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def all_new_klasses(module_name)
|
|
161
|
-
Xmi::EaRoot.const_get(module_name).constants.select do |c|
|
|
162
|
-
Xmi::EaRoot.const_get(module_name).const_get(c).is_a? Class
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def get_abstract_klass_node(xmi_doc)
|
|
167
|
-
xmi_doc.at_xpath(
|
|
168
|
-
"//UMLProfiles//Stereotypes//Stereotype[@isAbstract='true']",
|
|
169
|
-
)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def get_klass_name_from_node(node)
|
|
173
|
-
return Lutaml::Model::Serializable.to_s unless node
|
|
174
|
-
|
|
175
|
-
node.attribute_nodes.find { |attr| attr.name == "name" }.value
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def gen_map_attribute_line(attr_name, attr_method)
|
|
179
|
-
space_before = " " * 10
|
|
180
|
-
method_name = Lutaml::Model::Utils.snake_case(attr_method)
|
|
181
|
-
|
|
182
|
-
map_attributes = MAP_ATTRIBUTES
|
|
183
|
-
.gsub("#ATTRIBUTE_NAME#", attr_name)
|
|
184
|
-
.gsub("#ATTRIBUTE_METHOD#", method_name)
|
|
185
|
-
|
|
186
|
-
"#{space_before}#{map_attributes}"
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def gen_attribute_line(tag_name, attribute_type = ":string")
|
|
190
|
-
tag_name = Lutaml::Model::Utils.snake_case(tag_name)
|
|
191
|
-
space_before = " " * 8
|
|
192
|
-
|
|
193
|
-
attribute_line = ATTRIBUTE_LINE
|
|
194
|
-
.gsub("#TAG_NAME#", tag_name)
|
|
195
|
-
.gsub("#ATTRIBUTE_TYPE#", attribute_type)
|
|
196
|
-
|
|
197
|
-
"#{space_before}#{attribute_line}"
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def get_tag_name(tag)
|
|
201
|
-
tag_name = tag.attribute_nodes.find { |attr| attr.name == "name" }.value
|
|
202
|
-
tag_name == "xmlns" ? "altered_xmlns" : tag_name
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def gen_tags(node)
|
|
206
|
-
tags = node.search("Tag")
|
|
207
|
-
attributes_lines = ""
|
|
208
|
-
|
|
209
|
-
tags.each do |tag|
|
|
210
|
-
tag_name = get_tag_name(tag)
|
|
211
|
-
attributes_lines += gen_attribute_line(tag_name)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
[attributes_lines, tags]
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def gen_abstract_klass
|
|
218
|
-
unless @abstract_klass_node
|
|
219
|
-
@abstract_tags = []
|
|
220
|
-
return ""
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
attributes_lines = ""
|
|
224
|
-
tags_lines, @abstract_tags = gen_tags(@abstract_klass_node)
|
|
225
|
-
attributes_lines += tags_lines
|
|
226
|
-
klass_name = get_klass_name_from_node(@abstract_klass_node)
|
|
227
|
-
|
|
228
|
-
KLASS_TEMPLATE
|
|
229
|
-
.gsub("#KLASS_NAME#", Lutaml::Model::Utils.classify(klass_name))
|
|
230
|
-
.gsub("#FROM_KLASS#", "Lutaml::Model::Serializable")
|
|
231
|
-
.gsub("#ATTRIBUTES#", attributes_lines.rstrip)
|
|
232
|
-
.gsub("#XML_MAPPING#", "")
|
|
233
|
-
.gsub("#ROOT_TAG_LINE#", "")
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
def gen_apply_types(node)
|
|
237
|
-
apply_types_lines = ""
|
|
238
|
-
apply_types_nodes = node.search("Apply")
|
|
239
|
-
apply_types_nodes.each do |n|
|
|
240
|
-
apply_types = n.attribute_nodes.map(&:value)
|
|
241
|
-
apply_types.each do |apply_type|
|
|
242
|
-
tag_name = "base_#{apply_type}"
|
|
243
|
-
apply_types_lines += gen_attribute_line(tag_name)
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
[apply_types_lines, apply_types_nodes]
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def gen_generic_klass(node, from_klass = nil)
|
|
251
|
-
node_name = get_klass_name_from_node(node)
|
|
252
|
-
attributes_lines, map_attributes_lines = gen_klass_tags(node)
|
|
253
|
-
apply_types_lines, apply_types_nodes = gen_apply_types(node)
|
|
254
|
-
attributes_lines, map_attributes_lines = gen_klass_apply_types(
|
|
255
|
-
attributes_lines, map_attributes_lines,
|
|
256
|
-
apply_types_lines, apply_types_nodes
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
map_attributes_lines = node_map_attributes(node_name,
|
|
260
|
-
from_klass, map_attributes_lines)
|
|
261
|
-
xml_mapping = replace_xml_mapping(node_name, map_attributes_lines)
|
|
262
|
-
|
|
263
|
-
replace_klass_template(node_name, attributes_lines,
|
|
264
|
-
xml_mapping, from_klass)
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def node_map_attributes(node_name, from_klass, map_attributes_lines)
|
|
268
|
-
if from_klass && @node_map.key?(from_klass.to_sym)
|
|
269
|
-
map_attributes_lines += @node_map[from_klass.to_sym].to_s
|
|
270
|
-
else
|
|
271
|
-
@node_map ||= {}
|
|
272
|
-
@node_map[node_name.to_sym] = map_attributes_lines
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
map_attributes_lines
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def gen_klass_apply_types(attributes_lines, map_attributes_lines,
|
|
279
|
-
apply_types_lines, apply_types_nodes)
|
|
280
|
-
unless apply_types_nodes.empty?
|
|
281
|
-
attributes_lines += apply_types_lines
|
|
282
|
-
apply_types_nodes.each do |n|
|
|
283
|
-
apply_types = n.attribute_nodes.map(&:value)
|
|
284
|
-
apply_types.each do |apply_type|
|
|
285
|
-
map_attributes_lines += gen_map_attribute_line(
|
|
286
|
-
"base_#{apply_type}", "base_#{apply_type}"
|
|
287
|
-
)
|
|
288
|
-
end
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
[attributes_lines, map_attributes_lines]
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def gen_klass_tags(node)
|
|
296
|
-
attributes_lines = ""
|
|
297
|
-
map_attributes_lines = ""
|
|
298
|
-
|
|
299
|
-
tags_lines, tags = gen_tags(node)
|
|
300
|
-
attributes_lines += tags_lines
|
|
301
|
-
(@abstract_tags + tags).each do |tag|
|
|
302
|
-
tag_name = get_tag_name(tag)
|
|
303
|
-
map_attributes_lines += gen_map_attribute_line(tag_name, tag_name)
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
[attributes_lines, map_attributes_lines]
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def replace_xml_mapping(node_name, map_attributes_lines)
|
|
310
|
-
# Look up namespace class by URI, or generate a new one if not found
|
|
311
|
-
ns_class = find_or_create_namespace_class
|
|
312
|
-
|
|
313
|
-
XML_MAPPING
|
|
314
|
-
.gsub("#ROOT_TAG#", node_name)
|
|
315
|
-
.gsub("#NAMESPACE_CLASS#", ns_class)
|
|
316
|
-
.gsub("#MAP_ATTRIBUTES#", "\n#{map_attributes_lines.rstrip}")
|
|
317
|
-
.rstrip
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
def find_or_create_namespace_class
|
|
321
|
-
uri = @def_namespace[:uri]
|
|
322
|
-
prefix = @def_namespace[:name]
|
|
323
|
-
|
|
324
|
-
# Try to find existing namespace class in registry
|
|
325
|
-
existing_class = NamespaceRegistry.resolve(uri)
|
|
326
|
-
return existing_class.name if existing_class
|
|
327
|
-
|
|
328
|
-
# Generate a new namespace class
|
|
329
|
-
# Format: Xmi::Namespace::Dynamic::{ModuleName}
|
|
330
|
-
module_name = @module_name
|
|
331
|
-
ns_class_name = "Xmi::Namespace::Dynamic::#{module_name}"
|
|
332
|
-
|
|
333
|
-
# Check if already defined
|
|
334
|
-
return ns_class_name if Object.const_defined?(ns_class_name)
|
|
335
|
-
|
|
336
|
-
# Create the namespace class
|
|
337
|
-
Namespace.ensure_dynamic_namespace_module_exists!
|
|
338
|
-
ns_class = Class.new(Lutaml::Xml::Namespace) do
|
|
339
|
-
define_singleton_method(:uri) { uri }
|
|
340
|
-
define_singleton_method(:prefix_default) { prefix }
|
|
341
|
-
end
|
|
342
|
-
Namespace::Dynamic.const_set(module_name, ns_class)
|
|
343
|
-
|
|
344
|
-
# Register in namespace registry
|
|
345
|
-
NamespaceRegistry.register(uri, ns_class)
|
|
346
|
-
|
|
347
|
-
ns_class_name
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
def replace_klass_template(node_name, attributes_lines, xml_mapping,
|
|
351
|
-
from_klass = nil)
|
|
352
|
-
abstract_klass_name = get_klass_name_from_node(@abstract_klass_node)
|
|
353
|
-
abstract_klass_name = from_klass if from_klass
|
|
354
|
-
root_tag_line = "def self.root_tag = \"#{node_name}\""
|
|
355
|
-
|
|
356
|
-
KLASS_TEMPLATE
|
|
357
|
-
.gsub("#KLASS_NAME#", Lutaml::Model::Utils.classify(node_name))
|
|
358
|
-
.gsub("#FROM_KLASS#", Lutaml::Model::Utils.classify(abstract_klass_name))
|
|
359
|
-
.gsub("#ROOT_TAG_LINE#", root_tag_line)
|
|
360
|
-
.gsub("#ATTRIBUTES#", attributes_lines.rstrip)
|
|
361
|
-
.gsub("#XML_MAPPING#", "\n\n#{xml_mapping}")
|
|
362
|
-
end
|
|
363
|
-
|
|
364
|
-
def gen_generic_klasses(xmi_doc)
|
|
365
|
-
nodes = xmi_doc.xpath("//UMLProfiles//Stereotypes//Stereotype[not(contains(@isAbstract, 'true'))]")
|
|
366
|
-
klasses_lines = ""
|
|
367
|
-
|
|
368
|
-
klasses_lines += "#{gen_generic_klasses_wo_base_stereotypes(nodes)}\n"
|
|
369
|
-
klasses_lines += "#{gen_generic_klasses_w_base_stereotypes(nodes)}\n"
|
|
370
|
-
|
|
371
|
-
klasses_lines
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def gen_generic_klasses_wo_base_stereotypes(nodes)
|
|
375
|
-
nodes = nodes.select { |n| n.attributes["baseStereotypes"].nil? }
|
|
376
|
-
klasses_lines = ""
|
|
377
|
-
|
|
378
|
-
nodes.each do |node|
|
|
379
|
-
klasses_lines += "#{gen_generic_klass(node)}\n"
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
klasses_lines
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def gen_generic_klasses_w_base_stereotypes(nodes)
|
|
386
|
-
nodes = nodes.reject { |n| n.attributes["baseStereotypes"].nil? }
|
|
387
|
-
klasses_lines = ""
|
|
388
|
-
|
|
389
|
-
nodes.each do |node|
|
|
390
|
-
base_stereotypes = node.attributes["baseStereotypes"].value
|
|
391
|
-
klasses_lines += "#{gen_generic_klass(node, base_stereotypes)}\n"
|
|
392
|
-
end
|
|
393
|
-
|
|
394
|
-
klasses_lines
|
|
395
|
-
end
|
|
396
|
-
|
|
397
|
-
def gen_klasses(xmi_doc)
|
|
398
|
-
@abstract_klass_node = get_abstract_klass_node(xmi_doc)
|
|
399
|
-
klasses_lines = ""
|
|
400
|
-
klasses_lines += "#{gen_abstract_klass}\n"
|
|
401
|
-
klasses_lines += gen_generic_klasses(xmi_doc).rstrip
|
|
402
|
-
klasses_lines
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
def gen_module(xmi_doc, module_name)
|
|
406
|
-
MODULE_TEMPLATE
|
|
407
|
-
.gsub("#MODULE_NAME#", module_name)
|
|
408
|
-
.gsub("#KLASSES#", gen_klasses(xmi_doc))
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def get_namespace_from_definition(xmi_doc)
|
|
412
|
-
namespace_key = get_module_name_from_definition(xmi_doc)
|
|
413
|
-
namespace_uri = get_module_uri(xmi_doc)
|
|
414
|
-
|
|
415
|
-
{ name: namespace_key, uri: namespace_uri }
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
def gen_content(xml)
|
|
419
|
-
xmi_doc = Nokogiri::XML(File.read(xml))
|
|
420
|
-
@module_name = get_module_name(xmi_doc)
|
|
421
|
-
@def_namespace = get_namespace_from_definition(xmi_doc)
|
|
422
|
-
gen_module(xmi_doc, @module_name)
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
def get_module_name(xmi_doc)
|
|
426
|
-
get_module_name_from_definition(xmi_doc).capitalize
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
def get_module_name_from_definition(xmi_doc)
|
|
430
|
-
node = xmi_doc.at_xpath("//UMLProfile/Documentation")
|
|
431
|
-
node.attribute_nodes.find { |attr| attr.name == "name" }&.value
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
def get_module_uri(xmi_doc)
|
|
435
|
-
node = xmi_doc.at_xpath("//UMLProfile/Documentation")
|
|
436
|
-
uri = node.attribute_nodes.find { |attr| attr.name == "URI" }&.value
|
|
437
|
-
|
|
438
|
-
return uri if uri
|
|
439
|
-
|
|
440
|
-
name = get_module_name_from_definition(xmi_doc)
|
|
441
|
-
ver = node.attribute_nodes.find { |attr| attr.name == "version" }&.value
|
|
442
|
-
|
|
443
|
-
"http://www.sparxsystems.com/profiles/#{name}/#{ver}"
|
|
444
|
-
end
|
|
445
52
|
end
|
|
446
53
|
end
|
|
447
54
|
end
|
|
@@ -10,6 +10,15 @@ module Xmi
|
|
|
10
10
|
class NamespaceDetector
|
|
11
11
|
VERSION_PATTERN = /(\d{8})/
|
|
12
12
|
|
|
13
|
+
# Regex to extract xmlns declarations without parsing the entire document.
|
|
14
|
+
# Matches both default namespace (xmlns="...") and prefixed (xmlns:foo="...").
|
|
15
|
+
# Namespace declarations are always on or near the root element, so scanning
|
|
16
|
+
# the first 8KB is sufficient for any XMI file.
|
|
17
|
+
NS_DECL_REGEX = /xmlns(?::(\w+))?\s*=\s*["']([^"']+)["']/
|
|
18
|
+
|
|
19
|
+
# How many bytes of the XML to scan for namespace declarations
|
|
20
|
+
NS_SCAN_BYTES = 8192
|
|
21
|
+
|
|
13
22
|
# Namespace URI patterns for OMG specifications
|
|
14
23
|
NS_PATTERNS = {
|
|
15
24
|
xmi: %r{http://www\.omg\.org/spec/XMI/(\d{8})},
|
|
@@ -33,11 +42,34 @@ module Xmi
|
|
|
33
42
|
}
|
|
34
43
|
end
|
|
35
44
|
|
|
36
|
-
# Extract all namespace URIs from XML content
|
|
45
|
+
# Extract all namespace URIs from XML content using regex on the first 8KB.
|
|
37
46
|
#
|
|
38
|
-
#
|
|
47
|
+
# This avoids a full Nokogiri parse — namespace declarations are always
|
|
48
|
+
# on or near the root element, so scanning the first few KB is sufficient
|
|
49
|
+
# and ~10x faster than parsing a 3.5MB document.
|
|
50
|
+
#
|
|
51
|
+
# @param xml_content [String] The XML content
|
|
39
52
|
# @return [Hash<String, String>] A hash mapping prefixes to namespace URIs
|
|
40
53
|
def self.extract_namespace_uris(xml_content)
|
|
54
|
+
head = xml_content.byteslice(0, NS_SCAN_BYTES)
|
|
55
|
+
unless head.valid_encoding?
|
|
56
|
+
head = head.encode("UTF-8", invalid: :replace,
|
|
57
|
+
undef: :replace)
|
|
58
|
+
end
|
|
59
|
+
result = {}
|
|
60
|
+
head.scan(NS_DECL_REGEX) do |prefix, uri|
|
|
61
|
+
key = prefix.nil? ? "xmlns" : prefix
|
|
62
|
+
result[key] = uri unless result.key?(key)
|
|
63
|
+
end
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Extract namespace URIs via Nokogiri (full parse).
|
|
68
|
+
# Used by `analyze` when the complete namespace map is needed.
|
|
69
|
+
#
|
|
70
|
+
# @param xml_content [String] The XML content
|
|
71
|
+
# @return [Hash<String, String>] A hash mapping prefixes to namespace URIs
|
|
72
|
+
def self.extract_namespace_uris_full(xml_content)
|
|
41
73
|
doc = Nokogiri::XML(xml_content, &:noent)
|
|
42
74
|
doc.collect_namespaces
|
|
43
75
|
rescue Nokogiri::XML::SyntaxError
|
|
@@ -117,7 +149,7 @@ module Xmi
|
|
|
117
149
|
def self.analyze(xml_content)
|
|
118
150
|
versions = detect_versions(xml_content)
|
|
119
151
|
uris = detect_namespace_uris(xml_content)
|
|
120
|
-
raw_namespaces =
|
|
152
|
+
raw_namespaces = extract_namespace_uris_full(xml_content)
|
|
121
153
|
|
|
122
154
|
{
|
|
123
155
|
versions: versions,
|
data/lib/xmi/parser_pipeline.rb
CHANGED
|
@@ -8,9 +8,9 @@ module Xmi
|
|
|
8
8
|
# without modifying existing code — Open/Closed Principle.
|
|
9
9
|
#
|
|
10
10
|
# @example
|
|
11
|
-
# context = { xml: xml_content, root_class: Xmi::Sparx::
|
|
11
|
+
# context = { xml: xml_content, root_class: Xmi::Sparx::Root }
|
|
12
12
|
# result = Xmi::ParserPipeline.run(context)
|
|
13
|
-
# result[:root] # => parsed
|
|
13
|
+
# result[:root] # => parsed Root instance
|
|
14
14
|
#
|
|
15
15
|
module ParserPipeline
|
|
16
16
|
module Steps
|
|
@@ -18,11 +18,11 @@ module Xmi
|
|
|
18
18
|
def self.call(ctx)
|
|
19
19
|
xml = ctx[:xml]
|
|
20
20
|
if xml.respond_to?(:valid_encoding?) && !xml.valid_encoding?
|
|
21
|
-
xml = xml
|
|
21
|
+
ctx[:xml] = xml
|
|
22
22
|
.encode("UTF-16be", invalid: :replace, replace: "?")
|
|
23
23
|
.encode("UTF-8")
|
|
24
24
|
end
|
|
25
|
-
ctx
|
|
25
|
+
ctx
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -36,16 +36,16 @@ module Xmi
|
|
|
36
36
|
module ParseXml
|
|
37
37
|
def self.call(ctx)
|
|
38
38
|
root_class = ctx[:root_class]
|
|
39
|
-
root = VersionRegistry.parse_with_detected_version(
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
ctx[:root] = VersionRegistry.parse_with_detected_version(
|
|
40
|
+
ctx[:xml], root_class
|
|
41
|
+
)
|
|
42
|
+
ctx
|
|
42
43
|
end
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
module BuildIndex
|
|
46
47
|
def self.call(ctx)
|
|
47
|
-
root
|
|
48
|
-
root.build_index if root.respond_to?(:build_index)
|
|
48
|
+
ctx[:root].build_index if ctx[:root].respond_to?(:build_index)
|
|
49
49
|
ctx
|
|
50
50
|
end
|
|
51
51
|
end
|
data/lib/xmi/sparx/index.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Xmi
|
|
4
4
|
module Sparx
|
|
5
|
-
# Builds all commonly needed indexes from a parsed
|
|
5
|
+
# Builds all commonly needed indexes from a parsed Root in a single
|
|
6
6
|
# targeted walk, avoiding the generic map_id_name approach that visits every
|
|
7
7
|
# attribute of every node.
|
|
8
8
|
#
|
|
@@ -24,7 +24,7 @@ module Xmi
|
|
|
24
24
|
|
|
25
25
|
PackagedElement = ::Xmi::Uml::PackagedElement
|
|
26
26
|
|
|
27
|
-
# @param root [Xmi::Sparx::
|
|
27
|
+
# @param root [Xmi::Sparx::Root] parsed XMI model
|
|
28
28
|
def initialize(root)
|
|
29
29
|
@id_name_map = {}
|
|
30
30
|
@packaged_elements = []
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Xmi
|
|
4
4
|
module Sparx
|
|
5
|
-
module
|
|
5
|
+
module Mappings
|
|
6
6
|
# Base XML mapping class for Sparx EA XMI documents.
|
|
7
7
|
#
|
|
8
8
|
# This reusable mapping class encapsulates all the XML element → attribute
|
|
9
|
-
# mappings for
|
|
9
|
+
# mappings for Root.
|
|
10
10
|
#
|
|
11
11
|
# @example Use in a model class
|
|
12
|
-
# class
|
|
13
|
-
# xml
|
|
12
|
+
# class Root < ::Xmi::Root
|
|
13
|
+
# xml Mappings::BaseMapping
|
|
14
14
|
# end
|
|
15
15
|
class BaseMapping < Lutaml::Xml::Mapping
|
|
16
16
|
xml do
|
data/lib/xmi/sparx/mappings.rb
CHANGED
data/lib/xmi/sparx/root.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Xmi
|
|
4
4
|
module Sparx
|
|
5
|
-
class
|
|
5
|
+
class Root < ::Xmi::Root
|
|
6
6
|
attribute :modelica_parameter, SysPhS
|
|
7
7
|
|
|
8
8
|
attribute :eauml_import, EaUml::Import, collection: true
|
|
@@ -26,7 +26,7 @@ module Xmi
|
|
|
26
26
|
# encoding fix → version detection → XML parsing → index building.
|
|
27
27
|
#
|
|
28
28
|
# @param xml_content [String] The raw XMI XML content
|
|
29
|
-
# @return [
|
|
29
|
+
# @return [Root] The parsed Ruby object with index built
|
|
30
30
|
#
|
|
31
31
|
# @see Xmi::ParserPipeline
|
|
32
32
|
# @see Xmi.parse
|
|
@@ -55,7 +55,7 @@ module Xmi
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Use the reusable BaseMapping class instead of eval hack
|
|
58
|
-
xml
|
|
58
|
+
xml Mappings::BaseMapping
|
|
59
59
|
|
|
60
60
|
# Build index for fast lookups
|
|
61
61
|
# @return [Sparx::Index]
|
data/lib/xmi/sparx.rb
CHANGED
|
@@ -17,8 +17,8 @@ module Xmi
|
|
|
17
17
|
autoload :EaStub, "xmi/sparx/ea_stub"
|
|
18
18
|
autoload :SysPhS, "xmi/sparx/sys_ph_s"
|
|
19
19
|
autoload :Extension, "xmi/sparx/extension"
|
|
20
|
-
autoload :
|
|
21
|
-
autoload :
|
|
20
|
+
autoload :Root, "xmi/sparx/root"
|
|
21
|
+
autoload :Mappings, "xmi/sparx/mappings"
|
|
22
22
|
autoload :Index, "xmi/sparx/index"
|
|
23
23
|
end
|
|
24
24
|
end
|
data/lib/xmi/version.rb
CHANGED