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.
- 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/refpath.rb +65 -4
- 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
|
@@ -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:[^,>]+(?:,[^>]+)
|
|
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.
|
data/lib/expressir/mapping.rb
CHANGED
|
@@ -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
|