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.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +125 -0
  3. data/Rakefile +12 -4
  4. data/TODO.next/00-publish-blocking-bugs.md +74 -0
  5. data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
  6. data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
  7. data/TODO.next/03-slim-lutaml-uml.md +79 -0
  8. data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
  9. data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
  10. data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
  11. data/TODO.next/07-transformer-registry-in-factory.md +20 -0
  12. data/TODO.next/08-connector-type-registry.md +27 -0
  13. data/TODO.next/09-element-renderer-registry.md +29 -0
  14. data/TODO.next/10-connector-renderer-lsp.md +18 -0
  15. data/TODO.next/11-consolidate-style-knowledge.md +33 -0
  16. data/TODO.next/12-data-driven-from-db-row.md +24 -0
  17. data/TODO.next/13-extract-duplicated-methods.md +17 -0
  18. data/TODO.next/14-remove-dead-code.md +10 -0
  19. data/TODO.next/15-narrow-exception-handling.md +39 -0
  20. data/TODO.next/16-repository-indexes.md +28 -0
  21. data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
  22. data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
  23. data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
  24. data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
  25. data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
  26. data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
  27. data/TODO.next/23-cleanup-idallocator.md +41 -0
  28. data/TODO.next/24-tighten-parity-specs.md +42 -0
  29. data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
  30. data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
  31. data/TODO.next/27-extract-cardinality-module.md +68 -0
  32. data/TODO.next/28-extract-xml-sanitizer.md +51 -0
  33. data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
  34. data/TODO.next/30-struct-return-for-association-end.md +37 -0
  35. data/TODO.next/31-idallocator-specs.md +27 -0
  36. data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
  37. data/TODO.next/33-normalize-lower-cleanup.md +30 -0
  38. data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
  39. data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
  40. data/TODO.next/36-wire-interface-realization.md +50 -0
  41. data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
  42. data/config/diagram_styles.yml +200 -0
  43. data/config/model_transformations.yml +266 -0
  44. data/config/qea_schema.yml +1024 -0
  45. data/docs/ea_to_uml_type_mapping.md +89 -0
  46. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  47. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  48. data/examples/lur/basic.lur +0 -0
  49. data/examples/lur/test-output.lur +0 -0
  50. data/examples/lur/test.lur +0 -0
  51. data/examples/lur_basic_usage.rb +221 -0
  52. data/examples/lur_cli_workflow.rb +263 -0
  53. data/examples/lur_statistics.rb +326 -0
  54. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  55. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  56. data/examples/qea/README_qea_parser.adoc +230 -0
  57. data/examples/qea/UmlModel_template.qea +0 -0
  58. data/examples/qea/basic.qea +0 -0
  59. data/examples/qea/simple.qea +0 -0
  60. data/examples/qea/simple_example.qea +0 -0
  61. data/examples/qea/test.qea +0 -0
  62. data/examples/qea_standalone_query.rb +73 -0
  63. data/examples/qea_to_repository.rb +51 -0
  64. data/examples/smoke_test_real_qea.rb +81 -0
  65. data/exe/ea +7 -0
  66. data/lib/ea/cli/app.rb +72 -0
  67. data/lib/ea/cli/command/base.rb +80 -0
  68. data/lib/ea/cli/command/convert.rb +62 -0
  69. data/lib/ea/cli/command/diagrams.rb +81 -0
  70. data/lib/ea/cli/command/list.rb +61 -0
  71. data/lib/ea/cli/command/parse.rb +29 -0
  72. data/lib/ea/cli/command/stats.rb +20 -0
  73. data/lib/ea/cli/command/validate.rb +41 -0
  74. data/lib/ea/cli/command.rb +15 -0
  75. data/lib/ea/cli/error.rb +34 -0
  76. data/lib/ea/cli/output/formatter.rb +34 -0
  77. data/lib/ea/cli/output/json_formatter.rb +20 -0
  78. data/lib/ea/cli/output/table_formatter.rb +42 -0
  79. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  80. data/lib/ea/cli/output.rb +56 -0
  81. data/lib/ea/cli.rb +17 -0
  82. data/lib/ea/diagram/configuration.rb +379 -0
  83. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  84. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  85. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  86. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  87. data/lib/ea/diagram/element_renderers.rb +43 -0
  88. data/lib/ea/diagram/extractor.rb +560 -0
  89. data/lib/ea/diagram/layout_engine.rb +170 -0
  90. data/lib/ea/diagram/path_builder.rb +202 -0
  91. data/lib/ea/diagram/style_parser.rb +42 -0
  92. data/lib/ea/diagram/style_resolver.rb +276 -0
  93. data/lib/ea/diagram/svg_renderer.rb +274 -0
  94. data/lib/ea/diagram/util.rb +73 -0
  95. data/lib/ea/diagram.rb +47 -0
  96. data/lib/ea/qea/benchmark.rb +210 -0
  97. data/lib/ea/qea/database.rb +308 -0
  98. data/lib/ea/qea/factory/association_builder.rb +203 -0
  99. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  100. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  101. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  102. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  103. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  104. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  105. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  106. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  107. data/lib/ea/qea/factory/document_builder.rb +283 -0
  108. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  109. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  110. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  111. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  112. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  113. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  114. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  115. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  116. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  117. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  118. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  119. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  120. data/lib/ea/qea/factory.rb +37 -0
  121. data/lib/ea/qea/file_detector.rb +178 -0
  122. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  123. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  124. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  125. data/lib/ea/qea/infrastructure.rb +12 -0
  126. data/lib/ea/qea/models/base_model.rb +59 -0
  127. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  128. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  129. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  130. data/lib/ea/qea/models/ea_connector.rb +160 -0
  131. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  132. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  133. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  134. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  135. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  136. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  137. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  138. data/lib/ea/qea/models/ea_document.rb +63 -0
  139. data/lib/ea/qea/models/ea_object.rb +223 -0
  140. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  141. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  142. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  143. data/lib/ea/qea/models/ea_operation.rb +127 -0
  144. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  145. data/lib/ea/qea/models/ea_package.rb +78 -0
  146. data/lib/ea/qea/models/ea_script.rb +62 -0
  147. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  148. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  149. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  150. data/lib/ea/qea/models/ea_xref.rb +165 -0
  151. data/lib/ea/qea/models.rb +35 -0
  152. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  153. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  154. data/lib/ea/qea/repositories.rb +10 -0
  155. data/lib/ea/qea/services/configuration.rb +211 -0
  156. data/lib/ea/qea/services/database_loader.rb +191 -0
  157. data/lib/ea/qea/services.rb +10 -0
  158. data/lib/ea/qea/validation/association_validator.rb +73 -0
  159. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  160. data/lib/ea/qea/validation/base_validator.rb +331 -0
  161. data/lib/ea/qea/validation/class_validator.rb +121 -0
  162. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  163. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  164. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  165. data/lib/ea/qea/validation/database.rb +16 -0
  166. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  167. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  168. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  169. data/lib/ea/qea/validation/formatters.rb +12 -0
  170. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  171. data/lib/ea/qea/validation/package_validator.rb +111 -0
  172. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  173. data/lib/ea/qea/validation/validation_message.rb +144 -0
  174. data/lib/ea/qea/validation/validation_result.rb +210 -0
  175. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  176. data/lib/ea/qea/validation.rb +28 -0
  177. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  178. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  179. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  180. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  181. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  182. data/lib/ea/qea/verification.rb +14 -0
  183. data/lib/ea/qea.rb +185 -0
  184. data/lib/ea/transformations/configuration.rb +333 -0
  185. data/lib/ea/transformations/format_registry.rb +366 -0
  186. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  187. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  188. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  189. data/lib/ea/transformations/transformation_engine.rb +390 -0
  190. data/lib/ea/transformations.rb +85 -0
  191. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  192. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  193. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  194. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  195. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  196. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  197. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  198. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  199. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  200. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  201. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  202. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  203. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  204. data/lib/ea/transformers.rb +34 -0
  205. data/lib/ea/version.rb +1 -1
  206. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  207. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  208. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  209. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  210. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  211. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  212. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  213. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  214. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  215. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  216. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  217. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  218. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  219. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  220. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  221. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  222. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  223. data/lib/ea/xmi/lookup_service.rb +89 -0
  224. data/lib/ea/xmi/parser.rb +919 -0
  225. data/lib/ea/xmi.rb +35 -0
  226. data/lib/ea.rb +10 -1
  227. metadata +382 -9
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformers
5
+ module QeaToXmi
6
+ # Pure conversions between Sparx EA GUID strings and XMI identifier
7
+ # strings. No state, no I/O — same input always yields same output.
8
+ #
9
+ # EA stores identifiers as `{ABCD-1234-...}` braced GUID strings. XMI
10
+ # serializations use unbraced, dash→underscore forms prefixed with
11
+ # `EAID_` (most elements) or `EAPK_` (packages at the model root).
12
+ module GuidFormat
13
+ # Braces and dashes both become underscores; collapse runs of
14
+ # consecutive underscores so `{AB-CD}` → `AB_CD` (not `_AB_CD_`).
15
+ SEP = /[-{}]/
16
+
17
+ module_function
18
+
19
+ # @param ea_guid [String, nil] e.g. "{AB-CD-EF}"
20
+ # @param prefix [String] "EAID" (default) or "EAPK"
21
+ # @return [String, nil] e.g. "EAID_AB_CD_EF"
22
+ def ea_guid_to_xmi_id(ea_guid, prefix: "EAID")
23
+ return nil if ea_guid.nil? || ea_guid.empty?
24
+
25
+ clean = ea_guid.gsub(SEP, "_").gsub(/_+/, "_").gsub(/\A_/, "").gsub(/_\z/, "")
26
+ "#{prefix}_#{clean}"
27
+ end
28
+
29
+ # @param xmi_id [String, nil] e.g. "EAID_AB_CD_EF"
30
+ # @return [String, nil] e.g. "{AB-CD-EF}"
31
+ def xmi_id_to_ea_guid(xmi_id)
32
+ return nil if xmi_id.nil? || xmi_id.empty?
33
+
34
+ body = xmi_id.sub(/\A(?:EAID|EAPK)_/, "")
35
+ "{#{body.tr('_', '-')}}"
36
+ end
37
+
38
+ # Build a member-end xmi:id for one side of a connector.
39
+ #
40
+ # Sparx EA's convention: take the connector's GUID, drop the first two
41
+ # hex characters of its first segment, and prepend `src` or `dst`.
42
+ #
43
+ # @param connector_xmi_id [String] e.g. "EAID_AB12CDEF_..."
44
+ # @param side [Symbol] :source or :destination
45
+ # @return [String] e.g. "EAID_src2CDEF_..."
46
+ def connector_end_xmi_id(connector_xmi_id, side:)
47
+ tag = side == :source ? "src" : "dst"
48
+ body = connector_xmi_id.sub(/\A(?:EAID|EAPK)_/, "")
49
+ first_segment, rest = body.split("_", 2)
50
+ trimmed = first_segment.length > 2 ? first_segment[2..] : first_segment
51
+ rest ? "EAID_#{tag}#{trimmed}_#{rest}" : "EAID_#{tag}#{trimmed}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Transformers
5
+ module QeaToXmi
6
+ # Allocates synthetic xmi:id values for elements that don't have a
7
+ # natural GUID-based one — e.g. literal `<lowerValue>` nodes, or
8
+ # `<ownedComment>` bodies synthesized from EA's Note objects.
9
+ #
10
+ # Sparx's EAID format reserves prefixes like `LI` (LiteralInt),
11
+ # `SL` (Slot), `NL` (NameLabel), `DB` (DiagramBounds), `OE` (OpaqueExpr),
12
+ # `RT` (Return parameter) for these synthesized identifiers.
13
+ #
14
+ # Output shape: `EAID_<PREFIX><NNNNNN><GUID_TAIL>` where:
15
+ #
16
+ # - `EAID_` matches the prefix used by all other Sparx XMI element IDs.
17
+ # - `<PREFIX>` is a Sparx-reserved literal prefix (LI, SL, OE, ...).
18
+ # - `<NNNNNN>` is a 6-digit zero-padded counter, scoped to the
19
+ # IdAllocator instance (one per `Transformer#serialize` call).
20
+ # - `<GUID_TAIL>` is the parent element's EA GUID normalised to
21
+ # Sparx's wire form. The leading underscore from the opening
22
+ # brace of `{GUID-...}` is preserved so the output matches
23
+ # real Sparx XMI byte-for-byte (e.g. `EAID_LI000001__EEB1_...`).
24
+ # When `parent_guid` is nil, no tail is emitted.
25
+ #
26
+ # The allocator is memoised by `seed`: same seed returns the same
27
+ # allocated ID. Different seeds get different counters.
28
+ class IdAllocator
29
+ # Well-known prefixes Sparx uses for synthesized IDs.
30
+ LITERAL_INTEGER = "LI"
31
+ OPAQUE_EXPRESSION = "OE"
32
+ SLOT = "SL"
33
+ NAME_LABEL = "NL"
34
+ DIAGRAM_BOUNDS = "DB"
35
+ RETURN_PARAMETER = "RT"
36
+
37
+ # Leading-underscore-preserving normalisation: `{AB-CD}` → `_AB_CD`.
38
+ # Matches the wire form Sparx emits for parent-guid-suffixed IDs.
39
+ GUID_BRACE_OR_DASH = /[-{}]/
40
+
41
+ def initialize
42
+ @counter = 0
43
+ @assigned = {}
44
+ end
45
+
46
+ # @param prefix [String] one of LITERAL_INTEGER, OPAQUE_EXPRESSION, ...
47
+ # @param seed [String, nil] stable seed for memoization (e.g. the
48
+ # source record's object_id). Same seed returns same allocated id.
49
+ # @param parent_guid [String, nil] the owning element's EA GUID
50
+ # (e.g. `{EEB1-...}`). When provided, the synthesised ID carries
51
+ # the parent GUID tail so it is traceable and round-trip-safe.
52
+ # @return [String] e.g. "EAID_LI000001__EEB1_4de7_98F5_670D6EE4A52B"
53
+ def allocate(prefix:, seed: nil, parent_guid: nil)
54
+ return @assigned[seed] if seed && @assigned.key?(seed)
55
+
56
+ @counter += 1
57
+ id = compose_id(prefix: prefix, n: @counter, parent_guid: parent_guid)
58
+ @assigned[seed] = id if seed
59
+ id
60
+ end
61
+
62
+ private
63
+
64
+ def compose_id(prefix:, n:, parent_guid:)
65
+ tail = guid_tail(parent_guid)
66
+ if tail
67
+ # The format is `EAID_LI<NN>_<guid_tail>` where guid_tail
68
+ # preserves its leading underscore. Together with the
69
+ # separator `_` that yields the double-underscore form
70
+ # Sparx emits (`EAID_LI000001__EEB1_...`).
71
+ format("EAID_%<prefix>s%<n>06d_%<tail>s", prefix: prefix, n: n, tail: tail)
72
+ else
73
+ format("EAID_%<prefix>s%<n>06d", prefix: prefix, n: n)
74
+ end
75
+ end
76
+
77
+ # Sparx preserves the leading underscore that comes from the
78
+ # opening brace of the EA GUID. Strip the closing-brace trailing
79
+ # underscore only.
80
+ #
81
+ # `{EEB1-4de7-98F5-670D6EE4A52B}` → `_EEB1_4de7_98F5_670D6EE4A52B`
82
+ def guid_tail(parent_guid)
83
+ return nil if parent_guid.nil? || parent_guid.empty?
84
+
85
+ parent_guid
86
+ .gsub(GUID_BRACE_OR_DASH, "_")
87
+ .sub(/_\z/, "")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module Ea
6
+ module Transformers
7
+ module QeaToXmi
8
+ # Pure-function parser for EA's RunState column on `t_object`.
9
+ #
10
+ # EA serialises instance-specification run-state as a delimited
11
+ # string of the form:
12
+ #
13
+ # @VAR;Variable=<name>;Value=<value>;Op=<op>;@ENDVAR;
14
+ #
15
+ # Multiple variables concatenate directly:
16
+ #
17
+ # @VAR;Variable=a;Value=1;Op==;@ENDVAR;@VAR;Variable=b;Value=2;Op==;@ENDVAR;
18
+ #
19
+ # Each `@VAR ... @ENDVAR;` block maps to one UML Slot. The
20
+ # `Variable` is the attribute name on the classifier (used to
21
+ # resolve the definingFeature EAID at transformation time).
22
+ # The `Value` plus `Op` form the OpaqueExpression body — Sparx
23
+ # prepends the operator character to the value (`Op==` →
24
+ # `body="=Value"`).
25
+ #
26
+ # The parser is pure: no I/O, no state. Same input always yields
27
+ # the same output array of {Variable, Value, Op} structs.
28
+ module RunState
29
+ # A single parsed RunState variable binding.
30
+ #
31
+ # @!attribute variable [String] the attribute name on the classifier
32
+ # @!attribute value [String] the literal value text
33
+ # @!attribute op [String] the operator character(s) EA stored
34
+ Binding = Struct.new(:variable, :value, :op) do
35
+ # Sparx serialises the value with the operator character
36
+ # prepended (`Op==` → `body="=Value"`). For other operators
37
+ # (`!=`, `<`, `>`) the full operator string is prepended.
38
+ #
39
+ # @return [String] the body to set on the OpaqueExpression
40
+ def body
41
+ return value if op.nil? || op.empty?
42
+
43
+ "#{op[0]}#{value}"
44
+ end
45
+ end
46
+
47
+ module_function
48
+
49
+ # @param raw [String, nil] the EA RunState column content
50
+ # @return [Array<Binding>] one Binding per @VAR block; empty
51
+ # array for nil/empty/blank input.
52
+ def parse(raw)
53
+ return [] if raw.nil? || raw.to_s.strip.empty?
54
+
55
+ stripped = raw.to_s
56
+ each_var_block(stripped).map do |block|
57
+ parse_binding(block)
58
+ end
59
+ end
60
+
61
+ # ---- Internal helpers ----
62
+
63
+ # Yields each `@VAR;...;@ENDVAR;` block body (without the
64
+ # delimiters). Tolerant of multiple blocks concatenated
65
+ # without separation.
66
+ def each_var_block(raw)
67
+ return enum_for(:each_var_block, raw) unless block_given?
68
+
69
+ scanner = StringScanner.new(raw)
70
+ until scanner.eos?
71
+ scanner.scan_until(/@VAR;/) || break
72
+ body = scanner.scan_until(/@ENDVAR;/)
73
+ break if body.nil?
74
+
75
+ yield body.sub(/@ENDVAR;\z/, "")
76
+ end
77
+ end
78
+
79
+ # @param block [String] the inside of one @VAR;...;@ENDVAR;
80
+ # block, e.g. `Variable=a;Value=1;Op==;`
81
+ # @return [Binding]
82
+ def parse_binding(block)
83
+ fields = parse_fields(block)
84
+ Binding.new(
85
+ fields["Variable"] || "",
86
+ fields["Value"] || "",
87
+ fields["Op"] || "",
88
+ )
89
+ end
90
+
91
+ # Parse `Key=value;` pairs into a Hash. Values may contain
92
+ # `;` literally — only split on `;` immediately followed by a
93
+ # known key boundary. In practice EA's values rarely contain
94
+ # semicolons, so a simple split is sufficient.
95
+ #
96
+ # @param block [String]
97
+ # @return [Hash{String=>String}]
98
+ def parse_fields(block)
99
+ block.split(";").each_with_object({}) do |pair, hash|
100
+ key, value = pair.split("=", 2)
101
+ hash[key] = value.to_s if key && !key.empty?
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end