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
@@ -11,6 +11,27 @@ require_relative "id_generator"
11
11
  module Lutaml
12
12
  module UmlRepository
13
13
  module StaticSite
14
+ # Resolves Liquid {% include %} paths to template files on disk.
15
+ # Unlike LocalFileSystem (which adds a "_" prefix), this resolves
16
+ # paths directly: "components/header" → "<root>/components/header.liquid"
17
+ class TemplateFileSystem
18
+ attr_reader :root
19
+
20
+ def initialize(root)
21
+ @root = root
22
+ end
23
+
24
+ def read_template_file(template_path)
25
+ full = File.expand_path("#{template_path}.liquid", @root)
26
+ unless full.start_with?(@root)
27
+ raise Liquid::FileSystemError,
28
+ "Illegal template path: #{template_path}"
29
+ end
30
+
31
+ File.read(full)
32
+ end
33
+ end
34
+
14
35
  # Main static site generator for LutaML UML Browser.
15
36
  #
16
37
  # Follows Dependency Inversion Principle by injecting dependencies
@@ -163,12 +184,9 @@ module Lutaml
163
184
  end
164
185
 
165
186
  def setup_liquid
166
- # Use environment instead of deprecated class-level setters
167
- environment = Liquid::Environment.new
168
- environment.file_system = Liquid::LocalFileSystem.new(@options[:template_path])
169
- # Changed from :strict to handle missing includes
170
- environment.error_mode = :lax
171
- @liquid_environment = environment
187
+ @liquid_environment = Liquid::Environment.new
188
+ @liquid_environment.file_system = TemplateFileSystem.new(@options[:template_path])
189
+ @liquid_environment.error_mode = :lax
172
190
  end
173
191
 
174
192
  # Generate single-file SPA
@@ -226,7 +244,7 @@ module Lutaml
226
244
 
227
245
  def render_components # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
228
246
  # Set up Liquid file system for recursive includes
229
- temp_file_system = Liquid::LocalFileSystem.new(@options[:template_path])
247
+ temp_file_system = TemplateFileSystem.new(@options[:template_path])
230
248
 
231
249
  component_names = ["header", "sidebar", "content", "package_details",
232
250
  "class_details"]
@@ -274,7 +292,10 @@ module Lutaml
274
292
  # Render and write index.html
275
293
  template_content = File.read(File.join(@options[:template_path],
276
294
  "multi_file.liquid"))
277
- template = Liquid::Template.parse(template_content)
295
+ file_system = TemplateFileSystem.new(@options[:template_path])
296
+ template = Liquid::Template.parse(template_content,
297
+ error_mode: :lax)
298
+ template.registers[:file_system] = file_system
278
299
  html = template.render(context)
279
300
  html = minify_html(html) if @options[:minify]
280
301
  File.write(File.join(output_dir, "index.html"), html)
@@ -271,10 +271,7 @@ module Lutaml
271
271
  current = klass
272
272
 
273
273
  while current
274
- if current.is_a?(Lutaml::Uml::TopElement)
275
- path_parts.unshift(current.name)
276
- current = current.namespace if current.respond_to?(:namespace)
277
- elsif current.is_a?(Lutaml::Uml::Package)
274
+ if current.is_a?(Lutaml::Uml::TopElement) || current.is_a?(Lutaml::Uml::Package)
278
275
  path_parts.unshift(current.name)
279
276
  current = current.namespace if current.respond_to?(:namespace)
280
277
  else
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../uml/model_helpers"
4
+
5
+ module Lutaml
6
+ module UmlRepository
7
+ module StaticSite
8
+ module Serializers
9
+ class AttributeSerializer
10
+ include Lutaml::Uml::ModelHelpers
11
+
12
+ def initialize(repository, id_generator, options)
13
+ @repository = repository
14
+ @id_generator = id_generator
15
+ @options = options
16
+ end
17
+
18
+ def build_map
19
+ attributes = {}
20
+ @repository.classes_index.each do |klass|
21
+ next unless klass.attributes
22
+
23
+ klass.attributes.each do |attr|
24
+ id = @id_generator.attribute_id(attr, klass)
25
+ attributes[id] = serialize(attr, klass, id)
26
+ end
27
+ end
28
+ attributes
29
+ end
30
+
31
+ def serialize(attribute, owner, id)
32
+ {
33
+ id: id,
34
+ name: attribute.name,
35
+ type: attribute.type,
36
+ visibility: attribute.visibility,
37
+ owner: @id_generator.class_id(owner),
38
+ ownerName: owner.name,
39
+ cardinality: serialize_cardinality(attribute.cardinality),
40
+ definition: format_definition(attribute.definition),
41
+ stereotypes: normalize_stereotypes(
42
+ attribute.respond_to?(:stereotype) ? attribute.stereotype : nil,
43
+ ),
44
+ isStatic: attribute.respond_to?(:is_static) ? attribute.is_static : false,
45
+ isReadOnly: attribute.respond_to?(:is_read_only) ? attribute.is_read_only : false,
46
+ defaultValue: attribute.respond_to?(:default) ? attribute.default : nil,
47
+ }
48
+ end
49
+
50
+ private
51
+
52
+ def serialize_cardinality(cardinality)
53
+ return nil unless cardinality
54
+
55
+ {
56
+ min: cardinality.min,
57
+ max: cardinality.max,
58
+ }
59
+ end
60
+
61
+ def format_definition(definition)
62
+ return nil if definition.nil? || definition.empty?
63
+
64
+ formatted = definition.strip
65
+ if @options[:max_definition_length] &&
66
+ formatted.length > @options[:max_definition_length]
67
+ formatted = "#{formatted[0...@options[:max_definition_length]]}..."
68
+ end
69
+ if @options[:format_definitions]
70
+ formatted = formatted.gsub(%r{(https?://[^\s]+)}, '[\1](\1)')
71
+ end
72
+ formatted
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../uml/model_helpers"
4
+
5
+ module Lutaml
6
+ module UmlRepository
7
+ module StaticSite
8
+ module Serializers
9
+ class ClassSerializer
10
+ include Lutaml::Uml::ModelHelpers
11
+
12
+ def initialize(repository, id_generator, options,
13
+ inheritance_resolver)
14
+ @repository = repository
15
+ @id_generator = id_generator
16
+ @options = options
17
+ @inheritance_resolver = inheritance_resolver
18
+ end
19
+
20
+ def build_map
21
+ classes = {}
22
+ @repository.classes_index.each do |klass|
23
+ id = @id_generator.class_id(klass)
24
+ classes[id] = serialize(klass, id)
25
+ end
26
+ classes
27
+ end
28
+
29
+ private
30
+
31
+ def serialize(klass, id) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
32
+ class_associations = find_class_associations(klass)
33
+ sorted_associations = sort_associations(class_associations, klass)
34
+
35
+ {
36
+ id: id,
37
+ xmiId: klass.xmi_id,
38
+ name: klass.name,
39
+ qualifiedName: qualified_name_for(klass),
40
+ type: class_type_for(klass),
41
+ package: package_id_for_class(klass),
42
+ stereotypes: normalize_stereotypes(
43
+ klass.respond_to?(:stereotype) ? klass.stereotype : nil,
44
+ ),
45
+ definition: format_definition(klass.definition),
46
+ attributes: (klass.attributes || []).sort_by { |a| a.name || "" }
47
+ .map { |attr| @id_generator.attribute_id(attr, klass) },
48
+ operations: serialize_class_operations(klass),
49
+ associations: sorted_associations,
50
+ generalizations: @inheritance_resolver.find_generalizations(klass),
51
+ specializations: @inheritance_resolver.find_specializations(klass),
52
+ isAbstract: klass.respond_to?(:is_abstract) ? klass.is_abstract : false,
53
+ literals: serialize_literals(klass),
54
+ inheritedAttributes: @inheritance_resolver.compute_inherited_attributes(klass),
55
+ inheritedAssociations: @inheritance_resolver.compute_inherited_associations(klass),
56
+ }
57
+ end
58
+
59
+ def find_class_associations(klass)
60
+ associations = @repository.associations_of(klass)
61
+ associations.map { |assoc| @id_generator.association_id(assoc) }
62
+ rescue StandardError
63
+ []
64
+ end
65
+
66
+ def sort_associations(assoc_ids, klass)
67
+ assoc_ids.sort_by do |assoc_id|
68
+ assoc = @repository.associations_index.find do |a|
69
+ @id_generator.association_id(a) == assoc_id
70
+ end
71
+ next "" unless assoc
72
+
73
+ if assoc.owner_end_xmi_id == klass.xmi_id
74
+ assoc.owner_end_attribute_name || assoc.owner_end || ""
75
+ elsif assoc.member_end_xmi_id == klass.xmi_id
76
+ assoc.member_end_attribute_name || assoc.member_end || ""
77
+ else
78
+ ""
79
+ end
80
+ end
81
+ end
82
+
83
+ def serialize_class_operations(klass)
84
+ return [] unless klass.respond_to?(:operations) && klass.operations
85
+
86
+ klass.operations.map { |op| @id_generator.operation_id(op, klass) }
87
+ end
88
+
89
+ def serialize_literals(klass)
90
+ return [] unless klass.is_a?(Lutaml::Uml::Enum) && klass.owned_literal
91
+
92
+ klass.owned_literal.map do |literal|
93
+ { name: literal.name,
94
+ definition: format_definition(literal.definition) }
95
+ end
96
+ rescue StandardError
97
+ []
98
+ end
99
+
100
+ def package_id_for_class(klass)
101
+ ns = klass.respond_to?(:namespace) ? klass.namespace : nil
102
+ return nil unless ns.is_a?(Lutaml::Uml::Package)
103
+
104
+ @id_generator.package_id(ns)
105
+ end
106
+
107
+ def format_definition(definition)
108
+ return nil if definition.nil? || definition.empty?
109
+
110
+ formatted = definition.strip
111
+ if @options[:max_definition_length] &&
112
+ formatted.length > @options[:max_definition_length]
113
+ formatted = "#{formatted[0...@options[:max_definition_length]]}..."
114
+ end
115
+ if @options[:format_definitions]
116
+ formatted = formatted.gsub(%r{(https?://[^\s]+)}, '[\1](\1)')
117
+ end
118
+ formatted
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module UmlRepository
5
+ module StaticSite
6
+ module Serializers
7
+ class DiagramSerializer
8
+ def initialize(repository, id_generator, options)
9
+ @repository = repository
10
+ @id_generator = id_generator
11
+ @options = options
12
+ end
13
+
14
+ def build_map
15
+ diagrams = {}
16
+ @repository.diagrams_index.each do |diagram|
17
+ id = @id_generator.diagram_id(diagram)
18
+ diagrams[id] = serialize(diagram, id)
19
+ end
20
+ diagrams
21
+ rescue StandardError
22
+ {}
23
+ end
24
+
25
+ private
26
+
27
+ def serialize(diagram, id)
28
+ {
29
+ id: id,
30
+ xmiId: diagram.xmi_id,
31
+ name: diagram.name,
32
+ type: diagram.diagram_type,
33
+ package: find_diagram_package(diagram),
34
+ }
35
+ end
36
+
37
+ def find_diagram_package(diagram)
38
+ @repository.packages_index.each do |pkg|
39
+ diagrams = package_diagrams(pkg)
40
+ if diagrams.any? { |d| d.xmi_id == diagram.xmi_id }
41
+ return @id_generator.package_id(pkg)
42
+ end
43
+ end
44
+ nil
45
+ rescue StandardError
46
+ nil
47
+ end
48
+
49
+ def package_diagrams(package)
50
+ return [] unless @options[:include_diagrams]
51
+
52
+ package.diagrams || []
53
+ rescue StandardError
54
+ []
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../uml/model_helpers"
4
+ require_relative "../../class_lookup_index"
5
+
6
+ module Lutaml
7
+ module UmlRepository
8
+ module StaticSite
9
+ module Serializers
10
+ class InheritanceResolver
11
+ include Lutaml::Uml::ModelHelpers
12
+
13
+ def initialize(repository, id_generator, options, generalization_map)
14
+ @repository = repository
15
+ @id_generator = id_generator
16
+ @options = options
17
+ @generalization_map = generalization_map
18
+ end
19
+
20
+ def find_generalizations(klass)
21
+ parent_xmi_ids = @generalization_map[klass.xmi_id]
22
+
23
+ if parent_xmi_ids && !parent_xmi_ids.empty?
24
+ parents = parent_xmi_ids.filter_map do |parent_xmi_id|
25
+ next if parent_xmi_id == klass.xmi_id
26
+
27
+ parent = class_lookup.by_xmi_id(parent_xmi_id)
28
+ parent ? @id_generator.class_id(parent) : nil
29
+ end
30
+ return parents unless parents.empty?
31
+ end
32
+
33
+ parent = @repository.supertype_of(klass)
34
+ return [] if parent && parent.xmi_id == klass.xmi_id
35
+
36
+ parent ? [@id_generator.class_id(parent)] : []
37
+ rescue StandardError => e
38
+ warn "Error finding generalizations for #{klass.name}: #{e.message}"
39
+ []
40
+ end
41
+
42
+ def find_specializations(klass)
43
+ children = @repository.subtypes_of(klass)
44
+ children.reject { |child| child.xmi_id == klass.xmi_id }
45
+ .map { |child| @id_generator.class_id(child) }
46
+ rescue StandardError
47
+ []
48
+ end
49
+
50
+ def compute_inherited_attributes(klass, visited = Set.new)
51
+ unless klass.respond_to?(:generalization) && klass.generalization
52
+ return []
53
+ end
54
+ return [] if visited.include?(klass.xmi_id)
55
+
56
+ visited.add(klass.xmi_id)
57
+ inherited = []
58
+ current_gen = klass.generalization
59
+ parent_order = 0
60
+
61
+ while current_gen
62
+ parent_class = class_lookup.by_xmi_id(current_gen.general_id)
63
+ break unless parent_class
64
+ break if visited.include?(parent_class.xmi_id)
65
+
66
+ visited.add(parent_class.xmi_id)
67
+
68
+ if parent_class.attributes
69
+ sorted_attrs = parent_class.attributes.sort_by do |a|
70
+ a.name || ""
71
+ end
72
+ sorted_attrs.each do |attr|
73
+ attr_id = @id_generator.attribute_id(attr, parent_class)
74
+ inherited << {
75
+ attributeId: attr_id,
76
+ attribute: serialize_attribute(attr, parent_class, attr_id),
77
+ inheritedFrom: @id_generator.class_id(parent_class),
78
+ inheritedFromName: parent_class.name,
79
+ parentOrder: parent_order,
80
+ }
81
+ end
82
+ end
83
+
84
+ parent_order += 1
85
+ current_gen = current_gen.general if current_gen.respond_to?(:general)
86
+ end
87
+
88
+ inherited
89
+ rescue StandardError => e
90
+ warn "Error computing inherited attributes: #{e.message}"
91
+ []
92
+ end
93
+
94
+ def compute_inherited_associations(klass, visited = Set.new)
95
+ unless klass.respond_to?(:generalization) && klass.generalization
96
+ return []
97
+ end
98
+ return [] if visited.include?(klass.xmi_id)
99
+
100
+ visited.add(klass.xmi_id)
101
+ inherited = []
102
+ current_gen = klass.generalization
103
+ parent_order = 0
104
+
105
+ while current_gen
106
+ parent_class = class_lookup.by_xmi_id(current_gen.general_id)
107
+ break unless parent_class
108
+ break if visited.include?(parent_class.xmi_id)
109
+
110
+ visited.add(parent_class.xmi_id)
111
+ parent_associations = find_class_associations(parent_class)
112
+
113
+ assoc_with_roles = parent_associations.filter_map do |assoc_id|
114
+ assoc = @repository.associations_index.find do |a|
115
+ @id_generator.association_id(a) == assoc_id
116
+ end
117
+ next unless assoc
118
+
119
+ local_role = if assoc.owner_end_xmi_id == parent_class.xmi_id
120
+ assoc.owner_end_attribute_name || assoc.owner_end || ""
121
+ elsif assoc.member_end_xmi_id == parent_class.xmi_id
122
+ assoc.member_end_attribute_name || assoc.member_end || ""
123
+ else
124
+ ""
125
+ end
126
+ { id: assoc_id, role: local_role }
127
+ end
128
+
129
+ assoc_with_roles.sort_by { |a| a[:role] }.each do |item|
130
+ inherited << {
131
+ associationId: item[:id],
132
+ inheritedFrom: @id_generator.class_id(parent_class),
133
+ inheritedFromName: parent_class.name,
134
+ parentOrder: parent_order,
135
+ localRole: item[:role],
136
+ }
137
+ end
138
+
139
+ parent_order += 1
140
+ current_gen = current_gen.general if current_gen.respond_to?(:general)
141
+ end
142
+
143
+ inherited
144
+ rescue StandardError => e
145
+ warn "Error computing inherited associations: #{e.message}"
146
+ []
147
+ end
148
+
149
+ def serialize_generalization(klass, visited = Set.new)
150
+ unless klass.respond_to?(:generalization) && klass.generalization
151
+ return nil
152
+ end
153
+ return nil if visited.include?(klass.xmi_id)
154
+
155
+ visited.add(klass.xmi_id)
156
+ gen = klass.generalization
157
+
158
+ {
159
+ generalId: gen.general_id,
160
+ generalName: gen.general_name,
161
+ generalUpperKlass: gen.respond_to?(:general_upper_klass) ? gen.general_upper_klass : nil,
162
+ hasGeneral: gen.respond_to?(:has_general) ? gen.has_general : false,
163
+ name: gen.name,
164
+ type: gen.type,
165
+ definition: format_definition(gen.definition),
166
+ stereotype: gen.respond_to?(:stereotype) ? gen.stereotype : nil,
167
+ ownedProps: serialize_general_attrs(gen, :owned_props),
168
+ assocProps: serialize_general_attrs(gen, :assoc_props),
169
+ inheritedProps: serialize_general_attrs(gen, :inherited_props),
170
+ inheritedAssocProps: serialize_general_attrs(gen,
171
+ :inherited_assoc_props),
172
+ }
173
+ rescue StandardError => e
174
+ warn "Error serializing generalization: #{e.message}"
175
+ nil
176
+ end
177
+
178
+ def serialize_general_attribute(attr)
179
+ return nil unless attr
180
+
181
+ {
182
+ name: attr.name,
183
+ type: attr.type,
184
+ cardinality: serialize_cardinality(attr.cardinality),
185
+ definition: format_definition(attr.definition),
186
+ upperKlass: attr.respond_to?(:upper_klass) ? attr.upper_klass : nil,
187
+ nameNs: attr.respond_to?(:name_ns) ? attr.name_ns : nil,
188
+ typeNs: attr.respond_to?(:type_ns) ? attr.type_ns : nil,
189
+ }
190
+ end
191
+
192
+ private
193
+
194
+ def class_lookup
195
+ @class_lookup ||= ClassLookupIndex.new(@repository.classes_index)
196
+ end
197
+
198
+ def find_class_associations(klass)
199
+ associations = @repository.associations_of(klass)
200
+ associations.map { |assoc| @id_generator.association_id(assoc) }
201
+ rescue StandardError
202
+ []
203
+ end
204
+
205
+ def serialize_attribute(attribute, owner, id)
206
+ {
207
+ id: id,
208
+ name: attribute.name,
209
+ type: attribute.type,
210
+ visibility: attribute.visibility,
211
+ owner: @id_generator.class_id(owner),
212
+ ownerName: owner.name,
213
+ cardinality: serialize_cardinality(attribute.cardinality),
214
+ definition: format_definition(attribute.definition),
215
+ stereotypes: normalize_stereotypes(
216
+ attribute.respond_to?(:stereotype) ? attribute.stereotype : nil,
217
+ ),
218
+ isStatic: attribute.respond_to?(:is_static) ? attribute.is_static : false,
219
+ isReadOnly: attribute.respond_to?(:is_read_only) ? attribute.is_read_only : false,
220
+ defaultValue: attribute.respond_to?(:default) ? attribute.default : nil,
221
+ }
222
+ end
223
+
224
+ def serialize_general_attrs(gen, method)
225
+ return [] unless gen.respond_to?(method)
226
+
227
+ (gen.send(method) || []).map do |attr|
228
+ serialize_general_attribute(attr)
229
+ end
230
+ end
231
+
232
+ def serialize_cardinality(cardinality)
233
+ return nil unless cardinality
234
+
235
+ {
236
+ min: cardinality.min,
237
+ max: cardinality.max,
238
+ }
239
+ end
240
+
241
+ def format_definition(definition)
242
+ return nil if definition.nil? || definition.empty?
243
+
244
+ formatted = definition.strip
245
+ if @options[:max_definition_length] &&
246
+ formatted.length > @options[:max_definition_length]
247
+ formatted = "#{formatted[0...@options[:max_definition_length]]}..."
248
+ end
249
+ if @options[:format_definitions]
250
+ formatted = formatted.gsub(%r{(https?://[^\s]+)}, '[\1](\1)')
251
+ end
252
+ formatted
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module UmlRepository
5
+ module StaticSite
6
+ module Serializers
7
+ class MetadataBuilder
8
+ def initialize(repository)
9
+ @repository = repository
10
+ end
11
+
12
+ def build
13
+ {
14
+ generated: Time.now.utc.iso8601,
15
+ generator: "LutaML Static Site Generator",
16
+ version: "1.0",
17
+ statistics: build_statistics,
18
+ }
19
+ end
20
+
21
+ private
22
+
23
+ def build_statistics
24
+ {
25
+ packages: @repository.packages_index.size,
26
+ classes: @repository.classes_index.size,
27
+ associations: @repository.associations_index.size,
28
+ attributes: count_total_attributes,
29
+ operations: count_total_operations,
30
+ }
31
+ end
32
+
33
+ def count_total_attributes
34
+ @repository.classes_index.sum do |klass|
35
+ klass.attributes&.size || 0
36
+ end
37
+ end
38
+
39
+ def count_total_operations
40
+ @repository.classes_index.sum do |klass|
41
+ (klass.respond_to?(:operations) ? klass.operations&.size : 0) || 0
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end