lutaml 0.10.13 → 0.10.14

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 (25) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +58 -18
  3. data/lib/lutaml/cli/interactive_shell/export_handler.rb +9 -7
  4. data/lib/lutaml/cli/interactive_shell/help_display.rb +39 -45
  5. data/lib/lutaml/cli/interactive_shell/navigation_commands.rb +36 -21
  6. data/lib/lutaml/cli/interactive_shell/query_commands.rb +63 -47
  7. data/lib/lutaml/cli/interactive_shell.rb +43 -25
  8. data/lib/lutaml/qea/factory/association_builder.rb +114 -125
  9. data/lib/lutaml/qea/factory/class_transformer.rb +67 -43
  10. data/lib/lutaml/qea/factory/enum_transformer.rb +22 -23
  11. data/lib/lutaml/qea/factory/generalization_builder.rb +146 -116
  12. data/lib/lutaml/qea/factory/stereotype_loader.rb +13 -7
  13. data/lib/lutaml/qea/lookup_indexes.rb +8 -1
  14. data/lib/lutaml/uml_repository/exporters/markdown/class_page_builder.rb +33 -25
  15. data/lib/lutaml/uml_repository/index_builders/association_index.rb +49 -47
  16. data/lib/lutaml/uml_repository/index_builders/class_index.rb +30 -23
  17. data/lib/lutaml/uml_repository/queries/class_query.rb +74 -48
  18. data/lib/lutaml/uml_repository/queries/inheritance_query.rb +42 -32
  19. data/lib/lutaml/uml_repository/static_site/data_transformer.rb +55 -35
  20. data/lib/lutaml/uml_repository/static_site/search_index_builder.rb +32 -19
  21. data/lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb +27 -17
  22. data/lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb +92 -68
  23. data/lib/lutaml/uml_repository/static_site/serializers/package_tree_builder.rb +32 -23
  24. data/lib/lutaml/version.rb +1 -1
  25. metadata +1 -1
@@ -9,7 +9,7 @@ module Lutaml
9
9
  module Qea
10
10
  module Factory
11
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
12
+ def load_generalization(object_id, visited = Set.new, is_leaf = true) # rubocop:disable Style/OptionalBooleanParameter
13
13
  return nil if object_id.nil?
14
14
 
15
15
  if visited.include?(object_id)
@@ -23,58 +23,13 @@ module Lutaml
23
23
  current_obj = find_object_by_id(object_id)
24
24
  return nil unless current_obj
25
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
26
+ generalization = build_generalization(object_id, current_obj)
36
27
  return nil unless generalization
37
28
 
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
29
+ populate_generalization_attrs(generalization, object_id)
57
30
 
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
31
+ ea_connector = ea_connector_for(object_id)
32
+ populate_parent_generalization(generalization, ea_connector, visited)
78
33
 
79
34
  if is_leaf && generalization.has_general
80
35
  collect_inherited_properties(generalization)
@@ -90,55 +45,155 @@ module Lutaml
90
45
  .select { |c| c.generalization? && c.start_object_id == object_id }
91
46
 
92
47
  gen_connectors.filter_map do |ea_connector|
93
- guid = ea_connector.ea_guid
94
- parent_object_id = ea_connector.end_object_id
48
+ build_assoc_generalization(ea_connector)
49
+ end
50
+ end
95
51
 
96
- parent_obj = find_object_by_id(parent_object_id)
97
- next unless parent_obj
52
+ def convert_to_general_attributes(attributes)
53
+ attributes.map { |attr| to_general_attribute(attr) }
54
+ end
98
55
 
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
56
+ def convert_to_top_element_attributes(attributes)
57
+ attributes.map { |attr| to_top_element_attribute(attr) }
58
+ end
59
+
60
+ def to_general_attribute(attr)
61
+ base = base_attr_hash(attr)
62
+ Lutaml::Uml::GeneralAttribute.new.tap do |gen_attr|
63
+ base.each { |k, v| gen_attr.public_send(:"#{k}=", v) }
64
+ gen_attr.is_derived = !!attr.is_derived
65
+ gen_attr.has_association = !!attr.association
105
66
  end
106
67
  end
107
68
 
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
69
+ def to_top_element_attribute(attr)
70
+ base = base_attr_hash(attr)
71
+ Lutaml::Uml::TopElementAttribute.new.tap do |top_attr|
72
+ base.each { |k, v| top_attr.public_send(:"#{k}=", v) }
73
+ top_attr.is_derived = !!attr.is_derived
74
+ end
75
+ end
76
+
77
+ def base_attr_hash(attr)
78
+ {
79
+ id: attr.id,
80
+ name: attr.name,
81
+ type: attr.type,
82
+ xmi_id: attr.xmi_id,
83
+ cardinality: attr.cardinality,
84
+ definition: attr.definition&.strip,
85
+ association: attr.association,
86
+ type_ns: attr.type_ns,
87
+ }
88
+ end
89
+
90
+ private
91
+
92
+ def tag_ancestor_attributes(gen, level)
93
+ [gen.general_attributes, gen.attributes].each do |attr_list|
94
+ attr_list&.each do |attr|
95
+ attr.upper_klass = gen.general_upper_klass
96
+ attr.level = level
121
97
  end
122
98
  end
123
99
  end
124
100
 
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
101
+ def collect_ancestor_attrs(gen, level, inherited_props,
102
+ inherited_assoc_props)
103
+ gen.attributes.reverse_each do |attr|
104
+ inherited_attr = attr.dup
105
+ inherited_attr.upper_klass = gen.general_upper_klass
106
+ inherited_attr.gen_name = gen.general_name
107
+ inherited_attr.level = level
108
+
109
+ if attr.has_association
110
+ inherited_assoc_props << inherited_attr
111
+ else
112
+ inherited_props << inherited_attr
137
113
  end
138
114
  end
139
115
  end
140
116
 
141
- private
117
+ def build_assoc_generalization(ea_connector)
118
+ parent_obj = find_object_by_id(ea_connector.end_object_id)
119
+ return nil unless parent_obj
120
+
121
+ Lutaml::Uml::AssociationGeneralization.new.tap do |ag|
122
+ ag.id = normalize_guid_to_xmi_format(ea_connector.ea_guid, "EAID")
123
+ ag.type = "uml:Generalization"
124
+ ag.general = normalize_guid_to_xmi_format(parent_obj.ea_guid,
125
+ "EAID")
126
+ end
127
+ end
128
+
129
+ def resolve_name_ns(type_ns, upper_klass)
130
+ ns = case type_ns
131
+ when "core", "gml"
132
+ upper_klass
133
+ else
134
+ type_ns
135
+ end
136
+ ns || upper_klass
137
+ end
138
+
139
+ def build_generalization(object_id, current_obj)
140
+ ea_connector = ea_connector_for(object_id)
141
+ gen_transformer = GeneralizationTransformer.new(database)
142
+ if ea_connector.nil?
143
+ gen_transformer.transform(nil, current_obj)
144
+ else
145
+ gen_transformer.transform(ea_connector, current_obj)
146
+ end
147
+ end
148
+
149
+ def ea_connector_for(object_id)
150
+ database.connectors_for_object(object_id)
151
+ .find { |c| c.generalization? && c.start_object_id == object_id }
152
+ end
153
+
154
+ def populate_generalization_attrs(generalization, object_id)
155
+ general_attrs = build_general_attrs(object_id)
156
+ apply_namespace_to_attrs(general_attrs, generalization)
157
+
158
+ generalization.general_attributes = general_attrs
159
+ .sort_by { |a| [a.name.to_s, a.id] }
160
+
161
+ generalization.attributes = transform_general_attributes(
162
+ generalization,
163
+ )
164
+
165
+ generalization.owned_props = generalization.attributes
166
+ .reject(&:has_association)
167
+ generalization.assoc_props = generalization.attributes
168
+ .select(&:has_association)
169
+ end
170
+
171
+ def build_general_attrs(object_id)
172
+ current_attrs = load_attributes(object_id)
173
+ current_assoc_attrs = AssociationBuilder.new(database)
174
+ .load_association_attributes(object_id)
175
+ convert_to_general_attributes(current_attrs + current_assoc_attrs)
176
+ end
177
+
178
+ def apply_namespace_to_attrs(general_attrs, generalization)
179
+ upper_klass = generalization.general_upper_klass
180
+ general_attrs.each do |attr|
181
+ attr.gen_name = generalization.general_name
182
+ attr.name_ns = resolve_name_ns(attr.type_ns, upper_klass)
183
+ end
184
+ end
185
+
186
+ def populate_parent_generalization(generalization, ea_connector,
187
+ visited)
188
+ parent_object_id = ea_connector&.end_object_id
189
+ return unless parent_object_id
190
+
191
+ parent_gen = load_generalization(parent_object_id, visited, false)
192
+ return unless parent_gen
193
+
194
+ generalization.general = parent_gen
195
+ generalization.has_general = true
196
+ end
142
197
 
143
198
  def load_attributes(object_id)
144
199
  return [] if object_id.nil?
@@ -152,18 +207,10 @@ module Lutaml
152
207
  def transform_general_attributes(generalization)
153
208
  upper_klass = generalization.general_upper_klass
154
209
  gen_name = generalization.general_name
155
- gen_attrs = generalization.general_attributes
156
210
 
157
- gen_attrs.map do |attr|
211
+ generalization.general_attributes.map do |attr|
158
212
  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
213
+ transformed.name_ns = resolve_name_ns(attr.type_ns, upper_klass)
167
214
  transformed.gen_name = gen_name
168
215
  transformed.name = "" if transformed.name.nil?
169
216
  transformed
@@ -177,26 +224,9 @@ module Lutaml
177
224
 
178
225
  current_gen = generalization.general
179
226
  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
227
+ tag_ancestor_attributes(current_gen, level)
228
+ collect_ancestor_attrs(current_gen, level, inherited_props,
229
+ inherited_assoc_props)
200
230
 
201
231
  level += 1
202
232
  current_gen = current_gen.general
@@ -12,21 +12,27 @@ module Lutaml
12
12
  return nil if ea_guid.nil?
13
13
  return nil unless @database.xrefs
14
14
 
15
- xref = @database.xrefs.find do |x|
15
+ xref = find_stereotype_xref(ea_guid)
16
+ return nil unless xref
17
+
18
+ extract_stereotype_name(xref.description)
19
+ end
20
+
21
+ private
22
+
23
+ def find_stereotype_xref(ea_guid)
24
+ @database.xrefs.find do |x|
16
25
  x.client == ea_guid && x.name == "Stereotypes" &&
17
26
  x.type == "element property"
18
27
  end
28
+ end
19
29
 
20
- return nil unless xref
21
-
22
- description = xref.description
30
+ def extract_stereotype_name(description)
23
31
  return nil if description.nil? || description.empty?
24
32
 
25
33
  if description =~ /@STEREO;Name=([^;]+);/
26
- return Regexp.last_match(1)
34
+ Regexp.last_match(1)
27
35
  end
28
-
29
- nil
30
36
  end
31
37
  end
32
38
  end
@@ -30,6 +30,11 @@ module Lutaml
30
30
 
31
31
  # Eagerly build all lazy lookup indexes before freezing
32
32
  def build_lookup_indexes
33
+ build_primary_indexes
34
+ build_secondary_indexes
35
+ end
36
+
37
+ def build_primary_indexes
33
38
  @objects_by_guid = build_group_index(objects, :ea_guid, single: true)
34
39
  @attributes_by_object_id = build_group_index(attributes, :ea_object_id)
35
40
  @operations_by_object_id = build_group_index(operations, :ea_object_id)
@@ -42,7 +47,9 @@ module Lutaml
42
47
  @diagrams_by_package_id = build_group_index(diagrams, :package_id)
43
48
  @diagram_objects_by_id = build_group_index(diagram_objects, :diagram_id)
44
49
  @diagram_links_by_id = build_group_index(diagram_links, :diagramid)
45
- # Also build hash indexes for find_* methods
50
+ end
51
+
52
+ def build_secondary_indexes
46
53
  @packages_by_id = build_group_index(packages, :package_id, single: true)
47
54
  @connectors_by_id = build_group_index(connectors, :connector_id,
48
55
  single: true)
@@ -39,7 +39,7 @@ module Lutaml
39
39
 
40
40
  ---
41
41
 
42
- [Back to Package](#{@link_resolver.package_link(pkg_path)}) | [Back to Index](../index.md)
42
+ #{build_navigation_links(pkg_path)}
43
43
  MARKDOWN
44
44
  end
45
45
 
@@ -67,21 +67,8 @@ module Lutaml
67
67
  return "" if parent.nil? && children.empty?
68
68
 
69
69
  content = "## Inheritance\n\n"
70
-
71
- if parent
72
- parent_qname = @link_resolver.qualified_name(parent)
73
- content += "**Extends**: [#{parent.name}](#{@link_resolver.class_link(parent_qname)})\n\n"
74
- end
75
-
76
- if children.any?
77
- content += "**Extended by**:\n\n"
78
- children.each do |child|
79
- child_qname = @link_resolver.qualified_name(child)
80
- content += "- [#{child.name}](#{@link_resolver.class_link(child_qname)})\n"
81
- end
82
- content += "\n"
83
- end
84
-
70
+ content += build_parent_link(parent) if parent
71
+ content += build_children_links(children) if children.any?
85
72
  content
86
73
  rescue StandardError
87
74
  ""
@@ -138,15 +125,7 @@ module Lutaml
138
125
  end
139
126
 
140
127
  def format_association_row(association, klass)
141
- source_end = association.member_end&.first
142
- target_end = association.member_end&.last
143
-
144
- end_obj = if source_end&.type&.xmi_id == klass.xmi_id
145
- target_end
146
- else
147
- source_end
148
- end
149
-
128
+ end_obj = resolve_target_end(association, klass)
150
129
  return "" unless end_obj&.type
151
130
 
152
131
  target_qname = @link_resolver.qualified_name(end_obj.type)
@@ -172,6 +151,35 @@ module Lutaml
172
151
 
173
152
  "#{content}\n"
174
153
  end
154
+
155
+ def build_navigation_links(pkg_path)
156
+ "[Back to Package](#{@link_resolver.package_link(pkg_path)}) | [Back to Index](../index.md)"
157
+ end
158
+
159
+ def build_parent_link(parent)
160
+ parent_qname = @link_resolver.qualified_name(parent)
161
+ "**Extends**: [#{parent.name}](#{@link_resolver.class_link(parent_qname)})\n\n"
162
+ end
163
+
164
+ def build_children_links(children)
165
+ content = "**Extended by**:\n\n"
166
+ children.each do |child|
167
+ child_qname = @link_resolver.qualified_name(child)
168
+ content += "- [#{child.name}](#{@link_resolver.class_link(child_qname)})\n"
169
+ end
170
+ "#{content}\n"
171
+ end
172
+
173
+ def resolve_target_end(association, klass)
174
+ source_end = association.member_end&.first
175
+ target_end = association.member_end&.last
176
+
177
+ if source_end&.type&.xmi_id == klass.xmi_id
178
+ target_end
179
+ else
180
+ source_end
181
+ end
182
+ end
175
183
  end
176
184
  end
177
185
  end
@@ -3,28 +3,35 @@
3
3
  module Lutaml
4
4
  module UmlRepository
5
5
  class IndexBuilder
6
- def build_association_index # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
7
- # Collect document-level associations (XMI format)
6
+ def build_association_index
7
+ index_document_associations
8
+ index_class_level_associations
9
+ end
10
+
11
+ def index_document_associations
8
12
  @document.associations&.each do |assoc|
9
13
  next unless assoc.xmi_id
10
14
 
11
15
  @associations[assoc.xmi_id] = assoc
12
16
  end
17
+ end
13
18
 
14
- # Collect class-level associations (QEA/EA format)
15
- # Note: This requires qualified_names index to be built first
19
+ def index_class_level_associations
16
20
  @qualified_names.each_value do |klass|
17
- next unless (klass.is_a?(Lutaml::Uml::Class) || klass.is_a?(Lutaml::Uml::DataType)) && klass.associations
21
+ next unless klassifiable?(klass) && klass.associations
18
22
 
19
23
  klass.associations.each do |assoc|
20
24
  next unless assoc.xmi_id
21
25
 
22
- # Avoid duplicates - only add if not already present
23
26
  @associations[assoc.xmi_id] ||= assoc
24
27
  end
25
28
  end
26
29
  end
27
30
 
31
+ def klassifiable?(klass)
32
+ klass.is_a?(Lutaml::Uml::Class) || klass.is_a?(Lutaml::Uml::DataType)
33
+ end
34
+
28
35
  # Build the inheritance graph index
29
36
  #
30
37
  # Creates a hash mapping parent qualified names to arrays of
@@ -49,46 +56,46 @@ module Lutaml
49
56
  #
50
57
  # @param classes [Array<Lutaml::Uml::Class>] Classes to process
51
58
  # @param package_path [String] Package path for these classes
52
- def process_generalizations(classes, package_path) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
59
+ def process_generalizations(classes, package_path)
53
60
  return unless classes
54
61
 
55
62
  classes.each do |klass|
56
63
  next unless klass.name
57
64
 
58
65
  child_qname = "#{package_path}::#{klass.name}"
66
+ index_generalization_edge(child_qname, klass, package_path)
67
+ index_inheritance_assoc_edges(child_qname, klass, package_path)
68
+ end
69
+ end
59
70
 
60
- # Handle generalization attribute
61
- if klass.generalization
62
- parent_name = extract_parent_name(klass.generalization)
63
- if parent_name
64
- parent_qname = resolve_qualified_name(parent_name, package_path)
65
- if parent_qname && child_qname != parent_qname
66
- @inheritance_graph[parent_qname] ||= []
67
- @inheritance_graph[parent_qname] << child_qname
68
- end
69
- end
70
- end
71
+ def index_generalization_edge(child_qname, klass, package_path)
72
+ return unless klass.generalization
71
73
 
72
- # Handle inheritance associations
73
- next unless klass.associations
74
+ parent_name = extract_parent_name(klass.generalization)
75
+ return unless parent_name
74
76
 
75
- klass.associations.each do |assoc|
76
- next unless assoc.member_end_type
77
- next unless assoc.member_end_type == "inheritance"
77
+ parent_qname = resolve_qualified_name(parent_name, package_path)
78
+ return unless parent_qname && child_qname != parent_qname
78
79
 
79
- parent_name = assoc.member_end
80
- next unless parent_name
80
+ (@inheritance_graph[parent_qname] ||= []) << child_qname
81
+ end
81
82
 
82
- parent_name = parent_name.name if parent_name.is_a?(Lutaml::Uml::Generalization)
83
- next unless parent_name.is_a?(String) && !parent_name.empty?
83
+ def index_inheritance_assoc_edges(child_qname, klass, package_path)
84
+ return unless klass.associations
84
85
 
85
- parent_qname = resolve_qualified_name(parent_name, package_path)
86
- next unless parent_qname
87
- next if child_qname == parent_qname
86
+ klass.associations.each do |assoc|
87
+ next unless assoc.member_end_type == "inheritance"
88
88
 
89
- @inheritance_graph[parent_qname] ||= []
90
- @inheritance_graph[parent_qname] << child_qname
91
- end
89
+ parent_name = assoc.member_end
90
+ next unless parent_name
91
+
92
+ parent_name = parent_name.name if parent_name.is_a?(Lutaml::Uml::Generalization)
93
+ next unless parent_name.is_a?(String) && !parent_name.empty?
94
+
95
+ parent_qname = resolve_qualified_name(parent_name, package_path)
96
+ next unless parent_qname && child_qname != parent_qname
97
+
98
+ (@inheritance_graph[parent_qname] ||= []) << child_qname
92
99
  end
93
100
  end
94
101
 
@@ -97,22 +104,10 @@ module Lutaml
97
104
  # @param generalization [Lutaml::Uml::Generalization]
98
105
  # Generalization object
99
106
  # @return [String, nil] Parent class name
100
- def extract_parent_name(generalization) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
107
+ def extract_parent_name(generalization)
101
108
  return nil unless generalization
102
109
 
103
- # Check for general attribute (could be a string or object)
104
- if generalization.general
105
- parent = generalization.general
106
- return parent.name if parent
107
- return parent.to_s if parent
108
- end
109
-
110
- # Check for name attribute directly
111
- if generalization.name
112
- return generalization.name
113
- end
114
-
115
- nil
110
+ name_from_general(generalization) || generalization.name
116
111
  end
117
112
 
118
113
  # Resolve a class name to its qualified name
@@ -124,6 +119,13 @@ module Lutaml
124
119
  # @param name [String] Class name to resolve
125
120
  # @param current_package_path [String] Current package context
126
121
  # @return [String, nil] Resolved qualified name
122
+ def name_from_general(generalization)
123
+ parent = generalization.general
124
+ return nil unless parent
125
+
126
+ parent.respond_to?(:name) ? parent.name : parent.to_s
127
+ end
128
+
127
129
  def resolve_qualified_name(name, current_package_path)
128
130
  # If name contains "::", it might already be qualified
129
131
  return name if @qualified_names.key?(name)