lutaml 0.10.4 → 0.10.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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +10 -0
  4. data/.rubocop_todo.yml +218 -94
  5. data/TODO.cleanups/01-resolve-production-todos.md +65 -0
  6. data/TODO.cleanups/02-reduce-metrics-offenses.md +37 -0
  7. data/TODO.cleanups/03-reduce-rspec-multiple-expectations.md +54 -0
  8. data/TODO.cleanups/04-reduce-rspec-example-length.md +45 -0
  9. data/TODO.cleanups/05-replace-marshal-load.md +37 -0
  10. data/TODO.cleanups/06-replace-eval-in-tests.md +41 -0
  11. data/TODO.cleanups/07-fix-lint-offenses.md +74 -0
  12. data/TODO.cleanups/08-reduce-memoized-helpers-and-nesting.md +43 -0
  13. data/TODO.cleanups/09-reduce-verified-doubles-and-rspec-style.md +57 -0
  14. data/TODO.cleanups/10-split-large-files.md +47 -0
  15. data/bin/console +0 -1
  16. data/exe/lutaml +1 -0
  17. data/lib/lutaml/cli/element_identifier.rb +3 -6
  18. data/lib/lutaml/cli/interactive_shell/bookmark_commands.rb +88 -0
  19. data/lib/lutaml/cli/interactive_shell/command_base.rb +32 -0
  20. data/lib/lutaml/cli/interactive_shell/export_handler.rb +67 -0
  21. data/lib/lutaml/cli/interactive_shell/help_display.rb +114 -0
  22. data/lib/lutaml/cli/interactive_shell/navigation_commands.rb +135 -0
  23. data/lib/lutaml/cli/interactive_shell/query_commands.rb +185 -0
  24. data/lib/lutaml/cli/interactive_shell.rb +116 -802
  25. data/lib/lutaml/cli/uml/build_command.rb +5 -5
  26. data/lib/lutaml/cli/uml/verify_command.rb +0 -1
  27. data/lib/lutaml/converter/xmi_to_uml.rb +3 -153
  28. data/lib/lutaml/converter/xmi_to_uml_generalization.rb +193 -0
  29. data/lib/lutaml/formatter/graphviz.rb +1 -2
  30. data/lib/lutaml/qea/database.rb +1 -47
  31. data/lib/lutaml/qea/factory/association_builder.rb +188 -0
  32. data/lib/lutaml/qea/factory/base_transformer.rb +0 -1
  33. data/lib/lutaml/qea/factory/class_transformer.rb +40 -590
  34. data/lib/lutaml/qea/factory/diagram_transformer.rb +0 -3
  35. data/lib/lutaml/qea/factory/generalization_builder.rb +211 -0
  36. data/lib/lutaml/qea/factory/package_transformer.rb +1 -2
  37. data/lib/lutaml/qea/factory/stereotype_loader.rb +34 -0
  38. data/lib/lutaml/qea/lookup_indexes.rb +54 -0
  39. data/lib/lutaml/qea/models/ea_datatype.rb +0 -2
  40. data/lib/lutaml/qea/validation/validation_engine.rb +0 -2
  41. data/lib/lutaml/uml/has_members.rb +0 -1
  42. data/lib/lutaml/uml/inheritance_walker.rb +92 -0
  43. data/lib/lutaml/uml/model_helpers.rb +129 -0
  44. data/lib/lutaml/uml/node/attribute.rb +3 -1
  45. data/lib/lutaml/uml/node/class_node.rb +3 -3
  46. data/lib/lutaml/uml/operation.rb +2 -0
  47. data/lib/lutaml/uml_repository/class_lookup_index.rb +40 -0
  48. data/lib/lutaml/uml_repository/exporters/markdown/class_page_builder.rb +179 -0
  49. data/lib/lutaml/uml_repository/exporters/markdown/formatting.rb +36 -0
  50. data/lib/lutaml/uml_repository/exporters/markdown/index_page_builder.rb +73 -0
  51. data/lib/lutaml/uml_repository/exporters/markdown/link_resolver.rb +40 -0
  52. data/lib/lutaml/uml_repository/exporters/markdown/package_page_builder.rb +107 -0
  53. data/lib/lutaml/uml_repository/exporters/markdown_exporter.rb +26 -538
  54. data/lib/lutaml/uml_repository/index_builder.rb +3 -271
  55. data/lib/lutaml/uml_repository/index_builders/association_index.rb +141 -0
  56. data/lib/lutaml/uml_repository/index_builders/class_index.rb +94 -0
  57. data/lib/lutaml/uml_repository/index_builders/package_index.rb +57 -0
  58. data/lib/lutaml/uml_repository/package_exporter.rb +10 -20
  59. data/lib/lutaml/uml_repository/package_loader.rb +37 -17
  60. data/lib/lutaml/uml_repository/repository/deprecated.rb +39 -0
  61. data/lib/lutaml/uml_repository/repository/loader.rb +112 -0
  62. data/lib/lutaml/uml_repository/repository.rb +7 -57
  63. data/lib/lutaml/uml_repository/static_site/association_serialization.rb +142 -0
  64. data/lib/lutaml/uml_repository/static_site/configuration.rb +0 -2
  65. data/lib/lutaml/uml_repository/static_site/data_transformer.rb +52 -873
  66. data/lib/lutaml/uml_repository/static_site/generator.rb +29 -8
  67. data/lib/lutaml/uml_repository/static_site/search_index_builder.rb +1 -4
  68. data/lib/lutaml/uml_repository/static_site/serializers/attribute_serializer.rb +78 -0
  69. data/lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb +124 -0
  70. data/lib/lutaml/uml_repository/static_site/serializers/diagram_serializer.rb +60 -0
  71. data/lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb +258 -0
  72. data/lib/lutaml/uml_repository/static_site/serializers/metadata_builder.rb +48 -0
  73. data/lib/lutaml/uml_repository/static_site/serializers/operation_serializer.rb +57 -0
  74. data/lib/lutaml/uml_repository/static_site/serializers/package_serializer.rb +94 -0
  75. data/lib/lutaml/uml_repository/static_site/serializers/package_tree_builder.rb +93 -0
  76. data/lib/lutaml/version.rb +1 -1
  77. data/lib/lutaml/xmi/liquid_drops/association_drop.rb +13 -35
  78. data/lib/lutaml/xmi/liquid_drops/attribute_drop.rb +12 -18
  79. data/lib/lutaml/xmi/liquid_drops/cardinality_drop.rb +14 -6
  80. data/lib/lutaml/xmi/liquid_drops/connector_drop.rb +0 -3
  81. data/lib/lutaml/xmi/liquid_drops/constraint_drop.rb +1 -3
  82. data/lib/lutaml/xmi/liquid_drops/data_type_drop.rb +13 -70
  83. data/lib/lutaml/xmi/liquid_drops/dependency_drop.rb +2 -5
  84. data/lib/lutaml/xmi/liquid_drops/diagram_drop.rb +5 -11
  85. data/lib/lutaml/xmi/liquid_drops/enum_drop.rb +8 -16
  86. data/lib/lutaml/xmi/liquid_drops/enum_owned_literal_drop.rb +3 -9
  87. data/lib/lutaml/xmi/liquid_drops/generalization_attribute_drop.rb +11 -13
  88. data/lib/lutaml/xmi/liquid_drops/generalization_drop.rb +27 -85
  89. data/lib/lutaml/xmi/liquid_drops/klass_drop.rb +39 -91
  90. data/lib/lutaml/xmi/liquid_drops/operation_drop.rb +3 -9
  91. data/lib/lutaml/xmi/liquid_drops/package_drop.rb +16 -44
  92. data/lib/lutaml/xmi/liquid_drops/root_drop.rb +3 -11
  93. data/lib/lutaml/xmi/liquid_drops/source_target_drop.rb +2 -5
  94. data/lib/lutaml/xmi/parsers/xmi_base.rb +2 -749
  95. data/lib/lutaml/xmi/parsers/xmi_class_members.rb +45 -0
  96. data/lib/lutaml/xmi/parsers/xmi_connector.rb +251 -0
  97. data/lib/lutaml/xmi/parsers/xml.rb +7 -120
  98. data/lib/lutaml/xmi/xmi_lookup_service.rb +42 -0
  99. data/lib/lutaml.rb +0 -1
  100. metadata +48 -21
  101. data/lib/lutaml/cli/commands/base_command.rb +0 -118
  102. data/lib/lutaml/command_line.rb +0 -272
  103. data/lib/lutaml/sysml/allocate.rb +0 -9
  104. data/lib/lutaml/sysml/allocated.rb +0 -9
  105. data/lib/lutaml/sysml/binding_connector.rb +0 -9
  106. data/lib/lutaml/sysml/block.rb +0 -32
  107. data/lib/lutaml/sysml/constraint_block.rb +0 -14
  108. data/lib/lutaml/sysml/copy.rb +0 -8
  109. data/lib/lutaml/sysml/derive_requirement.rb +0 -9
  110. data/lib/lutaml/sysml/nested_connector_end.rb +0 -13
  111. data/lib/lutaml/sysml/refine.rb +0 -9
  112. data/lib/lutaml/sysml/requirement.rb +0 -44
  113. data/lib/lutaml/sysml/requirement_related.rb +0 -9
  114. data/lib/lutaml/sysml/satisfy.rb +0 -9
  115. data/lib/lutaml/sysml/test_case.rb +0 -25
  116. data/lib/lutaml/sysml/trace.rb +0 -9
  117. data/lib/lutaml/sysml/verify.rb +0 -8
  118. data/lib/lutaml/sysml/xmi_file.rb +0 -486
  119. data/lib/lutaml/sysml.rb +0 -11
@@ -25,9 +25,6 @@ module Lutaml
25
25
  diagram.name = ea_diagram.name
26
26
  diagram.xmi_id = normalize_guid_to_xmi_format(ea_diagram.ea_guid,
27
27
  "EAID")
28
- # TODO: Fix diagram_type assignment -
29
- # lutaml-model compatibility issue
30
- # diagram.diagram_type = ea_diagram.diagram_type
31
28
 
32
29
  # Map package relationship - use GUID not numeric ID
33
30
  if ea_diagram.package_id
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_transformer"
4
+ require_relative "attribute_transformer"
5
+ require_relative "generalization_transformer"
6
+ require_relative "association_builder"
7
+
8
+ module Lutaml
9
+ module Qea
10
+ module Factory
11
+ class GeneralizationBuilder < BaseTransformer
12
+ def load_generalization(object_id, visited = Set.new, is_leaf = true) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity,Style/OptionalBooleanParameter
13
+ return nil if object_id.nil?
14
+
15
+ if visited.include?(object_id)
16
+ warn "Circular inheritance detected for object_id #{object_id}, " \
17
+ "stopping recursion"
18
+ return nil
19
+ end
20
+
21
+ visited = visited.dup.add(object_id)
22
+
23
+ current_obj = find_object_by_id(object_id)
24
+ return nil unless current_obj
25
+
26
+ ea_connector = database.connectors_for_object(object_id)
27
+ .find { |c| c.generalization? && c.start_object_id == object_id }
28
+
29
+ gen_transformer = GeneralizationTransformer.new(database)
30
+ generalization = if ea_connector.nil?
31
+ gen_transformer.transform(nil, current_obj)
32
+ else
33
+ gen_transformer.transform(ea_connector,
34
+ current_obj)
35
+ end
36
+ return nil unless generalization
37
+
38
+ current_attrs = load_attributes(object_id)
39
+ current_assoc_attrs = AssociationBuilder.new(database)
40
+ .load_association_attributes(object_id)
41
+ general_attrs = convert_to_general_attributes(
42
+ current_attrs + current_assoc_attrs,
43
+ )
44
+
45
+ upper_klass = generalization.general_upper_klass
46
+ gen_name = generalization.general_name
47
+ general_attrs.each do |attr|
48
+ attr.gen_name = gen_name
49
+ name_ns = case attr.type_ns
50
+ when "core", "gml"
51
+ upper_klass
52
+ else
53
+ attr.type_ns
54
+ end
55
+ attr.name_ns = name_ns || upper_klass
56
+ end
57
+
58
+ generalization.general_attributes = general_attrs
59
+ .sort_by { |a| [a.name.to_s, a.id] }
60
+
61
+ generalization.attributes = transform_general_attributes(
62
+ generalization,
63
+ )
64
+
65
+ generalization.owned_props = generalization.attributes
66
+ .reject(&:has_association)
67
+ generalization.assoc_props = generalization.attributes
68
+ .select(&:has_association)
69
+
70
+ parent_object_id = ea_connector&.end_object_id
71
+ if parent_object_id
72
+ parent_gen = load_generalization(parent_object_id, visited, false)
73
+ if parent_gen
74
+ generalization.general = parent_gen
75
+ generalization.has_general = true
76
+ end
77
+ end
78
+
79
+ if is_leaf && generalization.has_general
80
+ collect_inherited_properties(generalization)
81
+ end
82
+
83
+ generalization
84
+ end
85
+
86
+ def load_association_generalizations(object_id)
87
+ return [] if object_id.nil?
88
+
89
+ gen_connectors = database.connectors_for_object(object_id)
90
+ .select { |c| c.generalization? && c.start_object_id == object_id }
91
+
92
+ gen_connectors.filter_map do |ea_connector|
93
+ guid = ea_connector.ea_guid
94
+ parent_object_id = ea_connector.end_object_id
95
+
96
+ parent_obj = find_object_by_id(parent_object_id)
97
+ next unless parent_obj
98
+
99
+ Lutaml::Uml::AssociationGeneralization.new.tap do |ag|
100
+ ag.id = normalize_guid_to_xmi_format(guid, "EAID")
101
+ ag.type = "uml:Generalization"
102
+ ag.general = normalize_guid_to_xmi_format(parent_obj.ea_guid,
103
+ "EAID")
104
+ end
105
+ end
106
+ end
107
+
108
+ def convert_to_general_attributes(attributes)
109
+ attributes.map do |attr|
110
+ Lutaml::Uml::GeneralAttribute.new.tap do |gen_attr|
111
+ gen_attr.id = attr.id
112
+ gen_attr.name = attr.name
113
+ gen_attr.type = attr.type
114
+ gen_attr.xmi_id = attr.xmi_id
115
+ gen_attr.is_derived = !!attr.is_derived
116
+ gen_attr.cardinality = attr.cardinality
117
+ gen_attr.definition = attr.definition&.strip
118
+ gen_attr.association = attr.association
119
+ gen_attr.has_association = !!attr.association
120
+ gen_attr.type_ns = attr.type_ns
121
+ end
122
+ end
123
+ end
124
+
125
+ def convert_to_top_element_attributes(attributes)
126
+ attributes.map do |attr|
127
+ Lutaml::Uml::TopElementAttribute.new.tap do |top_attr|
128
+ top_attr.id = attr.id
129
+ top_attr.name = attr.name
130
+ top_attr.type = attr.type
131
+ top_attr.xmi_id = attr.xmi_id
132
+ top_attr.cardinality = attr.cardinality
133
+ top_attr.definition = attr.definition&.strip
134
+ top_attr.association = attr.association
135
+ top_attr.type_ns = attr.type_ns
136
+ top_attr.is_derived = !!attr.is_derived
137
+ end
138
+ end
139
+ end
140
+
141
+ private
142
+
143
+ def load_attributes(object_id)
144
+ return [] if object_id.nil?
145
+
146
+ ea_attributes = database.attributes_for_object(object_id)
147
+ .sort_by { |a| a.pos || 0 }
148
+
149
+ AttributeTransformer.new(database).transform_collection(ea_attributes)
150
+ end
151
+
152
+ def transform_general_attributes(generalization)
153
+ upper_klass = generalization.general_upper_klass
154
+ gen_name = generalization.general_name
155
+ gen_attrs = generalization.general_attributes
156
+
157
+ gen_attrs.map do |attr|
158
+ transformed = attr.dup
159
+ name_ns = case attr.type_ns
160
+ when "core", "gml"
161
+ upper_klass
162
+ else
163
+ attr.type_ns
164
+ end
165
+ name_ns = upper_klass if name_ns.nil?
166
+ transformed.name_ns = name_ns
167
+ transformed.gen_name = gen_name
168
+ transformed.name = "" if transformed.name.nil?
169
+ transformed
170
+ end
171
+ end
172
+
173
+ def collect_inherited_properties(generalization)
174
+ inherited_props = []
175
+ inherited_assoc_props = []
176
+ level = 0
177
+
178
+ current_gen = generalization.general
179
+ while current_gen
180
+ [current_gen.general_attributes,
181
+ current_gen.attributes].each do |attr_list|
182
+ attr_list&.each do |attr|
183
+ attr.upper_klass = current_gen.general_upper_klass
184
+ attr.level = level
185
+ end
186
+ end
187
+
188
+ current_gen.attributes.reverse_each do |attr|
189
+ inherited_attr = attr.dup
190
+ inherited_attr.upper_klass = current_gen.general_upper_klass
191
+ inherited_attr.gen_name = current_gen.general_name
192
+ inherited_attr.level = level
193
+
194
+ if attr.has_association
195
+ inherited_assoc_props << inherited_attr
196
+ else
197
+ inherited_props << inherited_attr
198
+ end
199
+ end
200
+
201
+ level += 1
202
+ current_gen = current_gen.general
203
+ end
204
+
205
+ generalization.inherited_props = inherited_props.reverse
206
+ generalization.inherited_assoc_props = inherited_assoc_props.reverse
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -29,8 +29,7 @@ module Lutaml
29
29
  ea_package.notes.nil? || ea_package.notes.empty?
30
30
 
31
31
  # Load and transform tagged values
32
- # TODO: Fix tagged_values assignment - temporarily commented out
33
- # pkg.tagged_values = load_tagged_values(ea_package.ea_guid)
32
+ pkg.tagged_values = load_tagged_values(ea_package.ea_guid)
34
33
 
35
34
  # Load stereotype from t_xref
36
35
  stereotype = load_stereotype(ea_package.ea_guid)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Qea
5
+ module Factory
6
+ class StereotypeLoader
7
+ def initialize(database)
8
+ @database = database
9
+ end
10
+
11
+ def load_from_xref(ea_guid)
12
+ return nil if ea_guid.nil?
13
+ return nil unless @database.xrefs
14
+
15
+ xref = @database.xrefs.find do |x|
16
+ x.client == ea_guid && x.name == "Stereotypes" &&
17
+ x.type == "element property"
18
+ end
19
+
20
+ return nil unless xref
21
+
22
+ description = xref.description
23
+ return nil if description.nil? || description.empty?
24
+
25
+ if description =~ /@STEREO;Name=([^;]+);/
26
+ return Regexp.last_match(1)
27
+ end
28
+
29
+ nil
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Qea
5
+ class Database
6
+ private
7
+
8
+ # Build a group index from a collection by a given attribute.
9
+ # BaseRepository overrides group_by to take a symbol (not a block),
10
+ # so we use each_with_object instead.
11
+ #
12
+ # @param collection [Array] Collection to index
13
+ # @param method [Symbol] Attribute method to group by
14
+ # @param single [Boolean] If true, return single object (last match)
15
+ # instead of array
16
+ # @return [Hash] Group index
17
+ def build_group_index(collection, method, single: false)
18
+ if single
19
+ collection.each_with_object({}) do |item, hash|
20
+ key = item.send(method)
21
+ hash[key] = item if key
22
+ end
23
+ else
24
+ collection.each_with_object({}) do |item, hash|
25
+ key = item.send(method)
26
+ (hash[key] ||= []) << item if key
27
+ end
28
+ end
29
+ end
30
+
31
+ # Eagerly build all lazy lookup indexes before freezing
32
+ def build_lookup_indexes
33
+ @objects_by_guid = build_group_index(objects, :ea_guid, single: true)
34
+ @attributes_by_object_id = build_group_index(attributes, :ea_object_id)
35
+ @operations_by_object_id = build_group_index(operations, :ea_object_id)
36
+ @operation_params_by_id = build_group_index(operation_params,
37
+ :operationid)
38
+ @connectors_by_start = build_group_index(connectors, :start_object_id)
39
+ @connectors_by_end = build_group_index(connectors, :end_object_id)
40
+ @packages_by_parent = build_group_index(packages, :parent_id)
41
+ @objects_by_package_id = build_group_index(objects, :package_id)
42
+ @diagrams_by_package_id = build_group_index(diagrams, :package_id)
43
+ @diagram_objects_by_id = build_group_index(diagram_objects, :diagram_id)
44
+ @diagram_links_by_id = build_group_index(diagram_links, :diagramid)
45
+ # Also build hash indexes for find_* methods
46
+ @packages_by_id = build_group_index(packages, :package_id, single: true)
47
+ @connectors_by_id = build_group_index(connectors, :connector_id,
48
+ single: true)
49
+ @diagrams_by_id = build_group_index(diagrams, :diagram_id, single: true)
50
+ @attributes_by_id = build_group_index(attributes, :id, single: true)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -80,8 +80,6 @@ module Lutaml
80
80
  # @return [String]
81
81
  def type_signature
82
82
  case size
83
- when 0
84
- datatype
85
83
  when 1
86
84
  "#{datatype}(#{defaultlen})"
87
85
  when 2
@@ -433,8 +433,6 @@ module Lutaml
433
433
  # @return [void]
434
434
  def display_result(result, formatter)
435
435
  case formatter
436
- when :text
437
- display_text_result(result)
438
436
  when :json
439
437
  puts result.to_json
440
438
  else
@@ -5,7 +5,6 @@ module Lutaml
5
5
  module HasMembers
6
6
  class UnknownMemberTypeError < Lutaml::Error; end
7
7
 
8
- # TODO: move to Parslet::Transform
9
8
  def members=(value) # rubocop:disable Metrics/AbcSize
10
9
  value.group_by { |member| member.keys.first }
11
10
  .each do |(type, group)|
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Uml
5
+ # Service class for safely walking UML generalization chains.
6
+ #
7
+ # Walks from a class up through its generalization hierarchy (parent classes),
8
+ # collecting information about each ancestor. Supports cycle detection to
9
+ # prevent infinite loops in malformed models.
10
+ #
11
+ # @example
12
+ # walker = InheritanceWalker.new(repository)
13
+ # walker.walk(klass) do |ancestor, level|
14
+ # puts "#{'-' * level} #{ancestor.name}"
15
+ # end
16
+ class InheritanceWalker
17
+ # @param repository [#supertype_of] Object responding to #supertype_of(class)
18
+ def initialize(repository)
19
+ @repository = repository
20
+ @visited = Set.new
21
+ end
22
+
23
+ # Walk the generalization chain from a starting class.
24
+ #
25
+ # @param klass [Lutaml::Uml::Class] The class to start from
26
+ # @yield [ancestor, level] Yields each ancestor class and its depth (1-based)
27
+ # @return [Array<[klass, level]>] Array of [ancestor_class, level] pairs in visiting order
28
+ #
29
+ # @example
30
+ # walker.walk(D.class) do |parent, level|
31
+ # puts "#{' ' * (level - 1)}Parent #{level}: #{parent.name}"
32
+ # end
33
+ def walk(klass)
34
+ return [] unless klass.respond_to?(:generalization) && klass.generalization
35
+
36
+ ancestors = []
37
+ @visited.clear
38
+ collect_ancestors(klass, ancestors)
39
+ ancestors.reverse_each.with_index(1) do |ancestor, level|
40
+ break if @visited.include?(ancestor.xmi_id)
41
+
42
+ @visited.add(ancestor.xmi_id)
43
+ yield(ancestor, level) if block_given?
44
+ end
45
+ ancestors.reverse_each.with_index(1)
46
+ end
47
+
48
+ # Get the direct supertype (immediate parent) of a class.
49
+ #
50
+ # @param klass [Lutaml::Uml::Class]
51
+ # @return [Lutaml::Uml::Class, nil]
52
+ def supertype_of(klass)
53
+ @repository.supertype_of(klass)
54
+ end
55
+
56
+ # Get all ancestors of a class in order (immediate parent first).
57
+ #
58
+ # @param klass [Lutaml::Uml::Class]
59
+ # @return [Array<Lutaml::Uml::Class>]
60
+ def ancestors_of(klass)
61
+ result = []
62
+ walk(klass) { |ancestor, _level| result << ancestor }
63
+ result
64
+ end
65
+
66
+ private
67
+
68
+ # Collect ancestors recursively into the result array.
69
+ # Uses a trail set for cycle detection (avoids Set mutation issues across calls).
70
+ def collect_ancestors(klass, result, trail = [])
71
+ return [] if trail.include?(klass.xmi_id) # cycle guard
72
+ return [] unless klass.respond_to?(:generalization) && klass.generalization
73
+
74
+ trail = trail.dup
75
+ trail << klass.xmi_id
76
+
77
+ general_id = klass.generalization.respond_to?(:general_id) ? klass.generalization.general_id : nil
78
+ parent = general_id ? find_class_by_id(general_id) : nil
79
+
80
+ return [] unless parent
81
+
82
+ result << parent
83
+ collect_ancestors(parent, result, trail)
84
+ end
85
+
86
+ def find_class_by_id(xmi_id)
87
+ @repository.indexes&.dig(:qualified_names, xmi_id) ||
88
+ @repository.send(:classes_index)&.find { |c| c.xmi_id == xmi_id }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Uml
5
+ # Shared helper methods for UML model objects.
6
+ # These methods are used across transformers, serializers, and presenters
7
+ # to avoid duplication of common model traversal and formatting logic.
8
+ module ModelHelpers
9
+ # Normalize a stereotype value to a consistent Array format.
10
+ #
11
+ # @param stereotype [String, Array, nil, Symbol] The stereotype value
12
+ # @return [Array<String>] Array of stereotype strings
13
+ #
14
+ # @example
15
+ # normalize_stereotypes(nil) # => []
16
+ # normalize_stereotypes("enumeration") # => ["enumeration"]
17
+ # normalize_stereotypes(["a", "b"]) # => ["a", "b"]
18
+ # normalize_stereotypes(:enumeration) # => ["enumeration"]
19
+ def normalize_stereotypes(stereotype)
20
+ return [] if stereotype.nil?
21
+
22
+ case stereotype
23
+ when Array then stereotype.map(&:to_s)
24
+ when String then [stereotype]
25
+ when Symbol then [stereotype.to_s]
26
+ else [stereotype.to_s]
27
+ end
28
+ end
29
+
30
+ # Build a fully qualified name by walking the namespace chain of a UML element.
31
+ #
32
+ # @param uml_element [Lutaml::Uml::TopElement, Lutaml::Uml::Package] Any element with a namespace
33
+ # @return [String] Fully qualified name joined by '::'
34
+ #
35
+ # @example
36
+ # qualified_name_for(class_in_package) # => "ModelName::PackageName::ClassName"
37
+ def qualified_name_for(uml_element)
38
+ return uml_element.name unless uml_element.respond_to?(:namespace)
39
+
40
+ parts = []
41
+ current = uml_element
42
+
43
+ while current
44
+ parts.unshift(current.name) if current.name
45
+ current = if current.respond_to?(:namespace) && current.namespace
46
+ current.namespace
47
+ else
48
+ break
49
+ end
50
+ end
51
+
52
+ parts.join("::")
53
+ end
54
+
55
+ # Build a package-only namespace path (no class names).
56
+ #
57
+ # @param uml_element [Lutaml::Uml::TopElement, Lutaml::Uml::Package]
58
+ # @return [String] Package path joined by '::'
59
+ #
60
+ # @example
61
+ # package_path_for(class_in_nested_package) # => "ModelName::ParentPackage::ChildPackage"
62
+ def package_path_for(uml_element)
63
+ return uml_element.name unless uml_element.respond_to?(:namespace)
64
+
65
+ parts = []
66
+ current = uml_element
67
+
68
+ while current
69
+ if current.is_a?(Lutaml::Uml::Package)
70
+ parts.unshift(current.name) if current.name
71
+ current = current.namespace if current.respond_to?(:namespace)
72
+ elsif current.is_a?(Lutaml::Uml::TopElement)
73
+ # Stop at the first TopElement (class, enum, etc.) — namespace above is package
74
+ current = current.namespace if current.respond_to?(:namespace)
75
+ else
76
+ break
77
+ end
78
+ end
79
+
80
+ parts.join("::")
81
+ end
82
+
83
+ # Extract the leaf class name from a fully-qualified class name or class object.
84
+ #
85
+ # @param uml_class [String, Class] A class name string or a Lutaml::Uml::* class instance
86
+ # @return [String] The leaf class name
87
+ #
88
+ # @example
89
+ # class_type_for("Lutaml::Uml::Class") # => "Class"
90
+ # class_type_for(some_enum_object) # => "Enumeration"
91
+ def class_type_for(uml_class)
92
+ case uml_class
93
+ when String then uml_class.split("::").last
94
+ when Class then uml_class.name.split("::").last
95
+ else uml_class.class.name.split("::").last
96
+ end
97
+ end
98
+
99
+ # Format a cardinality object as a "min..max" string.
100
+ #
101
+ # @param cardinality [Lutaml::Uml::Cardinality, Hash, nil]
102
+ # @return [String, nil] Formatted cardinality string or nil
103
+ #
104
+ # @example
105
+ # format_cardinality(Lutaml::Uml::Cardinality.new(min: 0, max: 5)) # => "0..5"
106
+ # format_cardinality({min: 1, max: nil}) # => "1..*"
107
+ def format_cardinality(cardinality)
108
+ return nil unless cardinality
109
+
110
+ min = cardinality.respond_to?(:min) ? cardinality.min : cardinality[:min]
111
+ max = cardinality.respond_to?(:max) ? cardinality.max : cardinality[:max]
112
+ return nil if min.nil? && max.nil?
113
+
114
+ min_str = min.nil? ? "0" : min.to_s
115
+ max_str = max.nil? ? "*" : max.to_s
116
+ "#{min_str}..#{max_str}"
117
+ end
118
+
119
+ # Parse a cardinality string or hash into a {min:, max:} hash.
120
+ #
121
+ # @param min [String, nil]
122
+ # @param max [String, nil]
123
+ # @return [Hash{Symbol => String, nil}] Hash with :min and :max keys
124
+ def parse_cardinality(min, max)
125
+ { min: min, max: max }
126
+ end
127
+ end
128
+ end
129
+ end
@@ -23,8 +23,10 @@ module Lutaml
23
23
  @static = !!value
24
24
  end
25
25
 
26
+ VALID_ACCESS = %w[public private protected package].freeze
27
+
26
28
  def access=(value)
27
- @access = value.to_s # TODO: Validate?
29
+ @access = value.to_s
28
30
  end
29
31
  end
30
32
  end
@@ -16,13 +16,13 @@ module Lutaml
16
16
  attr_reader :modifier, :members
17
17
 
18
18
  def modifier=(value)
19
- @modifier = value.to_s # TODO: Validate?
19
+ @modifier = value.to_s
20
20
  end
21
21
 
22
22
  def members=(value) # rubocop:disable Metrics/MethodLength
23
23
  @members = value.to_a.map do |member|
24
- type = member.to_a[0][0] # TODO: This is dumb
25
- attributes = member.to_a[0][1]
24
+ type = member.keys.first
25
+ attributes = member.values.first
26
26
  attributes[:parent] = self
27
27
 
28
28
  case type
@@ -3,10 +3,12 @@
3
3
  module Lutaml
4
4
  module Uml
5
5
  class Operation < TopElement
6
+ attribute :id, :string
6
7
  attribute :return_type, :string
7
8
  attribute :parameter_type, :string
8
9
 
9
10
  yaml do
11
+ map "id", to: :id
10
12
  map "return_type", to: :return_type
11
13
  map "parameter_type", to: :parameter_type
12
14
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module UmlRepository
5
+ # O(1) lookup index for UML classes by various identifiers.
6
+ #
7
+ # Replaces O(n) linear scans that were repeated across multiple
8
+ # transformer and serializer classes.
9
+ #
10
+ # @example
11
+ # index = ClassLookupIndex.new(repository.classes_index)
12
+ # index.by_xmi_id("EAID_123...")
13
+ # index.by_object_id(42)
14
+ class ClassLookupIndex
15
+ def initialize(classes)
16
+ @by_xmi_id = {}
17
+ @by_object_id = {}
18
+
19
+ classes.each do |klass|
20
+ @by_xmi_id[klass.xmi_id] = klass if klass.xmi_id
21
+ if klass.respond_to?(:ea_object_id) && klass.ea_object_id
22
+ @by_object_id[klass.ea_object_id] = klass
23
+ end
24
+ end
25
+ end
26
+
27
+ # @param xmi_id [String]
28
+ # @return [Lutaml::Uml::Class, nil]
29
+ def by_xmi_id(xmi_id)
30
+ @by_xmi_id[xmi_id]
31
+ end
32
+
33
+ # @param object_id [String, Integer]
34
+ # @return [Lutaml::Uml::Class, nil]
35
+ def by_object_id(object_id)
36
+ @by_object_id[object_id]
37
+ end
38
+ end
39
+ end
40
+ end