expressir 2.4.32 → 2.4.33
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 +22 -0
- 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,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module P21
|
|
5
|
+
# A parsed ISO 10303-21 exchange structure (TODO.eengine/06).
|
|
6
|
+
# `header` — Hash: keyword => parameters (Array of values).
|
|
7
|
+
# `instances`— Hash: "#id" => Record.
|
|
8
|
+
#
|
|
9
|
+
# Values: String (string literal), Integer (reference id, keyed as
|
|
10
|
+
# "#id" in instances), Expressir::P21::Enumeration,
|
|
11
|
+
# Expressir::P21::Binary, Expressir::P21::TypedValue,
|
|
12
|
+
# Expressir::P21::ComplexRecord, :unset, :derived, or nested
|
|
13
|
+
# Arrays for lists.
|
|
14
|
+
class Population
|
|
15
|
+
attr_reader :header, :instances
|
|
16
|
+
|
|
17
|
+
def initialize(header: {}, instances: {})
|
|
18
|
+
@header = header
|
|
19
|
+
@instances = instances
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A data-section instance: `#12 = KEYWORD(param, ...) ;`
|
|
24
|
+
Record = Struct.new(:keyword, :params)
|
|
25
|
+
|
|
26
|
+
# `#21 = (A(...) B(...))` — complex entity instance.
|
|
27
|
+
ComplexRecord = Struct.new(:records)
|
|
28
|
+
|
|
29
|
+
TypedValue = Struct.new(:keyword, :value)
|
|
30
|
+
|
|
31
|
+
Enumeration = Struct.new(:name)
|
|
32
|
+
|
|
33
|
+
Binary = Struct.new(:hex)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module P21
|
|
5
|
+
# Validates a {Population} against a loaded schema
|
|
6
|
+
# (TODO.eengine/06 step 2). Single-schema scope: instance keywords
|
|
7
|
+
# must be declared in the given schema; USE/REFERENCE visibility is
|
|
8
|
+
# a later ladder step.
|
|
9
|
+
#
|
|
10
|
+
# Mapping follows ISO 10303-21 clause 12: derived attributes are
|
|
11
|
+
# not mapped (a supertype attribute redeclared as DERIVED keeps its
|
|
12
|
+
# slot, valued `*`), inverse attributes are not mapped, and the
|
|
13
|
+
# value order is supertype attributes first.
|
|
14
|
+
module Validator
|
|
15
|
+
Note = Struct.new(:id, :severity, :instance, :message, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# `short_names` maps exchange-structure short names (ISO 10303-21,
|
|
20
|
+
# clause 12.3) to schema entity names, e.g. {"CPT" => "cartesian_point"}.
|
|
21
|
+
def validate(population, schema, short_names: nil)
|
|
22
|
+
short_names = short_names&.transform_keys(&:downcase)
|
|
23
|
+
notes = []
|
|
24
|
+
check_file_schema(notes, population, schema)
|
|
25
|
+
population.instances.each do |id, instance|
|
|
26
|
+
if instance.is_a?(ComplexRecord)
|
|
27
|
+
check_complex(notes, schema, population, id, instance, short_names)
|
|
28
|
+
else
|
|
29
|
+
check_simple(notes, schema, population, id, instance, short_names)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
notes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def errors(notes)
|
|
36
|
+
notes.select { |note| note.severity == :error }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def warnings(notes)
|
|
40
|
+
notes.select { |note| note.severity == :warning }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def valid?(notes)
|
|
44
|
+
errors(notes).empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def note!(notes, id, severity, instance, message)
|
|
48
|
+
notes << Note.new(id: id, severity: severity, instance: instance,
|
|
49
|
+
message: message)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_file_schema(notes, population, schema)
|
|
53
|
+
declared = Array(population.header["FILE_SCHEMA"]).flatten.first
|
|
54
|
+
return unless declared && schema.id
|
|
55
|
+
return if declared.downcase == schema.id.downcase
|
|
56
|
+
|
|
57
|
+
note!(notes, :p21_schema_mismatch, :warning, nil,
|
|
58
|
+
"FILE_SCHEMA '#{declared}' does not match schema '#{schema.id}'")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check_simple(notes, schema, population, id, record, short_names)
|
|
62
|
+
entity = find_entity(schema, record.keyword, short_names)
|
|
63
|
+
unless entity
|
|
64
|
+
note!(notes, :p21_unknown_entity, :error, id,
|
|
65
|
+
"#{record.keyword} is not declared in schema '#{schema.id}'")
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
check_params(notes, schema, population, id, record.params,
|
|
70
|
+
mapped_slots(entity))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_complex(notes, schema, population, id, complex, short_names)
|
|
74
|
+
pairs = complex.records.filter_map do |record|
|
|
75
|
+
entity = find_entity(schema, record.keyword, short_names)
|
|
76
|
+
unless entity
|
|
77
|
+
note!(notes, :p21_unknown_entity, :error, id,
|
|
78
|
+
"#{record.keyword} is not declared in schema '#{schema.id}'")
|
|
79
|
+
next nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
[record, entity]
|
|
83
|
+
end
|
|
84
|
+
return if pairs.empty?
|
|
85
|
+
|
|
86
|
+
check_clique(notes, id, pairs.map { |_, entity| entity })
|
|
87
|
+
pairs.each do |record, entity|
|
|
88
|
+
slots = explicit_attributes(entity).map { |attr| [attr, false] }
|
|
89
|
+
check_params(notes, schema, population, id, record.params, slots)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A complex instance's records must form a valid supertype graph
|
|
94
|
+
# path: they share at least one common supertype.
|
|
95
|
+
def check_clique(notes, id, entities)
|
|
96
|
+
common = closure_keys(entities.first)
|
|
97
|
+
entities.drop(1).each { |entity| common &= closure_keys(entity) }
|
|
98
|
+
return unless common.empty?
|
|
99
|
+
|
|
100
|
+
note!(notes, :p21_complex_clique, :error, id,
|
|
101
|
+
"#{id}: complex records share no common supertype")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def closure_keys(entity)
|
|
105
|
+
keys = entity.supertype_closure.filter_map { |e| e.id&.downcase }
|
|
106
|
+
keys << entity.id&.downcase
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Part 21 value order: supertype attributes first; an attribute
|
|
110
|
+
# redeclared as DERIVED in the entity keeps its inherited slot,
|
|
111
|
+
# valued `*` (12.2.6).
|
|
112
|
+
def mapped_slots(entity)
|
|
113
|
+
own_derived_keys = own_derived_attributes(entity)
|
|
114
|
+
.filter_map { |attr| attr.id&.downcase }
|
|
115
|
+
inherited = entity.supertype_closure.flat_map do |supertype|
|
|
116
|
+
explicit_attributes(supertype)
|
|
117
|
+
end
|
|
118
|
+
inherited.map do |attr|
|
|
119
|
+
[attr, own_derived_keys.include?(attr.id&.downcase)]
|
|
120
|
+
end + explicit_attributes(entity).map { |attr| [attr, false] }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def check_params(notes, schema, population, id, params, slots)
|
|
124
|
+
if params.size != slots.size
|
|
125
|
+
note!(notes, :p21_arity_mismatch, :error, id,
|
|
126
|
+
"#{id}: #{params.size} values for #{slots.size} mapped attributes")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
slots.zip(params).each do |(attr, derived_slot), value|
|
|
130
|
+
next if value.nil?
|
|
131
|
+
|
|
132
|
+
if derived_slot
|
|
133
|
+
next if value == :derived
|
|
134
|
+
|
|
135
|
+
note!(notes, :p21_derived_slot, :error, id,
|
|
136
|
+
"#{id}: attribute '#{attr.id}' redeclared DERIVED must be '*'")
|
|
137
|
+
next
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
check_value(notes, schema, population, id, attr, value)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def check_value(notes, schema, population, id, attr, value)
|
|
145
|
+
case value
|
|
146
|
+
when String
|
|
147
|
+
return unless value.match?(/\A#\d+\z/)
|
|
148
|
+
|
|
149
|
+
return if population.instances.key?(value)
|
|
150
|
+
|
|
151
|
+
note!(notes, :p21_unknown_reference, :error, id,
|
|
152
|
+
"#{id}: reference #{value} has no instance")
|
|
153
|
+
when :derived
|
|
154
|
+
note!(notes, :p21_derived_value, :error, id,
|
|
155
|
+
"#{id}: '*' outside a derived slot")
|
|
156
|
+
when TypedValue
|
|
157
|
+
return if find_entity(schema, value.keyword) || find_type(schema, value.keyword)
|
|
158
|
+
|
|
159
|
+
note!(notes, :p21_unknown_typed_keyword, :error, id,
|
|
160
|
+
"#{id}: typed parameter '#{value.keyword}' does not resolve")
|
|
161
|
+
when Enumeration
|
|
162
|
+
items = enumeration_items(schema, attr)
|
|
163
|
+
return unless items
|
|
164
|
+
|
|
165
|
+
names = items.filter_map { |item| item.id&.downcase }
|
|
166
|
+
return if names.include?(value.name.downcase)
|
|
167
|
+
|
|
168
|
+
note!(notes, :p21_enum_value, :error, id,
|
|
169
|
+
"#{id}: .#{value.name}. is not in attribute '#{attr.id}' enumeration")
|
|
170
|
+
when Array
|
|
171
|
+
value.each { |item| check_value(notes, schema, population, id, attr, item) }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def explicit_attributes(entity)
|
|
176
|
+
Array(entity.attributes)
|
|
177
|
+
.select { |attr| attr.instance_of?(Model::Declarations::Attribute) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def own_derived_attributes(entity)
|
|
181
|
+
Array(entity.attributes)
|
|
182
|
+
.select { |attr| attr.instance_of?(Model::Declarations::DerivedAttribute) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def find_entity(schema, keyword, short_names = nil)
|
|
186
|
+
key = keyword.downcase
|
|
187
|
+
Array(schema.entities).find { |entity| entity.id&.downcase == key } ||
|
|
188
|
+
resolve_short_name(schema, key, short_names)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def resolve_short_name(schema, key, short_names)
|
|
192
|
+
return nil unless short_names.is_a?(Hash) && short_names[key]
|
|
193
|
+
|
|
194
|
+
find_entity(schema, short_names[key], nil)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def find_type(schema, keyword)
|
|
198
|
+
Array(schema.types).find { |type| type.id&.downcase == keyword.downcase }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def enumeration_items(schema, attr)
|
|
202
|
+
type = attr&.type
|
|
203
|
+
return Array(type.items) if type.is_a?(Model::DataTypes::Enumeration)
|
|
204
|
+
|
|
205
|
+
return nil unless type.is_a?(Model::References::SimpleReference)
|
|
206
|
+
|
|
207
|
+
declaration = find_type(schema, type.id.to_s)
|
|
208
|
+
underlying = declaration&.underlying_type
|
|
209
|
+
underlying.is_a?(Model::DataTypes::Enumeration) ? Array(underlying.items) : nil
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
# ISO 10303-21 exchange-structure support (TODO.eengine/06).
|
|
5
|
+
module P21
|
|
6
|
+
autoload :Population, "#{__dir__}/p21/population"
|
|
7
|
+
autoload :Record, "#{__dir__}/p21/population"
|
|
8
|
+
autoload :ComplexRecord, "#{__dir__}/p21/population"
|
|
9
|
+
autoload :TypedValue, "#{__dir__}/p21/population"
|
|
10
|
+
autoload :Enumeration, "#{__dir__}/p21/population"
|
|
11
|
+
autoload :Binary, "#{__dir__}/p21/population"
|
|
12
|
+
autoload :Grammar, "#{__dir__}/p21/grammar"
|
|
13
|
+
autoload :Builder, "#{__dir__}/p21/builder"
|
|
14
|
+
autoload :Validator, "#{__dir__}/p21/validator"
|
|
15
|
+
autoload :Parser, "#{__dir__}/p21/parser"
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/expressir/version.rb
CHANGED
data/lib/expressir.rb
CHANGED
|
@@ -30,6 +30,7 @@ module Expressir
|
|
|
30
30
|
|
|
31
31
|
autoload :Version, "#{__dir__}/expressir/version"
|
|
32
32
|
autoload :Mapping, "#{__dir__}/expressir/mapping"
|
|
33
|
+
autoload :P21, "#{__dir__}/expressir/p21"
|
|
33
34
|
|
|
34
35
|
# Error classes — autoload the same errors.rb file from each name.
|
|
35
36
|
# The first reference triggers the load; the file defines all 21 classes.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: expressir
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.33
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|
|
@@ -303,6 +303,16 @@ files:
|
|
|
303
303
|
- TODO.bugs/29-validate-ascii-god-class.md
|
|
304
304
|
- TODO.bugs/30-unicode-map-extraction.md
|
|
305
305
|
- TODO.bugs/README.md
|
|
306
|
+
- TODO.eengine/00-overview.md
|
|
307
|
+
- TODO.eengine/01-resolution-diagnostics.md
|
|
308
|
+
- TODO.eengine/02-check-catalog-completion.md
|
|
309
|
+
- TODO.eengine/03-entity-computations.md
|
|
310
|
+
- TODO.eengine/04-shtolo-rule-audit.md
|
|
311
|
+
- TODO.eengine/05-shtolo-longform-flatten.md
|
|
312
|
+
- TODO.eengine/06-population-p21.md
|
|
313
|
+
- TODO.eengine/07-schema-compare-patch.md
|
|
314
|
+
- TODO.eengine/08-official-corpus-gate.md
|
|
315
|
+
- TODO.eengine/09-mim-mapping-parser.md
|
|
306
316
|
- TODO.max-perf/01-restore-ci-green.md
|
|
307
317
|
- TODO.max-perf/02-streaming-parse-path.md
|
|
308
318
|
- TODO.max-perf/03-cli-parallel-opt-in.md
|
|
@@ -552,6 +562,7 @@ files:
|
|
|
552
562
|
- lib/expressir/express/remark_scanner.rb
|
|
553
563
|
- lib/expressir/express/resolve_references_model_visitor.rb
|
|
554
564
|
- lib/expressir/express/schema_block_scanner.rb
|
|
565
|
+
- lib/expressir/express/schema_compare.rb
|
|
555
566
|
- lib/expressir/express/schema_head_formatter.rb
|
|
556
567
|
- lib/expressir/express/schema_plain_source_formatter.rb
|
|
557
568
|
- lib/expressir/express/schema_source_formatter.rb
|
|
@@ -664,6 +675,12 @@ files:
|
|
|
664
675
|
- lib/expressir/model/supertype_expressions.rb
|
|
665
676
|
- lib/expressir/model/supertype_expressions/binary_supertype_expression.rb
|
|
666
677
|
- lib/expressir/model/supertype_expressions/oneof_supertype_expression.rb
|
|
678
|
+
- lib/expressir/p21.rb
|
|
679
|
+
- lib/expressir/p21/builder.rb
|
|
680
|
+
- lib/expressir/p21/grammar.rb
|
|
681
|
+
- lib/expressir/p21/parser.rb
|
|
682
|
+
- lib/expressir/p21/population.rb
|
|
683
|
+
- lib/expressir/p21/validator.rb
|
|
667
684
|
- lib/expressir/package.rb
|
|
668
685
|
- lib/expressir/package/builder.rb
|
|
669
686
|
- lib/expressir/package/metadata.rb
|