ea 0.5.0 → 0.5.2

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -1
  3. data/TODO.complete/51-remove-respond-to.md +25 -0
  4. data/TODO.complete/52-xsd-decouple-fixtures.md +23 -0
  5. data/TODO.complete/53-xmi-public-send-cleanup.md +26 -0
  6. data/TODO.complete/54-wire-stereotype-icon-renderer.md +20 -0
  7. data/TODO.complete/55-wire-shapescript.md +20 -0
  8. data/TODO.complete/56-wire-emf-renderer.md +27 -0
  9. data/TODO.complete/57-wire-ocl-evaluator.md +23 -0
  10. data/TODO.complete/58-diff-modifications.md +21 -0
  11. data/TODO.complete/59-delete-toy-xmi-export.md +30 -0
  12. data/TODO.complete/60-curate-json-export.md +20 -0
  13. data/TODO.complete/61-plantuml-relationships.md +22 -0
  14. data/TODO.complete/62-changelog.md +17 -0
  15. data/TODO.complete/63-dependabot-fix.md +18 -0
  16. data/TODO.complete/64-qeatoxmi-connectors.md +30 -0
  17. data/TODO.complete/65-qeatoxmi-diagrams.md +29 -0
  18. data/TODO.complete/66-qeatoxmi-style-tags-docs.md +28 -0
  19. data/TODO.complete/67-ocl-collection-ops.md +20 -0
  20. data/TODO.complete/68-ocl-comparison-ops.md +19 -0
  21. data/TODO.complete/69-spec-new-models.md +25 -0
  22. data/TODO.complete/STATUS.md +40 -64
  23. data/lib/ea/cli/command/convert.rb +1 -1
  24. data/lib/ea/cli/command/export.rb +23 -2
  25. data/lib/ea/diff/comparator.rb +28 -1
  26. data/lib/ea/export/json/generator.rb +84 -8
  27. data/lib/ea/export/plantuml/generator.rb +95 -28
  28. data/lib/ea/export/xsd/generator.rb +10 -15
  29. data/lib/ea/export.rb +0 -1
  30. data/lib/ea/lint/rules/missing_stereotype.rb +16 -11
  31. data/lib/ea/ocl/evaluator.rb +39 -2
  32. data/lib/ea/ocl/nodes.rb +9 -0
  33. data/lib/ea/ocl/parser.rb +60 -6
  34. data/lib/ea/qea/models/ea_implement.rb +2 -2
  35. data/lib/ea/qea/models/ea_object_effort.rb +2 -2
  36. data/lib/ea/qea/models/ea_object_problem.rb +2 -2
  37. data/lib/ea/qea/models/ea_object_require.rb +2 -2
  38. data/lib/ea/qea/models/ea_object_resource.rb +2 -2
  39. data/lib/ea/qea/models/ea_object_risk.rb +2 -2
  40. data/lib/ea/qea/models/ea_object_scenario.rb +2 -2
  41. data/lib/ea/qea/models/ea_object_test.rb +2 -2
  42. data/lib/ea/qea/models/ea_object_trx.rb +2 -2
  43. data/lib/ea/qea/models/ea_palette_item.rb +2 -2
  44. data/lib/ea/qea/models/ea_role_constraint.rb +2 -2
  45. data/lib/ea/qea/models/ea_secrypt.rb +2 -2
  46. data/lib/ea/qea/validation/database/ocl_constraint_validator.rb +65 -0
  47. data/lib/ea/qea/validation/database.rb +2 -0
  48. data/lib/ea/qea/validation/validation_engine.rb +2 -0
  49. data/lib/ea/qea/validation.rb +2 -0
  50. data/lib/ea/query/builder.rb +22 -3
  51. data/lib/ea/svg/ea_emitter/compartment/stereotype_icon.rb +32 -0
  52. data/lib/ea/svg/ea_emitter/compartment.rb +2 -0
  53. data/lib/ea/svg/ea_emitter/document.rb +33 -0
  54. data/lib/ea/svg/ea_emitter/element/stereotype_icon_renderer.rb +49 -12
  55. data/lib/ea/transformers/qea_to_xmi/transformer.rb +138 -2
  56. data/lib/ea/transformers.rb +3 -2
  57. data/lib/ea/validation/xmi_parity.rb +80 -0
  58. data/lib/ea/validation.rb +12 -0
  59. data/lib/ea/version.rb +1 -1
  60. data/lib/ea.rb +1 -0
  61. metadata +24 -3
  62. data/lib/ea/export/xmi/generator.rb +0 -77
  63. data/lib/ea/export/xmi.rb +0 -11
@@ -17,6 +17,9 @@ module Ea
17
17
  when Nodes::StringMatches then eval_string_matches(ast, context)
18
18
  when Nodes::CollectionExists then eval_exists(ast, context)
19
19
  when Nodes::CollectionForAll then eval_for_all(ast, context)
20
+ when Nodes::CollectionSize then eval_size(ast, context)
21
+ when Nodes::CollectionIsEmpty then eval_is_empty(ast, context)
22
+ when Nodes::Comparison then eval_comparison(ast, context)
20
23
  when Nodes::BinaryOp then eval_binary(ast, context)
21
24
  when Nodes::UnaryOp then eval_unary(ast, context)
22
25
  else
@@ -38,7 +41,7 @@ module Ea
38
41
 
39
42
  def eval_exists(ast, context)
40
43
  collection = evaluate(ast.collection, context)
41
- return false unless collection.respond_to?(:each)
44
+ return false unless collection.is_a?(Array)
42
45
 
43
46
  var_name = ast.var
44
47
  predicate = ast.predicate
@@ -49,7 +52,7 @@ module Ea
49
52
 
50
53
  def eval_for_all(ast, context)
51
54
  collection = evaluate(ast.collection, context)
52
- return true unless collection.respond_to?(:each)
55
+ return true unless collection.is_a?(Array)
53
56
 
54
57
  var_name = ast.var
55
58
  predicate = ast.predicate
@@ -58,6 +61,40 @@ module Ea
58
61
  end
59
62
  end
60
63
 
64
+ def eval_size(ast, context)
65
+ collection = evaluate(ast.collection, context)
66
+ return 0 unless collection.respond_to?(:size)
67
+
68
+ collection.size
69
+ end
70
+
71
+ def eval_is_empty(ast, context)
72
+ collection = evaluate(ast.collection, context)
73
+ return true unless collection.respond_to?(:empty?)
74
+
75
+ collection.empty?
76
+ end
77
+
78
+ def eval_comparison(ast, context)
79
+ left = evaluate(ast.left, context)
80
+ right = evaluate(ast.right, context)
81
+ unless left.is_a?(Numeric) && right.is_a?(Numeric)
82
+ raise UnsupportedError,
83
+ "Comparison requires numeric operands: #{ast.op} on #{left.inspect}, #{right.inspect}"
84
+ end
85
+
86
+ case ast.op
87
+ when ">" then left > right
88
+ when "<" then left < right
89
+ when ">=" then left >= right
90
+ when "<=" then left <= right
91
+ when "=" then left == right
92
+ when "==" then left == right
93
+ when "!=" then left != right
94
+ else raise UnsupportedError, "Unknown comparison op: #{ast.op}"
95
+ end
96
+ end
97
+
61
98
  def eval_binary(ast, context)
62
99
  left = evaluate(ast.left, context)
63
100
  right = evaluate(ast.right, context)
data/lib/ea/ocl/nodes.rb CHANGED
@@ -25,6 +25,15 @@ module Ea
25
25
  # `<a> and <b>`, `<a> or <b>`, `not <a>`
26
26
  BinaryOp = Struct.new(:op, :left, :right, keyword_init: true)
27
27
  UnaryOp = Struct.new(:op, :operand, keyword_init: true)
28
+
29
+ # `<collection>->size()` — returns integer cardinality.
30
+ CollectionSize = Struct.new(:collection, keyword_init: true)
31
+
32
+ # `<collection>->isEmpty()` — returns boolean.
33
+ CollectionIsEmpty = Struct.new(:collection, keyword_init: true)
34
+
35
+ # `<a> op <b>` where op ∈ {>, <, >=, <=, =, ==, !=}.
36
+ Comparison = Struct.new(:op, :left, :right, keyword_init: true)
28
37
  end
29
38
  end
30
39
  end
data/lib/ea/ocl/parser.rb CHANGED
@@ -33,7 +33,7 @@ module Ea
33
33
  end
34
34
 
35
35
  # Parse one expression. Returns a single AST node.
36
- def parse_expression(source)
36
+ def parse_expression(source) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
37
37
  s = source.strip
38
38
 
39
39
  # Strip outer parens
@@ -45,7 +45,14 @@ module Ea
45
45
  operand: parse_expression(s[4..]))
46
46
  end
47
47
 
48
- # 'and' / 'or' at top level (find the operator not inside parens)
48
+ # 'and' / 'or' / comparison ops comparison has higher
49
+ # precedence than logical, so check it first.
50
+ if (split = split_on_comparison_op(s))
51
+ op, left, right = split
52
+ return Nodes::Comparison.new(op: op,
53
+ left: parse_expression(left),
54
+ right: parse_expression(right))
55
+ end
49
56
  if (split = split_on_logical_op(s))
50
57
  op, left, right = split
51
58
  return Nodes::BinaryOp.new(op: op,
@@ -58,6 +65,16 @@ module Ea
58
65
  return collection_node(m[2].to_sym, m[1], m[3], m[4])
59
66
  end
60
67
 
68
+ # Collection ->size() — integer cardinality
69
+ if (m = s.match(/^(.+?)->size\(\)\z/))
70
+ return Nodes::CollectionSize.new(collection: parse_expression(m[1]))
71
+ end
72
+
73
+ # Collection ->isEmpty() — boolean
74
+ if (m = s.match(/^(.+?)->isEmpty\(\)\z/))
75
+ return Nodes::CollectionIsEmpty.new(collection: parse_expression(m[1]))
76
+ end
77
+
61
78
  # String.matches('regex')
62
79
  if (m = s.match(/^(.+?)\.matches\(\s*['"](.+?)['"]\s*\)\z/))
63
80
  return Nodes::StringMatches.new(string: parse_expression(m[1]),
@@ -99,7 +116,6 @@ module Ea
99
116
  # Find the topmost ` and ` or ` or ` outside of parens.
100
117
  # Returns [op, left_str, right_str] or nil.
101
118
  def split_on_logical_op(source)
102
- depth = 0
103
119
  # Look for ' or ' first (lower precedence than 'and' in OCL?)
104
120
  # Actually OCL: 'or' < 'xor' < 'and' < 'implies'. We pick the
105
121
  # rightmost top-level operator (left-associative).
@@ -113,6 +129,45 @@ module Ea
113
129
  nil
114
130
  end
115
131
 
132
+ # Split on top-level comparison operator (>, <, >=, <=, =, ==, !=).
133
+ # Comparison has higher precedence than and/or, so we split here
134
+ # BEFORE calling split_on_logical_op.
135
+ def split_on_comparison_op(source)
136
+ %w[>= <= == != = > <].each do |op|
137
+ # Need to be careful with `>=` vs `>`: scan in order so
138
+ # multi-char operators are detected first.
139
+ idx = find_top_level_op(source, op)
140
+ next unless idx
141
+
142
+ return [op, source[0...idx], source[(idx + op.length)..]]
143
+ end
144
+ nil
145
+ end
146
+
147
+ def find_top_level_op(source, op) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
148
+ depth = 0
149
+ i = 0
150
+ while i < source.length
151
+ c = source[i]
152
+ depth += 1 if c == "("
153
+ depth -= 1 if c == ")"
154
+ if depth.zero? && source[i, op.length] == op
155
+ # Each `next` clause below MUST fall through to `i += 1`
156
+ # at the loop's tail — using `next` here would skip the
157
+ # increment and infinite-loop on the same matching char.
158
+ should_skip =
159
+ (op == ">" && source[i + 1, 1] == "=") || # >= matches as >
160
+ (op == "<" && source[i + 1, 1] == "=") || # <= matches as <
161
+ (op == "=" && source[i + 1, 1] == "=") || # == matches as =
162
+ (op == ">" && source[i - 1, 1] == "-") || # -> arrow
163
+ (op == "<" && source[i - 1, 1] == "-") # <- arrow
164
+ return i unless should_skip
165
+ end
166
+ i += 1
167
+ end
168
+ nil
169
+ end
170
+
116
171
  # Find substring at depth 0 (not inside parens).
117
172
  def find_top_level(source, substring)
118
173
  depth = 0
@@ -121,9 +176,8 @@ module Ea
121
176
  c = source[i]
122
177
  depth += 1 if c == "("
123
178
  depth -= 1 if c == ")"
124
- if depth.zero? && source[i, substring.length] == substring
125
- return i
126
- end
179
+ return i if depth.zero? && source[i, substring.length] == substring
180
+
127
181
  i += 1
128
182
  end
129
183
  nil
@@ -6,7 +6,7 @@ module Ea
6
6
  # UML-to-code mapping hint from t_implement.
7
7
  class EaImplement < BaseModel
8
8
  attribute :implement_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :language, :string
11
11
  attribute :code, :string
12
12
 
@@ -20,7 +20,7 @@ module Ea
20
20
 
21
21
  COLUMN_MAP = {
22
22
  "ImplementID" => :implement_id,
23
- "Object_ID" => :object_id
23
+ "Object_ID" => :ea_object_id
24
24
  }.freeze
25
25
 
26
26
  def self.column_map
@@ -6,7 +6,7 @@ module Ea
6
6
  # Effort estimate from t_objecteffort.
7
7
  class EaObjectEffort < BaseModel
8
8
  attribute :object_effort_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :effort, :string
11
11
  attribute :effort_type, :string
12
12
  attribute :notes, :string
@@ -21,7 +21,7 @@ module Ea
21
21
 
22
22
  COLUMN_MAP = {
23
23
  "ObjectEffortID" => :object_effort_id,
24
- "Object_ID" => :object_id,
24
+ "Object_ID" => :ea_object_id,
25
25
  "EffortType" => :effort_type
26
26
  }.freeze
27
27
 
@@ -6,7 +6,7 @@ module Ea
6
6
  # Object problem record from t_objectproblems.
7
7
  class EaObjectProblem < BaseModel
8
8
  attribute :object_problem_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :problem, :string
11
11
  attribute :problem_type, :string
12
12
  attribute :date_resolved, :string
@@ -23,7 +23,7 @@ module Ea
23
23
 
24
24
  COLUMN_MAP = {
25
25
  "ObjectProblemID" => :object_problem_id,
26
- "Object_ID" => :object_id,
26
+ "Object_ID" => :ea_object_id,
27
27
  "ProblemType" => :problem_type,
28
28
  "DateResolved" => :date_resolved
29
29
  }.freeze
@@ -6,7 +6,7 @@ module Ea
6
6
  # Requirement trace from t_objectrequires.
7
7
  class EaObjectRequire < BaseModel
8
8
  attribute :object_require_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :requirement, :string
11
11
  attribute :requirement_type, :string
12
12
  attribute :notes, :string
@@ -21,7 +21,7 @@ module Ea
21
21
 
22
22
  COLUMN_MAP = {
23
23
  "ObjectReqID" => :object_require_id,
24
- "Object_ID" => :object_id,
24
+ "Object_ID" => :ea_object_id,
25
25
  "ReqType" => :requirement_type
26
26
  }.freeze
27
27
 
@@ -6,7 +6,7 @@ module Ea
6
6
  # Resource allocation from t_objectresource.
7
7
  class EaObjectResource < BaseModel
8
8
  attribute :object_resource_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :resource, :string
11
11
  attribute :role, :string
12
12
  attribute :time_allocated, :string
@@ -22,7 +22,7 @@ module Ea
22
22
 
23
23
  COLUMN_MAP = {
24
24
  "ObjectResourceID" => :object_resource_id,
25
- "Object_ID" => :object_id
25
+ "Object_ID" => :ea_object_id
26
26
  }.freeze
27
27
 
28
28
  def self.column_map
@@ -6,7 +6,7 @@ module Ea
6
6
  # Risk register entry from t_objectrisks.
7
7
  class EaObjectRisk < BaseModel
8
8
  attribute :object_risk_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :risk, :string
11
11
  attribute :risk_type, :string
12
12
  attribute :notes, :string
@@ -21,7 +21,7 @@ module Ea
21
21
 
22
22
  COLUMN_MAP = {
23
23
  "ObjectRiskID" => :object_risk_id,
24
- "Object_ID" => :object_id,
24
+ "Object_ID" => :ea_object_id,
25
25
  "RiskType" => :risk_type
26
26
  }.freeze
27
27
 
@@ -6,7 +6,7 @@ module Ea
6
6
  # Use case scenario from t_objectscenarios.
7
7
  class EaObjectScenario < BaseModel
8
8
  attribute :object_scenario_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :scenario, :string
11
11
  attribute :scenario_type, :string
12
12
  attribute :notes, :string
@@ -21,7 +21,7 @@ module Ea
21
21
 
22
22
  COLUMN_MAP = {
23
23
  "ObjectScenarioID" => :object_scenario_id,
24
- "Object_ID" => :object_id,
24
+ "Object_ID" => :ea_object_id,
25
25
  "ScenarioType" => :scenario_type
26
26
  }.freeze
27
27
 
@@ -6,7 +6,7 @@ module Ea
6
6
  # Test case from t_objecttests.
7
7
  class EaObjectTest < BaseModel
8
8
  attribute :object_test_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :test, :string
11
11
  attribute :test_type, :string
12
12
  attribute :status, :string
@@ -22,7 +22,7 @@ module Ea
22
22
 
23
23
  COLUMN_MAP = {
24
24
  "ObjectTestID" => :object_test_id,
25
- "Object_ID" => :object_id,
25
+ "Object_ID" => :ea_object_id,
26
26
  "TestType" => :test_type
27
27
  }.freeze
28
28
 
@@ -6,7 +6,7 @@ module Ea
6
6
  # Transaction record from t_objecttrx.
7
7
  class EaObjectTrx < BaseModel
8
8
  attribute :object_trx_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :trx, :string
11
11
  attribute :trx_type, :string
12
12
  attribute :notes, :string
@@ -21,7 +21,7 @@ module Ea
21
21
 
22
22
  COLUMN_MAP = {
23
23
  "ObjectTrxID" => :object_trx_id,
24
- "Object_ID" => :object_id,
24
+ "Object_ID" => :ea_object_id,
25
25
  "TrxType" => :trx_type
26
26
  }.freeze
27
27
 
@@ -7,7 +7,7 @@ module Ea
7
7
  # entry to its visual icon and default toolbox slot.
8
8
  class EaPaletteItem < BaseModel
9
9
  attribute :palette_id, :string
10
- attribute :object_id, :integer
10
+ attribute :ea_object_id, :integer
11
11
  attribute :name, :string
12
12
  attribute :alias, :string
13
13
  attribute :image_id, :integer
@@ -23,7 +23,7 @@ module Ea
23
23
 
24
24
  COLUMN_MAP = {
25
25
  "PaletteID" => :palette_id,
26
- "Object_ID" => :object_id,
26
+ "Object_ID" => :ea_object_id,
27
27
  "Image_ID" => :image_id
28
28
  }.freeze
29
29
 
@@ -6,7 +6,7 @@ module Ea
6
6
  # Role-based constraint from t_roleconstraint.
7
7
  class EaRoleConstraint < BaseModel
8
8
  attribute :roleconstraint_id, :integer
9
- attribute :object_id, :integer
9
+ attribute :ea_object_id, :integer
10
10
  attribute :role, :string
11
11
  attribute :notes, :string
12
12
 
@@ -20,7 +20,7 @@ module Ea
20
20
 
21
21
  COLUMN_MAP = {
22
22
  "RoleConstraintID" => :roleconstraint_id,
23
- "Object_ID" => :object_id
23
+ "Object_ID" => :ea_object_id
24
24
  }.freeze
25
25
 
26
26
  def self.column_map
@@ -7,7 +7,7 @@ module Ea
7
7
  # the QEA are encrypted and what algorithm was used.
8
8
  class EaSecrypt < BaseModel
9
9
  attribute :secrypt_id, :integer
10
- attribute :object_id, :integer
10
+ attribute :ea_object_id, :integer
11
11
  attribute :encrypt, :string
12
12
  attribute :password_hash, :string
13
13
 
@@ -21,7 +21,7 @@ module Ea
21
21
 
22
22
  COLUMN_MAP = {
23
23
  "SecryptID" => :secrypt_id,
24
- "Object_ID" => :object_id,
24
+ "Object_ID" => :ea_object_id,
25
25
  "PasswordHash" => :password_hash
26
26
  }.freeze
27
27
 
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Qea
5
+ module Validation
6
+ # Evaluates OCL invariants stored in t_objectconstraint.
7
+ # For each constraint, parse its OCL expression via
8
+ # Ea::Ocl::Parser and evaluate it against the owning
9
+ # object's attribute values. Failures surface as :warning
10
+ # messages — OCL failures aren't structural errors.
11
+ class OclConstraintValidator < BaseValidator
12
+ def validate
13
+ constraints = database.collections[:object_constraints] || []
14
+ constraints.each { |c| check_constraint(c) }
15
+ end
16
+
17
+ private
18
+
19
+ def check_constraint(constraint)
20
+ owner = owner_for(constraint)
21
+ return unless owner
22
+
23
+ ast = parse(constraint.constraint)
24
+ return unless ast
25
+
26
+ if evaluate(ast, owner)
27
+ result.add(:info, "OCL constraint '#{constraint.constraint_type}' " \
28
+ "passed for #{owner.name}",
29
+ entity_name: owner.name)
30
+ else
31
+ result.add(:warning, "OCL constraint '#{constraint.constraint_type}' " \
32
+ "failed for #{owner.name}: #{constraint.constraint}",
33
+ entity_name: owner.name)
34
+ end
35
+ rescue Ea::Ocl::UnsupportedError => e
36
+ result.add(:info, "Skipping OCL constraint on #{owner&.name} " \
37
+ "(unsupported syntax): #{e.message}",
38
+ entity_name: owner&.name)
39
+ end
40
+
41
+ # Parse the OCL body. Returns nil for empty / non-invariant
42
+ # constraints (we don't try to evaluate non-invariant forms).
43
+ def parse(body)
44
+ return nil if body.nil? || body.empty?
45
+
46
+ Ea::Ocl::Parser.parse(body)
47
+ end
48
+
49
+ def evaluate(ast, owner)
50
+ Ea::Ocl::Evaluator.evaluate(ast, owner)
51
+ end
52
+
53
+ def owner_for(constraint)
54
+ object_id = constraint.respond_to?(:ea_object_id) ?
55
+ constraint.ea_object_id :
56
+ constraint.object_id
57
+ return nil unless object_id
58
+
59
+ (database.collections[:objects] || [])
60
+ .find { |o| o.object_id == object_id }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -10,6 +10,8 @@ module Ea
10
10
  "ea/qea/validation/database/orphan_validator"
11
11
  autoload :ReferentialIntegrityValidator,
12
12
  "ea/qea/validation/database/referential_integrity_validator"
13
+ autoload :OclConstraintValidator,
14
+ "ea/qea/validation/database/ocl_constraint_validator"
13
15
  end
14
16
  end
15
17
  end
@@ -83,6 +83,7 @@ module Ea
83
83
  orphan
84
84
  circular_reference
85
85
  package
86
+ ocl_constraint
86
87
  ]
87
88
 
88
89
  validator_names = if validators
@@ -213,6 +214,7 @@ module Ea
213
214
  @registry.register(:circular_reference,
214
215
  CircularReferenceValidator)
215
216
  @registry.register(:package, PackageValidator)
217
+ @registry.register(:ocl_constraint, OclConstraintValidator)
216
218
 
217
219
  # Phase 2: UML Tree Structure Validators
218
220
  # DocumentStructureValidator lives in lutaml-uml and may not be
@@ -20,6 +20,8 @@ module Ea
20
20
  "ea/qea/validation/database/orphan_validator"
21
21
  autoload :CircularReferenceValidator,
22
22
  "ea/qea/validation/database/circular_reference_validator"
23
+ autoload :OclConstraintValidator,
24
+ "ea/qea/validation/database/ocl_constraint_validator"
23
25
  autoload :Database, "ea/qea/validation/database"
24
26
  autoload :Formatters, "ea/qea/validation/formatters"
25
27
  autoload :ValidationEngine, "ea/qea/validation/validation_engine"
@@ -58,13 +58,15 @@ module Ea
58
58
  filter_by { |o| o.package_id == pkg.package_id }
59
59
  end
60
60
 
61
- # Filter by applied stereotype name.
61
+ # Filter by applied stereotype name. Stereotype lookup walks
62
+ # t_xref for @STEREO blocks referencing the object's ea_guid.
62
63
  # @param name [String]
63
64
  # @return [Builder]
64
65
  def with_stereotype(name)
66
+ xrefs = model.collections[:xrefs] || []
65
67
  filter_by do |o|
66
- refs = o.respond_to?(:stereotype_refs) ? o.stereotype_refs : nil
67
- refs&.any? { |r| r == name }
68
+ applied = stereotype_names_for(o, xrefs)
69
+ applied.any? { |r| r == name }
68
70
  end
69
71
  end
70
72
 
@@ -90,6 +92,23 @@ module Ea
90
92
  Builder.new(model, filters: filters + [block], kind: @kind)
91
93
  end
92
94
 
95
+ # Extract stereotype names applied to an object via t_xref.
96
+ # Returns [] when no @STEREO block references the object's GUID.
97
+ # @param object [Ea::Qea::Models::EaObject]
98
+ # @param xrefs [Array<Ea::Qea::Models::EaXref>]
99
+ # @return [Array<String>]
100
+ def stereotype_names_for(object, xrefs)
101
+ return [] unless object.is_a?(Ea::Qea::Models::EaObject)
102
+ return [] unless object.ea_guid
103
+
104
+ xrefs.select do |xr|
105
+ xr.client == object.ea_guid && xr.description&.include?("@STEREO")
106
+ end.map do |xr|
107
+ xr.description.match(/Name=([^;]+)/)[1]
108
+ end.compact
109
+ end
110
+ private :stereotype_names_for
111
+
93
112
  # Materialize the filtered collection.
94
113
  # @return [Array]
95
114
  def call
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Svg
5
+ module EaEmitter
6
+ module Compartment
7
+ # Stereotype decorator icon compartment. Emits the small
8
+ # symbolic polygon that EA renders inside an element's body
9
+ # when a stereotype provides a custom icon (FeatureType
10
+ # diamond, Type polygon, etc.). Delegates to
11
+ # Element::StereotypeIconRenderer.
12
+ #
13
+ # Rendered after the header so the icon sits inside the
14
+ # element body below the stereotype label.
15
+ module StereotypeIcon
16
+ module_function
17
+
18
+ def render(context)
19
+ return nil unless context.classifier
20
+
21
+ svg = Element::StereotypeIconRenderer.render(
22
+ classifier: context.classifier,
23
+ bounds: context.bounds,
24
+ canvas: context.canvas
25
+ )
26
+ svg.empty? ? nil : svg
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -14,6 +14,7 @@ module Ea
14
14
  autoload :NoteBody, "ea/svg/ea_emitter/compartment/note_body"
15
15
  autoload :Header, "ea/svg/ea_emitter/compartment/header"
16
16
  autoload :HeaderDivider, "ea/svg/ea_emitter/compartment/header_divider"
17
+ autoload :StereotypeIcon, "ea/svg/ea_emitter/compartment/stereotype_icon"
17
18
  autoload :Attributes, "ea/svg/ea_emitter/compartment/attributes"
18
19
  autoload :Operations, "ea/svg/ea_emitter/compartment/operations"
19
20
  autoload :EnumLiterals, "ea/svg/ea_emitter/compartment/enum_literals"
@@ -32,6 +33,7 @@ module Ea
32
33
  NoteBody,
33
34
  Header,
34
35
  HeaderDivider,
36
+ StereotypeIcon,
35
37
  Attributes,
36
38
  Operations,
37
39
  EnumLiterals,
@@ -26,9 +26,42 @@ module Ea
26
26
  document: document)
27
27
  .layers
28
28
  .reject { |s| s.nil? || s.empty? }
29
+ image_layer = emit_images
30
+ layers << image_layer if image_layer
29
31
 
30
32
  %(<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">\n\n<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="#{canvas.width_cm}" height="#{canvas.height_cm}" viewBox="#{canvas.view_box}">\n<title></title>\n<desc>Created with Enterprise Architect (Build: #{BUILD_ID}) 2</desc>\n#{layers.join("\n")}\n</svg>)
31
33
  end
34
+
35
+ # Walks t_image rows (when the document carries a Database
36
+ # with an :images collection) and invokes EmfRenderer on
37
+ # each. Emits an `<image>` element per successfully converted
38
+ # SVG, or returns nil when no images or all conversions fail.
39
+ #
40
+ # The emfsvg gem currently can't parse EA's specific EMF
41
+ # variant (see TODO.diagrams/87 + TODO.complete/56). Until
42
+ # upstream emfsvg supports it, this method returns nil
43
+ # gracefully. The wiring is correct infrastructure for when
44
+ # emfsvg catches up.
45
+ def emit_images
46
+ return nil unless document
47
+ return nil unless document.is_a?(Ea::Qea::Database)
48
+
49
+ images = document.collections[:images] || []
50
+ return nil if images.empty?
51
+
52
+ fragments = images.filter_map { |image| render_image(image) }
53
+ return nil if fragments.empty?
54
+
55
+ %(<g id="images">\n#{fragments.join("\n")}\n</g>)
56
+ end
57
+
58
+ def render_image(image)
59
+ svg = Ea::Image::EmfRenderer.render(image.bytes)
60
+ return nil unless svg
61
+
62
+ %(<g>#{svg}</g>)
63
+ end
64
+ private :render_image
32
65
  end
33
66
  end
34
67
  end