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,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.
@@ -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
- safe_children.map do |base_item|
175
- create_interfaced_item(base_item.id, base_item)
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
- if base_item
181
- create_interfaced_item(item.id || base_item.id,
182
- base_item)
183
- end
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