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
|
@@ -127,6 +127,19 @@ module Expressir
|
|
|
127
127
|
@formatted_hyperlinked_adoc ||= Expressir::Express::AdocSourceFormatter.format(self)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
USE_ITEM_KINDS = [
|
|
131
|
+
Expressir::Model::Declarations::Entity,
|
|
132
|
+
Expressir::Model::Declarations::Type,
|
|
133
|
+
].freeze
|
|
134
|
+
|
|
135
|
+
REFERENCE_ITEM_KINDS = [
|
|
136
|
+
Expressir::Model::Declarations::Constant,
|
|
137
|
+
Expressir::Model::Declarations::Entity,
|
|
138
|
+
Expressir::Model::Declarations::Function,
|
|
139
|
+
Expressir::Model::Declarations::Procedure,
|
|
140
|
+
Expressir::Model::Declarations::Type,
|
|
141
|
+
].freeze
|
|
142
|
+
|
|
130
143
|
private
|
|
131
144
|
|
|
132
145
|
# @param [String] id
|
|
@@ -142,6 +155,7 @@ module Expressir
|
|
|
142
155
|
end
|
|
143
156
|
|
|
144
157
|
# @return [Array<InterfacedItem>]
|
|
158
|
+
|
|
145
159
|
def interfaced_items
|
|
146
160
|
return [] unless parent
|
|
147
161
|
|
|
@@ -151,6 +165,11 @@ module Expressir
|
|
|
151
165
|
end.compact
|
|
152
166
|
end
|
|
153
167
|
|
|
168
|
+
# ISO 10303-11 implicit interfacing: USE exposes entities and
|
|
169
|
+
# types only; REFERENCE has no implicit interfacing. Listed
|
|
170
|
+
# items follow eeng resolve-interface: use-from resolves via
|
|
171
|
+
# find-type/entity, reference-from via the 5-kind or-chain.
|
|
172
|
+
|
|
154
173
|
# Direct items of one interface plus the items visible through
|
|
155
174
|
# the foreign schema's own USE interfaces (transitive USE
|
|
156
175
|
# visibility, eeng :use-only → :use-from recursion).
|
|
@@ -171,16 +190,23 @@ module Expressir
|
|
|
171
190
|
end
|
|
172
191
|
|
|
173
192
|
own = if interface.items.empty?
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
if interface.kind == Interface::REFERENCE
|
|
194
|
+
[]
|
|
195
|
+
else
|
|
196
|
+
safe_children.filter_map do |base_item|
|
|
197
|
+
next nil unless USE_ITEM_KINDS.any? { |k| base_item.is_a?(k) }
|
|
198
|
+
|
|
199
|
+
create_interfaced_item(base_item.id, base_item)
|
|
200
|
+
end
|
|
176
201
|
end
|
|
177
202
|
else
|
|
178
203
|
interface.items.filter_map do |item|
|
|
179
204
|
base_item = children_by_id[item.ref.id.safe_downcase]
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
205
|
+
next nil unless base_item
|
|
206
|
+
next nil unless item_kind_interfacable?(interface.kind, base_item)
|
|
207
|
+
|
|
208
|
+
create_interfaced_item(item.id || base_item.id,
|
|
209
|
+
base_item)
|
|
184
210
|
end
|
|
185
211
|
end
|
|
186
212
|
|
|
@@ -197,6 +223,11 @@ module Expressir
|
|
|
197
223
|
end.flatten
|
|
198
224
|
end
|
|
199
225
|
|
|
226
|
+
def item_kind_interfacable?(kind, node)
|
|
227
|
+
kinds = kind == Interface::REFERENCE ? REFERENCE_ITEM_KINDS : USE_ITEM_KINDS
|
|
228
|
+
kinds.any? { |k| node.is_a?(k) }
|
|
229
|
+
end
|
|
230
|
+
|
|
200
231
|
# Find a foreign schema by name: same file first, then the
|
|
201
232
|
# enclosing repository (expressir's compiled/batch repository).
|
|
202
233
|
def foreign_schema(name)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module P21
|
|
5
|
+
# Turns a {Grammar} capture tree into a {Population}.
|
|
6
|
+
module Builder
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def build(tree)
|
|
10
|
+
header = build_header(tree[:header_section])
|
|
11
|
+
instances = build_instances(tree[:data_section])
|
|
12
|
+
Population.new(header: header, instances: instances)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_header(section)
|
|
16
|
+
Array(section[:header_entity]).each_with_object({}) do |entry, hash|
|
|
17
|
+
record = entry[:simple_record]
|
|
18
|
+
hash[record_keyword(record)] = record_params(record)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_instances(section)
|
|
23
|
+
list = section[:instance_list]
|
|
24
|
+
entries = list.is_a?(Array) ? list : [list]
|
|
25
|
+
entries.each_with_object({}) do |entry, hash|
|
|
26
|
+
if (complex = entry[:complex_entity_instance])
|
|
27
|
+
hash[instance_id(complex)] = build_complex(complex)
|
|
28
|
+
elsif (simple = entry[:simple_entity_instance])
|
|
29
|
+
record = simple[:simple_record]
|
|
30
|
+
hash[instance_id(simple)] = Record.new(record_keyword(record),
|
|
31
|
+
record_params(record))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def instance_id(body)
|
|
37
|
+
"##{body[:entity_instance_name].to_s.delete_prefix('#')}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_complex(body)
|
|
41
|
+
subs = body[:subsuper_record]
|
|
42
|
+
subs = [subs] unless subs.is_a?(Array)
|
|
43
|
+
records = subs.map do |sub|
|
|
44
|
+
record = sub[:simple_record]
|
|
45
|
+
Record.new(record_keyword(record), record_params(record))
|
|
46
|
+
end
|
|
47
|
+
ComplexRecord.new(records)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def record_keyword(record)
|
|
51
|
+
record[:keyword].to_s.delete_prefix("!")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# A parameter_list capture is a hash when there is a single parameter
|
|
55
|
+
# and an array of {parameter: ...} entries otherwise.
|
|
56
|
+
def parameter_list_values(capture)
|
|
57
|
+
return [] if capture.nil?
|
|
58
|
+
|
|
59
|
+
entries = capture.is_a?(Array) ? capture : [capture]
|
|
60
|
+
entries.map { |entry| build_value(entry[:parameter]) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def record_params(record)
|
|
64
|
+
parameter_list_values(record[:parameter_list])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_value(param)
|
|
68
|
+
if (typed = param[:typed_parameter])
|
|
69
|
+
TypedValue.new(typed[:keyword].to_s.delete_prefix("!"),
|
|
70
|
+
parameter_list_values(typed[:parameter_list]))
|
|
71
|
+
elsif param[:parameter_list]
|
|
72
|
+
parameter_list_values(param[:parameter_list])
|
|
73
|
+
elsif param[:entity_instance_name]
|
|
74
|
+
"##{param[:entity_instance_name].to_s.delete_prefix('#')}"
|
|
75
|
+
elsif param[:number_literal]
|
|
76
|
+
text = param[:number_literal].to_s
|
|
77
|
+
text.match?(/[.Ee]/) ? text.to_f : text.to_i
|
|
78
|
+
elsif param[:enumeration]
|
|
79
|
+
Enumeration.new(param[:enumeration].to_s.delete_prefix(".").delete_suffix("."))
|
|
80
|
+
elsif param[:binary_literal]
|
|
81
|
+
Binary.new(param[:binary_literal].to_s.delete_prefix('"').delete_suffix('"'))
|
|
82
|
+
elsif param[:string_literal]
|
|
83
|
+
param[:string_literal].to_s[1..-2].gsub("''", "'")
|
|
84
|
+
elsif param[:unset]
|
|
85
|
+
:unset
|
|
86
|
+
elsif param[:derived]
|
|
87
|
+
:derived
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "parsanol"
|
|
4
|
+
|
|
5
|
+
module Expressir
|
|
6
|
+
module P21
|
|
7
|
+
# ISO 10303-21 exchange-structure grammar (TODO.eengine/06 step 1).
|
|
8
|
+
# Parses the HEADER section and the DATA section's instance list
|
|
9
|
+
# into a capture tree; {Expressir::P21::PopulationBuilder} turns
|
|
10
|
+
# the tree into a {Expressir::P21::Population}.
|
|
11
|
+
#
|
|
12
|
+
# Reference: ISO 10303-21 exchange structure grammar
|
|
13
|
+
# (`~/src/mn/iso-10303-detached-docs/sources/iso-10303-21*/`);
|
|
14
|
+
# scope ladder in TODO.eengine/06 — the supported subset is the
|
|
15
|
+
# header FILE_* entities plus simple/complex data instances with
|
|
16
|
+
# lists, typed parameters, references, enumerations, binaries,
|
|
17
|
+
# strings, unset ($) and derived (*).
|
|
18
|
+
class Grammar < Parsanol::Parser
|
|
19
|
+
root(:exchange_structure)
|
|
20
|
+
|
|
21
|
+
rule(:comment) { str("/*") >> (str("*/").absent? >> any).repeat >> str("*/") }
|
|
22
|
+
rule(:ws?) { (match[" \t\r\n"] | comment).repeat }
|
|
23
|
+
rule(:ws1) { (match[" \t\r\n"] | comment).repeat(1) }
|
|
24
|
+
rule(:keyword) { match["!"].maybe >> match["A-Za-z0-9_"].repeat(1) }
|
|
25
|
+
rule(:entity_instance_name) { str("#") >> match["0-9"].repeat(1) }
|
|
26
|
+
|
|
27
|
+
rule(:string_literal) do
|
|
28
|
+
# ISO 10303-21: doubled quotes are escaped quotes
|
|
29
|
+
str("'") >> ((str("'").absent? >> any) | str("''")).repeat >> str("'")
|
|
30
|
+
end
|
|
31
|
+
rule(:binary_literal) { str('"') >> match["0-9A-Fa-f"].repeat >> str('"') }
|
|
32
|
+
rule(:enumeration) { str(".") >> match["A-Za-z0-9_"].repeat(1) >> str(".") }
|
|
33
|
+
rule(:unset) { str("$") }
|
|
34
|
+
rule(:derived) { str("*") }
|
|
35
|
+
|
|
36
|
+
rule(:parameter_list) do
|
|
37
|
+
str("(") >> ws? >>
|
|
38
|
+
(parameter.as(:parameter) >>
|
|
39
|
+
(str(",") >> ws? >> parameter.as(:parameter)).repeat).maybe >>
|
|
40
|
+
str(")") >> ws?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
rule(:typed_parameter) do
|
|
44
|
+
keyword.as(:keyword) >> ws? >> parameter_list.as(:parameter_list)
|
|
45
|
+
end
|
|
46
|
+
rule(:list_parameter) { parameter_list }
|
|
47
|
+
|
|
48
|
+
rule(:number_literal) do
|
|
49
|
+
match["+-"].maybe >> match["0-9"].repeat(1) >>
|
|
50
|
+
(str(".") >> match["0-9"].repeat(1)).maybe >>
|
|
51
|
+
(match["Ee"] >> match["+-"].maybe >> match["0-9"].repeat(1)).maybe
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
rule(:parameter) do
|
|
55
|
+
typed_parameter.as(:typed_parameter) |
|
|
56
|
+
list_parameter.as(:parameter_list) |
|
|
57
|
+
entity_instance_name.as(:entity_instance_name) |
|
|
58
|
+
number_literal.as(:number_literal) |
|
|
59
|
+
enumeration.as(:enumeration) |
|
|
60
|
+
binary_literal.as(:binary_literal) |
|
|
61
|
+
string_literal.as(:string_literal) |
|
|
62
|
+
unset.as(:unset) | derived.as(:derived)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
rule(:simple_record) do
|
|
66
|
+
keyword.as(:keyword) >> ws? >> parameter_list.as(:parameter_list)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
rule(:subsuper_record) do
|
|
70
|
+
str("(") >> ws? >> simple_record.as(:simple_record).repeat(1) >>
|
|
71
|
+
str(")") >> ws?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
rule(:simple_entity_instance) do
|
|
75
|
+
(entity_instance_name.as(:entity_instance_name) >> ws? >>
|
|
76
|
+
str("=") >> ws? >> simple_record.as(:simple_record) >>
|
|
77
|
+
str(";") >> ws?).as(:simple_entity_instance)
|
|
78
|
+
end
|
|
79
|
+
rule(:complex_entity_instance) do
|
|
80
|
+
(entity_instance_name.as(:entity_instance_name) >> ws? >>
|
|
81
|
+
str("=") >> ws? >> subsuper_record.as(:subsuper_record) >>
|
|
82
|
+
str(";") >> ws?).as(:complex_entity_instance)
|
|
83
|
+
end
|
|
84
|
+
rule(:entity_instance) { complex_entity_instance | simple_entity_instance }
|
|
85
|
+
|
|
86
|
+
rule(:header_entity) do
|
|
87
|
+
simple_record.as(:simple_record) >> str(";") >> ws?
|
|
88
|
+
end
|
|
89
|
+
rule(:header_section) do
|
|
90
|
+
str("HEADER;") >> ws? >> header_entity.repeat(1).as(:header_entity) >>
|
|
91
|
+
str("ENDSEC;") >> ws?
|
|
92
|
+
end
|
|
93
|
+
rule(:data_section) do
|
|
94
|
+
str("DATA;") >> ws? >>
|
|
95
|
+
(str("(") >> ws? >> parameter >> str(");") >> ws?).maybe >>
|
|
96
|
+
entity_instance.repeat.as(:instance_list) >>
|
|
97
|
+
str("ENDSEC;") >> ws?
|
|
98
|
+
end
|
|
99
|
+
rule(:exchange_structure) do
|
|
100
|
+
ws? >> str("ISO-10303-21;") >> ws? >>
|
|
101
|
+
header_section.as(:header_section) >>
|
|
102
|
+
data_section.as(:data_section) >>
|
|
103
|
+
str("END-ISO-10303-21;") >> ws?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module P21
|
|
5
|
+
# Facade: parse an ISO 10303-21 exchange structure into a
|
|
6
|
+
# {Population} (TODO.eengine/06 step 1).
|
|
7
|
+
module Parser
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def parse(source)
|
|
11
|
+
tree = Grammar.new.parse(source)
|
|
12
|
+
Builder.build(tree)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -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.34
|
|
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-16 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
|