expressir 2.4.29-aarch64-linux → 2.4.35-aarch64-linux
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/.github/scripts/test_installed_gem.rb +18 -4
- data/.github/workflows/native-gems.yml +4 -1
- 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/TODO.parity-ee/00-overview.md +1 -1
- data/TODO.suma-improvements/07-part28-xml.md +10 -13
- data/expressir.gemspec +5 -5
- data/lib/expressir/3.2/expressir_core.so +0 -0
- data/lib/expressir/3.3/expressir_core.so +0 -0
- data/lib/expressir/3.4/expressir_core.so +0 -0
- data/lib/expressir/4.0/expressir_core.so +0 -0
- data/lib/expressir/cli.rb +7 -0
- data/lib/expressir/commands/parity_inputs.rb +6 -1
- data/lib/expressir/commands/smrl_xml_index.rb +25 -0
- data/lib/expressir/commands.rb +2 -1
- data/lib/expressir/express/builder.rb +17 -5
- data/lib/expressir/express/builders/entity_decl_builder.rb +17 -5
- data/lib/expressir/express/checker.rb +191 -3
- data/lib/expressir/express/core.rb +1 -0
- data/lib/expressir/express/formatters/declarations_formatter.rb +21 -14
- data/lib/expressir/express/grammar/parser.rb +29 -1
- data/lib/expressir/express/pretty_formatter.rb +9 -14
- 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 +37 -19
- /data/lib/expressir/commands/{version.rb → version_command.rb} +0 -0
|
@@ -82,6 +82,13 @@ module Expressir
|
|
|
82
82
|
blength insert remove
|
|
83
83
|
].freeze
|
|
84
84
|
|
|
85
|
+
USE_ITEM_KINDS = [Model::Declarations::Entity, Model::Declarations::Type].freeze
|
|
86
|
+
REFERENCE_ITEM_KINDS = [
|
|
87
|
+
Model::Declarations::Constant, Model::Declarations::Entity,
|
|
88
|
+
Model::Declarations::Function, Model::Declarations::Procedure,
|
|
89
|
+
Model::Declarations::Type
|
|
90
|
+
].freeze
|
|
91
|
+
|
|
85
92
|
private
|
|
86
93
|
|
|
87
94
|
def note!(id, severity, schema, message, node = nil)
|
|
@@ -92,6 +99,7 @@ module Expressir
|
|
|
92
99
|
def check_schema(schema)
|
|
93
100
|
check_duplicates(schema)
|
|
94
101
|
check_interfaces(schema)
|
|
102
|
+
check_type_resolutions(schema)
|
|
95
103
|
check_self_schema_reference(schema)
|
|
96
104
|
schema.entities.each { |e| check_entity(schema, e) }
|
|
97
105
|
schema.types.each do |t|
|
|
@@ -100,9 +108,11 @@ module Expressir
|
|
|
100
108
|
check_where_rules(schema, t, t.where_rules)
|
|
101
109
|
end
|
|
102
110
|
schema.rules.each { |r| check_where_rules(schema, r, r.where_rules) }
|
|
111
|
+
check_subtype_constraints(schema)
|
|
103
112
|
schema.entities.each do |entity|
|
|
104
113
|
check_aggregate_bounds(schema, entity)
|
|
105
114
|
check_attrib_name_fun(schema, entity)
|
|
115
|
+
check_attribute_redeclarations(schema, entity)
|
|
106
116
|
end
|
|
107
117
|
check_string_references(schema)
|
|
108
118
|
check_unresolved_refs(schema)
|
|
@@ -184,14 +194,44 @@ module Expressir
|
|
|
184
194
|
end
|
|
185
195
|
|
|
186
196
|
resource_names(iface).each do |name|
|
|
187
|
-
|
|
197
|
+
decl = foreign_declaration(foreign, name)
|
|
198
|
+
unless decl
|
|
199
|
+
note!(:check_unresolved_ref, :error, schema,
|
|
200
|
+
"interface resource '#{name}' not in schema '#{target}'", iface)
|
|
201
|
+
next
|
|
202
|
+
end
|
|
188
203
|
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
# eeng resolve-interface: use-from resolves via
|
|
205
|
+
# find-type/entity, reference-from via the 5-kind or-chain;
|
|
206
|
+
# anything else is "Resource '~A' not found".
|
|
207
|
+
next if item_kind_interfacable?(iface.kind, decl)
|
|
208
|
+
|
|
209
|
+
note!(:check_interface_kind_restriction, :error, schema,
|
|
210
|
+
"interface resource '#{name}' is a #{decl.class.name.split('::').last}: " + "#{iface.kind} resolves #{allowed_kinds(iface.kind)} only", iface)
|
|
191
211
|
end
|
|
192
212
|
end
|
|
193
213
|
end
|
|
194
214
|
|
|
215
|
+
def item_kind_interfacable?(kind, decl)
|
|
216
|
+
(kind == Model::Declarations::Interface::REFERENCE ? REFERENCE_ITEM_KINDS : USE_ITEM_KINDS)
|
|
217
|
+
.any? { |k| decl.is_a?(k) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def allowed_kinds(kind)
|
|
221
|
+
(kind == Model::Declarations::Interface::REFERENCE ? REFERENCE_ITEM_KINDS : USE_ITEM_KINDS)
|
|
222
|
+
.map { |k| k.name.split("::").last }.join(", ")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def foreign_declaration(schema, name)
|
|
226
|
+
key = name.safe_downcase
|
|
227
|
+
DECL_COLLECTIONS.each do |coll|
|
|
228
|
+
Array(schema.public_send(coll)).each do |d|
|
|
229
|
+
return d if d.id&.safe_downcase == key
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
nil
|
|
233
|
+
end
|
|
234
|
+
|
|
195
235
|
def iface_schema_name(iface)
|
|
196
236
|
iface.schema.is_a?(String) ? iface.schema : iface.schema&.id
|
|
197
237
|
end
|
|
@@ -210,6 +250,85 @@ module Expressir
|
|
|
210
250
|
end
|
|
211
251
|
end
|
|
212
252
|
|
|
253
|
+
# eeng check-p11 context checks (kernel/check.lisp:160-201,
|
|
254
|
+
# 299-336, 337-395): constant/local/parameter/return types must
|
|
255
|
+
# resolve to a TYPE or ENTITY visible in the schema (find-type/
|
|
256
|
+
# entity semantics - constants, functions, procedures do NOT
|
|
257
|
+
# satisfy them). Note ids are the eeng ones.
|
|
258
|
+
def check_type_resolutions(schema)
|
|
259
|
+
schema.constants.each do |constant|
|
|
260
|
+
check_type_ref(schema, Array(constant.type), :check_constant_type, constant.id, constant)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
schema.functions.each do |function|
|
|
264
|
+
Array(function.parameters).each do |param|
|
|
265
|
+
check_type_ref(schema, Array(param.type), :check_parameter_type, param.id, param)
|
|
266
|
+
end
|
|
267
|
+
check_type_ref(schema, Array(function.return_type),
|
|
268
|
+
:check_function_return_type, function.id, function)
|
|
269
|
+
Array(function.variables).each do |variable|
|
|
270
|
+
check_type_ref(schema, Array(variable.type), :check_local_type, variable.id, variable)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
schema.procedures.each do |procedure|
|
|
275
|
+
Array(procedure.parameters).each do |param|
|
|
276
|
+
check_type_ref(schema, Array(param.type), :check_parameter_type, param.id, param)
|
|
277
|
+
end
|
|
278
|
+
Array(procedure.variables).each do |variable|
|
|
279
|
+
check_type_ref(schema, Array(variable.type), :check_local_type, variable.id, variable)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
schema.rules.each do |rule|
|
|
284
|
+
Array(rule.applies_to).each do |entity_ref|
|
|
285
|
+
# parameter-rule: the FOR entities resolve via
|
|
286
|
+
# find-type/entity too (kernel/check.lisp:311-322)
|
|
287
|
+
check_entity_ref(schema, Array(entity_ref), :check_parameter_type, rule.id, rule)
|
|
288
|
+
end
|
|
289
|
+
Array(rule.variables).each do |variable|
|
|
290
|
+
check_type_ref(schema, Array(variable.type), :check_local_type, variable.id, variable)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def check_type_ref(schema, nodes, note_id, name, node)
|
|
296
|
+
nodes.each do |type|
|
|
297
|
+
next unless type.is_a?(Model::References::SimpleReference)
|
|
298
|
+
next if type.id.nil? || builtin?(type.id)
|
|
299
|
+
next if resolves_to_type_or_entity?(schema, type.id)
|
|
300
|
+
|
|
301
|
+
note!(note_id, :error, schema,
|
|
302
|
+
"type '#{type.id}' of '#{name}' does not resolve to a type or entity", node || type)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def check_entity_ref(schema, nodes, note_id, name, node)
|
|
307
|
+
nodes.each do |ref|
|
|
308
|
+
next unless ref.is_a?(Model::References::SimpleReference)
|
|
309
|
+
next if ref.id.nil?
|
|
310
|
+
next if resolves_to_type_or_entity?(schema, ref.id)
|
|
311
|
+
|
|
312
|
+
note!(note_id, :error, schema,
|
|
313
|
+
"entity '#{ref.id}' of '#{name}' does not resolve to a type or entity", node || ref)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def resolves_to_type_or_entity?(schema, name)
|
|
318
|
+
key = name.safe_downcase
|
|
319
|
+
schema.children.any? do |child|
|
|
320
|
+
next false unless child.id&.safe_downcase == key
|
|
321
|
+
|
|
322
|
+
child.is_a?(Model::Declarations::Type) || child.is_a?(Model::Declarations::Entity) ||
|
|
323
|
+
(child.is_a?(Model::Declarations::InterfacedItem) && interface_item_type_or_entity?(child))
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def interface_item_type_or_entity?(item)
|
|
328
|
+
base = item.base_item
|
|
329
|
+
base.is_a?(Model::Declarations::Type) || base.is_a?(Model::Declarations::Entity)
|
|
330
|
+
end
|
|
331
|
+
|
|
213
332
|
def check_entity(schema, entity)
|
|
214
333
|
Array(entity.subtype_of).each do |ref|
|
|
215
334
|
id = ref.is_a?(String) ? ref : ref.id
|
|
@@ -282,6 +401,75 @@ module Expressir
|
|
|
282
401
|
|
|
283
402
|
# eeng check-supertype-ref: every entity named in the SUPERTYPE
|
|
284
403
|
# expression must resolve to an entity of this schema.
|
|
404
|
+
# eeng check-p11 subtype_constraint (kernel/check.lisp:426) only
|
|
405
|
+
# checks the supertype expression; the note id matches the entity
|
|
406
|
+
# supertype path (kernel/check.lisp:523).
|
|
407
|
+
def explicit_attributes(entity)
|
|
408
|
+
Array(entity.attributes)
|
|
409
|
+
.select { |attr| attr.instance_of?(Model::Declarations::Attribute) }
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def check_subtype_constraints(schema)
|
|
413
|
+
schema.subtype_constraints.each do |constraint|
|
|
414
|
+
expr = constraint.supertype_expression
|
|
415
|
+
next unless expr
|
|
416
|
+
|
|
417
|
+
each_reference(expr) do |ref|
|
|
418
|
+
id = ref.id
|
|
419
|
+
next if id.nil?
|
|
420
|
+
# Names outside this schema's scope (foreign entities of a
|
|
421
|
+
# single-file parse) cannot be verified here.
|
|
422
|
+
next unless resolves_to_type_or_entity?(schema, id)
|
|
423
|
+
next if find_entity(schema, id)
|
|
424
|
+
|
|
425
|
+
note!(:check_supertype_ref, :error, schema,
|
|
426
|
+
"SUBTYPE_CONSTRAINT #{constraint.id}: supertype " \
|
|
427
|
+
"'#{id}' is not an entity", constraint)
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# A `SELF\<supertype>.<attr>` redeclaration must name a supertype
|
|
433
|
+
# in the inheritance closure carrying that attribute (ISO
|
|
434
|
+
# 10303-11:2004, 9.2.2.3). eeng kernel/check.lisp:255 leaves this
|
|
435
|
+
# as a TODO comment, so this check has no eeng note id.
|
|
436
|
+
def check_attribute_redeclarations(schema, entity)
|
|
437
|
+
explicit_attributes(entity).each do |attr|
|
|
438
|
+
tag = attr.supertype_attribute
|
|
439
|
+
next unless tag.is_a?(Model::References::AttributeReference)
|
|
440
|
+
|
|
441
|
+
sup_id = tag.ref&.entity&.id
|
|
442
|
+
attr_id = tag.attribute&.id
|
|
443
|
+
if sup_id.nil?
|
|
444
|
+
note!(:check_attribute_redeclaration, :error, schema,
|
|
445
|
+
"ENTITY #{entity.id}: redeclaration of '#{attr.id}' " \
|
|
446
|
+
"names no supertype", attr)
|
|
447
|
+
next
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
supertype = entity.supertype_closure
|
|
451
|
+
.find { |st| st.id&.safe_downcase == sup_id.safe_downcase }
|
|
452
|
+
if supertype.nil?
|
|
453
|
+
# The named supertype may be foreign (USE FROM) and thus
|
|
454
|
+
# outside the closure of a single-file parse; only flag it
|
|
455
|
+
# when it resolves in this scope yet is not a supertype.
|
|
456
|
+
if find_entity(schema, sup_id)
|
|
457
|
+
note!(:check_attribute_redeclaration, :error, schema,
|
|
458
|
+
"ENTITY #{entity.id}: redeclaration '#{attr.id}' names " \
|
|
459
|
+
"'#{sup_id}', which is not a supertype", attr)
|
|
460
|
+
end
|
|
461
|
+
next
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
next if explicit_attributes(supertype)
|
|
465
|
+
.any? { |candidate| candidate.id&.safe_downcase == attr_id&.safe_downcase }
|
|
466
|
+
|
|
467
|
+
note!(:check_attribute_redeclaration, :error, schema,
|
|
468
|
+
"ENTITY #{entity.id}: redeclaration '#{attr.id}': supertype " \
|
|
469
|
+
"'#{sup_id}' has no attribute '#{attr_id}'", attr)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
285
473
|
def check_supertype_refs(schema, entity)
|
|
286
474
|
expr = entity.supertype_expression
|
|
287
475
|
return unless expr
|
|
@@ -7,6 +7,7 @@ module Expressir
|
|
|
7
7
|
# Ruby bridge to the expressir-core Rust crate (parse + model
|
|
8
8
|
# extraction). Available when the native extension is built;
|
|
9
9
|
# callers fall back to the pure-Ruby parser otherwise.
|
|
10
|
+
#
|
|
10
11
|
module Core
|
|
11
12
|
NATIVE_AVAILABLE = begin
|
|
12
13
|
require "expressir/expressir_core"
|
|
@@ -110,20 +110,15 @@ module Expressir
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def format_declarations_entity(node)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
x.kind == Model::Declarations::Attribute::DERIVED
|
|
123
|
-
end
|
|
124
|
-
inverse_attributes = node.attributes.select do |x|
|
|
125
|
-
x.kind == Model::Declarations::Attribute::INVERSE
|
|
126
|
-
end
|
|
113
|
+
attrs = entity_attribute_items(node)
|
|
114
|
+
explicit_attributes = attrs.select do |x|
|
|
115
|
+
x.kind == Model::Declarations::Attribute::EXPLICIT
|
|
116
|
+
end
|
|
117
|
+
derived_attributes = attrs.select do |x|
|
|
118
|
+
x.kind == Model::Declarations::Attribute::DERIVED
|
|
119
|
+
end
|
|
120
|
+
inverse_attributes = attrs.select do |x|
|
|
121
|
+
x.kind == Model::Declarations::Attribute::INVERSE
|
|
127
122
|
end
|
|
128
123
|
|
|
129
124
|
indent_const = self.class.const_get(:INDENT)
|
|
@@ -231,6 +226,18 @@ module Expressir
|
|
|
231
226
|
].join("\n")
|
|
232
227
|
end
|
|
233
228
|
|
|
229
|
+
# Entity#attributes is the Ruby-parse layout (every attribute kind
|
|
230
|
+
# in one list). The core path parks derived/inverse in their own
|
|
231
|
+
# collections instead. Both may also arrive as Array-of-Array when
|
|
232
|
+
# a builder nested list productions. Normalize to a flat list of
|
|
233
|
+
# Attribute-like nodes before selecting on `kind`.
|
|
234
|
+
def entity_attribute_items(node)
|
|
235
|
+
items = Array(node.attributes)
|
|
236
|
+
items += Array(node.derived_attributes) if node.respond_to?(:derived_attributes)
|
|
237
|
+
items += Array(node.inverse_attributes) if node.respond_to?(:inverse_attributes)
|
|
238
|
+
items.flatten.compact.uniq(&:object_id)
|
|
239
|
+
end
|
|
240
|
+
|
|
234
241
|
def format_declarations_function(node)
|
|
235
242
|
indent_char = self.class.const_get(:INDENT_CHAR)
|
|
236
243
|
[
|
|
@@ -27,6 +27,28 @@ module Expressir
|
|
|
27
27
|
# Files above this use parse_fresh which has no packrat cache
|
|
28
28
|
LARGE_FILE_THRESHOLD = 1024 * 1024 # 1 MB
|
|
29
29
|
|
|
30
|
+
# Whitespace rides the engine's injected trivia (parsanol-ruby
|
|
31
|
+
# #180's source-preserving mode, 1.3.82+): each unit records
|
|
32
|
+
# verbatim under :space and attaches to the next Named capture.
|
|
33
|
+
# Remarks stay OUT of the skip rule — they are product data,
|
|
34
|
+
# captured by the tailRemark/embeddedRemark rules and attached
|
|
35
|
+
# by RemarkScanner; tail remarks attach backward, the engine's
|
|
36
|
+
# next-capture channel is the wrong direction for them.
|
|
37
|
+
rule(:trivia) { match[" \r\n\t\f"].repeat(1) }
|
|
38
|
+
# exempt (parsanol#195, shipped 1.3.89): token rules whose bodies
|
|
39
|
+
# manage their own internals — injection before a char-class tail
|
|
40
|
+
# repetition absorbs the following token across whitespace
|
|
41
|
+
# ("RULE r FOR" parsed as one simpleId). simpleId additionally
|
|
42
|
+
# matches as a single quantified Re, which needs no exemption;
|
|
43
|
+
# the two mechanisms agree and stay for defense in depth.
|
|
44
|
+
# The whitespace/remark rules manage their own trivia end to end
|
|
45
|
+
# (they ARE the trivia surface for remarks); injection around
|
|
46
|
+
# them breaks consecutive remark lines on the Ruby engine
|
|
47
|
+
# ("-- X\n--\nSCHEMA ..." failed - the #487 windows follow-up,
|
|
48
|
+
# parsanol-ruby#195 family).
|
|
49
|
+
skip(:trivia, whitespace: :space,
|
|
50
|
+
exempt: %i[simpleId digits spaces space embeddedRemark tailRemark])
|
|
51
|
+
|
|
30
52
|
# Get cached parser instance (thread-safe)
|
|
31
53
|
# Reusing the parser avoids the overhead of reinitializing all rule definitions
|
|
32
54
|
def self.cached_parser
|
|
@@ -604,7 +626,13 @@ module Expressir
|
|
|
604
626
|
(unaryOp >> simpleFactorExpression).as(:simpleFactorUnaryExpression)
|
|
605
627
|
end
|
|
606
628
|
rule(:simpleId) do
|
|
607
|
-
|
|
629
|
+
# One quantified Re, not lead >> tail-repeat: under skip
|
|
630
|
+
# injection the DSL interleaves trivia before the tail
|
|
631
|
+
# repetition, so a two-atom ident would absorb the following
|
|
632
|
+
# whitespace-separated identifier ("RULE r FOR (e)" parses
|
|
633
|
+
# "r FOR" as one simpleId). A quantified Re consumes the
|
|
634
|
+
# whole identifier in a single step — parsanol GH-69.
|
|
635
|
+
anyKeyword.absent? >> cts(match("[a-zA-Z_][a-zA-Z0-9_]*").as(:str)).as(:simpleId)
|
|
608
636
|
end
|
|
609
637
|
rule(:simpleStringLiteral) do
|
|
610
638
|
cts((str("'") >> (str("'").absent? >> any).repeat >> str("'")).as(:str)).as(:simpleStringLiteral)
|
|
@@ -112,20 +112,15 @@ module Expressir
|
|
|
112
112
|
# @param node [Model::Declarations::Entity] Entity node
|
|
113
113
|
# @return [String] Formatted entity
|
|
114
114
|
def format_declarations_entity(node)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
x.kind == Model::Declarations::Attribute::DERIVED
|
|
125
|
-
end
|
|
126
|
-
inverse_attributes = node.attributes.select do |x|
|
|
127
|
-
x.kind == Model::Declarations::Attribute::INVERSE
|
|
128
|
-
end
|
|
115
|
+
attrs = entity_attribute_items(node)
|
|
116
|
+
explicit_attributes = attrs.select do |x|
|
|
117
|
+
x.kind == Model::Declarations::Attribute::EXPLICIT
|
|
118
|
+
end
|
|
119
|
+
derived_attributes = attrs.select do |x|
|
|
120
|
+
x.kind == Model::Declarations::Attribute::DERIVED
|
|
121
|
+
end
|
|
122
|
+
inverse_attributes = attrs.select do |x|
|
|
123
|
+
x.kind == Model::Declarations::Attribute::INVERSE
|
|
129
124
|
end
|
|
130
125
|
|
|
131
126
|
indent_str = " " * @config.indent
|
|
@@ -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
|