expressir 2.4.37-aarch64-linux → 2.4.39-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/TODO.instance/00-overview.md +73 -0
- data/TODO.instance/01-instance-typing.md +81 -0
- data/TODO.instance/02-population-rules.md +82 -0
- data/TODO.instance/03-p21-ladder-and-population-management.md +97 -0
- data/TODO.instance/04-arm-mim-instance-mapping.md +83 -0
- data/lib/expressir/cli.rb +3 -0
- data/lib/expressir/commands/population.rb +63 -0
- data/lib/expressir/commands.rb +1 -0
- data/lib/expressir/eval/population.rb +186 -0
- data/lib/expressir/eval/rules.rb +0 -1
- data/lib/expressir/eval.rb +1 -0
- data/lib/expressir/mapping/instance_verification.rb +405 -0
- data/lib/expressir/mapping.rb +1 -0
- data/lib/expressir/p21/builder.rb +78 -2
- data/lib/expressir/p21/dataset_manifest.rb +62 -0
- data/lib/expressir/p21/diff.rb +40 -0
- data/lib/expressir/p21/grammar.rb +48 -6
- data/lib/expressir/p21/json.rb +128 -0
- data/lib/expressir/p21/population.rb +6 -2
- data/lib/expressir/p21/receipt.rb +45 -0
- data/lib/expressir/p21/typing.rb +116 -0
- data/lib/expressir/p21/validator.rb +419 -8
- data/lib/expressir/p21/writer.rb +107 -0
- data/lib/expressir/p21.rb +6 -0
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +1 -0
- data/lib/tasks/instance.rake +77 -0
- metadata +17 -2
|
@@ -20,13 +20,17 @@ module Expressir
|
|
|
20
20
|
# clause 12.3) to schema entity names, e.g. {"CPT" => "cartesian_point"}.
|
|
21
21
|
def validate(population, schema, short_names: nil)
|
|
22
22
|
short_names = short_names&.transform_keys(&:downcase)
|
|
23
|
+
typing = Typing.new(schema, short_names: short_names)
|
|
23
24
|
notes = []
|
|
24
25
|
check_file_schema(notes, population, schema)
|
|
26
|
+
check_sections(notes, population, schema)
|
|
25
27
|
population.instances.each do |id, instance|
|
|
26
28
|
if instance.is_a?(ComplexRecord)
|
|
27
|
-
check_complex(notes, schema, population, id, instance,
|
|
29
|
+
check_complex(notes, schema, population, id, instance,
|
|
30
|
+
short_names, typing)
|
|
28
31
|
else
|
|
29
|
-
check_simple(notes, schema, population, id, instance,
|
|
32
|
+
check_simple(notes, schema, population, id, instance,
|
|
33
|
+
short_names, typing)
|
|
30
34
|
end
|
|
31
35
|
end
|
|
32
36
|
notes
|
|
@@ -49,6 +53,24 @@ module Expressir
|
|
|
49
53
|
message: message)
|
|
50
54
|
end
|
|
51
55
|
|
|
56
|
+
# Annex E (ISO 10303-21 clause 11.2): each DATA section's schema
|
|
57
|
+
# contexts must intersect the declared schema list.
|
|
58
|
+
def check_sections(notes, population, schema)
|
|
59
|
+
known = ([schema.id] +
|
|
60
|
+
Array(population.header["FILE_SCHEMA"]).flatten)
|
|
61
|
+
.filter_map { |name| name.to_s.downcase unless name.to_s.empty? }
|
|
62
|
+
Array(population.sections).each do |section|
|
|
63
|
+
next if section.contexts.empty?
|
|
64
|
+
next if section.contexts.any? do |context|
|
|
65
|
+
known.include?(context.to_s.downcase)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
note!(notes, :p21_section_context, :warning, nil,
|
|
69
|
+
"DATA section '#{section.name}' context(s) " \
|
|
70
|
+
"#{section.contexts.join(', ')} are not in the schema list")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
52
74
|
def check_file_schema(notes, population, schema)
|
|
53
75
|
declared = Array(population.header["FILE_SCHEMA"]).flatten.first
|
|
54
76
|
return unless declared && schema.id
|
|
@@ -58,7 +80,8 @@ module Expressir
|
|
|
58
80
|
"FILE_SCHEMA '#{declared}' does not match schema '#{schema.id}'")
|
|
59
81
|
end
|
|
60
82
|
|
|
61
|
-
def check_simple(notes, schema, population, id, record, short_names
|
|
83
|
+
def check_simple(notes, schema, population, id, record, short_names,
|
|
84
|
+
typing)
|
|
62
85
|
entity = find_entity(schema, record.keyword, short_names)
|
|
63
86
|
unless entity
|
|
64
87
|
note!(notes, :p21_unknown_entity, :error, id,
|
|
@@ -67,10 +90,11 @@ module Expressir
|
|
|
67
90
|
end
|
|
68
91
|
|
|
69
92
|
check_params(notes, schema, population, id, record.params,
|
|
70
|
-
mapped_slots(entity))
|
|
93
|
+
mapped_slots(entity), typing)
|
|
71
94
|
end
|
|
72
95
|
|
|
73
|
-
def check_complex(notes, schema, population, id, complex, short_names
|
|
96
|
+
def check_complex(notes, schema, population, id, complex, short_names,
|
|
97
|
+
typing)
|
|
74
98
|
pairs = complex.records.filter_map do |record|
|
|
75
99
|
entity = find_entity(schema, record.keyword, short_names)
|
|
76
100
|
unless entity
|
|
@@ -84,9 +108,14 @@ module Expressir
|
|
|
84
108
|
return if pairs.empty?
|
|
85
109
|
|
|
86
110
|
check_clique(notes, id, pairs.map { |_, entity| entity })
|
|
111
|
+
typing.issues(complex).each do |message|
|
|
112
|
+
note!(notes, :check_p21_complex_instantiation, :error, id,
|
|
113
|
+
"#{id}: #{message}")
|
|
114
|
+
end
|
|
87
115
|
pairs.each do |record, entity|
|
|
88
116
|
slots = explicit_attributes(entity).map { |attr| [attr, false] }
|
|
89
|
-
check_params(notes, schema, population, id, record.params, slots
|
|
117
|
+
check_params(notes, schema, population, id, record.params, slots,
|
|
118
|
+
typing)
|
|
90
119
|
end
|
|
91
120
|
end
|
|
92
121
|
|
|
@@ -120,14 +149,14 @@ module Expressir
|
|
|
120
149
|
end + explicit_attributes(entity).map { |attr| [attr, false] }
|
|
121
150
|
end
|
|
122
151
|
|
|
123
|
-
def check_params(notes, schema, population, id, params, slots)
|
|
152
|
+
def check_params(notes, schema, population, id, params, slots, typing)
|
|
124
153
|
if params.size != slots.size
|
|
125
154
|
note!(notes, :p21_arity_mismatch, :error, id,
|
|
126
155
|
"#{id}: #{params.size} values for #{slots.size} mapped attributes")
|
|
127
156
|
end
|
|
128
157
|
|
|
129
158
|
slots.zip(params).each do |(attr, derived_slot), value|
|
|
130
|
-
next if value.nil?
|
|
159
|
+
next if value.nil? || value == :unset
|
|
131
160
|
|
|
132
161
|
if derived_slot
|
|
133
162
|
next if value == :derived
|
|
@@ -138,6 +167,11 @@ module Expressir
|
|
|
138
167
|
end
|
|
139
168
|
|
|
140
169
|
check_value(notes, schema, population, id, attr, value)
|
|
170
|
+
slot_type_issues(schema, population, attr&.type, value, typing)
|
|
171
|
+
.each do |(category, message)|
|
|
172
|
+
note!(notes, FINDING_IDS.fetch(category), :error, id,
|
|
173
|
+
"#{id}: #{message}")
|
|
174
|
+
end
|
|
141
175
|
end
|
|
142
176
|
end
|
|
143
177
|
|
|
@@ -147,6 +181,9 @@ module Expressir
|
|
|
147
181
|
return unless value.match?(/\A#\d+\z/)
|
|
148
182
|
|
|
149
183
|
return if population.instances.key?(value)
|
|
184
|
+
# An id bound in the REFERENCE section names an external
|
|
185
|
+
# instance (ISO 10303-21 clause 10); resolution is remote.
|
|
186
|
+
return if population.references.key?(value)
|
|
150
187
|
|
|
151
188
|
note!(notes, :p21_unknown_reference, :error, id,
|
|
152
189
|
"#{id}: reference #{value} has no instance")
|
|
@@ -208,6 +245,380 @@ module Expressir
|
|
|
208
245
|
underlying = declaration&.underlying_type
|
|
209
246
|
underlying.is_a?(Model::DataTypes::Enumeration) ? Array(underlying.items) : nil
|
|
210
247
|
end
|
|
248
|
+
|
|
249
|
+
# ---- TODO.instance/01: value/type conformance ----
|
|
250
|
+
# ISO 10303-11 ch.8 domains over the clause-12 slots. Every
|
|
251
|
+
# finding carries its clause: simple types 8.1.x, aggregation
|
|
252
|
+
# types 8.2.x, named types 8.4.x, attribute domains 9.2.1,
|
|
253
|
+
# complex instantiation 9.2.5/9.2.5.2, domain rules 9.2.2.2.
|
|
254
|
+
|
|
255
|
+
FINDING_IDS = {
|
|
256
|
+
domain: :check_p21_value_domain,
|
|
257
|
+
aggregate: :check_p21_aggregate,
|
|
258
|
+
reference: :check_p21_reference_type,
|
|
259
|
+
select: :check_p21_select_arm,
|
|
260
|
+
complex: :check_p21_complex_instantiation,
|
|
261
|
+
}.freeze
|
|
262
|
+
MAX_TYPE_DEPTH = 16
|
|
263
|
+
PRIMITIVE_DOMAINS = {
|
|
264
|
+
Model::DataTypes::Integer => ["INTEGER", "an unquoted integer (8.1.3)"],
|
|
265
|
+
Model::DataTypes::Real => ["REAL", "a number (8.1.2)"],
|
|
266
|
+
Model::DataTypes::Number => ["NUMBER", "a number (8.1.1)"],
|
|
267
|
+
Model::DataTypes::String => ["STRING", "a single-quoted string (8.1.6)"],
|
|
268
|
+
Model::DataTypes::Binary => ["BINARY", "a binary literal (8.1.7)"],
|
|
269
|
+
Model::DataTypes::Boolean => ["BOOLEAN", ".T. or .F. (8.1.5)"],
|
|
270
|
+
Model::DataTypes::Logical => ["LOGICAL", ".T., .F. or .U. (8.1.4)"],
|
|
271
|
+
}.freeze
|
|
272
|
+
private_constant :FINDING_IDS, :MAX_TYPE_DEPTH, :PRIMITIVE_DOMAINS
|
|
273
|
+
|
|
274
|
+
def slot_type_issues(schema, population, type, value, typing, depth = 0,
|
|
275
|
+
seen = {}.compare_by_identity)
|
|
276
|
+
return [] unless type.is_a?(Model::ModelElement)
|
|
277
|
+
return [] if depth >= MAX_TYPE_DEPTH
|
|
278
|
+
if value.is_a?(TypedValue)
|
|
279
|
+
return typed_value_issues(schema, population, type, value, typing,
|
|
280
|
+
depth, seen)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
case type
|
|
284
|
+
when Model::Declarations::Type
|
|
285
|
+
defined_type_issues(schema, population, type, value, typing,
|
|
286
|
+
depth, seen)
|
|
287
|
+
when Model::Declarations::Entity
|
|
288
|
+
reference_issues(schema, population, value, type, typing)
|
|
289
|
+
when Model::References::SimpleReference
|
|
290
|
+
reference_slot_issues(schema, population, type, value, typing,
|
|
291
|
+
depth, seen)
|
|
292
|
+
when Model::DataTypes::List, Model::DataTypes::Array,
|
|
293
|
+
Model::DataTypes::Set, Model::DataTypes::Bag
|
|
294
|
+
aggregate_issues(schema, population, type, value, typing, depth, seen)
|
|
295
|
+
when Model::DataTypes::Select
|
|
296
|
+
select_issues(schema, population, type, value, typing, depth, seen)
|
|
297
|
+
when Model::DataTypes::Enumeration
|
|
298
|
+
[] # 8.4.1 membership is checked by check_value
|
|
299
|
+
else
|
|
300
|
+
primitive_issues(type, value)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def reference_slot_issues(schema, population, type, value, typing,
|
|
305
|
+
depth, seen)
|
|
306
|
+
declaration = find_type(schema, type.id.to_s)
|
|
307
|
+
if declaration.is_a?(Model::Declarations::Type)
|
|
308
|
+
return defined_type_issues(schema, population, declaration, value,
|
|
309
|
+
typing, depth, seen)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
entity = find_entity(schema, type.id.to_s)
|
|
313
|
+
return [] unless entity
|
|
314
|
+
|
|
315
|
+
reference_issues(schema, population, value, entity, typing)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def defined_type_issues(schema, population, declaration, value, typing,
|
|
319
|
+
depth, seen)
|
|
320
|
+
return [] if seen.include?(declaration)
|
|
321
|
+
|
|
322
|
+
seen[declaration] = true
|
|
323
|
+
domain_rule_issues(schema, value, declaration) +
|
|
324
|
+
slot_type_issues(schema, population, declaration.underlying_type,
|
|
325
|
+
value, typing, depth + 1, seen)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def domain_rule_issues(schema, value, declaration)
|
|
329
|
+
self_value = domain_value(value, declaration)
|
|
330
|
+
return [] if self_value.nil?
|
|
331
|
+
|
|
332
|
+
Array(declaration.where_rules).filter_map do |rule|
|
|
333
|
+
next unless rule.expression
|
|
334
|
+
|
|
335
|
+
env = Eval::Environment.new(schema: schema).define("self", self_value)
|
|
336
|
+
result = Eval::Expression.evaluate(rule.expression, env)
|
|
337
|
+
next unless logical_false?(result)
|
|
338
|
+
|
|
339
|
+
[:domain,
|
|
340
|
+
"value violates domain rule '#{rule.id}' of " \
|
|
341
|
+
"TYPE '#{declaration.id}' (9.2.2.2)"]
|
|
342
|
+
rescue StandardError
|
|
343
|
+
next # an unevaluable domain rule is not a finding here
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def domain_value(value, declaration)
|
|
348
|
+
case value
|
|
349
|
+
when Integer, Float then value
|
|
350
|
+
when String then reference?(value) ? nil : value
|
|
351
|
+
when Enumeration
|
|
352
|
+
Eval::Values::Enumeration.new(type: declaration.id, item: value.name)
|
|
353
|
+
when Binary then Eval::Values::Binary.new(bits: value.hex)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def logical_false?(result)
|
|
358
|
+
return true if [false, Eval::Values::LOGICAL_FALSE].include?(result)
|
|
359
|
+
return result.value == Eval::Values::LOGICAL_FALSE if result.is_a?(Eval::Values::Logical)
|
|
360
|
+
|
|
361
|
+
false
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def primitive_issues(type, value)
|
|
365
|
+
domain, expectation = PRIMITIVE_DOMAINS[type.class]
|
|
366
|
+
return [] unless domain
|
|
367
|
+
|
|
368
|
+
ok = case type
|
|
369
|
+
when Model::DataTypes::Integer then value.is_a?(Integer)
|
|
370
|
+
when Model::DataTypes::Real, Model::DataTypes::Number
|
|
371
|
+
value.is_a?(Numeric)
|
|
372
|
+
when Model::DataTypes::String
|
|
373
|
+
value.is_a?(String) && !reference?(value)
|
|
374
|
+
when Model::DataTypes::Binary then value.is_a?(Binary)
|
|
375
|
+
when Model::DataTypes::Boolean, Model::DataTypes::Logical
|
|
376
|
+
logical_literal?(value)
|
|
377
|
+
end
|
|
378
|
+
return [] if ok
|
|
379
|
+
|
|
380
|
+
[[:domain, "#{domain} slot requires #{expectation}"]]
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def logical_literal?(value)
|
|
384
|
+
return true if [true, false].include?(value)
|
|
385
|
+
|
|
386
|
+
value.is_a?(Enumeration) && %w[T F U].include?(value.name.to_s.upcase)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def reference_issues(_schema, population, value, entity, typing)
|
|
390
|
+
return [] unless reference?(value)
|
|
391
|
+
|
|
392
|
+
target = population.instances[value]
|
|
393
|
+
return [] unless target
|
|
394
|
+
|
|
395
|
+
conformed = typing.of(target)
|
|
396
|
+
return [] if conformed.any? do |candidate|
|
|
397
|
+
candidate.id&.safe_downcase == entity.id&.safe_downcase
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
[[:reference,
|
|
401
|
+
"reference #{value} conforms to " \
|
|
402
|
+
"#{conformed.filter_map(&:id).join(', ')} but '#{entity.id}' is " \
|
|
403
|
+
"required (9.2.1)"]]
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def typed_value_issues(schema, population, _type, value, typing, depth,
|
|
407
|
+
seen)
|
|
408
|
+
entity = find_entity(schema, value.keyword)
|
|
409
|
+
if entity
|
|
410
|
+
return reference_issues(schema, population, value.value, entity,
|
|
411
|
+
typing)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
declaration = find_type(schema, value.keyword)
|
|
415
|
+
return [] unless declaration.is_a?(Model::Declarations::Type)
|
|
416
|
+
|
|
417
|
+
underlying = declaration.underlying_type
|
|
418
|
+
inner = value.value
|
|
419
|
+
if inner.is_a?(Array) && inner.size == 1 && !aggregate?(underlying)
|
|
420
|
+
inner = inner.first
|
|
421
|
+
end
|
|
422
|
+
slot_type_issues(schema, population, underlying, inner, typing,
|
|
423
|
+
depth + 1, seen)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def aggregate?(type)
|
|
427
|
+
type.is_a?(Model::DataTypes::List) || type.is_a?(Model::DataTypes::Array) ||
|
|
428
|
+
type.is_a?(Model::DataTypes::Set) || type.is_a?(Model::DataTypes::Bag)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def aggregate_issues(schema, population, type, value, typing, depth,
|
|
432
|
+
seen)
|
|
433
|
+
unless value.is_a?(Array)
|
|
434
|
+
return [[:domain, "#{aggregate_name(type)} attribute requires an " + "aggregate value (#{aggregate_section(type)})"]]
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
aggregate_bound_issues(type, value) +
|
|
438
|
+
aggregate_unique_issues(type, value) +
|
|
439
|
+
aggregate_element_issues(schema, population, type, value, typing,
|
|
440
|
+
depth, seen)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def aggregate_bound_issues(type, value)
|
|
444
|
+
lower, upper = aggregate_bounds(type)
|
|
445
|
+
issues = []
|
|
446
|
+
if lower && value.size < lower
|
|
447
|
+
issues << [:aggregate, "#{aggregate_name(type)} has #{value.size} " + "element(s), below the bound #{lower} " + "(#{aggregate_section(type)})"]
|
|
448
|
+
end
|
|
449
|
+
if upper && value.size > upper
|
|
450
|
+
issues << [:aggregate, "#{aggregate_name(type)} has #{value.size} " + "element(s), above the bound #{upper} " + "(#{aggregate_section(type)})"]
|
|
451
|
+
end
|
|
452
|
+
issues
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def aggregate_bounds(type)
|
|
456
|
+
[bound_value(type.bound1), bound_value(type.bound2)]
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def bound_value(bound)
|
|
460
|
+
return nil unless bound.is_a?(Model::Literals::Integer)
|
|
461
|
+
|
|
462
|
+
bound.value.to_i
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def aggregate_unique_issues(type, value)
|
|
466
|
+
return [] unless type.is_a?(Model::DataTypes::Set)
|
|
467
|
+
|
|
468
|
+
keys = value.map { |element| p21_value_key(element) }
|
|
469
|
+
return [] if keys.uniq.size == keys.size
|
|
470
|
+
|
|
471
|
+
[[:aggregate, "SET has duplicate elements (8.2.4)"]]
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def aggregate_element_issues(schema, population, type, value, typing,
|
|
475
|
+
depth, _seen)
|
|
476
|
+
issues = []
|
|
477
|
+
value.each_with_index do |element, index|
|
|
478
|
+
unset = element.nil? || element == :unset
|
|
479
|
+
if unset
|
|
480
|
+
next unless type.is_a?(Model::DataTypes::Array) && !type.optional
|
|
481
|
+
|
|
482
|
+
issues << [:aggregate, "ARRAY element #{index} is unset but the array is not OPTIONAL (8.2.1)"]
|
|
483
|
+
next
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Sibling elements re-walk the same types with different
|
|
487
|
+
# values: only the walk along one nesting path shares `seen`.
|
|
488
|
+
slot_type_issues(schema, population, type.base_type, element,
|
|
489
|
+
typing, depth + 1, {}.compare_by_identity)
|
|
490
|
+
.each do |(category, message)|
|
|
491
|
+
issues << [category, "element #{index}: #{message}"]
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
issues
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def select_issues(schema, population, type, value, typing, depth, seen)
|
|
498
|
+
arms = select_arms(schema, type, seen)
|
|
499
|
+
return [] if arms.empty?
|
|
500
|
+
|
|
501
|
+
select_arm_issues(schema, population, value, arms, typing, depth, seen)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# Leaf domains of a SELECT: item entities, item defined types, and
|
|
505
|
+
# nested selects, plus the BASED_ON chain (8.4.2).
|
|
506
|
+
def select_arms(schema, type, seen)
|
|
507
|
+
return [] if seen.include?(type)
|
|
508
|
+
|
|
509
|
+
seen[type] = true
|
|
510
|
+
arms = []
|
|
511
|
+
Array(type.items).each do |item|
|
|
512
|
+
next unless item.respond_to?(:id) && item.id
|
|
513
|
+
|
|
514
|
+
declaration = find_type(schema, item.id.to_s)
|
|
515
|
+
if declaration.is_a?(Model::Declarations::Type)
|
|
516
|
+
underlying = declaration.underlying_type
|
|
517
|
+
if underlying.is_a?(Model::DataTypes::Select)
|
|
518
|
+
arms.concat(select_arms(schema, underlying, seen))
|
|
519
|
+
else
|
|
520
|
+
arms << [:type, declaration]
|
|
521
|
+
end
|
|
522
|
+
next
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
entity = find_entity(schema, item.id.to_s)
|
|
526
|
+
arms << [:entity, entity] if entity
|
|
527
|
+
end
|
|
528
|
+
base = based_on_select(schema, type)
|
|
529
|
+
arms.concat(select_arms(schema, base, seen)) if base
|
|
530
|
+
arms
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def based_on_select(schema, type)
|
|
534
|
+
based = type.based_on
|
|
535
|
+
return nil unless based.respond_to?(:id) && based.id
|
|
536
|
+
|
|
537
|
+
declaration = find_type(schema, based.id.to_s)
|
|
538
|
+
return nil unless declaration.is_a?(Model::Declarations::Type)
|
|
539
|
+
|
|
540
|
+
underlying = declaration.underlying_type
|
|
541
|
+
underlying.is_a?(Model::DataTypes::Select) ? underlying : nil
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def select_arm_issues(schema, population, value, arms, typing, depth,
|
|
545
|
+
seen)
|
|
546
|
+
if reference?(value)
|
|
547
|
+
target = population.instances[value]
|
|
548
|
+
return [] unless target
|
|
549
|
+
|
|
550
|
+
conformed = typing.of(target)
|
|
551
|
+
ok = arms.any? do |(kind, node)|
|
|
552
|
+
kind == :entity &&
|
|
553
|
+
conformed.any? do |candidate|
|
|
554
|
+
candidate.id&.safe_downcase == node.id&.safe_downcase
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
return [] if ok
|
|
558
|
+
|
|
559
|
+
return [[:select, "reference #{value} fits no arm of the SELECT (8.4.2)"]]
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
if value.is_a?(TypedValue)
|
|
563
|
+
ok = arms.any? do |(_kind, node)|
|
|
564
|
+
node.id&.safe_downcase == value.keyword&.downcase
|
|
565
|
+
end
|
|
566
|
+
return [] if ok
|
|
567
|
+
|
|
568
|
+
return [[:select, "typed value '#{value.keyword}' fits no arm of the SELECT (8.4.2)"]]
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
return [] if value.is_a?(Enumeration) # 8.4.1 membership via check_value
|
|
572
|
+
|
|
573
|
+
literal_ok = arms.any? do |(kind, node)|
|
|
574
|
+
kind == :type && slot_type_issues(
|
|
575
|
+
schema, population, node.underlying_type, value, typing,
|
|
576
|
+
depth + 1, seen
|
|
577
|
+
).empty?
|
|
578
|
+
end
|
|
579
|
+
return [] if literal_ok
|
|
580
|
+
|
|
581
|
+
[[:select, "value #{p21_display(value)} fits no arm of the SELECT (8.4.2)"]]
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def reference?(value)
|
|
585
|
+
value.is_a?(String) && value.match?(/\A#\d+\z/)
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def p21_value_key(value)
|
|
589
|
+
case value
|
|
590
|
+
when Enumeration then [:enum, value.name&.downcase]
|
|
591
|
+
when Binary then [:binary, value.hex]
|
|
592
|
+
when Array then [:list, *value.map { |element| p21_value_key(element) }]
|
|
593
|
+
else [:value, value]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def p21_display(value)
|
|
598
|
+
case value
|
|
599
|
+
when Enumeration then ".#{value.name}."
|
|
600
|
+
when Binary then "%#{value.hex}"
|
|
601
|
+
else value.to_s
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def aggregate_name(type)
|
|
606
|
+
case type
|
|
607
|
+
when Model::DataTypes::List then "LIST"
|
|
608
|
+
when Model::DataTypes::Array then "ARRAY"
|
|
609
|
+
when Model::DataTypes::Set then "SET"
|
|
610
|
+
else "BAG"
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def aggregate_section(type)
|
|
615
|
+
case type
|
|
616
|
+
when Model::DataTypes::List then "8.2.2"
|
|
617
|
+
when Model::DataTypes::Array then "8.2.1"
|
|
618
|
+
when Model::DataTypes::Set then "8.2.4"
|
|
619
|
+
else "8.2.3"
|
|
620
|
+
end
|
|
621
|
+
end
|
|
211
622
|
end
|
|
212
623
|
end
|
|
213
624
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module P21
|
|
5
|
+
# Emits a {Population} back to ISO 10303-21 text (TODO.instance/03).
|
|
6
|
+
# Deterministic: the same population always serializes to the same
|
|
7
|
+
# bytes — HEADER entities in insertion order, DATA instances in id
|
|
8
|
+
# order, anchors/references/named sections re-emitted when present.
|
|
9
|
+
module Writer
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def emit(population)
|
|
13
|
+
lines = ["ISO-10303-21;", "HEADER;"]
|
|
14
|
+
population.header.each do |keyword, params|
|
|
15
|
+
lines << "#{keyword}(#{emit_params(params)});"
|
|
16
|
+
end
|
|
17
|
+
lines << "ENDSEC;"
|
|
18
|
+
unless population.anchors.empty?
|
|
19
|
+
lines << "ANCHOR;"
|
|
20
|
+
population.anchors.each do |label, value|
|
|
21
|
+
lines << "<#{label}> = #{emit_value(value)};"
|
|
22
|
+
end
|
|
23
|
+
lines << "ENDSEC;"
|
|
24
|
+
end
|
|
25
|
+
unless population.references.empty?
|
|
26
|
+
lines << "REFERENCE;"
|
|
27
|
+
population.references.each do |id, anchor|
|
|
28
|
+
lines << "#{id} = #{anchor};"
|
|
29
|
+
end
|
|
30
|
+
lines << "ENDSEC;"
|
|
31
|
+
end
|
|
32
|
+
emit_data_sections(population, lines)
|
|
33
|
+
lines << "END-ISO-10303-21;"
|
|
34
|
+
"#{lines.join("\n")}\n"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def emit_data_sections(population, lines)
|
|
38
|
+
if population.sections.empty?
|
|
39
|
+
lines << "DATA;"
|
|
40
|
+
emit_instances(population.instances, lines)
|
|
41
|
+
lines << "ENDSEC;"
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
population.sections.each do |section|
|
|
46
|
+
lines << named_data_line(section)
|
|
47
|
+
section.instance_ids.each do |id|
|
|
48
|
+
lines << "#{id}=#{emit_record(population.instances[id])};"
|
|
49
|
+
end
|
|
50
|
+
lines << "ENDSEC;"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def named_data_line(section)
|
|
55
|
+
return "DATA;" if section.name.nil?
|
|
56
|
+
|
|
57
|
+
contexts = section.contexts
|
|
58
|
+
.map { |context| "'#{context}'" }.join(", ")
|
|
59
|
+
"DATA ('#{section.name}', (#{contexts}));"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def emit_instances(instances, lines)
|
|
63
|
+
instances.each do |id, record|
|
|
64
|
+
lines << "#{id}=#{emit_record(record)};"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def emit_record(record)
|
|
69
|
+
if record.is_a?(ComplexRecord)
|
|
70
|
+
"(#{record.records.map { |part| emit_simple(part) }.join})"
|
|
71
|
+
else
|
|
72
|
+
emit_simple(record)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def emit_simple(record)
|
|
77
|
+
"#{record.keyword}(#{emit_params(record.params)})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def emit_params(params)
|
|
81
|
+
params.map { |value| emit_value(value) }.join(",")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def emit_value(value)
|
|
85
|
+
case value
|
|
86
|
+
when Array then "(#{value.map { |item| emit_value(item) }.join(',')})"
|
|
87
|
+
when TypedValue
|
|
88
|
+
"#{value.keyword}(#{emit_params(value.value)})"
|
|
89
|
+
when Enumeration then ".#{value.name}."
|
|
90
|
+
when Binary then "\"#{value.hex}\""
|
|
91
|
+
when :unset then "$"
|
|
92
|
+
when :derived then "*"
|
|
93
|
+
when true then ".T."
|
|
94
|
+
when false then ".F."
|
|
95
|
+
when String then string_or_reference(value)
|
|
96
|
+
else value.to_s
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def string_or_reference(value)
|
|
101
|
+
return value if value.match?(/\A#\d+\z/)
|
|
102
|
+
|
|
103
|
+
"'#{value.gsub("'", "''")}'"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/expressir/p21.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Expressir
|
|
|
12
12
|
autoload :Grammar, "#{__dir__}/p21/grammar"
|
|
13
13
|
autoload :Builder, "#{__dir__}/p21/builder"
|
|
14
14
|
autoload :Validator, "#{__dir__}/p21/validator"
|
|
15
|
+
autoload :Typing, "#{__dir__}/p21/typing"
|
|
16
|
+
autoload :DatasetManifest, "#{__dir__}/p21/dataset_manifest"
|
|
17
|
+
autoload :Receipt, "#{__dir__}/p21/receipt"
|
|
18
|
+
autoload :Diff, "#{__dir__}/p21/diff"
|
|
15
19
|
autoload :Parser, "#{__dir__}/p21/parser"
|
|
20
|
+
autoload :Writer, "#{__dir__}/p21/writer"
|
|
21
|
+
autoload :Json, "#{__dir__}/p21/json"
|
|
16
22
|
end
|
|
17
23
|
end
|
data/lib/expressir/version.rb
CHANGED
data/lib/expressir.rb
CHANGED
|
@@ -76,6 +76,7 @@ module Expressir
|
|
|
76
76
|
autoload :Model, "#{__dir__}/expressir/model"
|
|
77
77
|
autoload :Commands, "#{__dir__}/expressir/commands"
|
|
78
78
|
autoload :Changes, "#{__dir__}/expressir/changes"
|
|
79
|
+
autoload :Eval, "#{__dir__}/expressir/eval"
|
|
79
80
|
autoload :Conformance, "#{__dir__}/expressir/conformance"
|
|
80
81
|
autoload :Package, "#{__dir__}/expressir/package"
|
|
81
82
|
autoload :Manifest, "#{__dir__}/expressir/manifest"
|