lutaml 0.10.0 → 0.10.2

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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +73 -31
  3. data/Gemfile +1 -5
  4. data/lib/lutaml/cli/enhanced_formatter.rb +4 -2
  5. data/lib/lutaml/cli/tree_view_formatter.rb +1 -1
  6. data/lib/lutaml/cli/uml/diagram_command.rb +2 -6
  7. data/lib/lutaml/converter/xmi_to_uml.rb +11 -7
  8. data/lib/lutaml/ea/diagram/extractor.rb +8 -3
  9. data/lib/lutaml/model_transformations/parsers/base_parser.rb +90 -13
  10. data/lib/lutaml/qea/factory/class_transformer.rb +8 -19
  11. data/lib/lutaml/qea/factory/data_type_transformer.rb +2 -9
  12. data/lib/lutaml/qea/factory/diagram_transformer.rb +0 -3
  13. data/lib/lutaml/qea/factory/enum_transformer.rb +3 -5
  14. data/lib/lutaml/qea/factory/generalization_transformer.rb +1 -1
  15. data/lib/lutaml/qea/factory/package_transformer.rb +45 -19
  16. data/lib/lutaml/qea/validation/base_validator.rb +5 -0
  17. data/lib/lutaml/qea/verification/document_normalizer.rb +4 -12
  18. data/lib/lutaml/qea/verification/document_verifier.rb +25 -15
  19. data/lib/lutaml/uml_repository/index_builder.rb +27 -9
  20. data/lib/lutaml/uml_repository/package_exporter.rb +14 -3
  21. data/lib/lutaml/uml_repository/presenters/diagram_presenter.rb +2 -7
  22. data/lib/lutaml/uml_repository/queries/inheritance_query.rb +66 -0
  23. data/lib/lutaml/uml_repository/queries/search_query.rb +1 -1
  24. data/lib/lutaml/uml_repository/repository.rb +34 -0
  25. data/lib/lutaml/uml_repository/validators/repository_validator.rb +1 -1
  26. data/lib/lutaml/uml_repository/web_ui/app.rb +25 -7
  27. data/lib/lutaml/version.rb +1 -1
  28. data/lib/lutaml/xmi/parsers/xmi_base.rb +67 -150
  29. data/lutaml.gemspec +3 -0
  30. metadata +44 -2
@@ -3,6 +3,8 @@
3
3
  require_relative "base_transformer"
4
4
  require_relative "tagged_value_transformer"
5
5
  require_relative "instance_transformer"
6
+ require_relative "enum_transformer"
7
+ require_relative "data_type_transformer"
6
8
  require "lutaml/uml"
7
9
 
8
10
  module Lutaml
@@ -32,7 +34,7 @@ module Lutaml
32
34
 
33
35
  # Load stereotype from t_xref
34
36
  stereotype = load_stereotype(ea_package.ea_guid)
35
- pkg.stereotype = stereotype if stereotype
37
+ pkg.stereotype = [stereotype] if stereotype
36
38
 
37
39
  # Note: Child packages and contents will be loaded separately
38
40
  # to avoid circular dependencies and allow lazy loading
@@ -97,30 +99,54 @@ module Lutaml
97
99
 
98
100
  ea_objects = rows.map { |row| Models::EaObject.from_db_row(row) }
99
101
 
100
- # Transform classes - include ALL class-type objects,
101
- # even without names
102
- # Also include Text objects that appear on diagrams
103
- # (EA exports these as classes in XMI)
104
102
  class_transformer = ClassTransformer.new(database)
103
+ enum_transformer = EnumTransformer.new(database)
104
+ data_type_transformer = DataTypeTransformer.new(database)
105
+ instance_transformer = InstanceTransformer.new(database)
106
+
105
107
  ea_objects.each do |ea_obj|
106
- is_class_type = ea_obj.uml_class? || ea_obj.interface?
107
- is_text_on_diagram = ea_obj.object_type == "Text" &&
108
- appears_on_diagram?(ea_obj.ea_object_id)
108
+ if enum_object?(ea_obj)
109
+ uml_enum = enum_transformer.transform(ea_obj)
110
+ pkg.enums << uml_enum if uml_enum
111
+ elsif data_type_object?(ea_obj)
112
+ uml_dt = data_type_transformer.transform(ea_obj)
113
+ pkg.data_types << uml_dt if uml_dt
114
+ elsif class_object?(ea_obj)
115
+ uml_class = class_transformer.transform(ea_obj)
116
+ pkg.classes << uml_class if uml_class
117
+ elsif ea_obj.instance?
118
+ uml_instance = instance_transformer.transform(ea_obj)
119
+ pkg.instances << uml_instance if uml_instance
120
+ end
121
+ end
122
+ end
109
123
 
110
- next unless is_class_type || is_text_on_diagram
124
+ # Check if an EA object should be classified as an enum.
125
+ # EA stores enums as either object_type="Enumeration" or
126
+ # object_type="Class" with stereotype="enumeration".
127
+ def enum_object?(ea_obj)
128
+ ea_obj.enumeration? || stereotype_is?(ea_obj, "enumeration")
129
+ end
111
130
 
112
- uml_class = class_transformer.transform(ea_obj)
113
- pkg.classes << uml_class if uml_class
114
- end
131
+ # Check if an EA object should be classified as a data type.
132
+ def data_type_object?(ea_obj)
133
+ ea_obj.data_type?
134
+ end
115
135
 
116
- # Transform instances (Object type)
117
- instance_transformer = InstanceTransformer.new(database)
118
- ea_objects.select(&:instance?).each do |ea_obj|
119
- uml_instance = instance_transformer.transform(ea_obj)
120
- pkg.instances << uml_instance if uml_instance
121
- end
136
+ # Check if an EA object should be classified as a class.
137
+ def class_object?(ea_obj)
138
+ return false if enum_object?(ea_obj) || data_type_object?(ea_obj)
139
+
140
+ ea_obj.uml_class? || ea_obj.interface? ||
141
+ ea_obj.object_type == "Text" ||
142
+ ea_obj.object_type == "ProxyConnector"
143
+ end
144
+
145
+ # Check if an object's stereotype matches (case-insensitive).
146
+ def stereotype_is?(ea_obj, expected)
147
+ return false unless ea_obj.stereotype
122
148
 
123
- # Note: Enums and DataTypes could be added similarly
149
+ ea_obj.stereotype.downcase == expected
124
150
  end
125
151
 
126
152
  # Load diagrams for a package
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require_relative "validation_result"
4
5
 
5
6
  module Lutaml
@@ -247,9 +248,13 @@ module Lutaml
247
248
 
248
249
  path_parts = []
249
250
  current_id = package_id
251
+ visited = Set.new
250
252
 
251
253
  # Walk up the parent chain to build full path
252
254
  while current_id && !current_id.zero?
255
+ break if visited.include?(current_id)
256
+
257
+ visited.add(current_id)
253
258
  package = database.packages.find { |p| p.package_id == current_id }
254
259
 
255
260
  if package
@@ -11,11 +11,10 @@ module Lutaml
11
11
  # @param document [Lutaml::Uml::Document] The document to normalize
12
12
  # @return [Lutaml::Uml::Document] A normalized copy
13
13
  def normalize(document)
14
- normalized = deep_copy(document)
15
- remove_xmi_ids(normalized)
16
- sort_collections(normalized)
17
- normalize_strings_in_document(normalized)
18
- normalized
14
+ remove_xmi_ids(document)
15
+ sort_collections(document)
16
+ normalize_strings_in_document(document)
17
+ document
19
18
  end
20
19
 
21
20
  # Remove all XMI IDs from document
@@ -71,13 +70,6 @@ module Lutaml
71
70
 
72
71
  private
73
72
 
74
- # Deep copy document to avoid modifying original
75
- def deep_copy(document)
76
- # Use YAML serialization for deep copy
77
- yaml = document.to_yaml
78
- Lutaml::Uml::Document.from_yaml(yaml)
79
- end
80
-
81
73
  # Process packages recursively to remove XMI IDs
82
74
  def process_packages(packages) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
83
75
  packages.each do |package|
@@ -65,23 +65,29 @@ module Lutaml
65
65
  result
66
66
  end
67
67
 
68
+ # Reset cached match results (call between verifications)
69
+ def reset_cache
70
+ @cached_class_matches = nil
71
+ @cached_package_matches = nil
72
+ end
73
+
68
74
  # Compare element counts
69
75
  #
70
76
  # @param xmi_doc [Lutaml::Uml::Document] XMI document
71
77
  # @param qea_doc [Lutaml::Uml::Document] QEA document
72
78
  # @return [void]
73
79
  def verify_structure(xmi_doc, qea_doc) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
74
- # Compare package counts
75
- match_result = matcher.match_packages(xmi_doc, qea_doc)
76
- result.add_matches(:packages, match_result[:matches].size)
77
- result.add_xmi_only(:packages, match_result[:xmi_only])
78
- result.add_qea_only(:packages, match_result[:qea_only])
79
-
80
- # Compare class counts
81
- match_result = matcher.match_classes(xmi_doc, qea_doc)
82
- result.add_matches(:classes, match_result[:matches].size)
83
- result.add_xmi_only(:classes, match_result[:xmi_only])
84
- result.add_qea_only(:classes, match_result[:qea_only])
80
+ # Compare package counts (cache for reuse in verify_properties)
81
+ @cached_package_matches = matcher.match_packages(xmi_doc, qea_doc)
82
+ result.add_matches(:packages, @cached_package_matches[:matches].size)
83
+ result.add_xmi_only(:packages, @cached_package_matches[:xmi_only])
84
+ result.add_qea_only(:packages, @cached_package_matches[:qea_only])
85
+
86
+ # Compare class counts (cache for reuse in verify_properties)
87
+ @cached_class_matches = matcher.match_classes(xmi_doc, qea_doc)
88
+ result.add_matches(:classes, @cached_class_matches[:matches].size)
89
+ result.add_xmi_only(:classes, @cached_class_matches[:xmi_only])
90
+ result.add_qea_only(:classes, @cached_class_matches[:qea_only])
85
91
 
86
92
  # Compare enum counts
87
93
  xmi_enums = count_all_enums(xmi_doc)
@@ -130,12 +136,16 @@ module Lutaml
130
136
  # @param qea_doc [Lutaml::Uml::Document] QEA document
131
137
  # @return [void]
132
138
  def verify_properties(xmi_doc, qea_doc)
133
- # Verify class properties
134
- class_matches = matcher.match_classes(xmi_doc, qea_doc)
139
+ # Verify class properties (reuse cached matches from verify_structure)
140
+ class_matches = @cached_class_matches || matcher.match_classes(
141
+ xmi_doc, qea_doc
142
+ )
135
143
  verify_class_properties(class_matches[:matches])
136
144
 
137
- # Verify package properties
138
- package_matches = matcher.match_packages(xmi_doc, qea_doc)
145
+ # Verify package properties (reuse cached matches from verify_structure)
146
+ package_matches = @cached_package_matches || matcher.match_packages(
147
+ xmi_doc, qea_doc
148
+ )
139
149
  verify_package_properties(package_matches[:matches])
140
150
  end
141
151
 
@@ -388,20 +388,38 @@ module Lutaml
388
388
 
389
389
  classes.each do |klass|
390
390
  next unless klass.name
391
- next unless klass.generalization
392
391
 
393
392
  child_qname = "#{package_path}::#{klass.name}"
394
393
 
395
- # Handle generalization - it could have a general attribute
396
- parent_name = extract_parent_name(klass.generalization)
397
- next unless parent_name
394
+ # Handle generalization attribute
395
+ if klass.generalization
396
+ parent_name = extract_parent_name(klass.generalization)
397
+ if parent_name
398
+ parent_qname = resolve_qualified_name(parent_name, package_path)
399
+ if parent_qname && child_qname != parent_qname
400
+ @inheritance_graph[parent_qname] ||= []
401
+ @inheritance_graph[parent_qname] << child_qname
402
+ end
403
+ end
404
+ end
405
+
406
+ # Handle inheritance associations
407
+ next unless klass.associations
408
+
409
+ klass.associations.each do |assoc|
410
+ next unless assoc.respond_to?(:member_end_type)
411
+ next unless assoc.member_end_type == "inheritance"
412
+
413
+ parent_name = assoc.member_end
414
+ next unless parent_name
415
+
416
+ parent_name = parent_name.name if parent_name.respond_to?(:name)
417
+ next unless parent_name.is_a?(String) && !parent_name.empty?
398
418
 
399
- # Try to resolve parent qualified name
400
- parent_qname = resolve_qualified_name(parent_name, package_path)
401
- next unless parent_qname
419
+ parent_qname = resolve_qualified_name(parent_name, package_path)
420
+ next unless parent_qname
421
+ next if child_qname == parent_qname
402
422
 
403
- # Avoid self-references
404
- if child_qname != parent_qname
405
423
  @inheritance_graph[parent_qname] ||= []
406
424
  @inheritance_graph[parent_qname] << child_qname
407
425
  end
@@ -76,9 +76,22 @@ module Lutaml
76
76
  # @raise [ArgumentError] If serialization format is invalid
77
77
  # @example
78
78
  # exporter.export("model.lur")
79
- def export(output_path)
79
+ def export(output_path) # rubocop:disable Metrics/MethodLength
80
80
  validate_options!
81
81
 
82
+ retries = 0
83
+ begin
84
+ write_lur_package(output_path)
85
+ rescue Errno::EACCES
86
+ retries += 1
87
+ retry if retries < 3
88
+ raise
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def write_lur_package(output_path)
82
95
  Zip::File.open(output_path, create: true) do |zip|
83
96
  write_metadata(zip)
84
97
  write_document(zip)
@@ -88,8 +101,6 @@ module Lutaml
88
101
  end
89
102
  end
90
103
 
91
- private
92
-
93
104
  # Get default export options.
94
105
  #
95
106
  # @return [Hash] Default options
@@ -424,14 +424,9 @@ module Lutaml
424
424
  return nil unless uml_element.respond_to?(:stereotype)
425
425
 
426
426
  stereotype = uml_element.stereotype
427
- return nil unless stereotype
427
+ return nil unless stereotype && !stereotype.empty?
428
428
 
429
- # Handle array of stereotypes
430
- if stereotype.is_a?(Array)
431
- stereotype.first
432
- else
433
- stereotype
434
- end
429
+ stereotype.is_a?(Array) ? stereotype.first : stereotype
435
430
  end
436
431
 
437
432
  # Extract attributes from element
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
3
4
  require_relative "base_query"
4
5
  require_relative "../../uml/qualified_name"
5
6
 
@@ -171,8 +172,73 @@ module Lutaml
171
172
  qname.nil? ? nil : qname
172
173
  end
173
174
 
175
+ # Build inheritance tree for a class.
176
+ #
177
+ # @param class_or_id [Lutaml::Uml::Class, String] The class object,
178
+ # qualified name, or xmi_id
179
+ # @return [Hash, nil] Tree structure with :class and :children keys
180
+ def inheritance_tree(class_or_id)
181
+ klass = resolve_by_id_or_qname(class_or_id)
182
+ return nil unless klass
183
+
184
+ qname = resolve_qname(klass)
185
+ return nil unless qname
186
+
187
+ child_qnames = indexes[:inheritance_graph][qname] || []
188
+ child_trees = child_qnames.filter_map do |child_qname|
189
+ inheritance_tree(child_qname)
190
+ end
191
+
192
+ {
193
+ class: klass,
194
+ children: child_trees,
195
+ }
196
+ end
197
+
198
+ # Check if a class has circular inheritance.
199
+ #
200
+ # @param class_or_id [Lutaml::Uml::Class, String] The class object,
201
+ # qualified name, or xmi_id
202
+ # @return [Boolean] true if circular inheritance detected
203
+ def has_circular_inheritance?(class_or_id, visited: Set.new)
204
+ qname = if class_or_id.is_a?(String) &&
205
+ indexes[:qualified_names].key?(class_or_id)
206
+ class_or_id
207
+ else
208
+ resolve_qname(class_or_id)
209
+ end
210
+ return false unless qname
211
+
212
+ return true if visited.include?(qname)
213
+
214
+ visited.add(qname)
215
+ child_qnames = indexes[:inheritance_graph][qname] || []
216
+ child_qnames.any? do |child_qname|
217
+ has_circular_inheritance?(child_qname, visited: visited.dup)
218
+ end
219
+ end
220
+
174
221
  private
175
222
 
223
+ # Resolve a class by xmi_id or qualified name
224
+ #
225
+ # @param class_or_id [String] Qualified name or xmi_id
226
+ # @return [Lutaml::Uml::Class, nil] The resolved class
227
+ def resolve_by_id_or_qname(class_or_id)
228
+ # Try as qualified name first
229
+ klass = indexes[:qualified_names][class_or_id]
230
+ return klass if klass
231
+
232
+ # Try as xmi_id - search in qualified_names
233
+ indexes[:qualified_names].each_value do |entity|
234
+ next unless entity.respond_to?(:xmi_id)
235
+
236
+ return entity if entity.xmi_id == class_or_id
237
+ end
238
+
239
+ nil
240
+ end
241
+
176
242
  # Get direct subtypes of a class
177
243
  #
178
244
  # @param qname_string [String] Qualified name of the parent class
@@ -158,7 +158,7 @@ module Lutaml
158
158
  .filter_map do |_stereotype, entities|
159
159
  entities.select do |entity|
160
160
  entity.respond_to?(:stereotype) &&
161
- entity.stereotype&.match?(pattern)
161
+ Array(entity.stereotype).any? { |s| s&.match?(pattern) }
162
162
  end.uniq
163
163
  end.uniq.flatten
164
164
 
@@ -345,6 +345,40 @@ module Lutaml
345
345
  @error_handler.class_not_found_error(qualified_name)
346
346
  end
347
347
 
348
+ # Find an attribute by its qualified name.
349
+ #
350
+ # The qualified name format is "PackagePath::ClassName::attributeName".
351
+ # Splits off the last segment as the attribute name, finds the containing
352
+ # class, then returns the matching attribute.
353
+ #
354
+ # @param qualified_name [String] Qualified name of the attribute
355
+ # @return [Lutaml::Uml::Attribute, nil] The attribute or nil
356
+ # @example
357
+ # attr = repo.find_attribute("ModelRoot::Core::Building::name")
358
+ def find_attribute(qualified_name)
359
+ class_qname, _, attr_name = qualified_name.rpartition("::")
360
+ return nil if class_qname.empty?
361
+
362
+ klass = class_query.find_by_qname(class_qname)
363
+ return nil unless klass
364
+
365
+ attrs = klass.attributes
366
+ return nil unless attrs
367
+
368
+ attrs.find { |a| a.name == attr_name }
369
+ end
370
+
371
+ # Get all attributes across all classes in the repository.
372
+ #
373
+ # @return [Array<Lutaml::Uml::Attribute>] All attribute objects
374
+ def all_attributes
375
+ indexes[:qualified_names].flat_map do |_qname, entity|
376
+ next [] unless entity.respond_to?(:attributes) && entity.attributes
377
+
378
+ entity.attributes
379
+ end
380
+ end
381
+
348
382
  # Find all classes with a specific stereotype.
349
383
  #
350
384
  # @param stereotype [String] The stereotype to search for
@@ -48,7 +48,7 @@ module Lutaml
48
48
 
49
49
  check_type_references
50
50
  check_generalization_references
51
- # DISABLED: check_circular_inheritance - has bugs with name resolution
51
+ check_circular_inheritance
52
52
  check_association_references
53
53
  check_multiplicities
54
54
 
@@ -76,13 +76,31 @@ module Lutaml
76
76
  # API: Package details (on-demand, optional optimization)
77
77
  get "/api/packages/:id" do
78
78
  content_type :json
79
- params[:id]
80
-
81
- # Find package by generated ID
82
- # This would require reverse lookup from ID to package
83
- # For now, use the full data endpoint
84
- halt 501, { error: "On-demand package loading not yet implemented. " \
85
- "Use /api/data" }.to_json
79
+ requested_id = params[:id]
80
+
81
+ id_gen = UmlRepository::StaticSite::IDGenerator.new
82
+
83
+ # Search for package by matching generated ID
84
+ found_package = nil
85
+ repository.indexes[:package_paths].each_value do |package|
86
+ next unless package.is_a?(Lutaml::Uml::Package)
87
+
88
+ if id_gen.package_id(package) == requested_id
89
+ found_package = package
90
+ break
91
+ end
92
+ end
93
+
94
+ unless found_package
95
+ halt 404, { error: "Package not found: #{requested_id}" }.to_json
96
+ end
97
+
98
+ # Build package response
99
+ {
100
+ id: requested_id,
101
+ name: found_package.name,
102
+ xmi_id: found_package.xmi_id,
103
+ }.to_json
86
104
  end
87
105
 
88
106
  # API: Class details (on-demand, optional optimization)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lutaml
4
- VERSION = "0.10.0"
4
+ VERSION = "0.10.2"
5
5
  end