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,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
|
|
@@ -295,10 +295,32 @@ module Expressir
|
|
|
295
295
|
convert_generic_entities!(artifact)
|
|
296
296
|
convert_renamed_attributes!(artifact)
|
|
297
297
|
rewrite_qualified_strings!(artifact)
|
|
298
|
+
add_provenance_remark!(artifact)
|
|
298
299
|
|
|
299
300
|
artifact
|
|
300
301
|
end
|
|
301
302
|
|
|
303
|
+
# G.1.10: optional embedded provenance remark — if any schema
|
|
304
|
+
# name is kept in the longform, ALL contributing schemas are
|
|
305
|
+
# recorded (`schema = <id> [schema_version_id = '<v>'];`, order
|
|
306
|
+
# insignificant).
|
|
307
|
+
def add_provenance_remark!(schema)
|
|
308
|
+
names = closure.values.map(&:id).sort
|
|
309
|
+
return if names.empty?
|
|
310
|
+
|
|
311
|
+
remarks = ["Original 2003 schemas:"] + names.map do |id|
|
|
312
|
+
contributing = closure.values.find { |s| s.id == id }
|
|
313
|
+
version = contributing&.version&.id
|
|
314
|
+
line = "schema = #{id};"
|
|
315
|
+
line = "schema = #{id} schema_version_id = '#{version}';" if version
|
|
316
|
+
line
|
|
317
|
+
end
|
|
318
|
+
schema.remark_items = [
|
|
319
|
+
Model::Declarations::RemarkItem.new(id: "provenance", remarks: remarks),
|
|
320
|
+
*Array(schema.remark_items),
|
|
321
|
+
]
|
|
322
|
+
end
|
|
323
|
+
|
|
302
324
|
# G.1.9 prune pass: the artifact keeps only what stays visible
|
|
303
325
|
# and reachable. Runs before the stage-2 rewrites so they never
|
|
304
326
|
# see declarations the longform would drop.
|
data/lib/expressir/express.rb
CHANGED
|
@@ -43,6 +43,8 @@ module Expressir
|
|
|
43
43
|
autoload :SchemaPlainSourceFormatter,
|
|
44
44
|
"#{__dir__}/express/schema_plain_source_formatter"
|
|
45
45
|
autoload :SchemaSourceFormatter, "#{__dir__}/express/schema_source_formatter"
|
|
46
|
+
autoload :SchemaCompare, "#{__dir__}/express/schema_compare"
|
|
47
|
+
autoload :SchemaPatch, "#{__dir__}/express/schema_compare"
|
|
46
48
|
autoload :ScopeResolver, "#{__dir__}/express/scope_resolver"
|
|
47
49
|
autoload :LazyRepository, "#{__dir__}/express/lazy_repository"
|
|
48
50
|
autoload :SmrlXml, "#{__dir__}/express/smrl_xml"
|
|
@@ -56,6 +56,138 @@ module Expressir
|
|
|
56
56
|
*remark_items,
|
|
57
57
|
]
|
|
58
58
|
end
|
|
59
|
+
|
|
60
|
+
# eeng compute-inheritance: the full supertype closure, nearest
|
|
61
|
+
# supertype first, cycle-safe. Cross-schema supertypes resolve
|
|
62
|
+
# through the enclosing repository.
|
|
63
|
+
# @return [Array<Entity>]
|
|
64
|
+
def supertype_closure
|
|
65
|
+
closure = []
|
|
66
|
+
visited = { id&.safe_downcase => true }
|
|
67
|
+
queue = Array(subtype_of).map { |ref| ref.respond_to?(:id) ? ref.id : ref }
|
|
68
|
+
while (name = queue.shift)
|
|
69
|
+
key = name&.safe_downcase
|
|
70
|
+
next if key.nil? || visited.key?(key)
|
|
71
|
+
|
|
72
|
+
supertype = find_supertype_entity(key)
|
|
73
|
+
next unless supertype
|
|
74
|
+
|
|
75
|
+
visited[key] = true
|
|
76
|
+
closure << supertype
|
|
77
|
+
queue.concat(Array(supertype.subtype_of).map do |ref|
|
|
78
|
+
ref.respond_to?(:id) ? ref.id : ref
|
|
79
|
+
end)
|
|
80
|
+
end
|
|
81
|
+
closure
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Explicit attributes inherited from the supertype closure,
|
|
85
|
+
# nearest supertype first, skipping ids this entity (or a
|
|
86
|
+
# nearer ancestor) redeclares.
|
|
87
|
+
# @return [Array<ExplicitAttribute>]
|
|
88
|
+
def inherited_explicit_attributes
|
|
89
|
+
claimed = explicit_only(Array(attributes)).filter_map(&:id).map(&:safe_downcase)
|
|
90
|
+
closure = supertype_closure
|
|
91
|
+
closure.flat_map do |supertype|
|
|
92
|
+
explicit_only(Array(supertype.attributes)).filter_map do |attr|
|
|
93
|
+
next nil unless attr.id
|
|
94
|
+
|
|
95
|
+
key = attr.id.safe_downcase
|
|
96
|
+
next nil if claimed.include?(key)
|
|
97
|
+
|
|
98
|
+
claimed << key
|
|
99
|
+
attr
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Derived attributes accumulated from the supertype closure.
|
|
105
|
+
# @return [Array<DerivedAttribute>]
|
|
106
|
+
def inherited_derived_attributes
|
|
107
|
+
closure = supertype_closure
|
|
108
|
+
seen = {}
|
|
109
|
+
closure.flat_map do |supertype|
|
|
110
|
+
derived_attribute_items(supertype).filter_map do |attr|
|
|
111
|
+
next nil unless attr.id
|
|
112
|
+
|
|
113
|
+
key = attr.id.safe_downcase
|
|
114
|
+
next nil if seen.key?(key)
|
|
115
|
+
|
|
116
|
+
seen[key] = true
|
|
117
|
+
attr
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Inverse attributes accumulated from the supertype closure.
|
|
123
|
+
# @return [Array<InverseAttribute>]
|
|
124
|
+
def inherited_inverse_attributes
|
|
125
|
+
closure = supertype_closure
|
|
126
|
+
seen = {}
|
|
127
|
+
closure.flat_map do |supertype|
|
|
128
|
+
inverse_attribute_items(supertype).filter_map do |attr|
|
|
129
|
+
next nil unless attr.id
|
|
130
|
+
|
|
131
|
+
key = attr.id.safe_downcase
|
|
132
|
+
next nil if seen.key?(key)
|
|
133
|
+
|
|
134
|
+
seen[key] = true
|
|
135
|
+
attr
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# All explicit attributes: own + inherited (eeng
|
|
141
|
+
# explicit-attrs materialization).
|
|
142
|
+
# @return [Array<ExplicitAttribute>]
|
|
143
|
+
def all_explicit_attributes
|
|
144
|
+
[*explicit_only(Array(attributes)), *inherited_explicit_attributes]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Entity#attributes carries every attribute kind; the explicit
|
|
150
|
+
# kind is Expressir::Model::Declarations::Attribute.
|
|
151
|
+
# DerivedAttribute/InverseAttribute subclass Attribute, so the
|
|
152
|
+
# explicit kind is the exact base class.
|
|
153
|
+
def explicit_only(attrs)
|
|
154
|
+
attrs.select { |a| a.instance_of?(Expressir::Model::Declarations::Attribute) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The Ruby parse path parks every attribute kind in `attributes`
|
|
158
|
+
# (typed by class); the core path fills derived_attributes/
|
|
159
|
+
# inverse_attributes instead. Both layouts resolve to the same
|
|
160
|
+
# items here.
|
|
161
|
+
def derived_attribute_items(entity)
|
|
162
|
+
own = Array(entity.derived_attributes)
|
|
163
|
+
return own if own.any?
|
|
164
|
+
|
|
165
|
+
Array(entity.attributes).grep(Expressir::Model::Declarations::DerivedAttribute)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def inverse_attribute_items(entity)
|
|
169
|
+
own = Array(entity.inverse_attributes)
|
|
170
|
+
return own if own.any?
|
|
171
|
+
|
|
172
|
+
Array(entity.attributes).grep(Expressir::Model::Declarations::InverseAttribute)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def find_supertype_entity(key)
|
|
176
|
+
container = parent
|
|
177
|
+
container = container.parent while container &&
|
|
178
|
+
!container.is_a?(Model::Repository) &&
|
|
179
|
+
!container.is_a?(Model::ExpFile)
|
|
180
|
+
return nil unless container.respond_to?(:schemas)
|
|
181
|
+
|
|
182
|
+
container.schemas.each do |foreign|
|
|
183
|
+
next unless foreign.respond_to?(:entities)
|
|
184
|
+
|
|
185
|
+
foreign.entities.each do |candidate|
|
|
186
|
+
return candidate if candidate.id&.safe_downcase == key
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
59
191
|
end
|
|
60
192
|
end
|
|
61
193
|
end
|
|
@@ -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
|