ea 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +125 -0
- data/Rakefile +12 -4
- data/TODO.next/00-publish-blocking-bugs.md +74 -0
- data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
- data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
- data/TODO.next/03-slim-lutaml-uml.md +79 -0
- data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
- data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
- data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
- data/TODO.next/07-transformer-registry-in-factory.md +20 -0
- data/TODO.next/08-connector-type-registry.md +27 -0
- data/TODO.next/09-element-renderer-registry.md +29 -0
- data/TODO.next/10-connector-renderer-lsp.md +18 -0
- data/TODO.next/11-consolidate-style-knowledge.md +33 -0
- data/TODO.next/12-data-driven-from-db-row.md +24 -0
- data/TODO.next/13-extract-duplicated-methods.md +17 -0
- data/TODO.next/14-remove-dead-code.md +10 -0
- data/TODO.next/15-narrow-exception-handling.md +39 -0
- data/TODO.next/16-repository-indexes.md +28 -0
- data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
- data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
- data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
- data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
- data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
- data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
- data/TODO.next/23-cleanup-idallocator.md +41 -0
- data/TODO.next/24-tighten-parity-specs.md +42 -0
- data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
- data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
- data/TODO.next/27-extract-cardinality-module.md +68 -0
- data/TODO.next/28-extract-xml-sanitizer.md +51 -0
- data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
- data/TODO.next/30-struct-return-for-association-end.md +37 -0
- data/TODO.next/31-idallocator-specs.md +27 -0
- data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
- data/TODO.next/33-normalize-lower-cleanup.md +30 -0
- data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
- data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
- data/TODO.next/36-wire-interface-realization.md +50 -0
- data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
- data/config/diagram_styles.yml +200 -0
- data/config/model_transformations.yml +266 -0
- data/config/qea_schema.yml +1024 -0
- data/docs/ea_to_uml_type_mapping.md +89 -0
- data/docs/xmi_qea_conversion_capabilities.md +99 -0
- data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
- data/examples/lur/basic.lur +0 -0
- data/examples/lur/test-output.lur +0 -0
- data/examples/lur/test.lur +0 -0
- data/examples/lur_basic_usage.rb +221 -0
- data/examples/lur_cli_workflow.rb +263 -0
- data/examples/lur_statistics.rb +326 -0
- data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
- data/examples/qea/ArcGISWorkspace_template.qea +0 -0
- data/examples/qea/README_qea_parser.adoc +230 -0
- data/examples/qea/UmlModel_template.qea +0 -0
- data/examples/qea/basic.qea +0 -0
- data/examples/qea/simple.qea +0 -0
- data/examples/qea/simple_example.qea +0 -0
- data/examples/qea/test.qea +0 -0
- data/examples/qea_standalone_query.rb +73 -0
- data/examples/qea_to_repository.rb +51 -0
- data/examples/smoke_test_real_qea.rb +81 -0
- data/exe/ea +7 -0
- data/lib/ea/cli/app.rb +72 -0
- data/lib/ea/cli/command/base.rb +80 -0
- data/lib/ea/cli/command/convert.rb +62 -0
- data/lib/ea/cli/command/diagrams.rb +81 -0
- data/lib/ea/cli/command/list.rb +61 -0
- data/lib/ea/cli/command/parse.rb +29 -0
- data/lib/ea/cli/command/stats.rb +20 -0
- data/lib/ea/cli/command/validate.rb +41 -0
- data/lib/ea/cli/command.rb +15 -0
- data/lib/ea/cli/error.rb +34 -0
- data/lib/ea/cli/output/formatter.rb +34 -0
- data/lib/ea/cli/output/json_formatter.rb +20 -0
- data/lib/ea/cli/output/table_formatter.rb +42 -0
- data/lib/ea/cli/output/yaml_formatter.rb +20 -0
- data/lib/ea/cli/output.rb +56 -0
- data/lib/ea/cli.rb +17 -0
- data/lib/ea/diagram/configuration.rb +379 -0
- data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
- data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
- data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
- data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
- data/lib/ea/diagram/element_renderers.rb +43 -0
- data/lib/ea/diagram/extractor.rb +560 -0
- data/lib/ea/diagram/layout_engine.rb +170 -0
- data/lib/ea/diagram/path_builder.rb +202 -0
- data/lib/ea/diagram/style_parser.rb +42 -0
- data/lib/ea/diagram/style_resolver.rb +276 -0
- data/lib/ea/diagram/svg_renderer.rb +274 -0
- data/lib/ea/diagram/util.rb +73 -0
- data/lib/ea/diagram.rb +47 -0
- data/lib/ea/qea/benchmark.rb +210 -0
- data/lib/ea/qea/database.rb +308 -0
- data/lib/ea/qea/factory/association_builder.rb +203 -0
- data/lib/ea/qea/factory/association_transformer.rb +91 -0
- data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
- data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
- data/lib/ea/qea/factory/base_transformer.rb +177 -0
- data/lib/ea/qea/factory/class_transformer.rb +116 -0
- data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
- data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
- data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
- data/lib/ea/qea/factory/document_builder.rb +283 -0
- data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
- data/lib/ea/qea/factory/enum_transformer.rb +74 -0
- data/lib/ea/qea/factory/generalization_builder.rb +227 -0
- data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
- data/lib/ea/qea/factory/instance_transformer.rb +68 -0
- data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
- data/lib/ea/qea/factory/operation_transformer.rb +66 -0
- data/lib/ea/qea/factory/package_transformer.rb +145 -0
- data/lib/ea/qea/factory/reference_resolver.rb +99 -0
- data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
- data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
- data/lib/ea/qea/factory/transformer_registry.rb +80 -0
- data/lib/ea/qea/factory.rb +37 -0
- data/lib/ea/qea/file_detector.rb +178 -0
- data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
- data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
- data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
- data/lib/ea/qea/infrastructure.rb +12 -0
- data/lib/ea/qea/models/base_model.rb +59 -0
- data/lib/ea/qea/models/ea_attribute.rb +109 -0
- data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
- data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
- data/lib/ea/qea/models/ea_connector.rb +160 -0
- data/lib/ea/qea/models/ea_connector_type.rb +60 -0
- data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
- data/lib/ea/qea/models/ea_datatype.rb +104 -0
- data/lib/ea/qea/models/ea_diagram.rb +115 -0
- data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
- data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
- data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
- data/lib/ea/qea/models/ea_document.rb +63 -0
- data/lib/ea/qea/models/ea_object.rb +223 -0
- data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
- data/lib/ea/qea/models/ea_object_property.rb +87 -0
- data/lib/ea/qea/models/ea_object_type.rb +73 -0
- data/lib/ea/qea/models/ea_operation.rb +127 -0
- data/lib/ea/qea/models/ea_operation_param.rb +76 -0
- data/lib/ea/qea/models/ea_package.rb +78 -0
- data/lib/ea/qea/models/ea_script.rb +62 -0
- data/lib/ea/qea/models/ea_status_type.rb +66 -0
- data/lib/ea/qea/models/ea_stereotype.rb +57 -0
- data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
- data/lib/ea/qea/models/ea_xref.rb +165 -0
- data/lib/ea/qea/models.rb +35 -0
- data/lib/ea/qea/repositories/base_repository.rb +225 -0
- data/lib/ea/qea/repositories/object_repository.rb +219 -0
- data/lib/ea/qea/repositories.rb +10 -0
- data/lib/ea/qea/services/configuration.rb +211 -0
- data/lib/ea/qea/services/database_loader.rb +191 -0
- data/lib/ea/qea/services.rb +10 -0
- data/lib/ea/qea/validation/association_validator.rb +73 -0
- data/lib/ea/qea/validation/attribute_validator.rb +91 -0
- data/lib/ea/qea/validation/base_validator.rb +331 -0
- data/lib/ea/qea/validation/class_validator.rb +121 -0
- data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
- data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
- data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
- data/lib/ea/qea/validation/database.rb +16 -0
- data/lib/ea/qea/validation/diagram_validator.rb +112 -0
- data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
- data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
- data/lib/ea/qea/validation/formatters.rb +12 -0
- data/lib/ea/qea/validation/operation_validator.rb +71 -0
- data/lib/ea/qea/validation/package_validator.rb +111 -0
- data/lib/ea/qea/validation/validation_engine.rb +387 -0
- data/lib/ea/qea/validation/validation_message.rb +144 -0
- data/lib/ea/qea/validation/validation_result.rb +210 -0
- data/lib/ea/qea/validation/validator_registry.rb +134 -0
- data/lib/ea/qea/validation.rb +28 -0
- data/lib/ea/qea/verification/comparison_result.rb +264 -0
- data/lib/ea/qea/verification/document_normalizer.rb +169 -0
- data/lib/ea/qea/verification/document_verifier.rb +322 -0
- data/lib/ea/qea/verification/element_comparator.rb +277 -0
- data/lib/ea/qea/verification/structure_matcher.rb +287 -0
- data/lib/ea/qea/verification.rb +14 -0
- data/lib/ea/qea.rb +185 -0
- data/lib/ea/transformations/configuration.rb +333 -0
- data/lib/ea/transformations/format_registry.rb +366 -0
- data/lib/ea/transformations/parsers/base_parser.rb +482 -0
- data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
- data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
- data/lib/ea/transformations/transformation_engine.rb +390 -0
- data/lib/ea/transformations.rb +85 -0
- data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
- data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
- data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
- data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
- data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
- data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
- data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
- data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
- data/lib/ea/transformers/qea_to_xmi.rb +29 -0
- data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
- data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
- data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
- data/lib/ea/transformers/uml_to_xmi.rb +16 -0
- data/lib/ea/transformers.rb +34 -0
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
- data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
- data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
- data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
- data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
- data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
- data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
- data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
- data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
- data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
- data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
- data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
- data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
- data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
- data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
- data/lib/ea/xmi/lookup_service.rb +89 -0
- data/lib/ea/xmi/parser.rb +919 -0
- data/lib/ea/xmi.rb +35 -0
- data/lib/ea.rb +10 -1
- metadata +382 -9
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Qea
|
|
5
|
+
# Database container for all loaded EA models
|
|
6
|
+
#
|
|
7
|
+
# This class provides a unified container for all EA table collections
|
|
8
|
+
# loaded from a QEA database. It stores collections by name and provides
|
|
9
|
+
# accessor methods, statistics, and lookup functionality.
|
|
10
|
+
#
|
|
11
|
+
# @example Load and access database
|
|
12
|
+
# database = Ea::Qea::Services::DatabaseLoader.new("file.qea").load
|
|
13
|
+
# puts database.stats
|
|
14
|
+
# # => {"objects" => 693, "attributes" => 1910, ...}
|
|
15
|
+
#
|
|
16
|
+
# classes = database.objects.find_by_type("Class")
|
|
17
|
+
# obj = database.find_object(123)
|
|
18
|
+
class Database
|
|
19
|
+
# @return [Hash<Symbol, Array>] Collections of records by name
|
|
20
|
+
attr_reader :collections
|
|
21
|
+
|
|
22
|
+
# @return [String] Path to the QEA file
|
|
23
|
+
attr_reader :qea_path
|
|
24
|
+
|
|
25
|
+
def initialize(qea_path, connection = nil)
|
|
26
|
+
@qea_path = qea_path
|
|
27
|
+
@connection = connection
|
|
28
|
+
@collections = {}
|
|
29
|
+
@mutex = Mutex.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Set database connection
|
|
33
|
+
#
|
|
34
|
+
# @param connection [SQLite3::Database] Database connection
|
|
35
|
+
# @return [void]
|
|
36
|
+
def connection=(connection)
|
|
37
|
+
@connection = connection
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Close the database connection if open
|
|
41
|
+
# @return [void]
|
|
42
|
+
def close_connection
|
|
43
|
+
return unless @connection && !@connection.closed?
|
|
44
|
+
|
|
45
|
+
@connection.close
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Add a collection to the database
|
|
49
|
+
#
|
|
50
|
+
# @param name [Symbol, String] Collection name (e.g., :objects)
|
|
51
|
+
# @param records [Array] Array of model instances
|
|
52
|
+
# @return [void]
|
|
53
|
+
def add_collection(name, records)
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
@collections[name.to_sym] = records.freeze
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
COLLECTION_ACCESSORS = %i[
|
|
60
|
+
attributes operations operation_params connectors packages
|
|
61
|
+
diagrams diagram_objects diagram_links object_constraints
|
|
62
|
+
tagged_values object_properties attribute_tags xrefs
|
|
63
|
+
stereotypes datatypes constraint_types connector_types
|
|
64
|
+
diagram_types object_types status_types complexity_types
|
|
65
|
+
documents scripts
|
|
66
|
+
].freeze
|
|
67
|
+
|
|
68
|
+
COLLECTION_ACCESSORS.each do |name|
|
|
69
|
+
define_method(name) do
|
|
70
|
+
@collections[name] || []
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get objects collection (special: wrapped in ObjectRepository)
|
|
75
|
+
#
|
|
76
|
+
# @return [Repositories::ObjectRepository] Repository for objects
|
|
77
|
+
def objects
|
|
78
|
+
return @objects if defined?(@objects)
|
|
79
|
+
|
|
80
|
+
@objects = Repositories::ObjectRepository.new(
|
|
81
|
+
@collections[:objects] || [],
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get statistics for all collections
|
|
86
|
+
#
|
|
87
|
+
# @return [Hash<String, Integer>] Record counts by collection name
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# database.stats
|
|
91
|
+
# # => {
|
|
92
|
+
# # "objects" => 693,
|
|
93
|
+
# # "attributes" => 1910,
|
|
94
|
+
# # "connectors" => 908,
|
|
95
|
+
# # ...
|
|
96
|
+
# # }
|
|
97
|
+
def stats
|
|
98
|
+
@collections.each_with_object({}) do |(name, records), hash|
|
|
99
|
+
hash[name.to_s] = records.size
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get total number of records across all collections
|
|
104
|
+
#
|
|
105
|
+
# @return [Integer] Total record count
|
|
106
|
+
def total_records
|
|
107
|
+
@collections.values.sum(&:size)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def find_package(id)
|
|
111
|
+
ensure_lookup_indexes
|
|
112
|
+
@packages_by_id[id]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def find_attribute(id)
|
|
116
|
+
ensure_lookup_indexes
|
|
117
|
+
@attributes_by_id[id]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def find_connector(id)
|
|
121
|
+
ensure_lookup_indexes
|
|
122
|
+
@connectors_by_id[id]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def find_diagram(id)
|
|
126
|
+
ensure_lookup_indexes
|
|
127
|
+
@diagrams_by_id[id]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def attributes_for_object(id)
|
|
131
|
+
ensure_lookup_indexes
|
|
132
|
+
@attributes_by_object_id[id] || []
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def operations_for_object(id)
|
|
136
|
+
ensure_lookup_indexes
|
|
137
|
+
@operations_by_object_id[id] || []
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def operation_params_for(id)
|
|
141
|
+
ensure_lookup_indexes
|
|
142
|
+
@operation_params_by_id[id] || []
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def child_packages_for(id)
|
|
146
|
+
ensure_lookup_indexes
|
|
147
|
+
@packages_by_parent[id] || []
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def objects_in_package(id)
|
|
151
|
+
ensure_lookup_indexes
|
|
152
|
+
@objects_by_package_id[id] || []
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def diagrams_in_package(id)
|
|
156
|
+
ensure_lookup_indexes
|
|
157
|
+
@diagrams_by_package_id[id] || []
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def diagram_objects_for(id)
|
|
161
|
+
ensure_lookup_indexes
|
|
162
|
+
@diagram_objects_by_id[id] || []
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def diagram_links_for(id)
|
|
166
|
+
ensure_lookup_indexes
|
|
167
|
+
@diagram_links_by_id[id] || []
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Find an object by ID
|
|
171
|
+
def find_object(id)
|
|
172
|
+
objects.find_by_key(:ea_object_id, id)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Find object by ea_guid
|
|
176
|
+
def find_object_by_guid(ea_guid)
|
|
177
|
+
ensure_lookup_indexes
|
|
178
|
+
@objects_by_guid[ea_guid]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Get connectors involving a specific object (start or end)
|
|
182
|
+
def connectors_for_object(object_id)
|
|
183
|
+
ensure_lookup_indexes
|
|
184
|
+
@connectors_by_object[object_id] || []
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def constraints_for_object(object_id)
|
|
188
|
+
ensure_lookup_indexes
|
|
189
|
+
@constraints_by_object_id[object_id] || []
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def tagged_values_for_element(ea_guid)
|
|
193
|
+
ensure_lookup_indexes
|
|
194
|
+
@tagged_values_by_element_id[ea_guid] || []
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def properties_for_object(object_id)
|
|
198
|
+
ensure_lookup_indexes
|
|
199
|
+
@properties_by_object_id[object_id] || []
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def xrefs_for_client(ea_guid)
|
|
203
|
+
ensure_lookup_indexes
|
|
204
|
+
@xrefs_by_client[ea_guid] || []
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Check if database is empty
|
|
208
|
+
#
|
|
209
|
+
# @return [Boolean] true if no collections loaded
|
|
210
|
+
def empty?
|
|
211
|
+
@collections.empty? || total_records.zero?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Get collection names
|
|
215
|
+
#
|
|
216
|
+
# @return [Array<Symbol>] Array of collection names
|
|
217
|
+
def collection_names
|
|
218
|
+
@collections.keys
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Freeze all collections to make database immutable
|
|
222
|
+
#
|
|
223
|
+
# @return [self]
|
|
224
|
+
def freeze
|
|
225
|
+
objects
|
|
226
|
+
ensure_lookup_indexes
|
|
227
|
+
@collections.freeze
|
|
228
|
+
super
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
def ensure_lookup_indexes
|
|
234
|
+
return if @lookup_indexes_built
|
|
235
|
+
|
|
236
|
+
build_lookup_indexes
|
|
237
|
+
@lookup_indexes_built = true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def build_group_index(collection, method, single: false)
|
|
241
|
+
collection.each_with_object({}) do |item, hash|
|
|
242
|
+
key = item.public_send(method)
|
|
243
|
+
next unless key
|
|
244
|
+
|
|
245
|
+
single ? (hash[key] = item) : ((hash[key] ||= []) << item)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def build_lookup_indexes
|
|
250
|
+
build_primary_indexes
|
|
251
|
+
build_secondary_indexes
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def build_primary_indexes
|
|
255
|
+
build_object_indexes
|
|
256
|
+
build_feature_indexes
|
|
257
|
+
build_connector_indexes
|
|
258
|
+
build_diagram_indexes
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def build_object_indexes
|
|
262
|
+
@objects_by_guid = build_group_index(objects, :ea_guid, single: true)
|
|
263
|
+
@objects_by_package_id = build_group_index(objects, :package_id)
|
|
264
|
+
@packages_by_parent = build_group_index(packages, :parent_id)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def build_feature_indexes
|
|
268
|
+
@attributes_by_object_id = build_group_index(attributes, :ea_object_id)
|
|
269
|
+
@operations_by_object_id = build_group_index(operations, :ea_object_id)
|
|
270
|
+
@operation_params_by_id = build_group_index(operation_params,
|
|
271
|
+
:operationid)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def build_connector_indexes
|
|
275
|
+
@connectors_by_start = build_group_index(connectors, :start_object_id)
|
|
276
|
+
@connectors_by_end = build_group_index(connectors, :end_object_id)
|
|
277
|
+
@connectors_by_object = {}
|
|
278
|
+
@connectors_by_start.each do |id, conns|
|
|
279
|
+
(@connectors_by_object[id] ||= []).concat(conns)
|
|
280
|
+
end
|
|
281
|
+
@connectors_by_end.each do |id, conns|
|
|
282
|
+
(@connectors_by_object[id] ||= []).concat(conns)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def build_diagram_indexes
|
|
287
|
+
@diagrams_by_package_id = build_group_index(diagrams, :package_id)
|
|
288
|
+
@diagram_objects_by_id = build_group_index(diagram_objects, :diagram_id)
|
|
289
|
+
@diagram_links_by_id = build_group_index(diagram_links, :diagramid)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def build_secondary_indexes
|
|
293
|
+
@packages_by_id = build_group_index(packages, :package_id, single: true)
|
|
294
|
+
@connectors_by_id = build_group_index(connectors, :connector_id,
|
|
295
|
+
single: true)
|
|
296
|
+
@diagrams_by_id = build_group_index(diagrams, :diagram_id, single: true)
|
|
297
|
+
@attributes_by_id = build_group_index(attributes, :id, single: true)
|
|
298
|
+
@constraints_by_object_id = build_group_index(object_constraints,
|
|
299
|
+
:ea_object_id)
|
|
300
|
+
@tagged_values_by_element_id = build_group_index(tagged_values,
|
|
301
|
+
:element_id)
|
|
302
|
+
@properties_by_object_id = build_group_index(object_properties,
|
|
303
|
+
:ea_object_id)
|
|
304
|
+
@xrefs_by_client = build_group_index(xrefs, :client)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Qea
|
|
5
|
+
module Factory
|
|
6
|
+
class AssociationBuilder < BaseTransformer
|
|
7
|
+
ASSOC_TYPES = ["Association", "Aggregation", "Composition"].freeze
|
|
8
|
+
|
|
9
|
+
def load_class_associations(object_id, object_guid)
|
|
10
|
+
return [] if object_id.nil?
|
|
11
|
+
|
|
12
|
+
normalized_owner_xmi_id = normalize_guid_to_xmi_format(object_guid,
|
|
13
|
+
"EAID")
|
|
14
|
+
|
|
15
|
+
assoc_connectors = database.connectors_for_object(object_id)
|
|
16
|
+
.select { |c| ASSOC_TYPES.include?(c.connector_type) }
|
|
17
|
+
|
|
18
|
+
assoc_connectors.filter_map do |ea_connector|
|
|
19
|
+
build_association(ea_connector, object_id, normalized_owner_xmi_id)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def load_association_attributes(object_id)
|
|
24
|
+
return [] if object_id.nil?
|
|
25
|
+
|
|
26
|
+
assoc_connectors = database.connectors_for_object(object_id)
|
|
27
|
+
.select { |c| ASSOC_TYPES.include?(c.connector_type) }
|
|
28
|
+
obj = find_object_by_id(object_id)
|
|
29
|
+
obj_pkg_name = find_package_name(obj&.package_id)
|
|
30
|
+
|
|
31
|
+
assoc_connectors.filter_map do |ea_connector|
|
|
32
|
+
build_connector_attribute(ea_connector, object_id, obj,
|
|
33
|
+
obj_pkg_name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_connector_attribute(ea_connector, object_id, obj,
|
|
38
|
+
obj_pkg_name)
|
|
39
|
+
if ea_connector.start_object_id == object_id
|
|
40
|
+
build_end_attribute(ea_connector, obj, obj_pkg_name)
|
|
41
|
+
elsif ea_connector.end_object_id == object_id
|
|
42
|
+
build_start_attribute(ea_connector, obj, obj_pkg_name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create_association_attribute( # rubocop:disable Metrics/ParameterLists
|
|
47
|
+
name:, type:, type_xmi_id:,
|
|
48
|
+
association_xmi_id:, cardinality:, definition:,
|
|
49
|
+
gen_name:, name_ns:, type_ns:, is_src: true
|
|
50
|
+
)
|
|
51
|
+
Lutaml::Uml::GeneralAttribute.new.tap do |attr|
|
|
52
|
+
assign_assoc_attr_basic(attr, name, type, gen_name, definition,
|
|
53
|
+
name_ns, type_ns)
|
|
54
|
+
attr.xmi_id = normalize_guid_to_xmi_format(type_xmi_id, "EAID")
|
|
55
|
+
attr.association = normalize_guid_to_xmi_format(
|
|
56
|
+
association_xmi_id, "EAID"
|
|
57
|
+
)
|
|
58
|
+
attr.has_association = true
|
|
59
|
+
attr.id = normalize_guid_to_xmi_src_dst_format(
|
|
60
|
+
association_xmi_id, "EAID", is_src
|
|
61
|
+
)
|
|
62
|
+
attr.cardinality = build_cardinality(cardinality)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def assign_assoc_attr_basic(attr, name, type, gen_name,
|
|
69
|
+
definition, name_ns, type_ns)
|
|
70
|
+
attr.name = name
|
|
71
|
+
attr.type = type
|
|
72
|
+
attr.gen_name = gen_name
|
|
73
|
+
attr.definition = definition
|
|
74
|
+
attr.name_ns = name_ns
|
|
75
|
+
attr.type_ns = type_ns
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_association(ea_connector, object_id, normalized_owner_xmi_id)
|
|
79
|
+
is_start = ea_connector.start_object_id == object_id
|
|
80
|
+
owner_role = is_start ? ea_connector.destrole : ea_connector.sourcerole
|
|
81
|
+
return nil if owner_role.nil? || owner_role.empty?
|
|
82
|
+
|
|
83
|
+
member_obj = resolve_member_object(ea_connector, is_start)
|
|
84
|
+
return nil unless member_obj
|
|
85
|
+
|
|
86
|
+
member_role = resolve_member_role(ea_connector, is_start, member_obj)
|
|
87
|
+
|
|
88
|
+
build_association_record(ea_connector, object_id, normalized_owner_xmi_id,
|
|
89
|
+
owner_role, member_obj, member_role, is_start)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def build_association_record(ea_connector, object_id, owner_xmi_id,
|
|
93
|
+
owner_role, member_obj, member_role, is_start)
|
|
94
|
+
cardinality_str = is_start ? ea_connector.destcard : ea_connector.sourcecard
|
|
95
|
+
|
|
96
|
+
Lutaml::Uml::Association.new.tap do |assoc|
|
|
97
|
+
assoc.xmi_id = normalize_guid_to_xmi_format(ea_connector.ea_guid,
|
|
98
|
+
"EAID")
|
|
99
|
+
assign_assoc_name(assoc, ea_connector)
|
|
100
|
+
assign_association_ends(assoc, object_id, owner_xmi_id,
|
|
101
|
+
owner_role, member_obj, member_role)
|
|
102
|
+
assoc.member_end_type = ea_connector.connector_type&.downcase
|
|
103
|
+
assoc.member_end_cardinality = build_cardinality(cardinality_str)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def assign_assoc_name(assoc, ea_connector)
|
|
108
|
+
return if ea_connector.name.nil? || ea_connector.name.empty?
|
|
109
|
+
|
|
110
|
+
assoc.name = ea_connector.name
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def assign_association_ends(assoc, object_id, owner_xmi_id,
|
|
114
|
+
owner_role, member_obj, member_role)
|
|
115
|
+
assoc.owner_end = find_object_by_id(object_id)&.name
|
|
116
|
+
assoc.owner_end_xmi_id = owner_xmi_id
|
|
117
|
+
assoc.owner_end_attribute_name = owner_role
|
|
118
|
+
assoc.member_end = member_obj.name
|
|
119
|
+
assoc.member_end_xmi_id = normalize_guid_to_xmi_format(
|
|
120
|
+
member_obj.ea_guid, "EAID"
|
|
121
|
+
)
|
|
122
|
+
assoc.member_end_attribute_name = member_role
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def resolve_member_object(ea_connector, is_start)
|
|
126
|
+
peer_id = is_start ? ea_connector.end_object_id : ea_connector.start_object_id
|
|
127
|
+
find_object_by_id(peer_id)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def resolve_member_role(ea_connector, is_start, member_obj)
|
|
131
|
+
role = is_start ? ea_connector.sourcerole : ea_connector.destrole
|
|
132
|
+
role.nil? || role.empty? ? member_obj.name : role
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def build_end_attribute(ea_connector, obj, obj_pkg_name)
|
|
136
|
+
return nil if ea_connector.destrole.nil? || ea_connector.destrole.empty?
|
|
137
|
+
|
|
138
|
+
target_obj = find_object_by_id(ea_connector.end_object_id)
|
|
139
|
+
return nil unless target_obj
|
|
140
|
+
|
|
141
|
+
build_directional_attribute(
|
|
142
|
+
role: ea_connector.destrole,
|
|
143
|
+
peer_obj: target_obj,
|
|
144
|
+
ea_connector: ea_connector,
|
|
145
|
+
cardinality: ea_connector.destcard,
|
|
146
|
+
obj: obj, obj_pkg_name: obj_pkg_name,
|
|
147
|
+
is_src: false
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def build_start_attribute(ea_connector, obj, obj_pkg_name)
|
|
152
|
+
return nil if ea_connector.sourcerole.nil? || ea_connector.sourcerole.empty?
|
|
153
|
+
|
|
154
|
+
source_obj = find_object_by_id(ea_connector.start_object_id)
|
|
155
|
+
return nil unless source_obj
|
|
156
|
+
|
|
157
|
+
build_directional_attribute(
|
|
158
|
+
role: ea_connector.sourcerole,
|
|
159
|
+
peer_obj: source_obj,
|
|
160
|
+
ea_connector: ea_connector,
|
|
161
|
+
cardinality: ea_connector.sourcecard,
|
|
162
|
+
obj: obj, obj_pkg_name: obj_pkg_name
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build_directional_attribute(role:, peer_obj:, ea_connector:,
|
|
167
|
+
cardinality:, obj:, obj_pkg_name:,
|
|
168
|
+
is_src: true)
|
|
169
|
+
create_association_attribute(
|
|
170
|
+
name: role,
|
|
171
|
+
type: peer_obj.name,
|
|
172
|
+
type_xmi_id: peer_obj.ea_guid,
|
|
173
|
+
association_xmi_id: ea_connector.ea_guid,
|
|
174
|
+
cardinality: cardinality,
|
|
175
|
+
definition: ea_connector.notes,
|
|
176
|
+
gen_name: obj.name,
|
|
177
|
+
name_ns: obj_pkg_name,
|
|
178
|
+
type_ns: find_package_name(peer_obj.package_id),
|
|
179
|
+
is_src: is_src,
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def build_cardinality(cardinality_str)
|
|
184
|
+
return nil unless cardinality_str && !cardinality_str.empty?
|
|
185
|
+
|
|
186
|
+
parsed = parse_cardinality(cardinality_str)
|
|
187
|
+
return nil unless parsed[:min] || parsed[:max]
|
|
188
|
+
|
|
189
|
+
Lutaml::Uml::Cardinality.new.tap do |card|
|
|
190
|
+
card.min = parsed[:min]
|
|
191
|
+
card.max = parsed[:max]
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def find_package_name(package_id)
|
|
196
|
+
return nil if package_id.nil?
|
|
197
|
+
|
|
198
|
+
database.find_package(package_id)&.name
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Qea
|
|
5
|
+
module Factory
|
|
6
|
+
# Transforms EA connectors (Association type) to UML associations
|
|
7
|
+
class AssociationTransformer < BaseTransformer
|
|
8
|
+
# Transform EA connector to UML association
|
|
9
|
+
# @param ea_connector [EaConnector] EA connector model
|
|
10
|
+
# @return [Lutaml::Uml::Association] UML association
|
|
11
|
+
def transform(ea_connector) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
12
|
+
return nil if ea_connector.nil?
|
|
13
|
+
return nil unless ea_connector.association?
|
|
14
|
+
|
|
15
|
+
Lutaml::Uml::Association.new.tap do |assoc| # rubocop:disable Metrics/BlockLength
|
|
16
|
+
# Map basic properties
|
|
17
|
+
assoc.name = ea_connector.name unless
|
|
18
|
+
ea_connector.name.nil? || ea_connector.name.empty?
|
|
19
|
+
assoc.xmi_id = normalize_guid_to_xmi_format(ea_connector.ea_guid,
|
|
20
|
+
"EAID")
|
|
21
|
+
|
|
22
|
+
# Map source (owner) end
|
|
23
|
+
source_obj = find_object(ea_connector.start_object_id)
|
|
24
|
+
if source_obj
|
|
25
|
+
assoc.owner_end = source_obj.name
|
|
26
|
+
assoc.owner_end_xmi_id = normalize_guid_to_xmi_format(
|
|
27
|
+
source_obj.ea_guid, "EAID"
|
|
28
|
+
)
|
|
29
|
+
assoc.owner_end_attribute_name = ea_connector.sourcerole
|
|
30
|
+
assoc.owner_end_cardinality = build_cardinality_from_string(
|
|
31
|
+
ea_connector.sourcecard,
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Map target (member) end
|
|
36
|
+
target_obj = find_object(ea_connector.end_object_id)
|
|
37
|
+
if target_obj
|
|
38
|
+
assoc.member_end = target_obj.name
|
|
39
|
+
assoc.member_end_xmi_id = normalize_guid_to_xmi_format(
|
|
40
|
+
target_obj.ea_guid, "EAID"
|
|
41
|
+
)
|
|
42
|
+
assoc.member_end_attribute_name = ea_connector.destrole
|
|
43
|
+
assoc.member_end_cardinality = build_cardinality_from_string(
|
|
44
|
+
ea_connector.destcard,
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Map definition/notes
|
|
49
|
+
assoc.definition = ea_connector.notes unless
|
|
50
|
+
ea_connector.notes.nil? || ea_connector.notes.empty?
|
|
51
|
+
|
|
52
|
+
# Map stereotype
|
|
53
|
+
if ea_connector.stereotype && !ea_connector.stereotype.empty?
|
|
54
|
+
assoc.stereotype = [ea_connector.stereotype]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Load and transform tagged values
|
|
58
|
+
assoc.tagged_values = load_tagged_values(ea_connector.ea_guid)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Find object by ID
|
|
65
|
+
# @param object_id [Integer] Object ID
|
|
66
|
+
# @return [EaObject, nil] EA object or nil if not found
|
|
67
|
+
def find_object(object_id)
|
|
68
|
+
return nil if object_id.nil?
|
|
69
|
+
|
|
70
|
+
database.find_object(object_id)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Build cardinality from string
|
|
74
|
+
# @param cardinality_str [String] Cardinality string
|
|
75
|
+
# @return [Lutaml::Uml::Cardinality, nil] Cardinality object
|
|
76
|
+
def build_cardinality_from_string(cardinality_str)
|
|
77
|
+
return nil if cardinality_str.nil? || cardinality_str.empty?
|
|
78
|
+
|
|
79
|
+
parsed = parse_cardinality(cardinality_str)
|
|
80
|
+
return nil if parsed[:min].nil? && parsed[:max].nil?
|
|
81
|
+
|
|
82
|
+
Lutaml::Uml::Cardinality.new.tap do |card|
|
|
83
|
+
card.min = parsed[:min]
|
|
84
|
+
card.max = parsed[:max]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Qea
|
|
5
|
+
module Factory
|
|
6
|
+
# Transforms EA AttributeTag to UML TaggedValue
|
|
7
|
+
#
|
|
8
|
+
# This transformer converts Enterprise Architect attribute tags
|
|
9
|
+
# (GML/XML Schema encoding metadata for attributes) to UML
|
|
10
|
+
# TaggedValue objects.
|
|
11
|
+
#
|
|
12
|
+
# @example Transform an attribute tag
|
|
13
|
+
# ea_tag = Models::EaAttributeTag.new(
|
|
14
|
+
# property_id: 1,
|
|
15
|
+
# element_id: 367,
|
|
16
|
+
# property: "isMetadata",
|
|
17
|
+
# value: "false"
|
|
18
|
+
# )
|
|
19
|
+
# transformer = AttributeTagTransformer.new
|
|
20
|
+
# uml_tag = transformer.transform(ea_tag)
|
|
21
|
+
class AttributeTagTransformer < BaseTransformer
|
|
22
|
+
# Transform EA attribute tag to UML TaggedValue
|
|
23
|
+
#
|
|
24
|
+
# Attribute tags enhance UML attributes with GML/XML encoding
|
|
25
|
+
# metadata. They are transformed into tagged values to preserve
|
|
26
|
+
# this semantic information.
|
|
27
|
+
#
|
|
28
|
+
# @param ea_tag [Models::EaAttributeTag] EA attribute tag
|
|
29
|
+
# @return [Lutaml::Uml::TaggedValue, nil] UML tagged value or nil
|
|
30
|
+
def transform(ea_tag)
|
|
31
|
+
return nil unless ea_tag
|
|
32
|
+
return nil unless ea_tag.property
|
|
33
|
+
|
|
34
|
+
Lutaml::Uml::TaggedValue.new.tap do |tag|
|
|
35
|
+
tag.name = ea_tag.property
|
|
36
|
+
tag.value = ea_tag.value || ""
|
|
37
|
+
tag.notes = format_notes(ea_tag)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Format notes from EA attribute tag
|
|
44
|
+
#
|
|
45
|
+
# @param ea_tag [Models::EaAttributeTag] EA tag
|
|
46
|
+
# @return [String, nil] Formatted notes
|
|
47
|
+
def format_notes(ea_tag)
|
|
48
|
+
return nil unless ea_tag.notes
|
|
49
|
+
|
|
50
|
+
# Clean up EA's note format
|
|
51
|
+
notes = ea_tag.notes.strip
|
|
52
|
+
notes.empty? ? nil : notes
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|