lutaml 0.10.13 → 0.10.15

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +29 -33
  3. data/lib/lutaml/cli/interactive_shell/export_handler.rb +25 -17
  4. data/lib/lutaml/cli/interactive_shell/help_display.rb +39 -45
  5. data/lib/lutaml/cli/interactive_shell/navigation_commands.rb +45 -26
  6. data/lib/lutaml/cli/interactive_shell/query_commands.rb +73 -47
  7. data/lib/lutaml/cli/interactive_shell.rb +53 -27
  8. data/lib/lutaml/cli/tree_view_formatter.rb +11 -3
  9. data/lib/lutaml/converter/xmi_to_uml.rb +11 -6
  10. data/lib/lutaml/formatter/graphviz.rb +65 -35
  11. data/lib/lutaml/model_transformations/parsers/base_parser.rb +27 -29
  12. data/lib/lutaml/qea/factory/association_builder.rb +144 -127
  13. data/lib/lutaml/qea/factory/class_transformer.rb +91 -53
  14. data/lib/lutaml/qea/factory/ea_to_uml_factory.rb +11 -22
  15. data/lib/lutaml/qea/factory/enum_transformer.rb +41 -31
  16. data/lib/lutaml/qea/factory/generalization_builder.rb +155 -125
  17. data/lib/lutaml/qea/factory/stereotype_loader.rb +13 -7
  18. data/lib/lutaml/qea/lookup_indexes.rb +31 -13
  19. data/lib/lutaml/uml/inheritance_walker.rb +11 -7
  20. data/lib/lutaml/uml_repository/exporters/markdown/class_page_builder.rb +33 -25
  21. data/lib/lutaml/uml_repository/exporters/markdown/index_page_builder.rb +17 -9
  22. data/lib/lutaml/uml_repository/exporters/markdown_exporter.rb +27 -20
  23. data/lib/lutaml/uml_repository/index_builders/association_index.rb +60 -48
  24. data/lib/lutaml/uml_repository/index_builders/class_index.rb +35 -24
  25. data/lib/lutaml/uml_repository/queries/class_query.rb +79 -48
  26. data/lib/lutaml/uml_repository/queries/inheritance_query.rb +42 -32
  27. data/lib/lutaml/uml_repository/queries/search_query.rb +93 -85
  28. data/lib/lutaml/uml_repository/query_dsl/conditions/package_condition.rb +9 -2
  29. data/lib/lutaml/uml_repository/repository/loader.rb +14 -7
  30. data/lib/lutaml/uml_repository/static_site/data_transformer.rb +64 -35
  31. data/lib/lutaml/uml_repository/static_site/search_index_builder.rb +32 -19
  32. data/lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb +36 -20
  33. data/lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb +131 -105
  34. data/lib/lutaml/uml_repository/static_site/serializers/package_serializer.rb +15 -9
  35. data/lib/lutaml/uml_repository/static_site/serializers/package_tree_builder.rb +38 -24
  36. data/lib/lutaml/version.rb +1 -1
  37. data/lib/lutaml/xmi/liquid_drops/klass_drop.rb +34 -18
  38. data/lib/lutaml/xmi/parsers/xmi_connector.rb +35 -23
  39. metadata +2 -9
  40. data/TODO.cleanups/01-resolve-production-todos.md +0 -65
  41. data/TODO.cleanups/02-reduce-metrics-offenses.md +0 -37
  42. data/TODO.cleanups/03-reduce-rspec-multiple-expectations.md +0 -54
  43. data/TODO.cleanups/04-reduce-rspec-example-length.md +0 -45
  44. data/TODO.cleanups/07-fix-lint-offenses.md +0 -74
  45. data/TODO.cleanups/08-reduce-memoized-helpers-and-nesting.md +0 -43
  46. data/TODO.cleanups/09-reduce-verified-doubles-and-rspec-style.md +0 -57
@@ -66,7 +66,7 @@ module Lutaml
66
66
  # @example Recursive query
67
67
  # classes = query.in_package("ModelRoot::i-UR", recursive: true)
68
68
  # # Returns classes in i-UR and all nested packages
69
- def in_package(package_path_string, recursive: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
69
+ def in_package(package_path_string, recursive: false)
70
70
  return [] if package_path_string.nil? || package_path_string.empty?
71
71
 
72
72
  pkg_to_classes = indexes[:package_to_classes]
@@ -81,69 +81,100 @@ module Lutaml
81
81
  private
82
82
 
83
83
  # O(1) indexed lookup for in_package
84
- def in_package_indexed(package_path_string, pkg_to_classes, recursive:) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
84
+ def in_package_indexed(package_path_string, pkg_to_classes, recursive:)
85
85
  is_absolute = package_path_string.start_with?("::")
86
86
  search_segs = package_path_string.split("::").reject(&:empty?)
87
87
 
88
88
  results = []
89
89
  pkg_to_classes.each do |path, classes|
90
- path_segs = path.split("::")
91
- matched = if is_absolute
92
- if recursive
93
- path == package_path_string ||
94
- path.start_with?("#{package_path_string}::")
95
- else
96
- path == package_path_string
97
- end
98
- elsif recursive
99
- # Relative: match when path ends with search segments
100
- (0..(path_segs.size - search_segs.size)).any? do |i|
101
- path_segs[i, search_segs.size] == search_segs
102
- end
103
- else
104
- path_segs.size >= search_segs.size &&
105
- path_segs[-search_segs.size..] == search_segs
106
- end
107
-
108
- results.concat(classes) if matched
90
+ results.concat(classes) if indexed_path_matches?(
91
+ path, package_path_string, is_absolute, search_segs, recursive
92
+ )
109
93
  end
110
94
  results
111
95
  end
112
96
 
97
+ def indexed_path_matches?(path, package_path_string, is_absolute,
98
+ search_segs, recursive)
99
+ if is_absolute
100
+ indexed_absolute_match?(path, package_path_string, recursive)
101
+ else
102
+ indexed_relative_match?(path.split("::"), search_segs, recursive)
103
+ end
104
+ end
105
+
106
+ def indexed_absolute_match?(path, package_path_string, recursive)
107
+ if recursive
108
+ path == package_path_string ||
109
+ path.start_with?("#{package_path_string}::")
110
+ else
111
+ path == package_path_string
112
+ end
113
+ end
114
+
115
+ def indexed_relative_match?(path_segs, search_segs, recursive)
116
+ if recursive
117
+ segments_overlap?(path_segs, search_segs)
118
+ else
119
+ segments_end_with?(path_segs, search_segs)
120
+ end
121
+ end
122
+
113
123
  # Fallback: original O(n) scan
114
124
  def in_package_scan(package_path_string, recursive:)
115
125
  package_path = Lutaml::Uml::PackagePath.new(package_path_string)
116
- results = []
117
126
  is_absolute = package_path.absolute?
118
127
 
119
- indexes[:qualified_names].each do |qname_string, klass|
120
- qname = Lutaml::Uml::QualifiedName.new(qname_string)
121
-
122
- matched = if is_absolute
123
- if recursive
124
- qname.package_path.starts_with?(package_path)
125
- else
126
- qname.package_path == package_path
127
- end
128
- else
129
- class_pkg_segs = qname.package_path.segments
130
- search_segs = package_path.segments
131
-
132
- if recursive
133
- (0..(class_pkg_segs.size - search_segs.size))
134
- .any? do |i|
135
- class_pkg_segs[i, search_segs.size] == search_segs
136
- end
137
- else
138
- class_pkg_segs.size >= search_segs.size &&
139
- class_pkg_segs[-search_segs.size..] == search_segs
140
- end
141
- end
142
-
143
- results << klass if matched
128
+ indexes[:qualified_names].each_value.select do |klass|
129
+ scan_matches_package?(klass, package_path, is_absolute, recursive)
144
130
  end
131
+ end
145
132
 
146
- results
133
+ def scan_matches_package?(klass, package_path, is_absolute, recursive)
134
+ qname = resolve_qname_for(klass)
135
+ return false unless qname
136
+
137
+ if is_absolute
138
+ match_absolute_path?(qname, package_path, recursive)
139
+ else
140
+ match_relative_path?(qname, package_path, recursive)
141
+ end
142
+ end
143
+
144
+ def resolve_qname_for(klass)
145
+ indexes[:qualified_names].find { |_, v| v == klass }&.first
146
+ end
147
+
148
+ def match_absolute_path?(qname, package_path, recursive)
149
+ qname = Lutaml::Uml::QualifiedName.new(qname)
150
+ if recursive
151
+ qname.package_path.starts_with?(package_path)
152
+ else
153
+ qname.package_path == package_path
154
+ end
155
+ end
156
+
157
+ def match_relative_path?(qname_string, package_path, recursive)
158
+ qname = Lutaml::Uml::QualifiedName.new(qname_string)
159
+ class_pkg_segs = qname.package_path.segments
160
+ search_segs = package_path.segments
161
+
162
+ if recursive
163
+ segments_overlap?(class_pkg_segs, search_segs)
164
+ else
165
+ segments_end_with?(class_pkg_segs, search_segs)
166
+ end
167
+ end
168
+
169
+ def segments_overlap?(class_segs, search_segs)
170
+ (0..(class_segs.size - search_segs.size)).any? do |i|
171
+ class_segs[i, search_segs.size] == search_segs
172
+ end
173
+ end
174
+
175
+ def segments_end_with?(class_segs, search_segs)
176
+ class_segs.size >= search_segs.size &&
177
+ class_segs[-search_segs.size..] == search_segs
147
178
  end
148
179
  end
149
180
  end
@@ -33,27 +33,15 @@ module Lutaml
33
33
  # parent = query.supertype("ModelRoot::Child")
34
34
  # # Or
35
35
  # parent = query.supertype(child_class)
36
- def supertype(class_or_qname) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
36
+ def supertype(class_or_qname)
37
37
  klass = resolve_class(class_or_qname)
38
- return nil unless klass
39
- return nil unless klass.is_a?(Lutaml::Uml::Class)
40
- return nil unless klass.generalization
38
+ return nil unless valid_supertype_target?(klass)
41
39
 
42
40
  parent_name = extract_parent_name(klass.generalization)
43
41
  return nil unless parent_name
44
- # avoid self-references
45
42
  return nil if parent_name == klass.name
46
43
 
47
- # Try to find in qualified_names index
48
- qname_string = resolve_qname(class_or_qname)
49
- return nil unless qname_string
50
-
51
- qname = Lutaml::Uml::QualifiedName.new(qname_string)
52
- package_path = qname.package_path.to_s
53
-
54
- # Try to resolve parent qualified name
55
- parent_qname = resolve_parent_qualified_name(parent_name,
56
- package_path)
44
+ parent_qname = resolve_parent_qname(class_or_qname, parent_name)
57
45
  return nil unless parent_qname
58
46
 
59
47
  indexes[:qualified_names][parent_qname]
@@ -200,14 +188,8 @@ module Lutaml
200
188
  # qualified name, or xmi_id
201
189
  # @return [Boolean] true if circular inheritance detected
202
190
  def has_circular_inheritance?(class_or_id, visited: Set.new)
203
- qname = if class_or_id.is_a?(String) &&
204
- indexes[:qualified_names].key?(class_or_id)
205
- class_or_id
206
- else
207
- resolve_qname(class_or_id)
208
- end
191
+ qname = resolve_to_qname(class_or_id)
209
192
  return false unless qname
210
-
211
193
  return true if visited.include?(qname)
212
194
 
213
195
  visited.add(qname)
@@ -219,6 +201,21 @@ module Lutaml
219
201
 
220
202
  private
221
203
 
204
+ def valid_supertype_target?(klass)
205
+ return false unless klass
206
+ return false unless klass.is_a?(Lutaml::Uml::Class)
207
+
208
+ klass.generalization ? true : false
209
+ end
210
+
211
+ def resolve_parent_qname(class_or_qname, parent_name)
212
+ qname_string = resolve_qname(class_or_qname)
213
+ return nil unless qname_string
214
+
215
+ qname = Lutaml::Uml::QualifiedName.new(qname_string)
216
+ resolve_parent_qualified_name(parent_name, qname.package_path.to_s)
217
+ end
218
+
222
219
  # Resolve a class by xmi_id or qualified name
223
220
  #
224
221
  # @param class_or_id [String] Qualified name or xmi_id
@@ -257,22 +254,24 @@ module Lutaml
257
254
  # @param max_depth [Integer, nil] Maximum depth to traverse
258
255
  # @param current_depth [Integer] Current depth
259
256
  # @return [Array] Array of descendant class objects
260
- def collect_descendants(qname_string, max_depth, current_depth) # rubocop:disable Metrics/MethodLength
257
+ def collect_descendants(qname_string, max_depth, current_depth)
261
258
  return [] if max_depth && current_depth >= max_depth
262
259
 
263
260
  children = direct_subtypes(qname_string)
264
261
  result = children.dup
262
+ collect_child_descendants(children, max_depth, current_depth, result)
263
+ result
264
+ end
265
265
 
266
+ def collect_child_descendants(children, max_depth, current_depth,
267
+ result)
266
268
  children.each do |child|
267
269
  child_qname = resolve_qname(child)
268
270
  next unless child_qname
269
271
 
270
- grandchildren = collect_descendants(child_qname, max_depth,
271
- current_depth + 1)
272
- result.concat(grandchildren)
272
+ result.concat(collect_descendants(child_qname, max_depth,
273
+ current_depth + 1))
273
274
  end
274
-
275
- result
276
275
  end
277
276
 
278
277
  # Extract parent name from generalization object
@@ -285,13 +284,24 @@ module Lutaml
285
284
  return nil unless generalization.is_a?(Lutaml::Uml::Generalization)
286
285
 
287
286
  parent = generalization.general
288
- if parent
289
- return parent.name if parent.is_a?(Lutaml::Uml::Generalization) && parent.name
287
+ return extract_name_from_parent(parent) if parent
288
+
289
+ generalization.name if generalization.name
290
+ end
290
291
 
291
- return parent.to_s
292
+ def resolve_to_qname(class_or_id)
293
+ if class_or_id.is_a?(String) &&
294
+ indexes[:qualified_names].key?(class_or_id)
295
+ class_or_id
296
+ else
297
+ resolve_qname(class_or_id)
292
298
  end
299
+ end
293
300
 
294
- generalization.name if generalization.name
301
+ def extract_name_from_parent(parent)
302
+ return parent.name if parent.is_a?(Lutaml::Uml::Generalization) && parent.name
303
+
304
+ parent.to_s
295
305
  end
296
306
 
297
307
  # Resolve a class name to its qualified name
@@ -113,7 +113,7 @@ module Lutaml
113
113
  # @param query [String] Query string
114
114
  # @param fields [Array<Symbol>] Fields to search in
115
115
  # @return [Array<SearchResult>] Matching search result objects
116
- def search_classes( # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
116
+ def search_classes( # rubocop:disable Metrics/MethodLength
117
117
  query, fields: %i[name documentation],
118
118
  case_sensitive: false
119
119
  )
@@ -122,55 +122,64 @@ module Lutaml
122
122
  )
123
123
 
124
124
  indexes[:qualified_names].filter_map do |qname, entity|
125
- match_field = nil
126
- qualified_name = nil
127
-
128
125
  next unless entity.is_a?(Lutaml::Uml::Class)
129
126
 
130
- # Check fields for match
131
- fields.each do |field|
132
- if entity.class.attributes.key?(field) &&
133
- entity.public_send(field)&.match?(pattern)
127
+ match_field = find_matching_field(entity, fields, pattern)
128
+ next unless match_field
134
129
 
135
- match_field = field
136
- qualified_name = qname
137
- end
138
- end
130
+ build_search_result(entity, :class, qname, match_field)
131
+ end.uniq
132
+ end
139
133
 
140
- if match_field
141
- SearchResult.new(
142
- element: entity,
143
- element_type: :class,
144
- qualified_name: qualified_name,
145
- package_path: extract_package_path(qualified_name),
146
- match_field: match_field,
147
- )
134
+ def find_matching_field(entity, fields, pattern)
135
+ last_match = nil
136
+ fields.each do |field|
137
+ if entity.class.attributes.key?(field) &&
138
+ entity.public_send(field)&.match?(pattern)
139
+ last_match = field
148
140
  end
149
- end.uniq
141
+ end
142
+ last_match
150
143
  end
151
144
 
152
- def search_by_stereotype(query, case_sensitive: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
145
+ def build_search_result(entity, type, qname, match_field, context = {})
146
+ SearchResult.new(
147
+ element: entity,
148
+ element_type: type,
149
+ qualified_name: qname,
150
+ package_path: extract_package_path(qname),
151
+ match_field: match_field,
152
+ match_context: context,
153
+ )
154
+ end
155
+
156
+ def search_by_stereotype(query, case_sensitive: false)
153
157
  pattern = regex_pattern_from_query(
154
158
  query, case_sensitive: case_sensitive
155
159
  )
156
160
 
157
- matched_entities = indexes[:stereotypes]
161
+ matched_entities = find_entities_by_stereotype_pattern(pattern)
162
+ matched_entities.map { |entity| build_stereotype_result(entity) }
163
+ end
164
+
165
+ def find_entities_by_stereotype_pattern(pattern)
166
+ indexes[:stereotypes]
158
167
  .filter_map do |_stereotype, entities|
159
168
  entities.select do |entity|
160
169
  entity.is_a?(Lutaml::Uml::Classifier) &&
161
170
  Array(entity.stereotype).any? { |s| s&.match?(pattern) }
162
171
  end.uniq
163
172
  end.uniq.flatten
173
+ end
164
174
 
165
- matched_entities.map do |entity|
166
- SearchResult.new(
167
- element: entity,
168
- element_type: entity.class.name.split("::").last.downcase,
169
- qualified_name: "",
170
- package_path: "",
171
- match_field: :stereotype,
172
- )
173
- end
175
+ def build_stereotype_result(entity)
176
+ SearchResult.new(
177
+ element: entity,
178
+ element_type: entity.class.name.split("::").last.downcase,
179
+ qualified_name: "",
180
+ package_path: "",
181
+ match_field: :stereotype,
182
+ )
174
183
  end
175
184
 
176
185
  # Search for packages matching the query
@@ -212,61 +221,63 @@ module Lutaml
212
221
  # @param query [String] Query string
213
222
  # @param fields [Array<Symbol>] Fields to search in
214
223
  # @return [Array<Lutaml::Uml::Class>] Matching search result objects
215
- def search_attributes(query, fields: [:name], case_sensitive: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
224
+ def search_attributes(query, fields: [:name], case_sensitive: false) # rubocop:disable Metrics/MethodLength
216
225
  pattern = regex_pattern_from_query(
217
226
  query, case_sensitive: case_sensitive
218
227
  )
219
228
 
220
- indexes[:qualified_names].filter_map do |class_qname, entity| # rubocop:disable Metrics/BlockLength
229
+ indexes[:qualified_names].filter_map do |class_qname, entity|
221
230
  next unless entity.is_a?(Lutaml::Uml::Classifier) && entity.attributes
222
231
 
223
- match_field = nil
224
- match_attr = nil
225
- qualified_name = nil
232
+ match_attr, match_field = find_matching_attribute(entity, fields,
233
+ pattern)
234
+ next unless match_field
226
235
 
227
- entity.attributes.each do |attr|
228
- # Check attribute for match
229
- fields.each do |field|
230
- if attr.class.attributes.key?(field) &&
231
- attr.public_send(field)&.match?(pattern)
236
+ build_attribute_result(match_attr, entity, class_qname, match_field)
237
+ end.uniq
238
+ end
232
239
 
233
- match_attr = attr
234
- match_field = field
235
- qualified_name = class_qname
236
- end
240
+ def find_matching_attribute(entity, fields, pattern)
241
+ match_attr = nil
242
+ match_field = nil
243
+ entity.attributes.each do |attr|
244
+ fields.each do |field|
245
+ if attr.class.attributes.key?(field) &&
246
+ attr.public_send(field)&.match?(pattern)
247
+ match_attr = attr
248
+ match_field = field
237
249
  end
238
250
  end
251
+ end
252
+ match_field ? [match_attr, match_field] : nil
253
+ end
239
254
 
240
- if match_field
241
- SearchResult.new(
242
- element: match_attr,
243
- element_type: :attribute,
244
- qualified_name: "#{qualified_name}::#{match_attr.name}",
245
- package_path: extract_package_path(qualified_name),
246
- match_field: match_field,
247
- match_context: {
248
- "class_name" => entity&.name,
249
- "class_qname" => qualified_name,
250
- },
251
- )
252
- end
253
- end.uniq
255
+ def build_attribute_result(attr, entity, class_qname, match_field)
256
+ SearchResult.new(
257
+ element: attr,
258
+ element_type: :attribute,
259
+ qualified_name: "#{class_qname}::#{attr.name}",
260
+ package_path: extract_package_path(class_qname),
261
+ match_field: match_field,
262
+ match_context: {
263
+ "class_name" => entity.name,
264
+ "class_qname" => class_qname,
265
+ },
266
+ )
254
267
  end
255
268
 
256
269
  # Get all associations in the model
257
270
  #
258
271
  # @return [Array<Lutaml::Uml::Association>] All association objects
259
- def get_all_associations # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
272
+ def get_all_associations
260
273
  all_associations = []
261
274
 
262
- # Get all associations defined at document level and
263
275
  if document.is_a?(Lutaml::Uml::Document) && document.associations
264
276
  all_associations << document.associations
265
277
  end
266
278
 
267
- # Get all associations defined within classes
268
279
  indexes[:qualified_names].each_value do |entity|
269
- next unless (entity.is_a?(Lutaml::Uml::Class) || entity.is_a?(Lutaml::Uml::DataType)) && entity.associations
280
+ next unless classifiable_with_associations?(entity)
270
281
 
271
282
  all_associations << entity.associations
272
283
  end
@@ -274,12 +285,16 @@ module Lutaml
274
285
  all_associations.flatten.uniq
275
286
  end
276
287
 
288
+ def classifiable_with_associations?(entity)
289
+ (entity.is_a?(Lutaml::Uml::Class) || entity.is_a?(Lutaml::Uml::DataType)) && entity.associations
290
+ end
291
+
277
292
  # Search for associations matching the query
278
293
  #
279
294
  # @param query [String] Query string
280
295
  # @param fields [Array<Symbol>] Fields to search in
281
296
  # @return [Array<SearchResult>] Matching search result objects
282
- def search_associations(query, # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
297
+ def search_associations(query, # rubocop:disable Metrics/MethodLength
283
298
  fields: %i[
284
299
  name owner_end member_end owner_end_attribute_name
285
300
  member_end_attribute_name documentation
@@ -291,27 +306,20 @@ module Lutaml
291
306
  )
292
307
 
293
308
  all_associations.filter_map do |assoc|
294
- match_field = nil
309
+ match_field = find_matching_field(assoc, fields, pattern)
310
+ next unless match_field
295
311
 
296
- fields.each do |field|
297
- if assoc.class.attributes.key?(field) && assoc.public_send(field)&.match?(pattern)
298
- match_field = field
299
- end
300
- end
301
-
302
- if match_field
303
- SearchResult.new(
304
- element: assoc,
305
- element_type: :association,
306
- qualified_name: assoc.name || "(unnamed)",
307
- package_path: "",
308
- match_field: match_field,
309
- match_context: {
310
- "source" => assoc.owner_end,
311
- "target" => assoc.member_end,
312
- },
313
- )
314
- end
312
+ SearchResult.new(
313
+ element: assoc,
314
+ element_type: :association,
315
+ qualified_name: assoc.name || "(unnamed)",
316
+ package_path: "",
317
+ match_field: match_field,
318
+ match_context: {
319
+ "source" => assoc.owner_end,
320
+ "target" => assoc.member_end,
321
+ },
322
+ )
315
323
  end.uniq
316
324
  end
317
325
 
@@ -68,10 +68,17 @@ module Lutaml
68
68
  # @param obj [Object] The object to extract path from
69
69
  # @return [PackagePath, nil] The object's package path
70
70
  def extract_package_path(obj)
71
- return nil unless obj.is_a?(Lutaml::Model::Serializable) &&
71
+ return nil unless serializable_with_path?(obj)
72
+
73
+ coerce_package_path(obj.package_path)
74
+ end
75
+
76
+ def serializable_with_path?(obj)
77
+ obj.is_a?(Lutaml::Model::Serializable) &&
72
78
  obj.class.attributes&.key?(:package_path)
79
+ end
73
80
 
74
- path = obj.package_path
81
+ def coerce_package_path(path)
75
82
  return nil unless path
76
83
 
77
84
  path.is_a?(PackagePath) ? path : PackagePath.new(path)
@@ -64,19 +64,26 @@ module Lutaml
64
64
  def self.from_file_cached(xmi_path, lur_path: nil)
65
65
  lur_path ||= xmi_path.sub(/\.xmi$/i, ".lur")
66
66
 
67
- if File.exist?(lur_path) && File.mtime(lur_path) >= File.mtime(xmi_path)
67
+ if cache_valid?(lur_path, xmi_path)
68
68
  puts "Using cached LUR package: #{lur_path}" if $VERBOSE
69
69
  from_package(lur_path)
70
70
  else
71
- puts "Building repository from XMI..." if $VERBOSE
72
- repo = from_xmi(xmi_path)
73
-
74
- puts "Caching as LUR package: #{lur_path}" if $VERBOSE
75
- repo.export_to_package(lur_path)
76
- repo
71
+ build_and_cache(xmi_path, lur_path)
77
72
  end
78
73
  end
79
74
 
75
+ def self.cache_valid?(lur_path, xmi_path)
76
+ File.exist?(lur_path) && File.mtime(lur_path) >= File.mtime(xmi_path)
77
+ end
78
+
79
+ def self.build_and_cache(xmi_path, lur_path)
80
+ puts "Building repository from XMI..." if $VERBOSE
81
+ repo = from_xmi(xmi_path)
82
+ puts "Caching as LUR package: #{lur_path}" if $VERBOSE
83
+ repo.export_to_package(lur_path)
84
+ repo
85
+ end
86
+
80
87
  # Load a Repository from a LUR package file.
81
88
  #
82
89
  # @param lur_path [String] Path to the .lur package file