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.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +218 -94
- data/TODO.cleanups/01-resolve-production-todos.md +65 -0
- data/TODO.cleanups/02-reduce-metrics-offenses.md +37 -0
- data/TODO.cleanups/03-reduce-rspec-multiple-expectations.md +54 -0
- data/TODO.cleanups/04-reduce-rspec-example-length.md +45 -0
- data/TODO.cleanups/05-replace-marshal-load.md +37 -0
- data/TODO.cleanups/06-replace-eval-in-tests.md +41 -0
- data/TODO.cleanups/07-fix-lint-offenses.md +74 -0
- data/TODO.cleanups/08-reduce-memoized-helpers-and-nesting.md +43 -0
- data/TODO.cleanups/09-reduce-verified-doubles-and-rspec-style.md +57 -0
- data/TODO.cleanups/10-split-large-files.md +47 -0
- data/bin/console +0 -1
- data/exe/lutaml +1 -0
- data/lib/lutaml/cli/element_identifier.rb +3 -6
- data/lib/lutaml/cli/interactive_shell/bookmark_commands.rb +88 -0
- data/lib/lutaml/cli/interactive_shell/command_base.rb +32 -0
- data/lib/lutaml/cli/interactive_shell/export_handler.rb +67 -0
- data/lib/lutaml/cli/interactive_shell/help_display.rb +114 -0
- data/lib/lutaml/cli/interactive_shell/navigation_commands.rb +135 -0
- data/lib/lutaml/cli/interactive_shell/query_commands.rb +185 -0
- data/lib/lutaml/cli/interactive_shell.rb +116 -802
- data/lib/lutaml/cli/uml/build_command.rb +5 -5
- data/lib/lutaml/cli/uml/verify_command.rb +0 -1
- data/lib/lutaml/converter/xmi_to_uml.rb +3 -153
- data/lib/lutaml/converter/xmi_to_uml_generalization.rb +193 -0
- data/lib/lutaml/formatter/graphviz.rb +1 -2
- data/lib/lutaml/qea/database.rb +1 -47
- data/lib/lutaml/qea/factory/association_builder.rb +188 -0
- data/lib/lutaml/qea/factory/base_transformer.rb +0 -1
- data/lib/lutaml/qea/factory/class_transformer.rb +40 -590
- data/lib/lutaml/qea/factory/diagram_transformer.rb +0 -3
- data/lib/lutaml/qea/factory/generalization_builder.rb +211 -0
- data/lib/lutaml/qea/factory/package_transformer.rb +1 -2
- data/lib/lutaml/qea/factory/stereotype_loader.rb +34 -0
- data/lib/lutaml/qea/lookup_indexes.rb +54 -0
- data/lib/lutaml/qea/models/ea_datatype.rb +0 -2
- data/lib/lutaml/qea/validation/validation_engine.rb +0 -2
- data/lib/lutaml/uml/has_members.rb +0 -1
- data/lib/lutaml/uml/inheritance_walker.rb +92 -0
- data/lib/lutaml/uml/model_helpers.rb +129 -0
- data/lib/lutaml/uml/node/attribute.rb +3 -1
- data/lib/lutaml/uml/node/class_node.rb +3 -3
- data/lib/lutaml/uml/operation.rb +2 -0
- data/lib/lutaml/uml_repository/class_lookup_index.rb +40 -0
- data/lib/lutaml/uml_repository/exporters/markdown/class_page_builder.rb +179 -0
- data/lib/lutaml/uml_repository/exporters/markdown/formatting.rb +36 -0
- data/lib/lutaml/uml_repository/exporters/markdown/index_page_builder.rb +73 -0
- data/lib/lutaml/uml_repository/exporters/markdown/link_resolver.rb +40 -0
- data/lib/lutaml/uml_repository/exporters/markdown/package_page_builder.rb +107 -0
- data/lib/lutaml/uml_repository/exporters/markdown_exporter.rb +26 -538
- data/lib/lutaml/uml_repository/index_builder.rb +3 -271
- data/lib/lutaml/uml_repository/index_builders/association_index.rb +141 -0
- data/lib/lutaml/uml_repository/index_builders/class_index.rb +94 -0
- data/lib/lutaml/uml_repository/index_builders/package_index.rb +57 -0
- data/lib/lutaml/uml_repository/package_exporter.rb +10 -20
- data/lib/lutaml/uml_repository/package_loader.rb +37 -17
- data/lib/lutaml/uml_repository/repository/deprecated.rb +39 -0
- data/lib/lutaml/uml_repository/repository/loader.rb +112 -0
- data/lib/lutaml/uml_repository/repository.rb +7 -57
- data/lib/lutaml/uml_repository/static_site/association_serialization.rb +142 -0
- data/lib/lutaml/uml_repository/static_site/configuration.rb +0 -2
- data/lib/lutaml/uml_repository/static_site/data_transformer.rb +52 -873
- data/lib/lutaml/uml_repository/static_site/generator.rb +29 -8
- data/lib/lutaml/uml_repository/static_site/search_index_builder.rb +1 -4
- data/lib/lutaml/uml_repository/static_site/serializers/attribute_serializer.rb +78 -0
- data/lib/lutaml/uml_repository/static_site/serializers/class_serializer.rb +124 -0
- data/lib/lutaml/uml_repository/static_site/serializers/diagram_serializer.rb +60 -0
- data/lib/lutaml/uml_repository/static_site/serializers/inheritance_resolver.rb +258 -0
- data/lib/lutaml/uml_repository/static_site/serializers/metadata_builder.rb +48 -0
- data/lib/lutaml/uml_repository/static_site/serializers/operation_serializer.rb +57 -0
- data/lib/lutaml/uml_repository/static_site/serializers/package_serializer.rb +94 -0
- data/lib/lutaml/uml_repository/static_site/serializers/package_tree_builder.rb +93 -0
- data/lib/lutaml/version.rb +1 -1
- data/lib/lutaml/xmi/liquid_drops/association_drop.rb +13 -35
- data/lib/lutaml/xmi/liquid_drops/attribute_drop.rb +12 -18
- data/lib/lutaml/xmi/liquid_drops/cardinality_drop.rb +14 -6
- data/lib/lutaml/xmi/liquid_drops/connector_drop.rb +0 -3
- data/lib/lutaml/xmi/liquid_drops/constraint_drop.rb +1 -3
- data/lib/lutaml/xmi/liquid_drops/data_type_drop.rb +13 -70
- data/lib/lutaml/xmi/liquid_drops/dependency_drop.rb +2 -5
- data/lib/lutaml/xmi/liquid_drops/diagram_drop.rb +5 -11
- data/lib/lutaml/xmi/liquid_drops/enum_drop.rb +8 -16
- data/lib/lutaml/xmi/liquid_drops/enum_owned_literal_drop.rb +3 -9
- data/lib/lutaml/xmi/liquid_drops/generalization_attribute_drop.rb +11 -13
- data/lib/lutaml/xmi/liquid_drops/generalization_drop.rb +27 -85
- data/lib/lutaml/xmi/liquid_drops/klass_drop.rb +39 -91
- data/lib/lutaml/xmi/liquid_drops/operation_drop.rb +3 -9
- data/lib/lutaml/xmi/liquid_drops/package_drop.rb +16 -44
- data/lib/lutaml/xmi/liquid_drops/root_drop.rb +3 -11
- data/lib/lutaml/xmi/liquid_drops/source_target_drop.rb +2 -5
- data/lib/lutaml/xmi/parsers/xmi_base.rb +2 -749
- data/lib/lutaml/xmi/parsers/xmi_class_members.rb +45 -0
- data/lib/lutaml/xmi/parsers/xmi_connector.rb +251 -0
- data/lib/lutaml/xmi/parsers/xml.rb +7 -120
- data/lib/lutaml/xmi/xmi_lookup_service.rb +42 -0
- data/lib/lutaml.rb +0 -1
- metadata +48 -21
- data/lib/lutaml/cli/commands/base_command.rb +0 -118
- data/lib/lutaml/command_line.rb +0 -272
- data/lib/lutaml/sysml/allocate.rb +0 -9
- data/lib/lutaml/sysml/allocated.rb +0 -9
- data/lib/lutaml/sysml/binding_connector.rb +0 -9
- data/lib/lutaml/sysml/block.rb +0 -32
- data/lib/lutaml/sysml/constraint_block.rb +0 -14
- data/lib/lutaml/sysml/copy.rb +0 -8
- data/lib/lutaml/sysml/derive_requirement.rb +0 -9
- data/lib/lutaml/sysml/nested_connector_end.rb +0 -13
- data/lib/lutaml/sysml/refine.rb +0 -9
- data/lib/lutaml/sysml/requirement.rb +0 -44
- data/lib/lutaml/sysml/requirement_related.rb +0 -9
- data/lib/lutaml/sysml/satisfy.rb +0 -9
- data/lib/lutaml/sysml/test_case.rb +0 -25
- data/lib/lutaml/sysml/trace.rb +0 -9
- data/lib/lutaml/sysml/verify.rb +0 -8
- data/lib/lutaml/sysml/xmi_file.rb +0 -486
- data/lib/lutaml/sysml.rb +0 -11
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "id_generator"
|
|
4
|
+
require_relative "../../uml/model_helpers"
|
|
5
|
+
require_relative "../class_lookup_index"
|
|
6
|
+
require_relative "association_serialization"
|
|
7
|
+
require_relative "serializers/metadata_builder"
|
|
8
|
+
require_relative "serializers/package_tree_builder"
|
|
9
|
+
require_relative "serializers/package_serializer"
|
|
10
|
+
require_relative "serializers/class_serializer"
|
|
11
|
+
require_relative "serializers/attribute_serializer"
|
|
12
|
+
require_relative "serializers/operation_serializer"
|
|
13
|
+
require_relative "serializers/diagram_serializer"
|
|
14
|
+
require_relative "serializers/inheritance_resolver"
|
|
4
15
|
|
|
5
16
|
module Lutaml
|
|
6
17
|
module UmlRepository
|
|
7
18
|
module StaticSite
|
|
8
|
-
# Transforms a UmlRepository into a normalized JSON data structure
|
|
9
|
-
# optimized for client-side navigation and search.
|
|
10
|
-
#
|
|
11
|
-
# The output follows a normalized structure with:
|
|
12
|
-
# - Flat maps for packages, classes, attributes, associations
|
|
13
|
-
# - References by stable IDs
|
|
14
|
-
# - Hierarchical package tree for navigation
|
|
15
|
-
#
|
|
16
|
-
# @example
|
|
17
|
-
# repository = UmlRepository.from_package("model.lur")
|
|
18
|
-
# transformer = DataTransformer.new(repository)
|
|
19
|
-
# json_data = transformer.transform
|
|
20
19
|
class DataTransformer
|
|
20
|
+
include AssociationSerialization
|
|
21
|
+
include Lutaml::Uml::ModelHelpers
|
|
22
|
+
|
|
21
23
|
attr_reader :repository, :id_generator, :options
|
|
22
24
|
|
|
23
|
-
# Initialize transformer
|
|
24
|
-
#
|
|
25
|
-
# @param repository [UmlRepository] The repository to transform
|
|
26
|
-
# @param options [Hash] Transformation options
|
|
27
|
-
# @option options [Boolean] :include_diagrams Include diagram
|
|
28
|
-
# information
|
|
29
|
-
# @option options [Boolean] :format_definitions Format definitions
|
|
30
|
-
# as markdown
|
|
31
25
|
def initialize(repository, options = {})
|
|
32
26
|
@repository = repository
|
|
33
27
|
@options = default_options.merge(options)
|
|
@@ -35,19 +29,27 @@ module Lutaml
|
|
|
35
29
|
@generalization_map = build_generalization_map
|
|
36
30
|
end
|
|
37
31
|
|
|
38
|
-
# Transform repository to JSON structure
|
|
39
|
-
#
|
|
40
|
-
# @return [Hash] Normalized JSON data structure
|
|
41
32
|
def transform
|
|
42
33
|
{
|
|
43
|
-
metadata:
|
|
44
|
-
packageTree:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
metadata: Serializers::MetadataBuilder.new(repository).build,
|
|
35
|
+
packageTree: Serializers::PackageTreeBuilder.new(repository,
|
|
36
|
+
id_generator).build,
|
|
37
|
+
packages: Serializers::PackageSerializer.new(repository,
|
|
38
|
+
id_generator, options).build_map,
|
|
39
|
+
classes: Serializers::ClassSerializer.new(repository, id_generator,
|
|
40
|
+
options, inheritance_resolver).build_map,
|
|
41
|
+
attributes: Serializers::AttributeSerializer.new(repository,
|
|
42
|
+
id_generator, options).build_map,
|
|
48
43
|
associations: build_associations_map,
|
|
49
|
-
operations:
|
|
50
|
-
|
|
44
|
+
operations: Serializers::OperationSerializer.new(repository,
|
|
45
|
+
id_generator).build_map,
|
|
46
|
+
diagrams: (if options[:include_diagrams]
|
|
47
|
+
Serializers::DiagramSerializer.new(
|
|
48
|
+
repository, id_generator, options
|
|
49
|
+
).build_map
|
|
50
|
+
else
|
|
51
|
+
{}
|
|
52
|
+
end),
|
|
51
53
|
}
|
|
52
54
|
end
|
|
53
55
|
|
|
@@ -61,32 +63,29 @@ module Lutaml
|
|
|
61
63
|
}
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
def inheritance_resolver
|
|
67
|
+
@inheritance_resolver ||= Serializers::InheritanceResolver.new(
|
|
68
|
+
repository, id_generator, options, @generalization_map
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_generalization_map
|
|
66
73
|
map = Hash.new { |h, k| h[k] = [] }
|
|
67
74
|
|
|
68
|
-
# Scan all classes for generalization relationships
|
|
69
75
|
repository.classes_index.each do |klass|
|
|
70
76
|
next unless klass.respond_to?(:association_generalization)
|
|
71
|
-
unless klass.association_generalization &&
|
|
72
|
-
!klass.association_generalization.empty?
|
|
77
|
+
unless klass.association_generalization && !klass.association_generalization.empty?
|
|
73
78
|
next
|
|
74
79
|
end
|
|
75
80
|
|
|
76
|
-
# Each class has an association_generalization array with
|
|
77
|
-
# AssociationGeneralization objects
|
|
78
81
|
klass.association_generalization.each do |assoc_gen|
|
|
79
|
-
# Access lutaml-model object attributes directly
|
|
80
82
|
next unless assoc_gen.respond_to?(:parent_object_id)
|
|
81
83
|
|
|
82
84
|
parent_object_id = assoc_gen.parent_object_id
|
|
83
85
|
next unless parent_object_id
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
parent_class = find_class_by_object_id(parent_object_id)
|
|
87
|
+
parent_class = class_lookup.by_object_id(parent_object_id)
|
|
87
88
|
if parent_class&.xmi_id
|
|
88
|
-
# Skip self-referential generalization
|
|
89
|
-
# (class can't be its own parent)
|
|
90
89
|
next if parent_class.xmi_id == klass.xmi_id
|
|
91
90
|
|
|
92
91
|
unless map[klass.xmi_id].include?(parent_class.xmi_id)
|
|
@@ -99,545 +98,22 @@ module Lutaml
|
|
|
99
98
|
map
|
|
100
99
|
end
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
generated: Time.now.utc.iso8601,
|
|
106
|
-
generator: "LutaML Static Site Generator",
|
|
107
|
-
version: "1.0",
|
|
108
|
-
statistics: build_statistics,
|
|
109
|
-
}
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Build statistics
|
|
113
|
-
def build_statistics
|
|
114
|
-
{
|
|
115
|
-
packages: repository.packages_index.size,
|
|
116
|
-
classes: repository.classes_index.size,
|
|
117
|
-
associations: repository.associations_index.size,
|
|
118
|
-
attributes: count_total_attributes,
|
|
119
|
-
operations: count_total_operations,
|
|
120
|
-
}
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def count_total_attributes
|
|
124
|
-
repository.classes_index.sum do |klass|
|
|
125
|
-
klass.attributes&.size || 0
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def count_total_operations
|
|
130
|
-
repository.classes_index.sum do |klass|
|
|
131
|
-
(klass.respond_to?(:operations) ? klass.operations&.size : 0) || 0
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Build hierarchical package tree
|
|
136
|
-
def build_package_tree # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
137
|
-
# Get root packages from document.packages (not from index)
|
|
138
|
-
root_packages = if repository.document.respond_to?(:packages) &&
|
|
139
|
-
repository.document.packages
|
|
140
|
-
repository.document.packages
|
|
141
|
-
else
|
|
142
|
-
# Fallback: find packages without parent namespace
|
|
143
|
-
repository.packages_index.select do |pkg|
|
|
144
|
-
!pkg.respond_to?(:namespace) ||
|
|
145
|
-
pkg.namespace.nil? ||
|
|
146
|
-
!pkg.namespace.is_a?(Lutaml::Uml::Package)
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
if root_packages.size == 1
|
|
151
|
-
build_tree_node(root_packages.first)
|
|
152
|
-
else
|
|
153
|
-
# Multiple roots - create virtual root
|
|
154
|
-
{
|
|
155
|
-
id: "root",
|
|
156
|
-
name: "Model",
|
|
157
|
-
path: "",
|
|
158
|
-
classCount: 0,
|
|
159
|
-
children: root_packages.map { |pkg| build_tree_node(pkg) },
|
|
160
|
-
}
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def build_tree_node(package) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
165
|
-
pkg_id = @id_generator.package_id(package)
|
|
166
|
-
|
|
167
|
-
# Sort child packages by name
|
|
168
|
-
sorted_children = (package.packages || []).sort_by do |p|
|
|
169
|
-
p.name || ""
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Sort classes by name, filtering out unnamed classes
|
|
173
|
-
# This prevents unnamed classes from appearing in the tree or being
|
|
174
|
-
# counted
|
|
175
|
-
sorted_classes = (package.classes || [])
|
|
176
|
-
.reject { |c| c.name.nil? || c.name.empty? }
|
|
177
|
-
.sort_by(&:name)
|
|
178
|
-
|
|
179
|
-
# Build child nodes first to get their counts
|
|
180
|
-
child_nodes = sorted_children.map do |child|
|
|
181
|
-
build_tree_node(child)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Calculate total count including nested packages
|
|
185
|
-
# Only counts named classes (unnamed classes are already filtered out)
|
|
186
|
-
total_class_count = sorted_classes.size + child_nodes.sum do |child|
|
|
187
|
-
child[:classCount] || 0
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
{
|
|
191
|
-
id: pkg_id,
|
|
192
|
-
name: package.name,
|
|
193
|
-
path: package_path(package),
|
|
194
|
-
stereotypes: normalize_stereotypes(
|
|
195
|
-
package.respond_to?(:stereotype) ? package.stereotype : nil,
|
|
196
|
-
),
|
|
197
|
-
classCount: total_class_count,
|
|
198
|
-
classes: sorted_classes.map do |c|
|
|
199
|
-
{
|
|
200
|
-
id: @id_generator.class_id(c),
|
|
201
|
-
name: c.name,
|
|
202
|
-
stereotypes: normalize_stereotypes(
|
|
203
|
-
c.respond_to?(:stereotype) ? c.stereotype : nil,
|
|
204
|
-
),
|
|
205
|
-
}
|
|
206
|
-
end,
|
|
207
|
-
children: child_nodes,
|
|
208
|
-
}
|
|
101
|
+
def class_lookup
|
|
102
|
+
@class_lookup ||= ClassLookupIndex.new(repository.classes_index)
|
|
209
103
|
end
|
|
210
104
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
packages = {}
|
|
214
|
-
|
|
215
|
-
repository.packages_index.each do |package|
|
|
216
|
-
id = @id_generator.package_id(package)
|
|
217
|
-
packages[id] = serialize_package(package, id)
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
packages
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def serialize_package(package, id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
224
|
-
{
|
|
225
|
-
id: id,
|
|
226
|
-
xmiId: package.respond_to?(:xmi_id) ? package.xmi_id : nil,
|
|
227
|
-
name: package.name,
|
|
228
|
-
path: package_path(package),
|
|
229
|
-
definition: format_definition(
|
|
230
|
-
package.respond_to?(:definition) ? package.definition : nil,
|
|
231
|
-
),
|
|
232
|
-
stereotypes: normalize_stereotypes(
|
|
233
|
-
package.respond_to?(:stereotype) ? package.stereotype : nil,
|
|
234
|
-
),
|
|
235
|
-
classes: (package.classes || []).map do |c|
|
|
236
|
-
@id_generator.class_id(c)
|
|
237
|
-
end,
|
|
238
|
-
subPackages: (package.packages || []).map do |p|
|
|
239
|
-
@id_generator.package_id(p)
|
|
240
|
-
end,
|
|
241
|
-
diagrams: package_diagrams(package).map do |d|
|
|
242
|
-
@id_generator.diagram_id(d)
|
|
243
|
-
end,
|
|
244
|
-
parent: if package.respond_to?(:namespace) &&
|
|
245
|
-
package.namespace.is_a?(Lutaml::Uml::Package)
|
|
246
|
-
@id_generator.package_id(package.namespace)
|
|
247
|
-
end,
|
|
248
|
-
}
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Build classes map
|
|
252
|
-
def build_classes_map
|
|
253
|
-
classes = {}
|
|
254
|
-
|
|
255
|
-
repository.classes_index.each do |klass|
|
|
256
|
-
id = @id_generator.class_id(klass)
|
|
257
|
-
classes[id] = serialize_class(klass, id)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
classes
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def serialize_class(klass, id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
264
|
-
# Get associations and sort by local role
|
|
265
|
-
class_associations = find_class_associations(klass)
|
|
266
|
-
sorted_associations = class_associations.sort_by do |assoc_id|
|
|
267
|
-
assoc = repository.associations_index.find do |a|
|
|
268
|
-
@id_generator.association_id(a) == assoc_id
|
|
269
|
-
end
|
|
270
|
-
next "" unless assoc
|
|
271
|
-
|
|
272
|
-
# Determine local role for this class
|
|
273
|
-
if assoc.owner_end_xmi_id == klass.xmi_id
|
|
274
|
-
assoc.owner_end_attribute_name || assoc.owner_end || ""
|
|
275
|
-
elsif assoc.member_end_xmi_id == klass.xmi_id
|
|
276
|
-
assoc.member_end_attribute_name || assoc.member_end || ""
|
|
277
|
-
else
|
|
278
|
-
""
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
{
|
|
283
|
-
id: id,
|
|
284
|
-
xmiId: klass.xmi_id,
|
|
285
|
-
name: klass.name,
|
|
286
|
-
qualifiedName: qualified_name(klass),
|
|
287
|
-
type: class_type(klass),
|
|
288
|
-
package: package_id_for_class(klass),
|
|
289
|
-
stereotypes: normalize_stereotypes(
|
|
290
|
-
if klass.respond_to?(:stereotype)
|
|
291
|
-
klass.stereotype
|
|
292
|
-
end,
|
|
293
|
-
),
|
|
294
|
-
definition: format_definition(klass.definition),
|
|
295
|
-
attributes: (klass.attributes || []).sort_by do |a|
|
|
296
|
-
a.name || ""
|
|
297
|
-
end.map do |attr|
|
|
298
|
-
@id_generator.attribute_id(attr, klass)
|
|
299
|
-
end,
|
|
300
|
-
operations: serialize_class_operations(klass),
|
|
301
|
-
associations: sorted_associations,
|
|
302
|
-
generalizations: find_generalizations(klass),
|
|
303
|
-
specializations: find_specializations(klass),
|
|
304
|
-
isAbstract: if klass.respond_to?(:is_abstract)
|
|
305
|
-
klass.is_abstract
|
|
306
|
-
else
|
|
307
|
-
false
|
|
308
|
-
end,
|
|
309
|
-
literals: serialize_literals(klass),
|
|
310
|
-
inheritedAttributes: compute_inherited_attributes(klass),
|
|
311
|
-
inheritedAssociations: compute_inherited_associations(klass),
|
|
312
|
-
}
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
# Build attributes map
|
|
316
|
-
def build_attributes_map
|
|
317
|
-
attributes = {}
|
|
318
|
-
|
|
319
|
-
repository.classes_index.each do |klass|
|
|
320
|
-
next unless klass.attributes
|
|
321
|
-
|
|
322
|
-
klass.attributes.each do |attr|
|
|
323
|
-
id = @id_generator.attribute_id(attr, klass)
|
|
324
|
-
attributes[id] = serialize_attribute(attr, klass, id)
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
attributes
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
def serialize_attribute(attribute, owner, id) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
332
|
-
{
|
|
333
|
-
id: id,
|
|
334
|
-
name: attribute.name,
|
|
335
|
-
type: attribute.type,
|
|
336
|
-
visibility: attribute.visibility,
|
|
337
|
-
owner: @id_generator.class_id(owner),
|
|
338
|
-
ownerName: owner.name,
|
|
339
|
-
cardinality: serialize_cardinality(attribute.cardinality),
|
|
340
|
-
definition: format_definition(attribute.definition),
|
|
341
|
-
stereotypes: normalize_stereotypes(
|
|
342
|
-
attribute.respond_to?(:stereotype) ? attribute.stereotype : nil,
|
|
343
|
-
),
|
|
344
|
-
isStatic: if attribute.respond_to?(:is_static)
|
|
345
|
-
attribute.is_static
|
|
346
|
-
else
|
|
347
|
-
false
|
|
348
|
-
end,
|
|
349
|
-
isReadOnly: if attribute.respond_to?(:is_read_only)
|
|
350
|
-
attribute.is_read_only
|
|
351
|
-
else
|
|
352
|
-
false
|
|
353
|
-
end,
|
|
354
|
-
defaultValue: if attribute.respond_to?(:default)
|
|
355
|
-
attribute.default
|
|
356
|
-
end,
|
|
357
|
-
}
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
# Build associations map
|
|
361
|
-
# Uses repository.associations_index which handles both XMI
|
|
362
|
-
# and QEA formats
|
|
363
|
-
def build_associations_map
|
|
364
|
-
associations = {}
|
|
365
|
-
|
|
366
|
-
# Repository.associations_index collects from both:
|
|
367
|
-
# - Document-level associations (XMI format)
|
|
368
|
-
# - Class-level associations (QEA/EA format)
|
|
369
|
-
repository.associations_index.each do |assoc|
|
|
370
|
-
id = @id_generator.association_id(assoc)
|
|
371
|
-
associations[id] = serialize_association(assoc, id)
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
associations
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def serialize_association(association, id) # rubocop:disable Metrics/MethodLength
|
|
378
|
-
# Association model has member_end/owner_end as strings (class names)
|
|
379
|
-
# Use member_end_xmi_id, member_end_type etc for more details
|
|
380
|
-
# IMPORTANT: Do NOT generate synthetic names - use actual data only
|
|
381
|
-
|
|
382
|
-
# If association.name is nil, use the role name as fallback
|
|
383
|
-
# In EA models, the association name is often stored in the role
|
|
384
|
-
# fields
|
|
385
|
-
# IMPORTANT: Prioritize owner_end_attribute_name which has the
|
|
386
|
-
# actual role name
|
|
387
|
-
assoc_name = association.name
|
|
388
|
-
if assoc_name.nil? || assoc_name.empty?
|
|
389
|
-
# Try owner_end_attribute_name first (this is the role from owner's
|
|
390
|
-
# perspective)
|
|
391
|
-
assoc_name = association.owner_end_attribute_name
|
|
392
|
-
# Fallback to member_end_attribute_name (but this often contains
|
|
393
|
-
# class name, not role)
|
|
394
|
-
assoc_name = if assoc_name.nil? || assoc_name.empty?
|
|
395
|
-
association.member_end_attribute_name
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
{
|
|
400
|
-
id: id,
|
|
401
|
-
xmiId: association.xmi_id,
|
|
402
|
-
name: assoc_name,
|
|
403
|
-
type: "Association",
|
|
404
|
-
definition: format_definition(
|
|
405
|
-
if association.respond_to?(:definition)
|
|
406
|
-
association.definition
|
|
407
|
-
end,
|
|
408
|
-
),
|
|
409
|
-
source: build_association_source(association),
|
|
410
|
-
target: build_association_target(association),
|
|
411
|
-
}
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def build_association_source(association)
|
|
415
|
-
return nil unless association.owner_end
|
|
416
|
-
|
|
417
|
-
{
|
|
418
|
-
class: association.owner_end_xmi_id,
|
|
419
|
-
className: association.owner_end,
|
|
420
|
-
role: association.owner_end_attribute_name,
|
|
421
|
-
cardinality: serialize_cardinality(
|
|
422
|
-
association.owner_end_cardinality,
|
|
423
|
-
),
|
|
424
|
-
aggregation: association.owner_end_type,
|
|
425
|
-
}
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
def build_association_target(association)
|
|
429
|
-
return nil unless association.member_end
|
|
430
|
-
|
|
431
|
-
{
|
|
432
|
-
class: association.member_end_xmi_id,
|
|
433
|
-
className: association.member_end,
|
|
434
|
-
role: association.member_end_attribute_name,
|
|
435
|
-
cardinality: serialize_cardinality(
|
|
436
|
-
association.member_end_cardinality,
|
|
437
|
-
),
|
|
438
|
-
aggregation: association.member_end_type,
|
|
439
|
-
}
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
def serialize_association_end(end_obj) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
443
|
-
return nil unless end_obj
|
|
444
|
-
return nil unless end_obj.respond_to?(:type) && end_obj.type
|
|
445
|
-
|
|
446
|
-
# end_obj.type can be a String (class name) or a Class object
|
|
447
|
-
type_value = end_obj.type
|
|
448
|
-
|
|
449
|
-
if type_value.is_a?(String)
|
|
450
|
-
# Type is a string reference (class name)
|
|
451
|
-
{
|
|
452
|
-
class: nil, # Can't generate ID without class object
|
|
453
|
-
className: type_value,
|
|
454
|
-
role: end_obj.respond_to?(:name) ? end_obj.name : nil,
|
|
455
|
-
cardinality: serialize_cardinality(
|
|
456
|
-
if end_obj.respond_to?(:cardinality)
|
|
457
|
-
end_obj.cardinality
|
|
458
|
-
end,
|
|
459
|
-
),
|
|
460
|
-
navigable: if end_obj.respond_to?(:navigable?)
|
|
461
|
-
end_obj.navigable?
|
|
462
|
-
else
|
|
463
|
-
false
|
|
464
|
-
end,
|
|
465
|
-
aggregation: if end_obj.respond_to?(:aggregation)
|
|
466
|
-
end_obj.aggregation
|
|
467
|
-
end,
|
|
468
|
-
visibility: if end_obj.respond_to?(:visibility)
|
|
469
|
-
end_obj.visibility
|
|
470
|
-
end,
|
|
471
|
-
}
|
|
472
|
-
else
|
|
473
|
-
# Type is a class object
|
|
474
|
-
{
|
|
475
|
-
class: @id_generator.class_id(type_value),
|
|
476
|
-
className: if type_value.respond_to?(:name)
|
|
477
|
-
type_value.name
|
|
478
|
-
else
|
|
479
|
-
type_value.to_s
|
|
480
|
-
end,
|
|
481
|
-
role: end_obj.respond_to?(:name) ? end_obj.name : nil,
|
|
482
|
-
cardinality: serialize_cardinality(
|
|
483
|
-
if end_obj.respond_to?(:cardinality)
|
|
484
|
-
end_obj.cardinality
|
|
485
|
-
end,
|
|
486
|
-
),
|
|
487
|
-
navigable: if end_obj.respond_to?(:navigable?)
|
|
488
|
-
end_obj.navigable?
|
|
489
|
-
else
|
|
490
|
-
false
|
|
491
|
-
end,
|
|
492
|
-
aggregation: if end_obj.respond_to?(:aggregation)
|
|
493
|
-
end_obj.aggregation
|
|
494
|
-
end,
|
|
495
|
-
visibility: if end_obj.respond_to?(:visibility)
|
|
496
|
-
end_obj.visibility
|
|
497
|
-
end,
|
|
498
|
-
}
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
# Build operations map
|
|
503
|
-
def build_operations_map
|
|
504
|
-
operations = {}
|
|
505
|
-
|
|
506
|
-
repository.classes_index.each do |klass|
|
|
507
|
-
next unless klass.respond_to?(:operations) && klass.operations
|
|
508
|
-
|
|
509
|
-
klass.operations.each do |op|
|
|
510
|
-
id = @id_generator.operation_id(op, klass)
|
|
511
|
-
operations[id] = serialize_operation(op, klass, id)
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
operations
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
def serialize_operation(operation, owner, id) # rubocop:disable Metrics/MethodLength
|
|
519
|
-
{
|
|
520
|
-
id: id,
|
|
521
|
-
name: operation.name,
|
|
522
|
-
visibility: operation.visibility,
|
|
523
|
-
returnType: operation.return_type,
|
|
524
|
-
owner: @id_generator.class_id(owner),
|
|
525
|
-
ownerName: owner.name,
|
|
526
|
-
parameters: serialize_parameters(operation),
|
|
527
|
-
isStatic: if operation.respond_to?(:is_static)
|
|
528
|
-
operation.is_static
|
|
529
|
-
else
|
|
530
|
-
false
|
|
531
|
-
end,
|
|
532
|
-
isAbstract: if operation.respond_to?(:is_abstract)
|
|
533
|
-
operation.is_abstract
|
|
534
|
-
else
|
|
535
|
-
false
|
|
536
|
-
end,
|
|
537
|
-
}
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
def serialize_parameters(operation) # rubocop:disable Metrics/MethodLength
|
|
541
|
-
unless operation.respond_to?(:owned_parameter) &&
|
|
542
|
-
operation.owned_parameter
|
|
543
|
-
return []
|
|
544
|
-
end
|
|
545
|
-
|
|
546
|
-
operation.owned_parameter.map do |param|
|
|
547
|
-
{
|
|
548
|
-
name: param.name,
|
|
549
|
-
type: param.type,
|
|
550
|
-
direction: param.respond_to?(:direction) ? param.direction : "in",
|
|
551
|
-
}
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
# Build diagrams map
|
|
556
|
-
def build_diagrams_map
|
|
557
|
-
diagrams = {}
|
|
558
|
-
|
|
559
|
-
repository.diagrams_index.each do |diagram|
|
|
560
|
-
id = @id_generator.diagram_id(diagram)
|
|
561
|
-
diagrams[id] = serialize_diagram(diagram, id)
|
|
562
|
-
end
|
|
105
|
+
def find_class_by_xmi_id(xmi_id)
|
|
106
|
+
return nil unless xmi_id
|
|
563
107
|
|
|
564
|
-
|
|
108
|
+
class_lookup.by_xmi_id(xmi_id)
|
|
565
109
|
rescue StandardError
|
|
566
|
-
|
|
567
|
-
{}
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
def serialize_diagram(diagram, id)
|
|
571
|
-
{
|
|
572
|
-
id: id,
|
|
573
|
-
xmiId: diagram.xmi_id,
|
|
574
|
-
name: diagram.name,
|
|
575
|
-
type: diagram.diagram_type,
|
|
576
|
-
package: find_diagram_package(diagram),
|
|
577
|
-
}
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
# Helper methods
|
|
581
|
-
|
|
582
|
-
def package_path(package)
|
|
583
|
-
unless package.respond_to?(:namespace) && package.namespace
|
|
584
|
-
return package.name
|
|
585
|
-
end
|
|
586
|
-
return package.name unless package.namespace.is_a?(Lutaml::Uml::Package)
|
|
587
|
-
|
|
588
|
-
"#{package_path(package.namespace)}::#{package.name}"
|
|
589
|
-
end
|
|
590
|
-
|
|
591
|
-
def qualified_name(klass) # rubocop:disable Metrics/MethodLength
|
|
592
|
-
path_parts = []
|
|
593
|
-
current = klass
|
|
594
|
-
|
|
595
|
-
# Walk up the namespace chain
|
|
596
|
-
while current
|
|
597
|
-
if current.is_a?(Lutaml::Uml::TopElement)
|
|
598
|
-
path_parts.unshift(current.name)
|
|
599
|
-
current = current.namespace if current.respond_to?(:namespace)
|
|
600
|
-
elsif current.is_a?(Lutaml::Uml::Package)
|
|
601
|
-
path_parts.unshift(current.name)
|
|
602
|
-
current = current.namespace
|
|
603
|
-
else
|
|
604
|
-
break
|
|
605
|
-
end
|
|
606
|
-
end
|
|
607
|
-
|
|
608
|
-
path_parts.join("::")
|
|
609
|
-
end
|
|
610
|
-
|
|
611
|
-
def class_type(klass)
|
|
612
|
-
klass.class.name.split("::").last
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
def package_id_for_class(klass)
|
|
616
|
-
ns = klass.respond_to?(:namespace) ? klass.namespace : nil
|
|
617
|
-
return nil unless ns.is_a?(Lutaml::Uml::Package)
|
|
618
|
-
|
|
619
|
-
@id_generator.package_id(ns)
|
|
110
|
+
nil
|
|
620
111
|
end
|
|
621
112
|
|
|
622
|
-
def
|
|
623
|
-
return
|
|
624
|
-
|
|
625
|
-
# Use the package's direct diagrams attribute instead of querying
|
|
626
|
-
package.diagrams || []
|
|
627
|
-
rescue StandardError => e
|
|
628
|
-
warn "Error getting diagrams for #{package.name}: #{e.message}"
|
|
629
|
-
[]
|
|
630
|
-
end
|
|
113
|
+
def find_class_by_object_id(object_id)
|
|
114
|
+
return nil unless object_id
|
|
631
115
|
|
|
632
|
-
|
|
633
|
-
# Try to find which package owns this diagram
|
|
634
|
-
repository.packages_index.each do |pkg|
|
|
635
|
-
diagrams = package_diagrams(pkg)
|
|
636
|
-
if diagrams.any? { |d| d.xmi_id == diagram.xmi_id }
|
|
637
|
-
return @id_generator.package_id(pkg)
|
|
638
|
-
end
|
|
639
|
-
end
|
|
640
|
-
nil
|
|
116
|
+
class_lookup.by_object_id(object_id)
|
|
641
117
|
rescue StandardError
|
|
642
118
|
nil
|
|
643
119
|
end
|
|
@@ -651,316 +127,19 @@ module Lutaml
|
|
|
651
127
|
}
|
|
652
128
|
end
|
|
653
129
|
|
|
654
|
-
def format_definition(definition)
|
|
130
|
+
def format_definition(definition)
|
|
655
131
|
return nil if definition.nil? || definition.empty?
|
|
656
132
|
|
|
657
133
|
formatted = definition.strip
|
|
658
|
-
|
|
659
|
-
# Optionally truncate
|
|
660
134
|
if @options[:max_definition_length] &&
|
|
661
135
|
formatted.length > @options[:max_definition_length]
|
|
662
136
|
formatted = "#{formatted[0...@options[:max_definition_length]]}..."
|
|
663
137
|
end
|
|
664
|
-
|
|
665
|
-
# Optionally format as markdown (basic)
|
|
666
138
|
if @options[:format_definitions]
|
|
667
|
-
formatted =
|
|
139
|
+
formatted = formatted.gsub(%r{(https?://[^\s]+)}, '[\1](\1)')
|
|
668
140
|
end
|
|
669
|
-
|
|
670
141
|
formatted
|
|
671
142
|
end
|
|
672
|
-
|
|
673
|
-
def format_as_markdown(text)
|
|
674
|
-
# Basic markdown formatting
|
|
675
|
-
# - Preserve line breaks
|
|
676
|
-
# - Convert URLs to links
|
|
677
|
-
text.gsub(%r{(https?://[^\s]+)}, '[\1](\1)')
|
|
678
|
-
end
|
|
679
|
-
|
|
680
|
-
def serialize_class_operations(klass)
|
|
681
|
-
return [] unless klass.respond_to?(:operations) && klass.operations
|
|
682
|
-
|
|
683
|
-
klass.operations.map { |op| @id_generator.operation_id(op, klass) }
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
def find_class_associations(klass)
|
|
687
|
-
associations = repository.associations_of(klass)
|
|
688
|
-
associations.map { |assoc| @id_generator.association_id(assoc) }
|
|
689
|
-
rescue StandardError
|
|
690
|
-
[]
|
|
691
|
-
end
|
|
692
|
-
|
|
693
|
-
def find_generalizations(klass) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
694
|
-
# Use the pre-built generalization map for multiple inheritance
|
|
695
|
-
parent_xmi_ids = @generalization_map[klass.xmi_id]
|
|
696
|
-
|
|
697
|
-
if parent_xmi_ids && !parent_xmi_ids.empty?
|
|
698
|
-
# Map each parent XMI ID to class ID
|
|
699
|
-
parents = parent_xmi_ids.filter_map do |parent_xmi_id|
|
|
700
|
-
# Skip self-referential generalization
|
|
701
|
-
next if parent_xmi_id == klass.xmi_id
|
|
702
|
-
|
|
703
|
-
parent = find_class_by_xmi_id(parent_xmi_id)
|
|
704
|
-
parent ? @id_generator.class_id(parent) : nil
|
|
705
|
-
end
|
|
706
|
-
|
|
707
|
-
return parents unless parents.empty?
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
# Fallback: single parent via repository query
|
|
711
|
-
parent = repository.supertype_of(klass)
|
|
712
|
-
# Skip if parent is self (self-referential generalization)
|
|
713
|
-
return [] if parent && parent.xmi_id == klass.xmi_id
|
|
714
|
-
|
|
715
|
-
parent ? [@id_generator.class_id(parent)] : []
|
|
716
|
-
rescue StandardError => e
|
|
717
|
-
warn "Error finding generalizations for #{klass.name}: #{e.message}"
|
|
718
|
-
[]
|
|
719
|
-
end
|
|
720
|
-
|
|
721
|
-
def find_specializations(klass)
|
|
722
|
-
children = repository.subtypes_of(klass)
|
|
723
|
-
# Filter out self if somehow included
|
|
724
|
-
children.reject do |child|
|
|
725
|
-
child.xmi_id == klass.xmi_id
|
|
726
|
-
end.map { |child| @id_generator.class_id(child) }
|
|
727
|
-
rescue StandardError
|
|
728
|
-
[]
|
|
729
|
-
end
|
|
730
|
-
|
|
731
|
-
def serialize_literals(klass)
|
|
732
|
-
return [] unless klass.is_a?(Lutaml::Uml::Enum) && klass.owned_literal
|
|
733
|
-
|
|
734
|
-
klass.owned_literal.map do |literal|
|
|
735
|
-
{
|
|
736
|
-
name: literal.name,
|
|
737
|
-
definition: format_definition(literal.definition),
|
|
738
|
-
}
|
|
739
|
-
end
|
|
740
|
-
rescue StandardError
|
|
741
|
-
[]
|
|
742
|
-
end
|
|
743
|
-
|
|
744
|
-
def serialize_generalization(klass, visited = Set.new) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
745
|
-
unless klass.respond_to?(:generalization) && klass.generalization
|
|
746
|
-
return nil
|
|
747
|
-
end
|
|
748
|
-
return nil if visited.include?(klass.xmi_id) # Prevent infinite loops
|
|
749
|
-
|
|
750
|
-
visited.add(klass.xmi_id)
|
|
751
|
-
gen = klass.generalization
|
|
752
|
-
|
|
753
|
-
{
|
|
754
|
-
generalId: gen.general_id,
|
|
755
|
-
generalName: gen.general_name,
|
|
756
|
-
generalUpperKlass: if gen.respond_to?(:general_upper_klass)
|
|
757
|
-
gen.general_upper_klass
|
|
758
|
-
end,
|
|
759
|
-
hasGeneral: gen.respond_to?(:has_general) ? gen.has_general : false,
|
|
760
|
-
name: gen.name,
|
|
761
|
-
type: gen.type,
|
|
762
|
-
definition: format_definition(gen.definition),
|
|
763
|
-
stereotype: gen.respond_to?(:stereotype) ? gen.stereotype : nil,
|
|
764
|
-
ownedProps: (gen.respond_to?(:owned_props) ? gen.owned_props : [])
|
|
765
|
-
.map { |attr| serialize_general_attribute(attr) },
|
|
766
|
-
assocProps: (gen.respond_to?(:assoc_props) ? gen.assoc_props : [])
|
|
767
|
-
.map { |attr| serialize_general_attribute(attr) },
|
|
768
|
-
inheritedProps: (
|
|
769
|
-
gen.respond_to?(:inherited_props) ? gen.inherited_props : []
|
|
770
|
-
).map { |attr| serialize_general_attribute(attr) },
|
|
771
|
-
inheritedAssocProps: (
|
|
772
|
-
if gen.respond_to?(:inherited_assoc_props)
|
|
773
|
-
gen.inherited_assoc_props
|
|
774
|
-
else
|
|
775
|
-
[]
|
|
776
|
-
end
|
|
777
|
-
).map { |attr| serialize_general_attribute(attr) },
|
|
778
|
-
}
|
|
779
|
-
rescue StandardError => e
|
|
780
|
-
warn "Error serializing generalization: #{e.message}"
|
|
781
|
-
nil
|
|
782
|
-
end
|
|
783
|
-
|
|
784
|
-
def serialize_general_attribute(attr)
|
|
785
|
-
return nil unless attr
|
|
786
|
-
|
|
787
|
-
{
|
|
788
|
-
name: attr.name,
|
|
789
|
-
type: attr.type,
|
|
790
|
-
cardinality: serialize_cardinality(attr.cardinality),
|
|
791
|
-
definition: format_definition(attr.definition),
|
|
792
|
-
upperKlass: attr.respond_to?(:upper_klass) ? attr.upper_klass : nil,
|
|
793
|
-
nameNs: attr.respond_to?(:name_ns) ? attr.name_ns : nil,
|
|
794
|
-
typeNs: attr.respond_to?(:type_ns) ? attr.type_ns : nil,
|
|
795
|
-
}
|
|
796
|
-
end
|
|
797
|
-
|
|
798
|
-
def find_generalization(parent_id)
|
|
799
|
-
parent = find_class_by_xmi_id(parent_id)
|
|
800
|
-
return nil unless parent
|
|
801
|
-
|
|
802
|
-
# Recursively find all ancestors
|
|
803
|
-
find_all_ancestors(parent, []) || []
|
|
804
|
-
end
|
|
805
|
-
|
|
806
|
-
def find_all_ancestors(klass, ancestors = [])
|
|
807
|
-
return ancestors if klass.nil?
|
|
808
|
-
|
|
809
|
-
unless ancestors.include?(klass.xmi_id)
|
|
810
|
-
ancestors << klass.xmi_id
|
|
811
|
-
if klass.generalization&.general_classierarchy
|
|
812
|
-
find_all_ancestors(klass.generalization&.general_class,
|
|
813
|
-
ancestors)
|
|
814
|
-
end
|
|
815
|
-
end
|
|
816
|
-
ancestors
|
|
817
|
-
end
|
|
818
|
-
|
|
819
|
-
# Compute inherited attributes from generalization chain
|
|
820
|
-
def compute_inherited_attributes(klass, visited = Set.new) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
821
|
-
unless klass.respond_to?(:generalization) && klass.generalization
|
|
822
|
-
return []
|
|
823
|
-
end
|
|
824
|
-
return [] if visited.include?(klass.xmi_id) # Prevent infinite loops
|
|
825
|
-
|
|
826
|
-
visited.add(klass.xmi_id)
|
|
827
|
-
inherited = []
|
|
828
|
-
current_gen = klass.generalization
|
|
829
|
-
parent_order = 0
|
|
830
|
-
|
|
831
|
-
while current_gen
|
|
832
|
-
parent_class = find_class_by_xmi_id(current_gen.general_id)
|
|
833
|
-
break unless parent_class
|
|
834
|
-
break if visited.include?(parent_class.xmi_id) # Prevent cycles
|
|
835
|
-
|
|
836
|
-
visited.add(parent_class.xmi_id)
|
|
837
|
-
|
|
838
|
-
if parent_class.attributes
|
|
839
|
-
# Sort attributes by name within this parent
|
|
840
|
-
sorted_attrs = parent_class.attributes.sort_by do |a|
|
|
841
|
-
a.name || ""
|
|
842
|
-
end
|
|
843
|
-
sorted_attrs.each do |attr|
|
|
844
|
-
attr_id = @id_generator.attribute_id(attr, parent_class)
|
|
845
|
-
inherited << {
|
|
846
|
-
attributeId: attr_id,
|
|
847
|
-
attribute: serialize_attribute(attr, parent_class, attr_id),
|
|
848
|
-
inheritedFrom: @id_generator.class_id(parent_class),
|
|
849
|
-
inheritedFromName: parent_class.name,
|
|
850
|
-
parentOrder: parent_order, # Track hierarchy order
|
|
851
|
-
}
|
|
852
|
-
end
|
|
853
|
-
end
|
|
854
|
-
|
|
855
|
-
# Move to parent's parent
|
|
856
|
-
parent_order += 1
|
|
857
|
-
current_gen = if current_gen.respond_to?(:general)
|
|
858
|
-
current_gen.general
|
|
859
|
-
end
|
|
860
|
-
end
|
|
861
|
-
|
|
862
|
-
# Already sorted by parent hierarchy, then by name within parent
|
|
863
|
-
inherited
|
|
864
|
-
rescue StandardError => e
|
|
865
|
-
warn "Error computing inherited attributes: #{e.message}"
|
|
866
|
-
[]
|
|
867
|
-
end
|
|
868
|
-
|
|
869
|
-
# Compute inherited associations from generalization chain
|
|
870
|
-
def compute_inherited_associations(klass, visited = Set.new) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
871
|
-
unless klass.respond_to?(:generalization) && klass.generalization
|
|
872
|
-
return []
|
|
873
|
-
end
|
|
874
|
-
return [] if visited.include?(klass.xmi_id) # Prevent infinite loops
|
|
875
|
-
|
|
876
|
-
visited.add(klass.xmi_id)
|
|
877
|
-
inherited = []
|
|
878
|
-
current_gen = klass.generalization
|
|
879
|
-
parent_order = 0
|
|
880
|
-
|
|
881
|
-
while current_gen
|
|
882
|
-
parent_class = find_class_by_xmi_id(current_gen.general_id)
|
|
883
|
-
break unless parent_class
|
|
884
|
-
break if visited.include?(parent_class.xmi_id) # Prevent cycles
|
|
885
|
-
|
|
886
|
-
visited.add(parent_class.xmi_id)
|
|
887
|
-
|
|
888
|
-
parent_associations = find_class_associations(parent_class)
|
|
889
|
-
|
|
890
|
-
# Get association details and determine local role from parent's
|
|
891
|
-
# perspective
|
|
892
|
-
assoc_with_roles = parent_associations.filter_map do |assoc_id|
|
|
893
|
-
assoc = repository.associations_index.find do |a|
|
|
894
|
-
@id_generator.association_id(a) == assoc_id
|
|
895
|
-
end
|
|
896
|
-
next unless assoc
|
|
897
|
-
|
|
898
|
-
# Determine which role is the "local" one for the parent class
|
|
899
|
-
# This becomes the inherited local role
|
|
900
|
-
local_role = if assoc.owner_end_xmi_id == parent_class.xmi_id
|
|
901
|
-
assoc.owner_end_attribute_name || assoc.owner_end || ""
|
|
902
|
-
elsif assoc.member_end_xmi_id == parent_class.xmi_id
|
|
903
|
-
assoc.member_end_attribute_name || assoc.member_end || ""
|
|
904
|
-
else
|
|
905
|
-
""
|
|
906
|
-
end
|
|
907
|
-
|
|
908
|
-
{ id: assoc_id, role: local_role }
|
|
909
|
-
end
|
|
910
|
-
|
|
911
|
-
# Sort by local role within this parent
|
|
912
|
-
assoc_with_roles.sort_by { |a| a[:role] }.each do |item|
|
|
913
|
-
inherited << {
|
|
914
|
-
associationId: item[:id],
|
|
915
|
-
inheritedFrom: @id_generator.class_id(parent_class),
|
|
916
|
-
inheritedFromName: parent_class.name,
|
|
917
|
-
parentOrder: parent_order,
|
|
918
|
-
localRole: item[:role], # Include for template use
|
|
919
|
-
}
|
|
920
|
-
end
|
|
921
|
-
|
|
922
|
-
# Move to parent's parent
|
|
923
|
-
parent_order += 1
|
|
924
|
-
current_gen = if current_gen.respond_to?(:general)
|
|
925
|
-
current_gen.general
|
|
926
|
-
end
|
|
927
|
-
end
|
|
928
|
-
|
|
929
|
-
inherited
|
|
930
|
-
rescue StandardError => e
|
|
931
|
-
warn "Error computing inherited associations: #{e.message}"
|
|
932
|
-
[]
|
|
933
|
-
end
|
|
934
|
-
|
|
935
|
-
# Find class by XMI ID
|
|
936
|
-
def find_class_by_xmi_id(xmi_id)
|
|
937
|
-
return nil unless xmi_id
|
|
938
|
-
|
|
939
|
-
repository.classes_index.find { |c| c.xmi_id == xmi_id }
|
|
940
|
-
rescue StandardError
|
|
941
|
-
nil
|
|
942
|
-
end
|
|
943
|
-
|
|
944
|
-
# Find class by object ID (EA object ID)
|
|
945
|
-
def find_class_by_object_id(object_id)
|
|
946
|
-
return nil unless object_id
|
|
947
|
-
|
|
948
|
-
repository.classes_index.find do |c|
|
|
949
|
-
c.respond_to?(:ea_object_id) && c.ea_object_id == object_id
|
|
950
|
-
end
|
|
951
|
-
rescue StandardError
|
|
952
|
-
nil
|
|
953
|
-
end
|
|
954
|
-
|
|
955
|
-
# Normalize stereotype to always be an array
|
|
956
|
-
# @param stereotype [String, Array, nil] Stereotype value
|
|
957
|
-
# @return [Array<String>] Array of stereotypes
|
|
958
|
-
def normalize_stereotypes(stereotype)
|
|
959
|
-
return [] if stereotype.nil?
|
|
960
|
-
return stereotype if stereotype.is_a?(Array)
|
|
961
|
-
|
|
962
|
-
[stereotype]
|
|
963
|
-
end
|
|
964
143
|
end
|
|
965
144
|
end
|
|
966
145
|
end
|