expressir 2.4.31-arm-linux → 2.4.36-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/.github/workflows/conformance.yml +34 -0
- data/.gitignore +3 -0
- data/Gemfile +1 -1
- data/TODO.eengine/00-overview.md +39 -0
- data/TODO.eengine/01-resolution-diagnostics.md +30 -0
- data/TODO.eengine/02-check-catalog-completion.md +53 -0
- data/TODO.eengine/03-entity-computations.md +22 -0
- data/TODO.eengine/04-shtolo-rule-audit.md +32 -0
- data/TODO.eengine/05-shtolo-longform-flatten.md +12 -0
- data/TODO.eengine/06-population-p21.md +34 -0
- data/TODO.eengine/07-schema-compare-patch.md +25 -0
- data/TODO.eengine/08-official-corpus-gate.md +7 -0
- data/TODO.eengine/09-mim-mapping-parser.md +9 -0
- data/TODO.parity-ee/00-overview.md +1 -1
- data/TODO.sota/00-overview.md +57 -0
- data/TODO.sota/01-type-checking.md +115 -0
- data/TODO.sota/02-evaluator.md +86 -0
- data/TODO.sota/03-conformance.md +53 -0
- data/TODO.sota/04-lsp.md +70 -0
- data/TODO.sota/05-interop.md +55 -0
- data/TODO.sota/06-dialects.md +57 -0
- data/TODO.sota/07-reference-artifacts.md +55 -0
- data/TODO.suma-improvements/07-part28-xml.md +10 -13
- data/docs/_guides/interop/index.adoc +25 -0
- data/docs/_guides/interop/json-schema.adoc +88 -0
- data/docs/_guides/interop/owl-rdf.adoc +87 -0
- data/docs/_guides/reference/checker-notes.adoc +55 -0
- data/docs/_guides/reference/checker.adoc +36 -0
- data/docs/_guides/reference/evaluator.adoc +40 -0
- data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
- data/docs/_guides/reference/index.adoc +22 -0
- data/docs/_guides/reference/lsp.adoc +41 -0
- data/docs/_guides/reference/p21.adoc +30 -0
- data/docs/_guides/reference/shtolo.adoc +36 -0
- data/exe/expressir-lsp +6 -0
- data/expressir.gemspec +2 -2
- data/lib/expressir/3.2/expressir_core.so +0 -0
- data/lib/expressir/3.3/expressir_core.so +0 -0
- data/lib/expressir/3.4/expressir_core.so +0 -0
- data/lib/expressir/4.0/expressir_core.so +0 -0
- data/lib/expressir/cli.rb +25 -0
- data/lib/expressir/commands/check.rb +3 -1
- data/lib/expressir/commands/export_json_schema.rb +29 -0
- data/lib/expressir/commands/export_owl.rb +26 -0
- data/lib/expressir/commands/smrl_xml_index.rb +25 -0
- data/lib/expressir/commands/validate.rb +3 -0
- data/lib/expressir/commands.rb +3 -0
- data/lib/expressir/conformance/trend.rb +100 -0
- data/lib/expressir/conformance.rb +8 -0
- data/lib/expressir/eval/environment.rb +83 -0
- data/lib/expressir/eval/expression.rb +610 -0
- data/lib/expressir/eval/interpreter.rb +316 -0
- data/lib/expressir/eval/rules.rb +88 -0
- data/lib/expressir/eval/values.rb +74 -0
- data/lib/expressir/eval.rb +17 -0
- data/lib/expressir/express/builder.rb +17 -5
- data/lib/expressir/express/builders/entity_decl_builder.rb +17 -5
- data/lib/expressir/express/builders/expression_builder.rb +9 -1
- data/lib/expressir/express/checker.rb +970 -5
- data/lib/expressir/express/dialect.rb +44 -0
- data/lib/expressir/express/formatters/declarations_formatter.rb +21 -14
- data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
- data/lib/expressir/express/parser.rb +9 -4
- data/lib/expressir/express/pretty_formatter.rb +9 -14
- data/lib/expressir/express/schema_compare.rb +194 -0
- data/lib/expressir/express/shtolo.rb +194 -63
- data/lib/expressir/express.rb +3 -0
- data/lib/expressir/interop/json_schema.rb +195 -0
- data/lib/expressir/interop/owl.rb +216 -0
- data/lib/expressir/interop.rb +13 -0
- data/lib/expressir/lsp/document.rb +226 -0
- data/lib/expressir/lsp/server.rb +184 -0
- data/lib/expressir/lsp/workspace.rb +402 -0
- data/lib/expressir/lsp.rb +11 -0
- data/lib/expressir/model/declarations/entity.rb +132 -0
- data/lib/expressir/model/declarations/schema.rb +37 -6
- data/lib/expressir/p21/builder.rb +92 -0
- data/lib/expressir/p21/grammar.rb +107 -0
- data/lib/expressir/p21/parser.rb +16 -0
- data/lib/expressir/p21/population.rb +35 -0
- data/lib/expressir/p21/validator.rb +213 -0
- data/lib/expressir/p21.rb +17 -0
- data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
- data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
- data/lib/expressir/reference/note_catalog.rb +54 -0
- data/lib/expressir/reference.rb +10 -0
- data/lib/expressir/version.rb +1 -1
- data/lib/expressir.rb +5 -0
- data/lib/tasks/conformance.rake +27 -0
- data/lib/tasks/reference.rake +25 -0
- metadata +68 -4
|
@@ -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
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module P21
|
|
5
|
+
# A parsed ISO 10303-21 exchange structure (TODO.eengine/06).
|
|
6
|
+
# `header` — Hash: keyword => parameters (Array of values).
|
|
7
|
+
# `instances`— Hash: "#id" => Record.
|
|
8
|
+
#
|
|
9
|
+
# Values: String (string literal), Integer (reference id, keyed as
|
|
10
|
+
# "#id" in instances), Expressir::P21::Enumeration,
|
|
11
|
+
# Expressir::P21::Binary, Expressir::P21::TypedValue,
|
|
12
|
+
# Expressir::P21::ComplexRecord, :unset, :derived, or nested
|
|
13
|
+
# Arrays for lists.
|
|
14
|
+
class Population
|
|
15
|
+
attr_reader :header, :instances
|
|
16
|
+
|
|
17
|
+
def initialize(header: {}, instances: {})
|
|
18
|
+
@header = header
|
|
19
|
+
@instances = instances
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# A data-section instance: `#12 = KEYWORD(param, ...) ;`
|
|
24
|
+
Record = Struct.new(:keyword, :params)
|
|
25
|
+
|
|
26
|
+
# `#21 = (A(...) B(...))` — complex entity instance.
|
|
27
|
+
ComplexRecord = Struct.new(:records)
|
|
28
|
+
|
|
29
|
+
TypedValue = Struct.new(:keyword, :value)
|
|
30
|
+
|
|
31
|
+
Enumeration = Struct.new(:name)
|
|
32
|
+
|
|
33
|
+
Binary = Struct.new(:hex)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module P21
|
|
5
|
+
# Validates a {Population} against a loaded schema
|
|
6
|
+
# (TODO.eengine/06 step 2). Single-schema scope: instance keywords
|
|
7
|
+
# must be declared in the given schema; USE/REFERENCE visibility is
|
|
8
|
+
# a later ladder step.
|
|
9
|
+
#
|
|
10
|
+
# Mapping follows ISO 10303-21 clause 12: derived attributes are
|
|
11
|
+
# not mapped (a supertype attribute redeclared as DERIVED keeps its
|
|
12
|
+
# slot, valued `*`), inverse attributes are not mapped, and the
|
|
13
|
+
# value order is supertype attributes first.
|
|
14
|
+
module Validator
|
|
15
|
+
Note = Struct.new(:id, :severity, :instance, :message, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# `short_names` maps exchange-structure short names (ISO 10303-21,
|
|
20
|
+
# clause 12.3) to schema entity names, e.g. {"CPT" => "cartesian_point"}.
|
|
21
|
+
def validate(population, schema, short_names: nil)
|
|
22
|
+
short_names = short_names&.transform_keys(&:downcase)
|
|
23
|
+
notes = []
|
|
24
|
+
check_file_schema(notes, population, schema)
|
|
25
|
+
population.instances.each do |id, instance|
|
|
26
|
+
if instance.is_a?(ComplexRecord)
|
|
27
|
+
check_complex(notes, schema, population, id, instance, short_names)
|
|
28
|
+
else
|
|
29
|
+
check_simple(notes, schema, population, id, instance, short_names)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
notes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def errors(notes)
|
|
36
|
+
notes.select { |note| note.severity == :error }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def warnings(notes)
|
|
40
|
+
notes.select { |note| note.severity == :warning }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def valid?(notes)
|
|
44
|
+
errors(notes).empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def note!(notes, id, severity, instance, message)
|
|
48
|
+
notes << Note.new(id: id, severity: severity, instance: instance,
|
|
49
|
+
message: message)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_file_schema(notes, population, schema)
|
|
53
|
+
declared = Array(population.header["FILE_SCHEMA"]).flatten.first
|
|
54
|
+
return unless declared && schema.id
|
|
55
|
+
return if declared.downcase == schema.id.downcase
|
|
56
|
+
|
|
57
|
+
note!(notes, :p21_schema_mismatch, :warning, nil,
|
|
58
|
+
"FILE_SCHEMA '#{declared}' does not match schema '#{schema.id}'")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check_simple(notes, schema, population, id, record, short_names)
|
|
62
|
+
entity = find_entity(schema, record.keyword, short_names)
|
|
63
|
+
unless entity
|
|
64
|
+
note!(notes, :p21_unknown_entity, :error, id,
|
|
65
|
+
"#{record.keyword} is not declared in schema '#{schema.id}'")
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
check_params(notes, schema, population, id, record.params,
|
|
70
|
+
mapped_slots(entity))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def check_complex(notes, schema, population, id, complex, short_names)
|
|
74
|
+
pairs = complex.records.filter_map do |record|
|
|
75
|
+
entity = find_entity(schema, record.keyword, short_names)
|
|
76
|
+
unless entity
|
|
77
|
+
note!(notes, :p21_unknown_entity, :error, id,
|
|
78
|
+
"#{record.keyword} is not declared in schema '#{schema.id}'")
|
|
79
|
+
next nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
[record, entity]
|
|
83
|
+
end
|
|
84
|
+
return if pairs.empty?
|
|
85
|
+
|
|
86
|
+
check_clique(notes, id, pairs.map { |_, entity| entity })
|
|
87
|
+
pairs.each do |record, entity|
|
|
88
|
+
slots = explicit_attributes(entity).map { |attr| [attr, false] }
|
|
89
|
+
check_params(notes, schema, population, id, record.params, slots)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A complex instance's records must form a valid supertype graph
|
|
94
|
+
# path: they share at least one common supertype.
|
|
95
|
+
def check_clique(notes, id, entities)
|
|
96
|
+
common = closure_keys(entities.first)
|
|
97
|
+
entities.drop(1).each { |entity| common &= closure_keys(entity) }
|
|
98
|
+
return unless common.empty?
|
|
99
|
+
|
|
100
|
+
note!(notes, :p21_complex_clique, :error, id,
|
|
101
|
+
"#{id}: complex records share no common supertype")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def closure_keys(entity)
|
|
105
|
+
keys = entity.supertype_closure.filter_map { |e| e.id&.downcase }
|
|
106
|
+
keys << entity.id&.downcase
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Part 21 value order: supertype attributes first; an attribute
|
|
110
|
+
# redeclared as DERIVED in the entity keeps its inherited slot,
|
|
111
|
+
# valued `*` (12.2.6).
|
|
112
|
+
def mapped_slots(entity)
|
|
113
|
+
own_derived_keys = own_derived_attributes(entity)
|
|
114
|
+
.filter_map { |attr| attr.id&.downcase }
|
|
115
|
+
inherited = entity.supertype_closure.flat_map do |supertype|
|
|
116
|
+
explicit_attributes(supertype)
|
|
117
|
+
end
|
|
118
|
+
inherited.map do |attr|
|
|
119
|
+
[attr, own_derived_keys.include?(attr.id&.downcase)]
|
|
120
|
+
end + explicit_attributes(entity).map { |attr| [attr, false] }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def check_params(notes, schema, population, id, params, slots)
|
|
124
|
+
if params.size != slots.size
|
|
125
|
+
note!(notes, :p21_arity_mismatch, :error, id,
|
|
126
|
+
"#{id}: #{params.size} values for #{slots.size} mapped attributes")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
slots.zip(params).each do |(attr, derived_slot), value|
|
|
130
|
+
next if value.nil?
|
|
131
|
+
|
|
132
|
+
if derived_slot
|
|
133
|
+
next if value == :derived
|
|
134
|
+
|
|
135
|
+
note!(notes, :p21_derived_slot, :error, id,
|
|
136
|
+
"#{id}: attribute '#{attr.id}' redeclared DERIVED must be '*'")
|
|
137
|
+
next
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
check_value(notes, schema, population, id, attr, value)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def check_value(notes, schema, population, id, attr, value)
|
|
145
|
+
case value
|
|
146
|
+
when String
|
|
147
|
+
return unless value.match?(/\A#\d+\z/)
|
|
148
|
+
|
|
149
|
+
return if population.instances.key?(value)
|
|
150
|
+
|
|
151
|
+
note!(notes, :p21_unknown_reference, :error, id,
|
|
152
|
+
"#{id}: reference #{value} has no instance")
|
|
153
|
+
when :derived
|
|
154
|
+
note!(notes, :p21_derived_value, :error, id,
|
|
155
|
+
"#{id}: '*' outside a derived slot")
|
|
156
|
+
when TypedValue
|
|
157
|
+
return if find_entity(schema, value.keyword) || find_type(schema, value.keyword)
|
|
158
|
+
|
|
159
|
+
note!(notes, :p21_unknown_typed_keyword, :error, id,
|
|
160
|
+
"#{id}: typed parameter '#{value.keyword}' does not resolve")
|
|
161
|
+
when Enumeration
|
|
162
|
+
items = enumeration_items(schema, attr)
|
|
163
|
+
return unless items
|
|
164
|
+
|
|
165
|
+
names = items.filter_map { |item| item.id&.downcase }
|
|
166
|
+
return if names.include?(value.name.downcase)
|
|
167
|
+
|
|
168
|
+
note!(notes, :p21_enum_value, :error, id,
|
|
169
|
+
"#{id}: .#{value.name}. is not in attribute '#{attr.id}' enumeration")
|
|
170
|
+
when Array
|
|
171
|
+
value.each { |item| check_value(notes, schema, population, id, attr, item) }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def explicit_attributes(entity)
|
|
176
|
+
Array(entity.attributes)
|
|
177
|
+
.select { |attr| attr.instance_of?(Model::Declarations::Attribute) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def own_derived_attributes(entity)
|
|
181
|
+
Array(entity.attributes)
|
|
182
|
+
.select { |attr| attr.instance_of?(Model::Declarations::DerivedAttribute) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def find_entity(schema, keyword, short_names = nil)
|
|
186
|
+
key = keyword.downcase
|
|
187
|
+
Array(schema.entities).find { |entity| entity.id&.downcase == key } ||
|
|
188
|
+
resolve_short_name(schema, key, short_names)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def resolve_short_name(schema, key, short_names)
|
|
192
|
+
return nil unless short_names.is_a?(Hash) && short_names[key]
|
|
193
|
+
|
|
194
|
+
find_entity(schema, short_names[key], nil)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def find_type(schema, keyword)
|
|
198
|
+
Array(schema.types).find { |type| type.id&.downcase == keyword.downcase }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def enumeration_items(schema, attr)
|
|
202
|
+
type = attr&.type
|
|
203
|
+
return Array(type.items) if type.is_a?(Model::DataTypes::Enumeration)
|
|
204
|
+
|
|
205
|
+
return nil unless type.is_a?(Model::References::SimpleReference)
|
|
206
|
+
|
|
207
|
+
declaration = find_type(schema, type.id.to_s)
|
|
208
|
+
underlying = declaration&.underlying_type
|
|
209
|
+
underlying.is_a?(Model::DataTypes::Enumeration) ? Array(underlying.items) : nil
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
# ISO 10303-21 exchange-structure support (TODO.eengine/06).
|
|
5
|
+
module P21
|
|
6
|
+
autoload :Population, "#{__dir__}/p21/population"
|
|
7
|
+
autoload :Record, "#{__dir__}/p21/population"
|
|
8
|
+
autoload :ComplexRecord, "#{__dir__}/p21/population"
|
|
9
|
+
autoload :TypedValue, "#{__dir__}/p21/population"
|
|
10
|
+
autoload :Enumeration, "#{__dir__}/p21/population"
|
|
11
|
+
autoload :Binary, "#{__dir__}/p21/population"
|
|
12
|
+
autoload :Grammar, "#{__dir__}/p21/grammar"
|
|
13
|
+
autoload :Builder, "#{__dir__}/p21/builder"
|
|
14
|
+
autoload :Validator, "#{__dir__}/p21/validator"
|
|
15
|
+
autoload :Parser, "#{__dir__}/p21/parser"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Expressir
|
|
6
|
+
module Reference
|
|
7
|
+
# Rule-by-rule cross-walk between the parsanol grammar and the
|
|
8
|
+
# ISO 10303-11 Annex A/B productions. The mapping data is generated
|
|
9
|
+
# by `rake reference:crosswalk`; the spec fails when a grammar rule
|
|
10
|
+
# has no row or the rendered guide is stale.
|
|
11
|
+
class GrammarCrosswalk
|
|
12
|
+
DATA_PATH = File.expand_path("grammar_crosswalk.yml", __dir__)
|
|
13
|
+
GRAMMAR_RULE = /rule\(:(\w+)/
|
|
14
|
+
PRODUCTION_STATUSES = %w[aligned decomposed].freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def data
|
|
18
|
+
@data ||= YAML.load_file(DATA_PATH)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def grammar_path
|
|
22
|
+
Expressir.root_path.join(data.dig("source", "grammar"))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def grammar_rules
|
|
26
|
+
File.read(grammar_path).scan(GRAMMAR_RULE).flatten.uniq.sort
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rows
|
|
30
|
+
data["rules"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def annex_without_rule
|
|
34
|
+
data["annex_without_rule"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def statuses
|
|
38
|
+
data["statuses"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the list of inconsistencies; empty means in sync.
|
|
42
|
+
def validate
|
|
43
|
+
errors = []
|
|
44
|
+
declared = rows.map { |row| row["rule"] }
|
|
45
|
+
missing = grammar_rules - declared
|
|
46
|
+
unknown = declared - grammar_rules
|
|
47
|
+
duplicates = declared.tally.select { |_, count| count > 1 }.keys
|
|
48
|
+
errors << "grammar rules without a cross-walk row: #{missing.join(', ')}" if missing.any?
|
|
49
|
+
errors << "cross-walk rows without a grammar rule: #{unknown.join(', ')}" if unknown.any?
|
|
50
|
+
errors << "duplicate cross-walk rows: #{duplicates.join(', ')}" if duplicates.any?
|
|
51
|
+
|
|
52
|
+
rows.each do |row|
|
|
53
|
+
status = row["status"]
|
|
54
|
+
unless statuses.key?(status)
|
|
55
|
+
errors << "#{row['rule']}: unknown status '#{status}'"
|
|
56
|
+
end
|
|
57
|
+
if PRODUCTION_STATUSES.include?(status) && !row["production"]
|
|
58
|
+
errors << "#{row['rule']}: #{status} row needs a production"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
errors
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render_adoc
|
|
65
|
+
lines = [
|
|
66
|
+
"= Grammar cross-walk",
|
|
67
|
+
"",
|
|
68
|
+
"// Generated by `rake reference:crosswalk` from",
|
|
69
|
+
"// #{data.dig('source', 'grammar')} and the Annex A snapshot.",
|
|
70
|
+
"// Do not edit by hand; the reference spec fails when this file is stale.",
|
|
71
|
+
"",
|
|
72
|
+
"== Purpose",
|
|
73
|
+
"",
|
|
74
|
+
"Every parsanol grammar rule is mapped to the ISO 10303-11:2004",
|
|
75
|
+
"Annex A production it implements. Divergences from the standard",
|
|
76
|
+
"grammar are classified with one of these statuses:",
|
|
77
|
+
"",
|
|
78
|
+
*statuses.map { |name, description| "* `#{name}` — #{description}" },
|
|
79
|
+
"",
|
|
80
|
+
"== Rules",
|
|
81
|
+
"",
|
|
82
|
+
'[cols="2,3,1,4", options="header"]',
|
|
83
|
+
"|===",
|
|
84
|
+
"| Parsanol rule | Annex A production | Status | Note",
|
|
85
|
+
"",
|
|
86
|
+
*rows.map { |row| rule_row(row) },
|
|
87
|
+
"|===",
|
|
88
|
+
"",
|
|
89
|
+
"== Annex A productions without a dedicated rule",
|
|
90
|
+
"",
|
|
91
|
+
"These productions are handled by supporting code rather than a",
|
|
92
|
+
"grammar rule:",
|
|
93
|
+
"",
|
|
94
|
+
'[cols="3,1,5", options="header"]',
|
|
95
|
+
"|===",
|
|
96
|
+
"| Annex A production | Status | Handled by",
|
|
97
|
+
"",
|
|
98
|
+
*annex_without_rule.map { |row| annex_row(row) },
|
|
99
|
+
"|===",
|
|
100
|
+
"",
|
|
101
|
+
]
|
|
102
|
+
lines.join("\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def rule_row(row)
|
|
108
|
+
note = row["literal"] ? "literal `#{row['literal']}`" : row["note"]
|
|
109
|
+
"| `#{row['rule']}` | #{cell(row['production'])} | #{row['status']} | #{cell(note)}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def annex_row(row)
|
|
113
|
+
"| `#{row['production']}` | #{row['status']} | #{cell(row['note'])}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def cell(text)
|
|
117
|
+
text.to_s.gsub("|", "\\|")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|