lutaml 0.10.2 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +40 -35
  4. data/CHANGELOG.md +108 -0
  5. data/lib/lutaml/cli/uml_commands.rb +2 -2
  6. data/lib/lutaml/command_line.rb +1 -1
  7. data/lib/lutaml/converter/xmi_to_uml.rb +12 -3
  8. data/lib/lutaml/model_transformations/parsers/base_parser.rb +1 -1
  9. data/lib/lutaml/model_transformations/transformation_engine.rb +1 -1
  10. data/lib/lutaml/qea/database.rb +157 -4
  11. data/lib/lutaml/qea/factory/association_transformer.rb +1 -5
  12. data/lib/lutaml/qea/factory/attribute_transformer.rb +4 -10
  13. data/lib/lutaml/qea/factory/base_transformer.rb +1 -5
  14. data/lib/lutaml/qea/factory/class_transformer.rb +26 -62
  15. data/lib/lutaml/qea/factory/data_type_transformer.rb +7 -21
  16. data/lib/lutaml/qea/factory/diagram_transformer.rb +6 -20
  17. data/lib/lutaml/qea/factory/document_builder.rb +1 -1
  18. data/lib/lutaml/qea/factory/enum_transformer.rb +3 -5
  19. data/lib/lutaml/qea/factory/generalization_transformer.rb +2 -7
  20. data/lib/lutaml/qea/factory/instance_transformer.rb +2 -10
  21. data/lib/lutaml/qea/factory/operation_transformer.rb +2 -8
  22. data/lib/lutaml/qea/factory/package_transformer.rb +4 -13
  23. data/lib/lutaml/qea/repositories/base_repository.rb +6 -6
  24. data/lib/lutaml/qea/validation/base_validator.rb +2 -3
  25. data/lib/lutaml/qea/validation/validation_message.rb +2 -2
  26. data/lib/lutaml/qea/validation/validation_result.rb +2 -2
  27. data/lib/lutaml/sysml.rb +1 -1
  28. data/lib/lutaml/uml/has_members.rb +1 -1
  29. data/lib/lutaml/uml/parsers/dsl.rb +1 -1
  30. data/lib/lutaml/uml.rb +1 -1
  31. data/lib/lutaml/uml_repository/index_builder.rb +19 -17
  32. data/lib/lutaml/uml_repository/queries/class_query.rb +44 -10
  33. data/lib/lutaml/uml_repository/queries/inheritance_query.rb +0 -1
  34. data/lib/lutaml/uml_repository/repository.rb +14 -10
  35. data/lib/lutaml/uml_repository/statistics_calculator.rb +28 -16
  36. data/lib/lutaml/version.rb +1 -1
  37. data/lib/lutaml/xmi/parsers/xmi_base.rb +28 -5
  38. data/lib/lutaml.rb +3 -1
  39. data/lutaml.gemspec +2 -2
  40. metadata +5 -4
@@ -69,38 +69,72 @@ module Lutaml
69
69
  def in_package(package_path_string, recursive: false) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
70
70
  return [] if package_path_string.nil? || package_path_string.empty?
71
71
 
72
- package_path = Lutaml::Uml::PackagePath.new(package_path_string)
72
+ pkg_to_classes = indexes[:package_to_classes]
73
+ if pkg_to_classes
74
+ in_package_indexed(package_path_string, pkg_to_classes,
75
+ recursive: recursive)
76
+ else
77
+ in_package_scan(package_path_string, recursive: recursive)
78
+ end
79
+ end
80
+
81
+ private
82
+
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
85
+ is_absolute = package_path_string.start_with?("::")
86
+ search_segs = package_path_string.split("::").reject(&:empty?)
87
+
73
88
  results = []
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
74
107
 
75
- # Check if the path is absolute (starts with ModelRoot)
108
+ results.concat(classes) if matched
109
+ end
110
+ results
111
+ end
112
+
113
+ # Fallback: original O(n) scan
114
+ def in_package_scan(package_path_string, recursive:)
115
+ package_path = Lutaml::Uml::PackagePath.new(package_path_string)
116
+ results = []
76
117
  is_absolute = package_path.absolute?
77
118
 
78
- indexes[:qualified_names].each do |qname_string, klass| # rubocop:disable Metrics/BlockLength
119
+ indexes[:qualified_names].each do |qname_string, klass|
79
120
  qname = Lutaml::Uml::QualifiedName.new(qname_string)
80
121
 
81
122
  matched = if is_absolute
82
- # Absolute path - exact match
83
123
  if recursive
84
124
  qname.package_path.starts_with?(package_path)
85
125
  else
86
126
  qname.package_path == package_path
87
127
  end
88
128
  else
89
- # Relative path - match if the class's package path ends with
90
- # the given path
91
129
  class_pkg_segs = qname.package_path.segments
92
130
  search_segs = package_path.segments
93
131
 
94
132
  if recursive
95
- # For recursive, check if any suffix of the class path
96
- # starts with search_segs
97
133
  (0..(class_pkg_segs.size - search_segs.size))
98
134
  .any? do |i|
99
135
  class_pkg_segs[i, search_segs.size] == search_segs
100
136
  end
101
137
  else
102
- # For non-recursive, check if class path ends with
103
- # search_segs
104
138
  class_pkg_segs.size >= search_segs.size &&
105
139
  class_pkg_segs[-search_segs.size..] == search_segs
106
140
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require_relative "base_query"
5
4
  require_relative "../../uml/qualified_name"
6
5
 
@@ -625,8 +625,8 @@ module Lutaml
625
625
  # results = repo.query! do |q|
626
626
  # q.classes.where(stereotype: 'featureType')
627
627
  # end
628
- def query!(&block)
629
- query(&block).execute
628
+ def query!(&)
629
+ query(&).execute
630
630
  end
631
631
 
632
632
  # Convenience methods for SPA data transformer
@@ -646,25 +646,29 @@ module Lutaml
646
646
  # Get all associations as an array
647
647
  # Collects from both document-level (XMI) and class-level (QEA/EA)
648
648
  # @return [Array<Lutaml::Uml::Association>] All associations
649
- def associations_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
649
+ def associations_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics:MethodLength,Metrics:PerceivedComplexity
650
650
  # Use cached index if available (built by IndexBuilder)
651
651
  return @indexes[:associations].values if @indexes[:associations]
652
652
 
653
653
  # Fallback for edge cases: collect from document and classes
654
+ seen = Set.new
654
655
  associations = []
655
656
 
656
- # Document-level associations (XMI format)
657
- associations.concat(@document.associations) if @document.associations
657
+ (@document.associations || []).each do |assoc|
658
+ if assoc.xmi_id && !seen.include?(assoc.xmi_id)
659
+ seen << assoc.xmi_id
660
+ associations << assoc
661
+ end
662
+ end
658
663
 
659
- # Class-level associations (QEA/EA format)
660
664
  classes_index.each do |klass|
661
665
  next unless klass.respond_to?(:associations) && klass.associations
662
666
 
663
667
  klass.associations.each do |assoc|
664
- # Avoid duplicates - check xmi_id
665
- next if associations.any? { |a| a.xmi_id == assoc.xmi_id }
666
-
667
- associations << assoc
668
+ if assoc.xmi_id && !seen.include?(assoc.xmi_id)
669
+ seen << assoc.xmi_id
670
+ associations << assoc
671
+ end
668
672
  end
669
673
  end
670
674
 
@@ -262,12 +262,14 @@ module Lutaml
262
262
  def max_inheritance_depth
263
263
  return 0 if @indexes[:inheritance_graph].empty?
264
264
 
265
- max = 0
265
+ @inheritance_depth_cache ||= {}
266
+ max_depth = 0
267
+
266
268
  @indexes[:qualified_names].each_key do |qname|
267
- depth = calculate_inheritance_depth(qname)
268
- max = depth if depth > max
269
+ depth = memoized_inheritance_depth(qname)
270
+ max_depth = depth if depth > max_depth
269
271
  end
270
- max
272
+ max_depth
271
273
  end
272
274
 
273
275
  # Calculate inheritance depth for a class
@@ -275,23 +277,33 @@ module Lutaml
275
277
  # @param qname [String] Qualified name of the class
276
278
  # @param visited [Set] Set of visited classes (to detect cycles)
277
279
  # @return [Integer] Depth of inheritance chain
278
- def calculate_inheritance_depth(qname, visited = Set.new) # rubocop:disable Metrics/MethodLength
279
- return 0 if visited.include?(qname)
280
-
281
- visited.add(qname)
280
+ def calculate_inheritance_depth(qname, visited = Set.new)
281
+ memoized_inheritance_depth(qname, visited)
282
+ end
282
283
 
283
- # Find parent
284
- parent_qname = nil
285
- @indexes[:inheritance_graph].each do |parent, children|
286
- if children.include?(qname)
287
- parent_qname = parent
288
- break
284
+ # Build reverse index: child_qname => parent_qname
285
+ def child_to_parent_index
286
+ @child_to_parent_index ||= begin
287
+ idx = {}
288
+ @indexes[:inheritance_graph].each do |parent, children|
289
+ children.each { |child| idx[child] = parent }
289
290
  end
291
+ idx
290
292
  end
293
+ end
294
+
295
+ # Memoized inheritance depth calculation using reverse index
296
+ def memoized_inheritance_depth(qname, visited = Set.new)
297
+ return 0 if visited.include?(qname)
298
+ return @inheritance_depth_cache[qname] if @inheritance_depth_cache.key?(qname)
291
299
 
292
- return 0 unless parent_qname
300
+ parent = child_to_parent_index[qname]
301
+ return 0 unless parent
293
302
 
294
- 1 + calculate_inheritance_depth(parent_qname, visited)
303
+ visited.add(qname)
304
+ depth = 1 + memoized_inheritance_depth(parent, visited)
305
+ @inheritance_depth_cache[qname] = depth
306
+ depth
295
307
  end
296
308
 
297
309
  # Get count of abstract classes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lutaml
4
- VERSION = "0.10.2"
4
+ VERSION = "0.10.4"
5
5
  end
@@ -485,9 +485,7 @@ module Lutaml
485
485
  # @return [Array<Hash>]
486
486
  # @note xpath %(//diagrams/diagram/model[@package="#{node['xmi:id']}"])
487
487
  def serialize_model_diagrams(node_id, with_package: false) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
488
- diagrams = @xmi_root_model.extension.diagrams.diagram.select do |d|
489
- d.model.package == node_id
490
- end
488
+ diagrams = diagram_lookup[node_id]
491
489
 
492
490
  diagrams.map do |diagram|
493
491
  h = {
@@ -506,6 +504,17 @@ module Lutaml
506
504
  end
507
505
  end
508
506
 
507
+ # Lazy-built hash index for O(1) diagram lookups by package
508
+ # @return [Hash] Mapping of package_id => [diagrams]
509
+ def diagram_lookup
510
+ @diagram_lookup ||= begin
511
+ idx = Hash.new { |h, k| h[k] = [] }
512
+ diagrams = @xmi_root_model.extension&.diagrams&.diagram || []
513
+ diagrams.each { |d| idx[d.model.package] << d if d.model&.package }
514
+ idx
515
+ end
516
+ end
517
+
509
518
  # @param xmi_id [String]
510
519
  # @return [Array<Hash>]
511
520
  # @note xpath %(//element[@xmi:idref="#{xmi_id}"]/links/*)
@@ -932,8 +941,22 @@ module Lutaml
932
941
  # @param source_or_target [String]
933
942
  # @return [String]
934
943
  def connector_node_by_id(xmi_id, source_or_target)
935
- @xmi_root_model.extension.connectors.connector.find do |con|
936
- con.send(source_or_target.to_sym).idref == xmi_id
944
+ connector_lookup[[source_or_target.to_sym, xmi_id]]
945
+ end
946
+
947
+ # Lazy-built hash index for O(1) connector lookups
948
+ # @return [Hash] Mapping of [direction, idref] => connector
949
+ def connector_lookup
950
+ @connector_lookup ||= begin
951
+ lookup = {}
952
+ connectors = @xmi_root_model.extension&.connectors&.connector || []
953
+ connectors.each do |con|
954
+ %i[source target].each do |dir|
955
+ idref = con.send(dir)&.idref
956
+ lookup[[dir, idref]] = con if idref
957
+ end
958
+ end
959
+ lookup
937
960
  end
938
961
  end
939
962
 
data/lib/lutaml.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "lutaml/version"
4
- require_relative "lutaml/parser"
5
4
 
6
5
  module Lutaml
6
+ class Error < StandardError; end
7
7
  end
8
8
 
9
+ require_relative "lutaml/parser"
10
+
9
11
  require_relative "lutaml/express"
10
12
  require_relative "lutaml/formatter"
11
13
  require_relative "lutaml/layout"
data/lutaml.gemspec CHANGED
@@ -4,7 +4,7 @@ Gem::Specification.new do |spec|
4
4
  spec.name = "lutaml"
5
5
  spec.version = Lutaml::VERSION
6
6
  spec.authors = ["Ribose Inc."]
7
- spec.email = ["open.source@ribose.com'"]
7
+ spec.email = ["open.source@ribose.com"]
8
8
 
9
9
  spec.summary = "LutaML: data models in textual form"
10
10
  spec.description = "LutaML: data models in textual form"
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.required_ruby_version = ">= 2.7.0" # rubocop:disable Gemspec/RequiredRubyVersion
30
+ spec.required_ruby_version = ">= 3.2.0"
31
31
 
32
32
  spec.add_dependency "expressir", "~> 2.3"
33
33
  # TODO: remove once reline declares fiddle as a dependency
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.2
4
+ version: 0.10.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-23 00:00:00.000000000 Z
11
+ date: 2026-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: expressir
@@ -312,7 +312,7 @@ dependencies:
312
312
  version: 0.5.2
313
313
  description: 'LutaML: data models in textual form'
314
314
  email:
315
- - open.source@ribose.com'
315
+ - open.source@ribose.com
316
316
  executables:
317
317
  - lutaml
318
318
  - lutaml-sysml
@@ -330,6 +330,7 @@ files:
330
330
  - ".rspec"
331
331
  - ".rubocop.yml"
332
332
  - ".rubocop_todo.yml"
333
+ - CHANGELOG.md
333
334
  - Gemfile
334
335
  - Makefile
335
336
  - README.adoc
@@ -960,7 +961,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
960
961
  requirements:
961
962
  - - ">="
962
963
  - !ruby/object:Gem::Version
963
- version: 2.7.0
964
+ version: 3.2.0
964
965
  required_rubygems_version: !ruby/object:Gem::Requirement
965
966
  requirements:
966
967
  - - ">="