expressir 2.4.35 → 2.4.37
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.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/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/ext/expressir_core/Cargo.toml +1 -1
- data/lib/expressir/cli.rb +18 -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/validate.rb +3 -0
- data/lib/expressir/commands.rb +2 -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/builders/expression_builder.rb +9 -1
- data/lib/expressir/express/checker.rb +779 -2
- data/lib/expressir/express/dialect.rb +44 -0
- data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
- data/lib/expressir/express/parser.rb +9 -4
- data/lib/expressir/express.rb +1 -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/mapping/refpath.rb +65 -4
- 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 +4 -0
- data/lib/tasks/conformance.rake +27 -0
- data/lib/tasks/reference.rake +25 -0
- metadata +50 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Part 21
|
|
3
|
+
parent: Reference
|
|
4
|
+
grand_parent: Guides
|
|
5
|
+
nav_order: 4
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
= Part 21
|
|
9
|
+
|
|
10
|
+
== Purpose
|
|
11
|
+
|
|
12
|
+
`Expressir::P21` reads ISO 10303-21 exchange structures: header
|
|
13
|
+
entities, simple and complex instances, nested lists, references,
|
|
14
|
+
typed parameters, enumerations and binaries.
|
|
15
|
+
|
|
16
|
+
== Validation
|
|
17
|
+
|
|
18
|
+
`Expressir::P21::Validator.validate(population, schema, short_names:)`
|
|
19
|
+
maps Part 21 records to EXPRESS attributes in clause 12 order —
|
|
20
|
+
supertype attributes first, derived slots valued `*`, inverse
|
|
21
|
+
attributes unmapped — and reports arity, unknown-entity,
|
|
22
|
+
unknown-reference, enum-value and derived-slot notes. Short names from
|
|
23
|
+
clause 12.3 are passed as a `{"CPT" => "cartesian_point"}` map.
|
|
24
|
+
|
|
25
|
+
== Conformance corpus
|
|
26
|
+
|
|
27
|
+
The annex examples from the ISO 10303-21 detachment live under
|
|
28
|
+
`spec/fixtures/p21/conformance/`; the gate spec parses each positive
|
|
29
|
+
fixture with zero notes and asserts the intended loud failure for
|
|
30
|
+
ladder-excluded forms.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SHTOLO and Annex G
|
|
3
|
+
parent: Reference
|
|
4
|
+
grand_parent: Guides
|
|
5
|
+
nav_order: 5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
= SHTOLO and Annex G
|
|
9
|
+
|
|
10
|
+
== Purpose
|
|
11
|
+
|
|
12
|
+
`Expressir::Express::Shtolo` converts a multi-schema shortform EXPRESS
|
|
13
|
+
model into an ISO 10303-11:1994 longform. It implements the two stages
|
|
14
|
+
of Annex G: multi-schema to an intermediate single artifact schema
|
|
15
|
+
(stage 1), then artifact to 1994-only constructs (stage 2).
|
|
16
|
+
|
|
17
|
+
== Rule coverage
|
|
18
|
+
|
|
19
|
+
Every Annex G rule has a live worked example in
|
|
20
|
+
`spec/expressir/express/shtolo_annex_g_rule_audit_spec.rb`. The
|
|
21
|
+
extracted rule list, the fixture that exercises each rule and the
|
|
22
|
+
eengine coverage verdict are in
|
|
23
|
+
`spec/fixtures/shtolo/annex-g-rules.md`.
|
|
24
|
+
|
|
25
|
+
Key rules: name munging (G.NM.1), USE wins over REFERENCE (G.1.2),
|
|
26
|
+
original-name copying for aliases (G.1.3), dependently-instantiable
|
|
27
|
+
rules (G.1.6), implicit interface closure (G.1.7), pruning (G.1.9),
|
|
28
|
+
extensible enumeration/select folding (G.2.3/G.2.4) and subtype
|
|
29
|
+
constraint removal (G.2.5). Where eeng and the annex disagree, the
|
|
30
|
+
annex wins; eeng output is a comparison input, not the contract.
|
|
31
|
+
|
|
32
|
+
== Structural safety
|
|
33
|
+
|
|
34
|
+
`Expressir::Express::SchemaCompare`/`SchemaPatch` provide the
|
|
35
|
+
declaration-level diff used to verify that a generated longform has
|
|
36
|
+
exactly the expected structural deltas.
|
data/exe/expressir-lsp
ADDED
data/expressir.gemspec
CHANGED
|
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
|
|
|
15
15
|
spec.license = "BSD-2-Clause"
|
|
16
16
|
|
|
17
17
|
spec.bindir = "exe"
|
|
18
|
-
spec.executables = %w[expressir]
|
|
18
|
+
spec.executables = %w[expressir expressir-lsp]
|
|
19
19
|
spec.require_paths = ["lib"]
|
|
20
20
|
|
|
21
21
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
@@ -47,7 +47,7 @@ Gem::Specification.new do |spec|
|
|
|
47
47
|
spec.add_dependency "benchmark-ips"
|
|
48
48
|
spec.add_dependency "csv"
|
|
49
49
|
spec.add_dependency "liquid", "~> 5"
|
|
50
|
-
spec.add_dependency "lutaml-model", "~> 0.8.
|
|
50
|
+
spec.add_dependency "lutaml-model", "~> 0.8.114"
|
|
51
51
|
spec.add_dependency "moxml"
|
|
52
52
|
spec.add_dependency "nokogiri", "~> 1.19"
|
|
53
53
|
spec.add_dependency "paint"
|
|
@@ -13,7 +13,7 @@ magnus = { version = "0.9" }
|
|
|
13
13
|
# Pinned core release (M1, TODO.parity-ee/19): the ext never floats
|
|
14
14
|
# main — bump the tag per expressir release, alongside the validation
|
|
15
15
|
# run that pins it.
|
|
16
|
-
expressir-rs = { git = "https://github.com/lutaml/expressir-rs", tag = "v0.1.
|
|
16
|
+
expressir-rs = { git = "https://github.com/lutaml/expressir-rs", tag = "v0.1.7" }
|
|
17
17
|
serde_json = "1.0"
|
|
18
18
|
|
|
19
19
|
|
data/lib/expressir/cli.rb
CHANGED
|
@@ -111,6 +111,24 @@ module Expressir
|
|
|
111
111
|
Expressir::Commands::Xsd.new(options).run(path)
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
desc "export-json-schema PATH",
|
|
115
|
+
"Write a JSON Schema rendering of the first EXPRESS schema at PATH"
|
|
116
|
+
method_option :output, type: :string, aliases: "-o",
|
|
117
|
+
desc: "Output file path (defaults to stdout)"
|
|
118
|
+
def export_json_schema(path)
|
|
119
|
+
Expressir::Commands::ExportJsonSchema.new(options).run(path)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
desc "export-owl PATH",
|
|
123
|
+
"Write an OWL/Turtle rendering of the first EXPRESS schema at PATH"
|
|
124
|
+
method_option :output, type: :string, aliases: "-o",
|
|
125
|
+
desc: "Output file path (defaults to stdout)"
|
|
126
|
+
method_option :base_iri, type: :string,
|
|
127
|
+
desc: "Ontology IRI base (default: expressir.org/owl/<schema>#)"
|
|
128
|
+
def export_owl(path)
|
|
129
|
+
Expressir::Commands::ExportOwl.new(options).run(path)
|
|
130
|
+
end
|
|
131
|
+
|
|
114
132
|
desc "xml-index DIR", "Write the SMRL index XML for every EXPRESS schema under DIR"
|
|
115
133
|
method_option :output, type: :string, aliases: "-o",
|
|
116
134
|
desc: "Output file path (defaults to stdout)"
|
|
@@ -25,7 +25,9 @@ module Expressir
|
|
|
25
25
|
raise Thor::Error, "#{failed} file(s) failed to parse"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
result = Expressir::Express::Checker.new(
|
|
28
|
+
result = Expressir::Express::Checker.new(
|
|
29
|
+
repository, dialect: options[:dialect]
|
|
30
|
+
).check
|
|
29
31
|
|
|
30
32
|
if options[:json]
|
|
31
33
|
say JSON.generate(
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Commands
|
|
5
|
+
# `expressir export-json-schema`: write the JSON Schema rendering of
|
|
6
|
+
# the first schema in PATH.
|
|
7
|
+
class ExportJsonSchema < Base
|
|
8
|
+
def run(path)
|
|
9
|
+
repository = Expressir::Express::Parser.from_file(path)
|
|
10
|
+
schema = repository.schemas.first
|
|
11
|
+
raise Thor::Error, "no schema in #{path}" unless schema
|
|
12
|
+
|
|
13
|
+
document = Expressir::Interop::JsonSchema.new(schema).to_json
|
|
14
|
+
write(document)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def write(document)
|
|
20
|
+
if options[:output]
|
|
21
|
+
File.write(options[:output], document)
|
|
22
|
+
say "written to #{options[:output]}"
|
|
23
|
+
else
|
|
24
|
+
$stdout.write(document)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Commands
|
|
5
|
+
# `expressir export-owl`: write the OWL/Turtle rendering of the
|
|
6
|
+
# first schema in PATH.
|
|
7
|
+
class ExportOwl < Base
|
|
8
|
+
def run(path)
|
|
9
|
+
repository = Expressir::Express::Parser.from_file(path)
|
|
10
|
+
schema = repository.schemas.first
|
|
11
|
+
raise Thor::Error, "no schema in #{path}" unless schema
|
|
12
|
+
|
|
13
|
+
document = Expressir::Interop::Owl.new(
|
|
14
|
+
schema,
|
|
15
|
+
base_iri: options[:base_iri],
|
|
16
|
+
).export
|
|
17
|
+
if options[:output]
|
|
18
|
+
File.write(options[:output], document)
|
|
19
|
+
say "written to #{options[:output]}"
|
|
20
|
+
else
|
|
21
|
+
$stdout.write(document)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -98,6 +98,9 @@ module Expressir
|
|
|
98
98
|
method_option :stepmod, type: :string,
|
|
99
99
|
desc: "STEPmod checkout root — explicit opt-in to directory-" \
|
|
100
100
|
"convention resolution as the fallback"
|
|
101
|
+
method_option :dialect, type: :string,
|
|
102
|
+
desc: "ISO 10303-11 edition for dialect diagnostics: " \
|
|
103
|
+
"'1994' or '2004' (default: no edition notes)"
|
|
101
104
|
def check(*paths)
|
|
102
105
|
Check.new(options).run(*paths)
|
|
103
106
|
end
|
data/lib/expressir/commands.rb
CHANGED
|
@@ -26,6 +26,8 @@ module Expressir
|
|
|
26
26
|
autoload :Manifest, "#{__dir__}/commands/manifest"
|
|
27
27
|
autoload :MappingValidate, "#{__dir__}/commands/mapping_validate"
|
|
28
28
|
autoload :Xsd, "#{__dir__}/commands/xsd"
|
|
29
|
+
autoload :ExportJsonSchema, "#{__dir__}/commands/export_json_schema"
|
|
30
|
+
autoload :ExportOwl, "#{__dir__}/commands/export_owl"
|
|
29
31
|
autoload :SmrlXmlIndex, "#{__dir__}/commands/smrl_xml_index"
|
|
30
32
|
end
|
|
31
33
|
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Expressir
|
|
6
|
+
module Conformance
|
|
7
|
+
# Trended property metrics over a corpus slice: parse rate and
|
|
8
|
+
# format idempotence rate are fractions; the diagnostic rate is the
|
|
9
|
+
# number of Checker notes per parsed file. Per-note-id counts make
|
|
10
|
+
# the report actionable. Published by the nightly conformance
|
|
11
|
+
# workflow as a JSON artifact so the numbers can be trended.
|
|
12
|
+
class Trend
|
|
13
|
+
def self.measure(files)
|
|
14
|
+
report = {
|
|
15
|
+
"files" => files.size,
|
|
16
|
+
"parsed" => 0,
|
|
17
|
+
"parse_failed" => 0,
|
|
18
|
+
"measurement_failed" => 0,
|
|
19
|
+
"idempotent" => 0,
|
|
20
|
+
"not_idempotent" => 0,
|
|
21
|
+
"notes" => 0,
|
|
22
|
+
"errors" => 0,
|
|
23
|
+
"warnings" => 0,
|
|
24
|
+
"note_ids" => {},
|
|
25
|
+
}
|
|
26
|
+
files.each do |path|
|
|
27
|
+
measure_file(path, report)
|
|
28
|
+
end
|
|
29
|
+
report.merge!(
|
|
30
|
+
"parse_rate" => rate(report["parsed"], report["files"]),
|
|
31
|
+
"idempotence_rate" => rate(report["idempotent"], report["parsed"]),
|
|
32
|
+
"diagnostic_rate" => rate(report["notes"], report["parsed"]),
|
|
33
|
+
)
|
|
34
|
+
report
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.run(files, output:)
|
|
38
|
+
report = measure(files)
|
|
39
|
+
File.write(output, "#{JSON.pretty_generate(report)}\n")
|
|
40
|
+
report
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.measure_file(path, report)
|
|
44
|
+
repository = parse_file(path)
|
|
45
|
+
unless repository
|
|
46
|
+
report["parse_failed"] += 1
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
report["parsed"] += 1
|
|
50
|
+
measure_properties(repository, report)
|
|
51
|
+
end
|
|
52
|
+
private_class_method :measure_file
|
|
53
|
+
|
|
54
|
+
def self.parse_file(path)
|
|
55
|
+
Expressir::Express::Parser.from_exp(File.read(path))
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
private_class_method :parse_file
|
|
60
|
+
|
|
61
|
+
def self.measure_properties(repository, report)
|
|
62
|
+
schema = Array(repository.schemas).first
|
|
63
|
+
if schema && idempotent?(schema)
|
|
64
|
+
report["idempotent"] += 1
|
|
65
|
+
else
|
|
66
|
+
report["not_idempotent"] += 1
|
|
67
|
+
end
|
|
68
|
+
notes = Expressir::Express::Checker.new(
|
|
69
|
+
Expressir::Model::Repository.new(files: [repository]),
|
|
70
|
+
).check.notes
|
|
71
|
+
report["notes"] += notes.size
|
|
72
|
+
notes.each do |note|
|
|
73
|
+
report[note.severity == :error ? "errors" : "warnings"] += 1
|
|
74
|
+
report["note_ids"][note.id.to_s] =
|
|
75
|
+
report["note_ids"].fetch(note.id.to_s, 0) + 1
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError
|
|
78
|
+
report["measurement_failed"] += 1
|
|
79
|
+
end
|
|
80
|
+
private_class_method :measure_properties
|
|
81
|
+
|
|
82
|
+
def self.idempotent?(schema)
|
|
83
|
+
once = Expressir::Express::Formatter.format(schema)
|
|
84
|
+
reparsed = Expressir::Express::Parser.from_exp(once)
|
|
85
|
+
twice = Expressir::Express::Formatter.format(reparsed.schemas.first)
|
|
86
|
+
once == twice
|
|
87
|
+
rescue StandardError
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
private_class_method :idempotent?
|
|
91
|
+
|
|
92
|
+
def self.rate(numerator, denominator)
|
|
93
|
+
return 0.0 if denominator.zero?
|
|
94
|
+
|
|
95
|
+
(numerator.to_f / denominator).round(4)
|
|
96
|
+
end
|
|
97
|
+
private_class_method :rate
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Eval
|
|
5
|
+
# Lexical scope chain: a name resolves to a value through the
|
|
6
|
+
# innermost scope that declares it. The schema provides type
|
|
7
|
+
# context; the population (step 3) provides instances for the
|
|
8
|
+
# instance-aware builtins.
|
|
9
|
+
class Environment
|
|
10
|
+
attr_reader :schema, :population
|
|
11
|
+
|
|
12
|
+
def initialize(schema:, population: nil, frames: [{}])
|
|
13
|
+
@schema = schema
|
|
14
|
+
@population = population
|
|
15
|
+
@frames = frames
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def push(frame)
|
|
19
|
+
Environment.new(schema: @schema, population: @population,
|
|
20
|
+
frames: @frames + [frame])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def define(name, value)
|
|
24
|
+
frame = @frames.last.dup
|
|
25
|
+
frame[name.safe_downcase] = value
|
|
26
|
+
Environment.new(schema: @schema, population: @population,
|
|
27
|
+
frames: @frames[0..-2] + [frame])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def lookup(name)
|
|
31
|
+
key = name.safe_downcase
|
|
32
|
+
@frames.reverse_each do |frame|
|
|
33
|
+
return frame[key] if frame.key?(key)
|
|
34
|
+
end
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def bound?(name)
|
|
39
|
+
!lookup(name).nil? || @frames.any? { |f| f.key?(name.safe_downcase) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def interpreter
|
|
43
|
+
@interpreter ||= Interpreter.new(schema: @schema,
|
|
44
|
+
population: @population)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Part 21 attribute slots for an entity: inherited explicit
|
|
48
|
+
# attributes (supertype closure order) then the entity's own —
|
|
49
|
+
# the same order the P21 validator maps values by.
|
|
50
|
+
def slots_for(entity)
|
|
51
|
+
@slots ||= {}
|
|
52
|
+
@slots[entity] ||= begin
|
|
53
|
+
inherited = entity.supertype_closure.flat_map do |supertype|
|
|
54
|
+
explicit_attributes(supertype)
|
|
55
|
+
end
|
|
56
|
+
(inherited + explicit_attributes(entity)).each_with_index
|
|
57
|
+
.to_h do |attribute, index|
|
|
58
|
+
[attribute.id&.safe_downcase, index]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def explicit_attributes(entity)
|
|
64
|
+
Array(entity.attributes)
|
|
65
|
+
.select { |a| a.instance_of?(Model::Declarations::Attribute) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def entity_for_keyword(keyword)
|
|
69
|
+
Array(@schema&.entities).find do |entity|
|
|
70
|
+
entity.id&.safe_downcase == keyword&.safe_downcase
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def instance_for(reference)
|
|
75
|
+
return nil unless @population && reference.is_a?(String)
|
|
76
|
+
return nil unless reference.start_with?("#")
|
|
77
|
+
|
|
78
|
+
record = @population.instances[reference]
|
|
79
|
+
record && Values::Instance.new(id: reference[1..], record: record)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|