expressir 2.4.37-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.
@@ -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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module P21
5
+ # ISO 10303-11 9.2.5 instance typing: which entities a Part 21
6
+ # instance conforms to. A simple record conforms to its keyword
7
+ # entity plus that entity's supertype closure; a complex record
8
+ # conforms to the union of its records' closures. ONEOF
9
+ # exclusivity (9.2.5.2) is reported per complex record.
10
+ class Typing
11
+ Result = Struct.new(:entities, :issues, keyword_init: true)
12
+
13
+ def initialize(schema, short_names: nil)
14
+ @schema = schema
15
+ @short_names = short_names&.transform_keys(&:downcase)
16
+ @results = {}.compare_by_identity
17
+ end
18
+
19
+ def of(record)
20
+ result(record).entities
21
+ end
22
+
23
+ def issues(record)
24
+ result(record).issues
25
+ end
26
+
27
+ def result(record)
28
+ @results[record] ||= compute(record)
29
+ end
30
+
31
+ private
32
+
33
+ def compute(record)
34
+ parts = record.is_a?(ComplexRecord) ? record.records : [record]
35
+ entities = []
36
+ leaves = []
37
+ issues = []
38
+ seen_leaves = {}
39
+ parts.each do |part|
40
+ entity = Validator.find_entity(@schema, part.keyword, @short_names)
41
+ if entity.nil?
42
+ issues << "keyword '#{part.keyword}' is not declared in " \
43
+ "schema '#{@schema.id}'"
44
+ next
45
+ end
46
+
47
+ key = entity.id&.safe_downcase
48
+ if seen_leaves[key]
49
+ issues << "complex instance instantiates '#{entity.id}' " \
50
+ "twice (9.2.5)"
51
+ end
52
+ seen_leaves[key] = true
53
+ leaves << entity
54
+ entities.push(entity, *entity.supertype_closure)
55
+ end
56
+ issues.concat(oneof_issues(leaves))
57
+ entities.uniq! { |entity| entity.id&.safe_downcase }
58
+ Result.new(entities: entities, issues: issues)
59
+ end
60
+
61
+ def oneof_issues(leaves)
62
+ leaves.combination(2).filter_map do |left, right|
63
+ next unless shared_oneof_group?(left, right)
64
+
65
+ "complex instance combines '#{left.id}' and '#{right.id}' " \
66
+ "in one ONEOF group (9.2.5.2)"
67
+ end
68
+ end
69
+
70
+ def shared_oneof_group?(left, right)
71
+ oneof_groups(left).any? do |group|
72
+ group.include?(left.id&.safe_downcase) &&
73
+ group.include?(right.id&.safe_downcase)
74
+ end
75
+ end
76
+
77
+ # ONEOF groups constraining an entity: its own supertype
78
+ # expression, every supertype's expression, and the ONEOF
79
+ # subtype constraints applied to any of them (9.7).
80
+ def oneof_groups(entity)
81
+ @oneof_groups ||= {}
82
+ key = entity.id&.safe_downcase
83
+ @oneof_groups[key] ||= begin
84
+ groups = []
85
+ [entity, *entity.supertype_closure].each do |supertype|
86
+ expression = supertype.supertype_expression
87
+ groups << oneof_members(expression) if oneof?(expression)
88
+ subtype_constraint_groups(supertype, groups)
89
+ end
90
+ groups
91
+ end
92
+ end
93
+
94
+ def subtype_constraint_groups(supertype, groups)
95
+ Array(@schema.subtype_constraints).each do |constraint|
96
+ next unless constraint.applies_to&.id&.safe_downcase == supertype.id&.safe_downcase
97
+
98
+ expression = constraint.supertype_expression
99
+ groups << oneof_members(expression) if oneof?(expression)
100
+ end
101
+ end
102
+
103
+ def oneof?(expression)
104
+ expression.is_a?(Model::SupertypeExpressions::OneofSupertypeExpression)
105
+ end
106
+
107
+ def oneof_members(expression)
108
+ Array(expression.operands).filter_map do |operand|
109
+ next unless operand.respond_to?(:id) && operand.id
110
+
111
+ operand.id.safe_downcase
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end