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.
- checksums.yaml +4 -4
- data/TODO.instance/00-overview.md +73 -0
- data/TODO.instance/01-instance-typing.md +81 -0
- data/TODO.instance/02-population-rules.md +82 -0
- data/TODO.instance/03-p21-ladder-and-population-management.md +97 -0
- data/TODO.instance/04-arm-mim-instance-mapping.md +83 -0
- data/lib/expressir/cli.rb +3 -0
- data/lib/expressir/commands/population.rb +63 -0
- data/lib/expressir/commands.rb +1 -0
- data/lib/expressir/eval/population.rb +186 -0
- data/lib/expressir/eval/rules.rb +0 -1
- data/lib/expressir/eval.rb +1 -0
- data/lib/expressir/mapping/instance_verification.rb +405 -0
- data/lib/expressir/mapping.rb +1 -0
- data/lib/expressir/p21/builder.rb +78 -2
- data/lib/expressir/p21/dataset_manifest.rb +45 -0
- data/lib/expressir/p21/diff.rb +40 -0
- data/lib/expressir/p21/grammar.rb +48 -6
- data/lib/expressir/p21/json.rb +128 -0
- data/lib/expressir/p21/population.rb +6 -2
- data/lib/expressir/p21/receipt.rb +45 -0
- data/lib/expressir/p21/typing.rb +116 -0
- data/lib/expressir/p21/validator.rb +419 -8
- data/lib/expressir/p21/writer.rb +107 -0
- data/lib/expressir/p21.rb +6 -0
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +1 -0
- data/lib/tasks/instance.rake +77 -0
- metadata +16 -1
|
@@ -8,8 +8,80 @@ module Expressir
|
|
|
8
8
|
|
|
9
9
|
def build(tree)
|
|
10
10
|
header = build_header(tree[:header_section])
|
|
11
|
-
instances =
|
|
12
|
-
|
|
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(:
|
|
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? >>
|
|
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
|
|
95
|
-
(
|
|
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
|
-
|
|
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
|