expressir 2.4.32 → 2.4.34
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/TODO.eengine/00-overview.md +39 -0
- data/TODO.eengine/01-resolution-diagnostics.md +30 -0
- data/TODO.eengine/02-check-catalog-completion.md +53 -0
- data/TODO.eengine/03-entity-computations.md +22 -0
- data/TODO.eengine/04-shtolo-rule-audit.md +32 -0
- data/TODO.eengine/05-shtolo-longform-flatten.md +12 -0
- data/TODO.eengine/06-population-p21.md +34 -0
- data/TODO.eengine/07-schema-compare-patch.md +25 -0
- data/TODO.eengine/08-official-corpus-gate.md +7 -0
- data/TODO.eengine/09-mim-mapping-parser.md +9 -0
- data/lib/expressir/express/checker.rb +191 -3
- data/lib/expressir/express/schema_compare.rb +194 -0
- data/lib/expressir/express/shtolo.rb +194 -63
- data/lib/expressir/express.rb +2 -0
- data/lib/expressir/model/declarations/entity.rb +132 -0
- data/lib/expressir/model/declarations/schema.rb +37 -6
- data/lib/expressir/p21/builder.rb +92 -0
- data/lib/expressir/p21/grammar.rb +107 -0
- data/lib/expressir/p21/parser.rb +16 -0
- data/lib/expressir/p21/population.rb +35 -0
- data/lib/expressir/p21/validator.rb +213 -0
- data/lib/expressir/p21.rb +17 -0
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +1 -0
- metadata +19 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Express
|
|
5
|
+
# Structural diff of two schemata with a machine-applicable patch
|
|
6
|
+
# (TODO.eengine/07; eeng --compare/--patch semantics, plugin/p11
|
|
7
|
+
# compare entry). Declarations match by canonical path
|
|
8
|
+
# `schema.item`; entity attributes get their own sub-deltas at
|
|
9
|
+
# `schema.item.attribute` carrying types, optionality and
|
|
10
|
+
# aggregates inside the signature.
|
|
11
|
+
#
|
|
12
|
+
# Interfaces are out of scope: the patch target is a flattened
|
|
13
|
+
# (longform) schema, which has none.
|
|
14
|
+
module SchemaCompare
|
|
15
|
+
# `node` carries the revised declaration for :added and :changed
|
|
16
|
+
# (nil for :removed) so {SchemaPatch.apply} can rebuild.
|
|
17
|
+
Delta = Struct.new(:op, :path, :node, keyword_init: true)
|
|
18
|
+
|
|
19
|
+
# Resolution artifacts and presentation metadata are not part of
|
|
20
|
+
# a declaration's structure.
|
|
21
|
+
SIGNATURE_IGNORE = %w[source include_source remarks remark_items
|
|
22
|
+
ref base_path file].freeze
|
|
23
|
+
|
|
24
|
+
COLLECTIONS = %i[constants types entities subtype_constraints
|
|
25
|
+
functions rules procedures].freeze
|
|
26
|
+
|
|
27
|
+
NODE_COLLECTION = {
|
|
28
|
+
"Expressir::Model::Declarations::Constant" => :constants,
|
|
29
|
+
"Expressir::Model::Declarations::Type" => :types,
|
|
30
|
+
"Expressir::Model::Declarations::Entity" => :entities,
|
|
31
|
+
"Expressir::Model::Declarations::SubtypeConstraint" => :subtype_constraints,
|
|
32
|
+
"Expressir::Model::Declarations::Function" => :functions,
|
|
33
|
+
"Expressir::Model::Declarations::Rule" => :rules,
|
|
34
|
+
"Expressir::Model::Declarations::Procedure" => :procedures,
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
def compare(base, revised)
|
|
40
|
+
COLLECTIONS.flat_map { |collection| collection_deltas(base, revised, collection) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def collection_deltas(base, revised, collection)
|
|
44
|
+
base_items = index(base, collection)
|
|
45
|
+
revised_items = index(revised, collection)
|
|
46
|
+
deltas = (revised_items.keys - base_items.keys).map do |path|
|
|
47
|
+
Delta.new(op: :added, path: path, node: revised_items[path])
|
|
48
|
+
end
|
|
49
|
+
(base_items.keys - revised_items.keys).each do |path|
|
|
50
|
+
deltas << Delta.new(op: :removed, path: path, node: nil)
|
|
51
|
+
end
|
|
52
|
+
(base_items.keys & revised_items.keys).each do |path|
|
|
53
|
+
old_item = base_items[path]
|
|
54
|
+
new_item = revised_items[path]
|
|
55
|
+
next if signature(old_item) == signature(new_item)
|
|
56
|
+
|
|
57
|
+
deltas << Delta.new(op: :changed, path: path, node: new_item)
|
|
58
|
+
next unless new_item.is_a?(Model::Declarations::Entity)
|
|
59
|
+
|
|
60
|
+
deltas.concat(attribute_deltas(old_item, new_item, path))
|
|
61
|
+
end
|
|
62
|
+
deltas
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def attribute_deltas(old_item, new_item, path)
|
|
66
|
+
old_attrs = index_by_id(Array(old_item.attributes))
|
|
67
|
+
new_attrs = index_by_id(Array(new_item.attributes))
|
|
68
|
+
deltas = (new_attrs.keys - old_attrs.keys).map do |name|
|
|
69
|
+
Delta.new(op: :added, path: "#{path}.#{name}",
|
|
70
|
+
node: new_attrs[name])
|
|
71
|
+
end
|
|
72
|
+
(old_attrs.keys - new_attrs.keys).each do |name|
|
|
73
|
+
deltas << Delta.new(op: :removed, path: "#{path}.#{name}", node: nil)
|
|
74
|
+
end
|
|
75
|
+
(old_attrs.keys & new_attrs.keys).each do |name|
|
|
76
|
+
next if signature(old_attrs[name]) == signature(new_attrs[name])
|
|
77
|
+
|
|
78
|
+
deltas << Delta.new(op: :changed, path: "#{path}.#{name}",
|
|
79
|
+
node: new_attrs[name])
|
|
80
|
+
end
|
|
81
|
+
deltas
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def index_by_id(items)
|
|
85
|
+
items.filter_map { |item| [item.id&.downcase, item] if item.id }.to_h
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def index(schema, collection)
|
|
89
|
+
index_by_id(Array(schema.send(collection)))
|
|
90
|
+
.transform_keys { |key| "#{schema.id.downcase}.#{key}" }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def signature(node)
|
|
94
|
+
strip(node.to_hash)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def strip(value)
|
|
98
|
+
case value
|
|
99
|
+
when Hash
|
|
100
|
+
value.each_with_object({}) do |(key, inner), out|
|
|
101
|
+
next if SIGNATURE_IGNORE.include?(key.to_s)
|
|
102
|
+
|
|
103
|
+
out[key] = strip(inner)
|
|
104
|
+
end
|
|
105
|
+
when Array then value.map { |inner| strip(inner) }
|
|
106
|
+
else value
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Applies {SchemaCompare} deltas to a schema model, returning the
|
|
112
|
+
# revised schema. The base schema is deep-copied through the
|
|
113
|
+
# lutaml-model round trip; the input schema is never mutated.
|
|
114
|
+
module SchemaPatch
|
|
115
|
+
module_function
|
|
116
|
+
|
|
117
|
+
def apply(base, deltas)
|
|
118
|
+
revised = base.class.from_hash(base.to_hash)
|
|
119
|
+
item_deltas, attribute_deltas = deltas.partition do |delta|
|
|
120
|
+
delta.path.split(".").size == 2
|
|
121
|
+
end
|
|
122
|
+
item_deltas.each { |delta| apply_item(revised, delta) }
|
|
123
|
+
# An item-level :added/:changed node already carries the final
|
|
124
|
+
# attribute list; its sub-deltas are informational only.
|
|
125
|
+
replaced_ops = %i[added changed].freeze
|
|
126
|
+
replaced = item_deltas
|
|
127
|
+
.select { |delta| replaced_ops.include?(delta.op) }
|
|
128
|
+
.map(&:path)
|
|
129
|
+
attribute_deltas
|
|
130
|
+
.reject { |delta| replaced.include?(delta.path.split(".")[0..1].join(".")) }
|
|
131
|
+
.each { |delta| apply_attribute(revised, delta) }
|
|
132
|
+
revised
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def apply_item(schema, delta)
|
|
136
|
+
item_key = delta.path.split(".").last.downcase
|
|
137
|
+
collection = if delta.op == :added
|
|
138
|
+
schema.send(SchemaCompare::NODE_COLLECTION.fetch(delta.node.class.name))
|
|
139
|
+
else
|
|
140
|
+
live_collection(schema, item_key)
|
|
141
|
+
end
|
|
142
|
+
return unless collection
|
|
143
|
+
|
|
144
|
+
case delta.op
|
|
145
|
+
when :added then collection << delta.node
|
|
146
|
+
when :removed then collection.delete_if { |item| key(item) == item_key }
|
|
147
|
+
when :changed
|
|
148
|
+
index = collection.index { |item| key(item) == item_key }
|
|
149
|
+
return unless index
|
|
150
|
+
|
|
151
|
+
collection.delete_at(index)
|
|
152
|
+
collection.insert(index, delta.node)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def apply_attribute(schema, delta)
|
|
157
|
+
segments = delta.path.split(".")
|
|
158
|
+
item = SchemaCompare.index_by_id(all_declarations(schema))[segments[1].downcase]
|
|
159
|
+
return unless item
|
|
160
|
+
|
|
161
|
+
attr_key = segments[2]
|
|
162
|
+
attrs = Array(item.attributes)
|
|
163
|
+
case delta.op
|
|
164
|
+
when :added
|
|
165
|
+
item.attributes = [*attrs, delta.node]
|
|
166
|
+
when :removed
|
|
167
|
+
item.attributes = attrs.reject { |attr| key(attr) == attr_key }
|
|
168
|
+
when :changed
|
|
169
|
+
index = attrs.index { |attr| key(attr) == attr_key }
|
|
170
|
+
return unless index
|
|
171
|
+
|
|
172
|
+
item.attributes = attrs
|
|
173
|
+
item.attributes.delete_at(index)
|
|
174
|
+
item.attributes.insert(index, delta.node)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def live_collection(schema, item_key)
|
|
179
|
+
SchemaCompare::COLLECTIONS.map { |name| schema.send(name) }
|
|
180
|
+
.find do |collection|
|
|
181
|
+
Array(collection).any? { |item| key(item) == item_key }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def all_declarations(schema)
|
|
186
|
+
SchemaCompare::COLLECTIONS.flat_map { |name| Array(schema.send(name)) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def key(item)
|
|
190
|
+
item.id&.downcase
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -130,16 +130,20 @@ module Expressir
|
|
|
130
130
|
schema.interfaces.each do |iface|
|
|
131
131
|
next unless iface.kind == Model::Declarations::Interface::REFERENCE
|
|
132
132
|
|
|
133
|
+
iface_schema = iface.schema.is_a?(String) ? iface.schema : iface.schema&.id
|
|
134
|
+
target = closure[iface_schema&.safe_downcase]
|
|
133
135
|
iface.items.each do |item|
|
|
134
136
|
original = original_name(item, schema)
|
|
135
|
-
next unless
|
|
137
|
+
next unless target&.entities&.find do |e|
|
|
138
|
+
e.id.safe_downcase == original.safe_downcase
|
|
139
|
+
end
|
|
136
140
|
|
|
137
141
|
@reference_entities << original.safe_downcase
|
|
138
142
|
end
|
|
139
143
|
end
|
|
140
144
|
end
|
|
141
145
|
|
|
142
|
-
declarations
|
|
146
|
+
declarations.concat(build_dependent_declarations) if @reference_entities.any?
|
|
143
147
|
|
|
144
148
|
Model::Declarations::Schema.new(
|
|
145
149
|
id: @longform_name,
|
|
@@ -166,7 +170,18 @@ module Expressir
|
|
|
166
170
|
# OWN schema's id, and the global map redirects the bare name so
|
|
167
171
|
# declarations from third schemas resolve to the first occurrence.
|
|
168
172
|
def plan_ids(schemas)
|
|
169
|
-
|
|
173
|
+
# G.NM.1: EVERY clashing declaration is prefixed (the annex
|
|
174
|
+
# example munges farming_dot_dog AND pet_shows_dot_dog), so the
|
|
175
|
+
# clash set is computed over the whole population first.
|
|
176
|
+
counts = Hash.new(0)
|
|
177
|
+
schemas.each do |schema|
|
|
178
|
+
all_decls(schema).each do |decl|
|
|
179
|
+
next unless decl.respond_to?(:id) && decl.id
|
|
180
|
+
|
|
181
|
+
counts[decl.id.safe_downcase] += 1
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
170
185
|
global = {} # downcase id => munged final
|
|
171
186
|
own_by_schema = {} # schema => {downcase id => final id}
|
|
172
187
|
schemas.each do |schema|
|
|
@@ -175,14 +190,11 @@ module Expressir
|
|
|
175
190
|
next unless decl.respond_to?(:id) && decl.id
|
|
176
191
|
|
|
177
192
|
key = decl.id.safe_downcase
|
|
178
|
-
if
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
seen_first[key] = schema
|
|
184
|
-
own[key] = decl.id
|
|
185
|
-
end
|
|
193
|
+
own[key] = if counts[key] > 1
|
|
194
|
+
global[key] = "#{schema.id.safe_downcase}_dot_#{decl.id}"
|
|
195
|
+
else
|
|
196
|
+
decl.id
|
|
197
|
+
end
|
|
186
198
|
end
|
|
187
199
|
own_by_schema[schema] = own
|
|
188
200
|
end
|
|
@@ -253,11 +265,20 @@ module Expressir
|
|
|
253
265
|
current == current.upcase ? new.upcase : new
|
|
254
266
|
end
|
|
255
267
|
|
|
256
|
-
# G.1.6: parse-templates keep the annex rule/function verbatim
|
|
257
|
-
|
|
268
|
+
# G.1.6: parse-templates keep the annex rule/function verbatim;
|
|
269
|
+
# the pair is emitted together (the rule's WHERE calls the
|
|
270
|
+
# function, and the prune pass would drop an uncalled function).
|
|
271
|
+
def build_dependent_declarations
|
|
272
|
+
snippet = parse_schema_snippet(dependent_template_src)
|
|
273
|
+
rule = snippet.schemas.first.rules.first
|
|
274
|
+
fn = snippet.schemas.first.functions.first
|
|
275
|
+
[rule, fn].compact
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def dependent_template_src
|
|
258
279
|
list = @reference_entities.map(&:upcase).join(", ")
|
|
259
280
|
union = @reference_entities.join("+")
|
|
260
|
-
|
|
281
|
+
<<~EXP
|
|
261
282
|
SCHEMA __dependent__;
|
|
262
283
|
RULE #{DEPENDENT_RULE_NAME} FOR (#{list});
|
|
263
284
|
LOCAL
|
|
@@ -270,9 +291,62 @@ module Expressir
|
|
|
270
291
|
WHERE
|
|
271
292
|
wr1: dependently_instantiated(all_instances, set_of_input_types, previous_in_chain);
|
|
272
293
|
END_RULE;
|
|
294
|
+
|
|
295
|
+
FUNCTION dependently_instantiated(
|
|
296
|
+
set_of_input_instances : SET OF GENERIC:igen;
|
|
297
|
+
set_of_input_types : SET OF STRING;
|
|
298
|
+
previous_in_chain : LIST OF GENERIC:cgen): BOOLEAN;
|
|
299
|
+
LOCAL
|
|
300
|
+
number_of_input_instances : INTEGER;
|
|
301
|
+
number_of_referring_instances : INTEGER;
|
|
302
|
+
bag_of_referring_instances : BAG OF GENERIC:igen := [];
|
|
303
|
+
dependently_instantiated_flag : BOOLEAN;
|
|
304
|
+
previous_in_chain_plus : LIST OF GENERIC:cgen := [];
|
|
305
|
+
result : BOOLEAN := true;
|
|
306
|
+
set_of_types : SET OF STRING := [];
|
|
307
|
+
END_LOCAL;
|
|
308
|
+
IF EXISTS(set_of_input_instances) THEN
|
|
309
|
+
number_of_input_instances := SIZEOF(set_of_input_instances);
|
|
310
|
+
set_of_input_types := set_of_input_types + 'GENERIC';
|
|
311
|
+
REPEAT i := 1 TO number_of_input_instances;
|
|
312
|
+
bag_of_referring_instances := USEDIN(set_of_input_instances[i], '');
|
|
313
|
+
IF EXISTS(bag_of_referring_instances) THEN
|
|
314
|
+
number_of_referring_instances := SIZEOF(bag_of_referring_instances);
|
|
315
|
+
dependently_instantiated_flag := false;
|
|
316
|
+
REPEAT j := 1 TO number_of_referring_instances;
|
|
317
|
+
set_of_types := TYPEOF(bag_of_referring_instances[j]);
|
|
318
|
+
IF set_of_types <= set_of_input_types THEN
|
|
319
|
+
IF NOT (bag_of_referring_instances[j] IN previous_in_chain) THEN
|
|
320
|
+
previous_in_chain_plus := previous_in_chain +
|
|
321
|
+
set_of_input_instances[i];
|
|
322
|
+
IF dependently_instantiated([bag_of_referring_instances[j]],
|
|
323
|
+
set_of_input_types,
|
|
324
|
+
previous_in_chain_plus) THEN
|
|
325
|
+
dependently_instantiated_flag := true;
|
|
326
|
+
ESCAPE;
|
|
327
|
+
ELSE
|
|
328
|
+
SKIP;
|
|
329
|
+
END_IF;
|
|
330
|
+
END_IF;
|
|
331
|
+
ELSE
|
|
332
|
+
dependently_instantiated_flag := true;
|
|
333
|
+
ESCAPE;
|
|
334
|
+
END_IF;
|
|
335
|
+
END_REPEAT;
|
|
336
|
+
IF NOT dependently_instantiated_flag THEN
|
|
337
|
+
RETURN(false);
|
|
338
|
+
END_IF;
|
|
339
|
+
ELSE
|
|
340
|
+
RETURN(false);
|
|
341
|
+
END_IF;
|
|
342
|
+
END_REPEAT;
|
|
343
|
+
ELSE
|
|
344
|
+
RETURN(false);
|
|
345
|
+
END_IF;
|
|
346
|
+
RETURN(true);
|
|
347
|
+
END_FUNCTION;
|
|
273
348
|
END_SCHEMA;
|
|
274
349
|
EXP
|
|
275
|
-
parse_schema_snippet(src).schemas.first.rules.first
|
|
276
350
|
end
|
|
277
351
|
|
|
278
352
|
def parse_schema_snippet(src)
|
|
@@ -295,10 +369,32 @@ module Expressir
|
|
|
295
369
|
convert_generic_entities!(artifact)
|
|
296
370
|
convert_renamed_attributes!(artifact)
|
|
297
371
|
rewrite_qualified_strings!(artifact)
|
|
372
|
+
add_provenance_remark!(artifact)
|
|
298
373
|
|
|
299
374
|
artifact
|
|
300
375
|
end
|
|
301
376
|
|
|
377
|
+
# G.1.10: optional embedded provenance remark — if any schema
|
|
378
|
+
# name is kept in the longform, ALL contributing schemas are
|
|
379
|
+
# recorded (`schema = <id> [schema_version_id = '<v>'];`, order
|
|
380
|
+
# insignificant).
|
|
381
|
+
def add_provenance_remark!(schema)
|
|
382
|
+
names = closure.values.map(&:id).sort
|
|
383
|
+
return if names.empty?
|
|
384
|
+
|
|
385
|
+
remarks = ["Original 2003 schemas:"] + names.map do |id|
|
|
386
|
+
contributing = closure.values.find { |s| s.id == id }
|
|
387
|
+
version = contributing&.version&.id
|
|
388
|
+
line = "schema = #{id};"
|
|
389
|
+
line = "schema = #{id} schema_version_id = '#{version}';" if version
|
|
390
|
+
line
|
|
391
|
+
end
|
|
392
|
+
schema.remark_items = [
|
|
393
|
+
Model::Declarations::RemarkItem.new(id: "provenance", remarks: remarks),
|
|
394
|
+
*Array(schema.remark_items),
|
|
395
|
+
]
|
|
396
|
+
end
|
|
397
|
+
|
|
302
398
|
# G.1.9 prune pass: the artifact keeps only what stays visible
|
|
303
399
|
# and reachable. Runs before the stage-2 rewrites so they never
|
|
304
400
|
# see declarations the longform would drop.
|
|
@@ -477,57 +573,95 @@ module Expressir
|
|
|
477
573
|
end
|
|
478
574
|
end
|
|
479
575
|
|
|
576
|
+
# G.2.2/G.2.4: every extensible/extending select becomes a
|
|
577
|
+
# non-extensible select whose items are the full domain in
|
|
578
|
+
# longform scope. Chains fold in dependency order (a node whose
|
|
579
|
+
# own BASED_ON is still pending is deferred), so an extending
|
|
580
|
+
# type that is itself extensible resolves against its base's
|
|
581
|
+
# folded domain.
|
|
582
|
+
# G.2.2/G.2.4: every extensible/extending select becomes a
|
|
583
|
+
# non-extensible select whose items are the full domain in
|
|
584
|
+
# longform scope. Chains fold in dependency order (a node whose
|
|
585
|
+
# own BASED_ON is still pending is deferred), so an extending
|
|
586
|
+
# type that is itself extensible resolves against its base's
|
|
587
|
+
# folded domain.
|
|
480
588
|
def resolve_extensible_selects!(schema)
|
|
481
|
-
schema.types.select do |
|
|
482
|
-
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
# every entity the artifact carries, so the longform select is
|
|
489
|
-
# complete against the closure. Flags stay as declared: the
|
|
490
|
-
# extensible/generic-entity markers are part of the shortform's
|
|
491
|
-
# semantics and are not expressir's to drop.
|
|
492
|
-
if sel.generic_entity
|
|
493
|
-
sel.items = schema.entities.map do |e|
|
|
494
|
-
Model::References::SimpleReference.new(id: e.id)
|
|
495
|
-
end
|
|
496
|
-
next
|
|
497
|
-
end
|
|
589
|
+
select_types = schema.types.select do |ty|
|
|
590
|
+
ty.underlying_type.is_a?(Model::DataTypes::Select)
|
|
591
|
+
end
|
|
592
|
+
select_types.select { |ty| sel_pending?(ty) }.each do |base|
|
|
593
|
+
fold_select_node(schema, select_types, base)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
498
596
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
597
|
+
def sel_pending?(select_type)
|
|
598
|
+
sel = select_type.underlying_type
|
|
599
|
+
sel.extensible || sel.based_on
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def fold_select_node(schema, select_types, base_type)
|
|
603
|
+
sel = base_type.underlying_type
|
|
604
|
+
return unless sel_pending?(base_type)
|
|
605
|
+
|
|
606
|
+
# all-extenders for GENERIC_ENTITY selects (WG12 requirement,
|
|
607
|
+
# #32): the extenders are the entity types themselves — fold in
|
|
608
|
+
# every entity the artifact carries, so the longform select is
|
|
609
|
+
# complete against the closure. Flags stay as declared: the
|
|
610
|
+
# extensible/generic-entity markers are part of the shortform's
|
|
611
|
+
# semantics and are not expressir's to drop.
|
|
612
|
+
if sel.generic_entity && !sel.based_on
|
|
613
|
+
sel.items = schema.entities.map do |e|
|
|
614
|
+
Model::References::SimpleReference.new(id: e.id)
|
|
503
615
|
end
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
616
|
+
return
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
own_items = Array(sel.items)
|
|
620
|
+
inherited = []
|
|
621
|
+
if sel.based_on
|
|
622
|
+
base_ty = select_types.find do |ty|
|
|
623
|
+
ty.id&.safe_downcase == sel.based_on&.id&.safe_downcase
|
|
512
624
|
end
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
625
|
+
fold_select_node(schema, select_types, base_ty) if base_ty && sel_pending?(base_ty)
|
|
626
|
+
inherited = base_ty ? Array(base_ty.underlying_type.items) : []
|
|
627
|
+
end
|
|
516
628
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
629
|
+
extenders = select_types.select do |ty|
|
|
630
|
+
!ty.equal?(base_type) &&
|
|
631
|
+
ty.underlying_type.based_on&.id&.safe_downcase == base_type.id&.safe_downcase
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
merged = inherited + own_items +
|
|
635
|
+
extenders.flat_map { |ty| Array(ty.underlying_type.items) }
|
|
636
|
+
seen = {}
|
|
637
|
+
uniq = merged.select do |item|
|
|
638
|
+
key = item.id.safe_downcase
|
|
639
|
+
next false if key.empty? || seen.key?(key)
|
|
640
|
+
|
|
641
|
+
seen[key] = true
|
|
642
|
+
end
|
|
643
|
+
sel.items = uniq
|
|
644
|
+
sel.extensible = false
|
|
645
|
+
sel.based_on = nil
|
|
646
|
+
|
|
647
|
+
extenders.each do |ty|
|
|
648
|
+
ext_sel = ty.underlying_type
|
|
649
|
+
keep = Array(ext_sel.items).map { |i| i.id.upcase }
|
|
650
|
+
base_ref = "#{@longform_name.upcase}.#{base_type.id.upcase}"
|
|
651
|
+
ty.underlying_type = Model::DataTypes::Select.new(
|
|
652
|
+
items: uniq.map { |i| Model::References::SimpleReference.new(id: i.id) },
|
|
653
|
+
)
|
|
654
|
+
ext_sel_items = ty.underlying_type.items
|
|
655
|
+
exclusions = uniq.reject { |i| keep.include?(i.id.upcase) }
|
|
656
|
+
ty.where_rules = exclusions.each_with_index.map do |item, i|
|
|
657
|
+
Model::Declarations::WhereRule.new(
|
|
658
|
+
id: "wr#{i + 1}",
|
|
659
|
+
expression: parse_expression(
|
|
660
|
+
"NOT ('#{base_ref}.#{item.id.upcase}' IN TYPEOF(SELF));",
|
|
661
|
+
),
|
|
523
662
|
)
|
|
524
|
-
ext.where_rules = exclusions.each_with_index.map do |item, i|
|
|
525
|
-
Model::Declarations::WhereRule.new(
|
|
526
|
-
id: "wr#{i + 1}",
|
|
527
|
-
expression: parse_expression("NOT ('#{base_ref}.#{item.id.upcase}' IN TYPEOF(SELF));"),
|
|
528
|
-
)
|
|
529
|
-
end
|
|
530
663
|
end
|
|
664
|
+
ext_sel_items
|
|
531
665
|
end
|
|
532
666
|
end
|
|
533
667
|
|
|
@@ -543,10 +677,7 @@ module Expressir
|
|
|
543
677
|
entity.supertype_expression =
|
|
544
678
|
entity.supertype_expression ? combine_andor(entity.supertype_expression, expr) : expr
|
|
545
679
|
end
|
|
546
|
-
if
|
|
547
|
-
# G.2.6: abstract entity ⇒ ABSTRACT SUPERTYPE (formatter
|
|
548
|
-
# renders from abstract + supertype status).
|
|
549
|
-
end
|
|
680
|
+
entity.abstract = true if sc.abstract
|
|
550
681
|
next if Array(sc.total_over).empty?
|
|
551
682
|
|
|
552
683
|
rule_name = "total_over_#{sc.id}"
|
data/lib/expressir/express.rb
CHANGED
|
@@ -43,6 +43,8 @@ module Expressir
|
|
|
43
43
|
autoload :SchemaPlainSourceFormatter,
|
|
44
44
|
"#{__dir__}/express/schema_plain_source_formatter"
|
|
45
45
|
autoload :SchemaSourceFormatter, "#{__dir__}/express/schema_source_formatter"
|
|
46
|
+
autoload :SchemaCompare, "#{__dir__}/express/schema_compare"
|
|
47
|
+
autoload :SchemaPatch, "#{__dir__}/express/schema_compare"
|
|
46
48
|
autoload :ScopeResolver, "#{__dir__}/express/scope_resolver"
|
|
47
49
|
autoload :LazyRepository, "#{__dir__}/express/lazy_repository"
|
|
48
50
|
autoload :SmrlXml, "#{__dir__}/express/smrl_xml"
|
|
@@ -56,6 +56,138 @@ module Expressir
|
|
|
56
56
|
*remark_items,
|
|
57
57
|
]
|
|
58
58
|
end
|
|
59
|
+
|
|
60
|
+
# eeng compute-inheritance: the full supertype closure, nearest
|
|
61
|
+
# supertype first, cycle-safe. Cross-schema supertypes resolve
|
|
62
|
+
# through the enclosing repository.
|
|
63
|
+
# @return [Array<Entity>]
|
|
64
|
+
def supertype_closure
|
|
65
|
+
closure = []
|
|
66
|
+
visited = { id&.safe_downcase => true }
|
|
67
|
+
queue = Array(subtype_of).map { |ref| ref.respond_to?(:id) ? ref.id : ref }
|
|
68
|
+
while (name = queue.shift)
|
|
69
|
+
key = name&.safe_downcase
|
|
70
|
+
next if key.nil? || visited.key?(key)
|
|
71
|
+
|
|
72
|
+
supertype = find_supertype_entity(key)
|
|
73
|
+
next unless supertype
|
|
74
|
+
|
|
75
|
+
visited[key] = true
|
|
76
|
+
closure << supertype
|
|
77
|
+
queue.concat(Array(supertype.subtype_of).map do |ref|
|
|
78
|
+
ref.respond_to?(:id) ? ref.id : ref
|
|
79
|
+
end)
|
|
80
|
+
end
|
|
81
|
+
closure
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Explicit attributes inherited from the supertype closure,
|
|
85
|
+
# nearest supertype first, skipping ids this entity (or a
|
|
86
|
+
# nearer ancestor) redeclares.
|
|
87
|
+
# @return [Array<ExplicitAttribute>]
|
|
88
|
+
def inherited_explicit_attributes
|
|
89
|
+
claimed = explicit_only(Array(attributes)).filter_map(&:id).map(&:safe_downcase)
|
|
90
|
+
closure = supertype_closure
|
|
91
|
+
closure.flat_map do |supertype|
|
|
92
|
+
explicit_only(Array(supertype.attributes)).filter_map do |attr|
|
|
93
|
+
next nil unless attr.id
|
|
94
|
+
|
|
95
|
+
key = attr.id.safe_downcase
|
|
96
|
+
next nil if claimed.include?(key)
|
|
97
|
+
|
|
98
|
+
claimed << key
|
|
99
|
+
attr
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Derived attributes accumulated from the supertype closure.
|
|
105
|
+
# @return [Array<DerivedAttribute>]
|
|
106
|
+
def inherited_derived_attributes
|
|
107
|
+
closure = supertype_closure
|
|
108
|
+
seen = {}
|
|
109
|
+
closure.flat_map do |supertype|
|
|
110
|
+
derived_attribute_items(supertype).filter_map do |attr|
|
|
111
|
+
next nil unless attr.id
|
|
112
|
+
|
|
113
|
+
key = attr.id.safe_downcase
|
|
114
|
+
next nil if seen.key?(key)
|
|
115
|
+
|
|
116
|
+
seen[key] = true
|
|
117
|
+
attr
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Inverse attributes accumulated from the supertype closure.
|
|
123
|
+
# @return [Array<InverseAttribute>]
|
|
124
|
+
def inherited_inverse_attributes
|
|
125
|
+
closure = supertype_closure
|
|
126
|
+
seen = {}
|
|
127
|
+
closure.flat_map do |supertype|
|
|
128
|
+
inverse_attribute_items(supertype).filter_map do |attr|
|
|
129
|
+
next nil unless attr.id
|
|
130
|
+
|
|
131
|
+
key = attr.id.safe_downcase
|
|
132
|
+
next nil if seen.key?(key)
|
|
133
|
+
|
|
134
|
+
seen[key] = true
|
|
135
|
+
attr
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# All explicit attributes: own + inherited (eeng
|
|
141
|
+
# explicit-attrs materialization).
|
|
142
|
+
# @return [Array<ExplicitAttribute>]
|
|
143
|
+
def all_explicit_attributes
|
|
144
|
+
[*explicit_only(Array(attributes)), *inherited_explicit_attributes]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Entity#attributes carries every attribute kind; the explicit
|
|
150
|
+
# kind is Expressir::Model::Declarations::Attribute.
|
|
151
|
+
# DerivedAttribute/InverseAttribute subclass Attribute, so the
|
|
152
|
+
# explicit kind is the exact base class.
|
|
153
|
+
def explicit_only(attrs)
|
|
154
|
+
attrs.select { |a| a.instance_of?(Expressir::Model::Declarations::Attribute) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The Ruby parse path parks every attribute kind in `attributes`
|
|
158
|
+
# (typed by class); the core path fills derived_attributes/
|
|
159
|
+
# inverse_attributes instead. Both layouts resolve to the same
|
|
160
|
+
# items here.
|
|
161
|
+
def derived_attribute_items(entity)
|
|
162
|
+
own = Array(entity.derived_attributes)
|
|
163
|
+
return own if own.any?
|
|
164
|
+
|
|
165
|
+
Array(entity.attributes).grep(Expressir::Model::Declarations::DerivedAttribute)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def inverse_attribute_items(entity)
|
|
169
|
+
own = Array(entity.inverse_attributes)
|
|
170
|
+
return own if own.any?
|
|
171
|
+
|
|
172
|
+
Array(entity.attributes).grep(Expressir::Model::Declarations::InverseAttribute)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def find_supertype_entity(key)
|
|
176
|
+
container = parent
|
|
177
|
+
container = container.parent while container &&
|
|
178
|
+
!container.is_a?(Model::Repository) &&
|
|
179
|
+
!container.is_a?(Model::ExpFile)
|
|
180
|
+
return nil unless container.respond_to?(:schemas)
|
|
181
|
+
|
|
182
|
+
container.schemas.each do |foreign|
|
|
183
|
+
next unless foreign.respond_to?(:entities)
|
|
184
|
+
|
|
185
|
+
foreign.entities.each do |candidate|
|
|
186
|
+
return candidate if candidate.id&.safe_downcase == key
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
59
191
|
end
|
|
60
192
|
end
|
|
61
193
|
end
|