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.
@@ -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
@@ -3,6 +3,6 @@ module Expressir
3
3
  # autoload it (Ruby autoload only works for module/class constants, not
4
4
  # for string constants like `Expressir::VERSION`).
5
5
  module Version
6
- VERSION = "2.4.32".freeze
6
+ VERSION = "2.4.33".freeze
7
7
  end
8
8
  end
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.32
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-14 00:00:00.000000000 Z
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