expressir 2.4.36-arm-linux → 2.4.38-arm-linux

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.
@@ -24,10 +24,16 @@ module Expressir
24
24
  # is referenced)
25
25
  # index := "[" ("i" | DIGITS) "]"
26
26
  # constraint := ["!"] "{" NAME [NAME "."] "=" STRING "}"
27
+ # supertype := "||" NAME "||" (§5 supertype entity marker)
28
+ # tree := "*" NAME "*" (§5 relationship tree marker)
27
29
  # MARKER := "{" | "}" | "[" | "]" | "(" | ")" | ":" | "|" | "!"
28
30
  # | "," | "/" | "*" | "<" | ">" (grouping/annotation;
29
31
  # never breaks a
30
- # pending link)
32
+ # pending link; the §5
33
+ # required-path marker
34
+ # "<path>" is transparent
35
+ # — its contents validate
36
+ # in the running context)
31
37
  #
32
38
  # Validation (ELF 5006 §8): a running path context tracks the
33
39
  # entity a path is "at"; each step must connect to it, attribute
@@ -104,7 +110,7 @@ module Expressir
104
110
  EXPRESS_LINK_PATH = /\A<<express:([^,>]+)(?:,[^>]+)?>>\z/
105
111
  private_constant :EXPRESS_LINK_PATH
106
112
 
107
- TOKEN = /<<express:[^,>]+(?:,[^>]+)?>>|->|<-|<=|=>|\*>|<\*|!?\{|[{}\[\]()=:|!,*\/<>]|'(?:''|[^'])*'|\w+(?:\.\w+)?/
113
+ TOKEN = /<<express:[^,>]+(?:,[^>]+)?>>|->|<-|<=|=>|\*>|<\*|\|\||!?\{|[{}\[\]()=:|!,*\/<>]|'(?:''|[^'])*'|\w+(?:\.\w+)?/
108
114
  MARKERS = ["{", "}", "[", "]", "(", ")", ":", "|", "!", ",", "/",
109
115
  "*", "<", ">", "MAPPING_OF", "!{"].freeze
110
116
  LINKS = ["->", "<-", "<=", "=>", "*>", "<*"].freeze
@@ -140,13 +146,19 @@ module Expressir
140
146
  errors = []
141
147
  pending_link = nil
142
148
  pending_type_assign = false
143
- false
144
149
  i = 0
145
150
  while i < tokens.length
146
151
  token = tokens[i]
147
152
  case token
153
+ when "||"
154
+ name_token = tokens[i + 1]
155
+ if name_token&.match?(/\A\w+\z/) && tokens[i + 2] == "||"
156
+ steps << Step.new(operator: "||", name: name_token)
157
+ i += 2
158
+ else
159
+ errors << "supertype marker '||' without an entity name"
160
+ end
148
161
  when "!{"
149
- true
150
162
  step, consumed = parse_constraint(tokens[i..], negated: true)
151
163
  steps << step
152
164
  i += consumed
@@ -182,6 +194,14 @@ module Expressir
182
194
  else
183
195
  steps << Step.new(operator: "[")
184
196
  end
197
+ when "*"
198
+ name_token = tokens[i + 1]
199
+ if name_token&.match?(/\A\w+\z/) && tokens[i + 2] == "*"
200
+ steps << Step.new(operator: "*", name: name_token)
201
+ i += 2
202
+ else
203
+ steps << Step.new(operator: token)
204
+ end
185
205
  when *MARKERS
186
206
  steps << Step.new(operator: token)
187
207
  when /\A<<express:/
@@ -302,6 +322,14 @@ module Expressir
302
322
  when "*>", "<*"
303
323
  issues.concat(select_extension_issues(step, pos, ctx, by_name))
304
324
  ctx = step.name&.safe_downcase
325
+ when "||"
326
+ issues.concat(supertype_marker_issues(step, pos, ctx, by_name))
327
+ if step.name
328
+ seen[step.name.safe_downcase] = true
329
+ ctx = step.name.safe_downcase
330
+ end
331
+ when "*"
332
+ issues.concat(tree_marker_issues(step, pos, by_name))
305
333
  else
306
334
  issues.concat(check_step(step, pos, ctx, last_qualified, by_name))
307
335
  if step.name
@@ -495,6 +523,39 @@ module Expressir
495
523
  end
496
524
  end
497
525
 
526
+ # ELF 5006 §5 supertype entity marker: `||supertype||` names an
527
+ # entity that must exist and, when the path has a running
528
+ # context, be a supertype of (or equal to) that context entity.
529
+ def supertype_marker_issues(step, pos, ctx, by_name)
530
+ issues = []
531
+ entity = step.name
532
+ check_exists(issues, pos, entity, by_name)
533
+ return issues if entity.nil?
534
+
535
+ decl = by_name[entity.safe_downcase]&.last
536
+ return issues unless decl.is_a?(Model::Declarations::Entity)
537
+
538
+ context_decl = ctx && by_name[ctx]&.last
539
+ if context_decl.is_a?(Model::Declarations::Entity) &&
540
+ !entity_reaches?(context_decl, entity.safe_downcase, by_name,
541
+ {}.compare_by_identity)
542
+ issues << Issue.new(step: pos,
543
+ message: "'#{entity}' is not a supertype of " \
544
+ "'#{ctx}'")
545
+ end
546
+ issues
547
+ end
548
+
549
+ # ELF 5006 §5 relationship tree marker: `*relationship*` names an
550
+ # entity whose instances may assemble into a tree; the path
551
+ # between it and the related entities is implied, so only the
552
+ # entity's existence is checkable.
553
+ def tree_marker_issues(step, pos, by_name)
554
+ issues = []
555
+ check_exists(issues, pos, step.name, by_name)
556
+ issues
557
+ end
558
+
498
559
  # ELF 5006 §8 navigation validation with a running context: the
499
560
  # attribute must exist on the context entity, and its type must
500
561
  # be able to reference the step's target entity.
@@ -14,6 +14,7 @@ module Expressir
14
14
  # the source files.
15
15
  module Mapping
16
16
  autoload :RefPath, "#{__dir__}/mapping/refpath"
17
+ autoload :InstanceVerification, "#{__dir__}/mapping/instance_verification"
17
18
 
18
19
  EXPRESS_LINK = /<<express:([^,>]+)(?:,([^>]+))?>>/
19
20
 
@@ -8,8 +8,80 @@ module Expressir
8
8
 
9
9
  def build(tree)
10
10
  header = build_header(tree[:header_section])
11
- instances = build_instances(tree[:data_section])
12
- Population.new(header: header, instances: instances)
11
+ instances = {}
12
+ sections = []
13
+ data_sections(tree).each do |section|
14
+ records = build_instances(section)
15
+ instances.update(records)
16
+ sections << build_section(section, records.keys)
17
+ end
18
+ Population.new(
19
+ header: header, instances: instances, sections: sections,
20
+ anchors: build_anchors(tree[:anchor_section]),
21
+ references: build_references(tree[:reference_section])
22
+ )
23
+ end
24
+
25
+ # A DATA section capture: hash for a single section, array when
26
+ # the exchange structure carries several (ISO 10303-21 annex E).
27
+ def data_sections(tree)
28
+ sections = tree[:data_section]
29
+ sections = [sections] unless sections.is_a?(Array)
30
+ sections.compact.map do |section|
31
+ # The repeat's own capture nests each section one level deep.
32
+ inner = section[:data_section]
33
+ inner.is_a?(Hash) ? inner : section
34
+ end
35
+ end
36
+
37
+ Section = Struct.new(:name, :contexts, :instance_ids,
38
+ keyword_init: true)
39
+
40
+ def build_section(section, instance_ids)
41
+ tag = section[:section_tag]
42
+ values = tag && parameter_list_values(tag.is_a?(Hash) ? { parameter: tag } : tag)
43
+ name = values && values.first.is_a?(String) ? values.first : nil
44
+ contexts = values && values[1].is_a?(Array) ? values[1] : []
45
+ Section.new(name: name, contexts: contexts, instance_ids: instance_ids)
46
+ end
47
+
48
+ def build_anchors(capture)
49
+ entries = capture.is_a?(Hash) ? [capture] : Array(capture)
50
+ unwrap_anchor_entries(entries).filter_map do |entry|
51
+ next unless entry.is_a?(Hash) && entry[:anchor_label]
52
+
53
+ value = entry[:entity_instance_name] ||
54
+ (entry[:unset] ? :unset : nil)
55
+ [entry[:anchor_label].to_s,
56
+ value == :unset ? :unset : "##{value.to_s.delete_prefix('#')}"]
57
+ end.to_h
58
+ end
59
+
60
+ # Parsanol captures nest one wrapper per repeat/rule level;
61
+ # collect the leaves that carry the anchor fields.
62
+ def unwrap_anchor_entries(entries)
63
+ entries.flat_map do |entry|
64
+ next [] unless entry.is_a?(Hash)
65
+
66
+ inner = entry[:anchor_entry]
67
+ if inner
68
+ unwrap_anchor_entries(inner.is_a?(Array) ? inner : [inner])
69
+ else
70
+ [entry]
71
+ end
72
+ end
73
+ end
74
+
75
+ def build_references(capture)
76
+ list = capture.nil? ? [] : capture[:instance_list]
77
+ entries = list.is_a?(Array) ? list : [list]
78
+ entries.filter_map do |entry|
79
+ next unless entry.is_a?(Hash) && entry[:external_reference_instance]
80
+
81
+ body = entry[:external_reference_instance]
82
+ ["##{body[:entity_instance_name].to_s.delete_prefix('#')}",
83
+ body[:external_anchor].to_s]
84
+ end.to_h
13
85
  end
14
86
 
15
87
  def build_header(section)
@@ -23,6 +95,8 @@ module Expressir
23
95
  list = section[:instance_list]
24
96
  entries = list.is_a?(Array) ? list : [list]
25
97
  entries.each_with_object({}) do |entry, hash|
98
+ next unless entry.is_a?(Hash)
99
+
26
100
  if (complex = entry[:complex_entity_instance])
27
101
  hash[instance_id(complex)] = build_complex(complex)
28
102
  elsif (simple = entry[:simple_entity_instance])
@@ -72,6 +146,8 @@ module Expressir
72
146
  parameter_list_values(param[:parameter_list])
73
147
  elsif param[:entity_instance_name]
74
148
  "##{param[:entity_instance_name].to_s.delete_prefix('#')}"
149
+ elsif param[:constant_reference]
150
+ param[:constant_reference].to_s
75
151
  elsif param[:number_literal]
76
152
  text = param[:number_literal].to_s
77
153
  text.match?(/[.Ee]/) ? text.to_f : text.to_i
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # A dataset manifest (TODO.instance/03): binds Part 21 files to a
6
+ # schema and a library release, so validation is reproducible.
7
+ class DatasetManifest < Lutaml::Model::Serializable
8
+ attribute :schema, :string
9
+ attribute :release, :string
10
+ attribute :files, :string, collection: true
11
+ attribute :short_names, :hash
12
+
13
+ key_value do
14
+ map "schema", to: :schema
15
+ map "release", to: :release
16
+ map "files", to: :files
17
+ map "short_names", to: :short_names
18
+ end
19
+
20
+ class << self
21
+ def from_file(path)
22
+ manifest = from_yaml(File.read(path))
23
+ manifest.base_dir = File.dirname(File.expand_path(path))
24
+ manifest
25
+ end
26
+ end
27
+
28
+ attr_accessor :base_dir
29
+
30
+ # The validator's short-name contract is downcased keys
31
+ # (ISO 10303-21 12.3).
32
+ def short_name_map
33
+ (short_names || {}).transform_keys(&:downcase)
34
+ end
35
+
36
+ def file_paths
37
+ Array(files).map { |file| File.expand_path(file, base_dir || ".") }
38
+ end
39
+
40
+ def schema_path
41
+ File.expand_path(schema, base_dir || ".")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # Instance-level comparison of two populations (TODO.instance/03):
6
+ # the population sibling of SchemaCompare. Instances are keyed by
7
+ # their #id; a change is a keyword or parameter digest difference.
8
+ module Diff
9
+ Digest = Struct.new(:keyword, :params, keyword_init: true) do
10
+ def to_s
11
+ "#{keyword}(#{params.join(', ')})"
12
+ end
13
+ end
14
+
15
+ module_function
16
+
17
+ def diff(base, revised)
18
+ added = (revised.instances.keys - base.instances.keys).sort
19
+ removed = (base.instances.keys - revised.instances.keys).sort
20
+ changed = (base.instances.keys & revised.instances.keys)
21
+ .reject do |id|
22
+ digest(base.instances[id]).to_s == digest(revised.instances[id]).to_s
23
+ end.sort
24
+ { added: added, removed: removed, changed: changed }
25
+ end
26
+
27
+ def digest(record)
28
+ if record.is_a?(ComplexRecord)
29
+ Digest.new(keyword: "__complex__",
30
+ params: record.records.map do |part|
31
+ Digest.new(keyword: part.keyword,
32
+ params: part.params)
33
+ end)
34
+ else
35
+ Digest.new(keyword: record.keyword, params: record.params)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -23,6 +23,12 @@ module Expressir
23
23
  rule(:ws1) { (match[" \t\r\n"] | comment).repeat(1) }
24
24
  rule(:keyword) { match["!"].maybe >> match["A-Za-z0-9_"].repeat(1) }
25
25
  rule(:entity_instance_name) { str("#") >> match["0-9"].repeat(1) }
26
+ rule(:constant_reference) do
27
+ str("#") >> match["A-Za-z_"] >> match["A-Za-z0-9_"].repeat
28
+ end
29
+ rule(:external_anchor) do
30
+ str("<") >> (str(">").absent? >> any).repeat >> str(">")
31
+ end
26
32
 
27
33
  rule(:string_literal) do
28
34
  # ISO 10303-21: doubled quotes are escaped quotes
@@ -55,6 +61,8 @@ module Expressir
55
61
  typed_parameter.as(:typed_parameter) |
56
62
  list_parameter.as(:parameter_list) |
57
63
  entity_instance_name.as(:entity_instance_name) |
64
+ constant_reference.as(:constant_reference) |
65
+ external_anchor.as(:external_anchor) |
58
66
  number_literal.as(:number_literal) |
59
67
  enumeration.as(:enumeration) |
60
68
  binary_literal.as(:binary_literal) |
@@ -81,25 +89,59 @@ module Expressir
81
89
  str("=") >> ws? >> subsuper_record.as(:subsuper_record) >>
82
90
  str(";") >> ws?).as(:complex_entity_instance)
83
91
  end
84
- rule(:entity_instance) { complex_entity_instance | simple_entity_instance }
92
+ rule(:external_reference_instance) do
93
+ (entity_instance_name.as(:entity_instance_name) >> ws? >>
94
+ str("=") >> ws? >> external_anchor.as(:external_anchor) >>
95
+ str(";") >> ws?).as(:external_reference_instance)
96
+ end
97
+ rule(:entity_instance) do
98
+ complex_entity_instance | simple_entity_instance |
99
+ external_reference_instance
100
+ end
85
101
 
86
102
  rule(:header_entity) do
87
103
  simple_record.as(:simple_record) >> str(";") >> ws?
88
104
  end
105
+ # ISO 10303-21:2016 clause 14: two signature lines open the
106
+ # header of a signed exchange structure.
107
+ rule(:signature_lines) { (str("..") >> ws?).repeat }
89
108
  rule(:header_section) do
90
- str("HEADER;") >> ws? >> header_entity.repeat(1).as(:header_entity) >>
109
+ str("HEADER;") >> ws? >> signature_lines >>
110
+ header_entity.repeat(1).as(:header_entity) >>
91
111
  str("ENDSEC;") >> ws?
92
112
  end
93
113
  rule(:data_section) do
94
- str("DATA;") >> ws? >>
95
- (str("(") >> ws? >> parameter >> str(");") >> ws?).maybe >>
114
+ (str("DATA") >> ws? >>
115
+ parameter_list.as(:section_tag).maybe >> str(";") >> ws? >>
96
116
  entity_instance.repeat.as(:instance_list) >>
97
- str("ENDSEC;") >> ws?
117
+ str("ENDSEC;") >> ws?).as(:data_section)
118
+ end
119
+ # ISO 10303-21:2016 clause 9: the anchor section binds labels to
120
+ # local instances (a promissory anchor binds the unset value).
121
+ rule(:anchor_entry) do
122
+ ((str("<") >> str(">").absent? >>
123
+ match["A-Za-z0-9_./:#?=&%-"].repeat.as(:anchor_label) >>
124
+ str(">") >> ws? >> str("=") >> ws? >>
125
+ (entity_instance_name.as(:entity_instance_name) |
126
+ unset.as(:unset))) >> str(";") >> ws?).as(:anchor_entry)
127
+ end
128
+ rule(:anchor_section) do
129
+ (str("ANCHOR;") >> ws? >> anchor_entry.repeat.as(:anchor_entry) >>
130
+ str("ENDSEC;") >> ws?).as(:anchor_section)
131
+ end
132
+ # ISO 10303-21:2016 clause 10: the reference section binds a
133
+ # local instance name to an external anchor.
134
+ rule(:reference_section) do
135
+ (str("REFERENCE;") >> ws? >>
136
+ entity_instance.repeat.as(:instance_list) >>
137
+ str("ENDSEC;") >> ws?).as(:reference_section)
98
138
  end
99
139
  rule(:exchange_structure) do
100
140
  ws? >> str("ISO-10303-21;") >> ws? >>
101
141
  header_section.as(:header_section) >>
102
- data_section.as(:data_section) >>
142
+ anchor_section.maybe >>
143
+ reference_section.maybe >>
144
+ data_section.repeat(1).as(:data_section) >>
103
145
  str("END-ISO-10303-21;") >> ws?
104
146
  end
105
147
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # JSON interchange for populations (TODO.instance/03). Values are
6
+ # tagged so the round trip is value-identical: references stay
7
+ # references, enumerations and binaries stay typed, and `$`/`*`
8
+ # survive as their Part 21 meanings.
9
+ module Json
10
+ module_function
11
+
12
+ def to_value(population)
13
+ {
14
+ "header" => population.header,
15
+ "instances" => population.instances.to_h do |id, record|
16
+ [id, record_value(record)]
17
+ end,
18
+ "sections" => population.sections.map do |section|
19
+ { "name" => section.name, "contexts" => section.contexts,
20
+ "instance_ids" => section.instance_ids }
21
+ end,
22
+ "anchors" => population.anchors.to_h do |label, value|
23
+ [label, anchor_value(value)]
24
+ end,
25
+ "references" => population.references,
26
+ }
27
+ end
28
+
29
+ def population(value)
30
+ instances = value.fetch("instances").to_h do |id, record|
31
+ [id, record_from(record)]
32
+ end
33
+ sections = value.fetch("sections").map do |section|
34
+ Builder::Section.new(name: section["name"],
35
+ contexts: section["contexts"],
36
+ instance_ids: section["instance_ids"])
37
+ end
38
+ anchors = value.fetch("anchors").to_h do |label, anchor|
39
+ [label, anchor == "unset" ? :unset : anchor]
40
+ end
41
+ Population.new(
42
+ header: value.fetch("header"),
43
+ instances: instances,
44
+ sections: sections,
45
+ anchors: anchors,
46
+ references: value.fetch("references"),
47
+ )
48
+ end
49
+
50
+ def record_value(record)
51
+ if record.is_a?(ComplexRecord)
52
+ { "complex" => record.records.map { |part| simple_value(part) } }
53
+ else
54
+ simple_value(record)
55
+ end
56
+ end
57
+
58
+ def simple_value(record)
59
+ { "keyword" => record.keyword,
60
+ "params" => value_params(record.params) }
61
+ end
62
+
63
+ def value_params(params)
64
+ params.map { |value| value_value(value) }
65
+ end
66
+
67
+ def value_value(value)
68
+ case value
69
+ when Array then value.map { |item| value_value(item) }
70
+ when TypedValue
71
+ { "typed" => { "keyword" => value.keyword,
72
+ "value" => value_value(value.value) } }
73
+ when Enumeration then { "enum" => value.name }
74
+ when Binary then { "binary" => value.hex }
75
+ when :unset then { "unset" => true }
76
+ when :derived then { "derived" => true }
77
+ when String
78
+ value.match?(/\A#\d+\z/) ? { "ref" => value } : value
79
+ else value
80
+ end
81
+ end
82
+
83
+ def record_from(value)
84
+ if (complex = value["complex"])
85
+ ComplexRecord.new(complex.map { |part| simple_from(part) })
86
+ else
87
+ simple_from(value)
88
+ end
89
+ end
90
+
91
+ def simple_from(value)
92
+ Record.new(value.fetch("keyword"), params_from(value.fetch("params")))
93
+ end
94
+
95
+ def params_from(params)
96
+ params.map { |value| value_from(value) }
97
+ end
98
+
99
+ def value_from(value)
100
+ case value
101
+ when Array then value.map { |item| value_from(item) }
102
+ when Hash
103
+ if (typed = value["typed"])
104
+ TypedValue.new(typed.fetch("keyword"),
105
+ params_from(Array(typed.fetch("value"))))
106
+ elsif (enum = value["enum"])
107
+ Enumeration.new(enum)
108
+ elsif (binary = value["binary"])
109
+ Binary.new(binary)
110
+ elsif value["unset"]
111
+ :unset
112
+ elsif value["derived"]
113
+ :derived
114
+ elsif (ref = value["ref"])
115
+ ref
116
+ else
117
+ value
118
+ end
119
+ else value
120
+ end
121
+ end
122
+
123
+ def anchor_value(value)
124
+ value == :unset ? "unset" : value
125
+ end
126
+ end
127
+ end
128
+ end
@@ -12,11 +12,15 @@ module Expressir
12
12
  # Expressir::P21::ComplexRecord, :unset, :derived, or nested
13
13
  # Arrays for lists.
14
14
  class Population
15
- attr_reader :header, :instances
15
+ attr_reader :header, :instances, :sections, :anchors, :references
16
16
 
17
- def initialize(header: {}, instances: {})
17
+ def initialize(header: {}, instances: {}, sections: [], anchors: {},
18
+ references: {})
18
19
  @header = header
19
20
  @instances = instances
21
+ @sections = sections
22
+ @anchors = anchors
23
+ @references = references
20
24
  end
21
25
  end
22
26
 
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "digest"
5
+
6
+ module Expressir
7
+ module P21
8
+ # A validation receipt (TODO.instance/03): the deterministic,
9
+ # citable record binding a dataset to a schema edition and a
10
+ # validator run. Same manifest + same files → byte-identical
11
+ # receipt.
12
+ module Receipt
13
+ module_function
14
+
15
+ def build(manifest, results)
16
+ files = results.keys.sort.map do |path|
17
+ {
18
+ "path" => path,
19
+ "digest" => Digest::SHA256.hexdigest(File.read(path)),
20
+ "notes" => results[path].map do |note|
21
+ {
22
+ "id" => note.id.to_s,
23
+ "severity" => note.severity.to_s,
24
+ "instance" => note.instance,
25
+ "message" => note.message,
26
+ }
27
+ end,
28
+ }
29
+ end
30
+ report = {
31
+ "release" => manifest.release.to_s,
32
+ "schema" => manifest.schema.to_s,
33
+ "validator" => "expressir #{Expressir::VERSION}",
34
+ "files" => files,
35
+ "summary" => {
36
+ "files" => files.size,
37
+ "errors" => files.sum { |file| file["notes"].count { |n| n["severity"] == "error" } },
38
+ "warnings" => files.sum { |file| file["notes"].count { |n| n["severity"] == "warning" } },
39
+ },
40
+ }
41
+ JSON.generate(report)
42
+ end
43
+ end
44
+ end
45
+ end