suma 0.5.9 → 0.5.10
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/.rubocop_todo.yml +27 -0
- data/CLAUDE.md +17 -5
- data/README.adoc +101 -0
- data/lib/suma/anchor_validator.rb +85 -0
- data/lib/suma/artifact_server.rb +102 -0
- data/lib/suma/cli/artifact_server_cli.rb +22 -0
- data/lib/suma/cli/build.rb +4 -0
- data/lib/suma/cli/core.rb +50 -4
- data/lib/suma/cli/export.rb +1 -1
- data/lib/suma/cli/extract_terms.rb +1 -1
- data/lib/suma/cli/generate_register.rb +1 -1
- data/lib/suma/cli/ingest_translations.rb +36 -0
- data/lib/suma/cli/package_gcr.rb +51 -0
- data/lib/suma/cli/validate.rb +16 -0
- data/lib/suma/cli.rb +4 -1
- data/lib/suma/closure_validator.rb +124 -20
- data/lib/suma/corpus_graph.rb +79 -16
- data/lib/suma/gcr_package.rb +230 -0
- data/lib/suma/link_validation_result.rb +8 -0
- data/lib/suma/link_validator.rb +0 -3
- data/lib/suma/output_canonicalizer.rb +107 -0
- data/lib/suma/processor.rb +94 -20
- data/lib/suma/provenance.rb +92 -12
- data/lib/suma/schema_collection.rb +1 -0
- data/lib/suma/schema_compiler.rb +79 -13
- data/lib/suma/schema_manifest_generator.rb +10 -3
- data/lib/suma/schema_profile.rb +12 -0
- data/lib/suma/schema_template/anchors.rb +154 -0
- data/lib/suma/schema_template/document.rb +2 -93
- data/lib/suma/schema_template/plain.rb +10 -6
- data/lib/suma/schema_template.rb +1 -0
- data/lib/suma/site_build.rb +153 -0
- data/lib/suma/staged_collection_builder.rb +11 -3
- data/lib/suma/translation_ingester.rb +169 -0
- data/lib/suma/version.rb +1 -1
- data/lib/suma.rb +7 -0
- metadata +12 -1
|
@@ -14,7 +14,7 @@ module Suma
|
|
|
14
14
|
desc: "URN for the dataset source " \
|
|
15
15
|
"(used for section references)"
|
|
16
16
|
option :profile, type: :string, default: "iso10303",
|
|
17
|
-
desc: "Corpus profile (iso10303 or
|
|
17
|
+
desc: "Corpus profile (iso10303, generic, or ifc)"
|
|
18
18
|
option :edition, type: :numeric,
|
|
19
19
|
desc: "Owning edition recorded on each concept"
|
|
20
20
|
option :language_codes, type: :array,
|
|
@@ -18,7 +18,7 @@ module Suma
|
|
|
18
18
|
option :owner, type: :string, default: Suma::RegisterManifestGenerator::DEFAULT_OWNER,
|
|
19
19
|
desc: "Owner of the dataset (e.g. 'ISO/TC 184/SC 4')"
|
|
20
20
|
option :profile, type: :string, default: "iso10303",
|
|
21
|
-
desc: "Corpus profile (iso10303 or
|
|
21
|
+
desc: "Corpus profile (iso10303, generic, or ifc)"
|
|
22
22
|
option :year, type: :numeric,
|
|
23
23
|
desc: "Edition year for the register"
|
|
24
24
|
option :supersedes, type: :string,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module Cli
|
|
7
|
+
# Ingest translated localized-concept content into extracted concepts.
|
|
8
|
+
class IngestTranslations < Thor
|
|
9
|
+
def self.exit_on_failure?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "ingest_translations CONCEPTS_DIR TRANSLATIONS_FILE",
|
|
14
|
+
"Overlay translated localized-concept content onto extracted " \
|
|
15
|
+
"Glossarist concepts; fields the translations omit fall back " \
|
|
16
|
+
"to the source-language content"
|
|
17
|
+
option :output, type: :string, aliases: "-o",
|
|
18
|
+
desc: "Write updated concepts here (default: " \
|
|
19
|
+
"update CONCEPTS_DIR in place)"
|
|
20
|
+
|
|
21
|
+
def ingest_translations(concepts_dir, translations_file)
|
|
22
|
+
unless File.file?(translations_file)
|
|
23
|
+
raise Errno::ENOENT, translations_file
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
applied = TranslationIngester.new(
|
|
27
|
+
concepts_dir: concepts_dir,
|
|
28
|
+
translations: translations_file,
|
|
29
|
+
output_dir: options[:output],
|
|
30
|
+
).ingest
|
|
31
|
+
Utils.log "Applied #{applied.values.sum(&:size)} translation(s) " \
|
|
32
|
+
"to #{applied.size} concept(s)."
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Suma
|
|
6
|
+
module Cli
|
|
7
|
+
# Package extracted concepts into a GCR archive for the Vocabulary
|
|
8
|
+
# Browser.
|
|
9
|
+
class PackageGcr < Thor
|
|
10
|
+
def self.exit_on_failure?
|
|
11
|
+
true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "package_gcr CONCEPTS_DIR",
|
|
15
|
+
"Package extracted Glossarist concepts into a .gcr archive"
|
|
16
|
+
option :register, type: :string, required: true,
|
|
17
|
+
desc: "register.yaml (dataset identity)"
|
|
18
|
+
option :shortname, type: :string, required: true,
|
|
19
|
+
desc: "Dataset id (matches datasets.yml `id`)"
|
|
20
|
+
option :version, type: :string, required: true,
|
|
21
|
+
desc: "Semver of this dataset release"
|
|
22
|
+
option :title, type: :string, required: true,
|
|
23
|
+
desc: "Human-readable dataset title"
|
|
24
|
+
option :description, type: :string, required: true,
|
|
25
|
+
desc: "Dataset description"
|
|
26
|
+
option :created_at, type: :string,
|
|
27
|
+
desc: "Package timestamp (ISO 8601); pin for " \
|
|
28
|
+
"reproducible archives"
|
|
29
|
+
option :output, type: :string, aliases: "-o", required: true,
|
|
30
|
+
desc: "Output directory for the .gcr archives"
|
|
31
|
+
|
|
32
|
+
def package_gcr(concepts_dir)
|
|
33
|
+
GcrPackage.new(
|
|
34
|
+
concepts_dir: concepts_dir,
|
|
35
|
+
register_file: options[:register],
|
|
36
|
+
shortname: options[:shortname],
|
|
37
|
+
version: options[:version],
|
|
38
|
+
title: options[:title],
|
|
39
|
+
description: options[:description],
|
|
40
|
+
created_at: pinned_time,
|
|
41
|
+
).write(options[:output])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def pinned_time
|
|
47
|
+
options[:created_at] && Time.iso8601(options[:created_at])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/suma/cli/validate.rb
CHANGED
|
@@ -62,6 +62,22 @@ module Suma
|
|
|
62
62
|
fail_on_new_drift(*drift.check(path), path)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
desc "anchors SCHEMA_DOCS_PATH",
|
|
66
|
+
"Validate rendered schema docs: every Expressir cross-reference " \
|
|
67
|
+
"and in-page link resolves to an anchor in its HTML"
|
|
68
|
+
def anchors(schema_docs_path = "schema_docs")
|
|
69
|
+
violations = AnchorValidator.new(schema_docs_path).validate
|
|
70
|
+
if violations.empty?
|
|
71
|
+
puts "All anchors resolve: no dead links."
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts violations.map { |v| "#{v.file}: #{v.link} — #{v.reason}" }
|
|
76
|
+
.join("\n")
|
|
77
|
+
raise Thor::Error,
|
|
78
|
+
"Anchor validation failed: #{violations.size} dead link(s)"
|
|
79
|
+
end
|
|
80
|
+
|
|
65
81
|
private
|
|
66
82
|
|
|
67
83
|
# Print the closure report and raise on any unresolved reference.
|
data/lib/suma/cli.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Suma
|
|
4
4
|
module Cli
|
|
5
|
-
autoload :Core,
|
|
5
|
+
autoload :Core, "suma/cli/core"
|
|
6
|
+
autoload :ArtifactServerCli, "suma/cli/artifact_server_cli"
|
|
6
7
|
autoload :Build, "suma/cli/build"
|
|
7
8
|
autoload :CheckSvgQuality, "suma/cli/check_svg_quality"
|
|
8
9
|
autoload :Compare, "suma/cli/compare"
|
|
@@ -11,6 +12,8 @@ module Suma
|
|
|
11
12
|
autoload :ExtractTerms, "suma/cli/extract_terms"
|
|
12
13
|
autoload :GenerateRegister, "suma/cli/generate_register"
|
|
13
14
|
autoload :GenerateSchemas, "suma/cli/generate_schemas"
|
|
15
|
+
autoload :IngestTranslations, "suma/cli/ingest_translations"
|
|
16
|
+
autoload :PackageGcr, "suma/cli/package_gcr"
|
|
14
17
|
autoload :Reformat, "suma/cli/reformat"
|
|
15
18
|
autoload :Resolve, "suma/cli/resolve"
|
|
16
19
|
autoload :Validate, "suma/cli/validate"
|
|
@@ -15,9 +15,11 @@ module Suma
|
|
|
15
15
|
# and inter-schema interfaces.
|
|
16
16
|
#
|
|
17
17
|
# A reference resolves against, in order: the enclosing schema's declared
|
|
18
|
-
# elements, elements of schemas it interfaces to
|
|
19
|
-
# own attributes (for entity-scoped
|
|
20
|
-
# applies-to attributes and variables,
|
|
18
|
+
# elements, elements of schemas it interfaces to (with USE-FROM cascade),
|
|
19
|
+
# the enclosing entity's own and inherited attributes (for entity-scoped
|
|
20
|
+
# expressions), the enclosing rule's applies-to attributes and variables,
|
|
21
|
+
# QUERY-bound variables, and the EXPRESS builtin vocabulary. Attribute
|
|
22
|
+
# accesses (var.attr) are not schema-level references and are excluded.
|
|
21
23
|
#
|
|
22
24
|
# The predicate is zero-tolerance: any unresolved reference is a
|
|
23
25
|
# violation, and `violations.empty?` is the corpus well-formedness gate.
|
|
@@ -56,10 +58,18 @@ module Suma
|
|
|
56
58
|
manifest = Expressir::SchemaManifest.from_file(
|
|
57
59
|
File.expand_path(manifest_file),
|
|
58
60
|
)
|
|
59
|
-
manifest.schemas.
|
|
60
|
-
|
|
61
|
+
parsed = manifest.schemas.map do |entry|
|
|
62
|
+
[entry, Expressir::Express::Parser.from_file(entry.path.to_s)]
|
|
63
|
+
end
|
|
64
|
+
corpus_entities = parsed.flat_map { |_, repo| repo.schemas }
|
|
65
|
+
.flat_map do |schema|
|
|
66
|
+
schema.children.grep(Expressir::Model::Declarations::Entity)
|
|
67
|
+
end
|
|
68
|
+
.to_h { |entity| [entity.id.downcase, entity] }
|
|
69
|
+
|
|
70
|
+
parsed.flat_map do |entry, repo|
|
|
61
71
|
repo.schemas.flat_map do |schema|
|
|
62
|
-
validate_schema(entry.path.to_s, schema)
|
|
72
|
+
validate_schema(entry.path.to_s, schema, corpus_entities)
|
|
63
73
|
end
|
|
64
74
|
end
|
|
65
75
|
end
|
|
@@ -68,10 +78,14 @@ module Suma
|
|
|
68
78
|
#
|
|
69
79
|
# @param file [String] source file path (for diagnostics)
|
|
70
80
|
# @param schema [Expressir::Model::Declarations::Schema]
|
|
81
|
+
# @param corpus_entities [Hash{String=>Entity}] every entity of the
|
|
82
|
+
# corpus by downcased id, for cross-schema attribute inheritance
|
|
71
83
|
# @return [Array<UnresolvedReference>]
|
|
72
|
-
def validate_schema(file, schema)
|
|
84
|
+
def validate_schema(file, schema, corpus_entities = {})
|
|
73
85
|
violations = []
|
|
74
86
|
scope = SchemaScope.new(schema, graph)
|
|
87
|
+
entities = schema.children.grep(Expressir::Model::Declarations::Entity)
|
|
88
|
+
.to_h { |entity| [entity.id.downcase, entity] }
|
|
75
89
|
|
|
76
90
|
schema.interfaces.each do |interface|
|
|
77
91
|
next if graph.schema_exists?(interface.schema.id)
|
|
@@ -83,7 +97,8 @@ module Suma
|
|
|
83
97
|
|
|
84
98
|
schema.children.each do |owner|
|
|
85
99
|
violations.concat(declared_element_references(file, schema.id, owner,
|
|
86
|
-
scope
|
|
100
|
+
scope, entities,
|
|
101
|
+
corpus_entities))
|
|
87
102
|
end
|
|
88
103
|
violations
|
|
89
104
|
end
|
|
@@ -91,8 +106,9 @@ module Suma
|
|
|
91
106
|
private
|
|
92
107
|
|
|
93
108
|
# Type- and structure-position references of one declared element.
|
|
94
|
-
def declared_element_references(file, schema_id, owner, scope
|
|
95
|
-
|
|
109
|
+
def declared_element_references(file, schema_id, owner, scope, entities,
|
|
110
|
+
corpus_entities)
|
|
111
|
+
refs = typed_references(owner, entities, corpus_entities)
|
|
96
112
|
refs.filter_map do |(kind, identifier)|
|
|
97
113
|
if resolvable?(identifier, scope)
|
|
98
114
|
nil
|
|
@@ -107,11 +123,13 @@ module Suma
|
|
|
107
123
|
end
|
|
108
124
|
|
|
109
125
|
# (kind, identifier) pairs from every position of one declaration.
|
|
110
|
-
def typed_references(owner)
|
|
126
|
+
def typed_references(owner, entities, corpus_entities)
|
|
111
127
|
case owner
|
|
112
|
-
when Expressir::Model::Declarations::Entity
|
|
128
|
+
when Expressir::Model::Declarations::Entity
|
|
129
|
+
entity_references(owner, entities, corpus_entities)
|
|
113
130
|
when Expressir::Model::Declarations::Type then type_references(owner)
|
|
114
|
-
when Expressir::Model::Declarations::Rule
|
|
131
|
+
when Expressir::Model::Declarations::Rule
|
|
132
|
+
rule_references(owner)
|
|
115
133
|
when Expressir::Model::Declarations::Constant
|
|
116
134
|
datatype_references(owner.type, "constant")
|
|
117
135
|
when Expressir::Model::Declarations::Function
|
|
@@ -122,9 +140,10 @@ module Suma
|
|
|
122
140
|
end
|
|
123
141
|
end
|
|
124
142
|
|
|
125
|
-
def entity_references(entity)
|
|
143
|
+
def entity_references(entity, entities, corpus_entities)
|
|
144
|
+
locals = attribute_scope(entity, entities, corpus_entities)
|
|
126
145
|
refs = (entity.where_rules || []).flat_map do |rule|
|
|
127
|
-
expression_references(rule.expression, "where")
|
|
146
|
+
expression_references(rule.expression, "where", locals)
|
|
128
147
|
end
|
|
129
148
|
entity.attributes.each do |attribute|
|
|
130
149
|
refs.concat(datatype_references(attribute.type, "attribute"))
|
|
@@ -141,8 +160,11 @@ module Suma
|
|
|
141
160
|
|
|
142
161
|
def rule_references(rule)
|
|
143
162
|
refs = rule.applies_to.map { |ref| ["rule_applies_to", ref.id] }
|
|
163
|
+
locals = Array(rule.variables).filter_map do |variable|
|
|
164
|
+
variable.id&.downcase
|
|
165
|
+
end
|
|
144
166
|
refs += (rule.where_rules || []).flat_map do |where|
|
|
145
|
-
expression_references(where.expression, "where")
|
|
167
|
+
expression_references(where.expression, "where", locals)
|
|
146
168
|
end
|
|
147
169
|
refs += (rule.variables || []).flat_map do |variable|
|
|
148
170
|
datatype_references(variable.type, "variable")
|
|
@@ -162,8 +184,20 @@ module Suma
|
|
|
162
184
|
|
|
163
185
|
def subtype_constraint_references(constraint)
|
|
164
186
|
refs = []
|
|
165
|
-
|
|
166
|
-
|
|
187
|
+
if constraint.applies_to.is_a?(Expressir::Model::References::SimpleReference)
|
|
188
|
+
refs << ["subtype_constraint", constraint.applies_to.id]
|
|
189
|
+
end
|
|
190
|
+
Array(constraint.total_over).each do |ref|
|
|
191
|
+
next unless ref.is_a?(Expressir::Model::References::SimpleReference)
|
|
192
|
+
|
|
193
|
+
refs << ["total_over", ref.id]
|
|
194
|
+
end
|
|
195
|
+
if constraint.supertype_expression
|
|
196
|
+
refs.concat(
|
|
197
|
+
expression_references(constraint.supertype_expression, "supertype"),
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
refs
|
|
167
201
|
end
|
|
168
202
|
|
|
169
203
|
def supertype_references(owner)
|
|
@@ -183,14 +217,84 @@ module Suma
|
|
|
183
217
|
expression_references(node, kind)
|
|
184
218
|
end
|
|
185
219
|
|
|
186
|
-
def expression_references(node, kind)
|
|
220
|
+
def expression_references(node, kind, locals = [])
|
|
221
|
+
scoped = locals + query_variables(node) +
|
|
222
|
+
attribute_access_identifiers(node)
|
|
187
223
|
refs = []
|
|
188
224
|
walk(node) do |child|
|
|
189
|
-
|
|
225
|
+
next unless simple_reference?(child)
|
|
226
|
+
next if child.id == "?"
|
|
227
|
+
next if scoped.include?(child.id.to_s.downcase)
|
|
228
|
+
|
|
229
|
+
refs << [kind, child.id]
|
|
190
230
|
end
|
|
191
231
|
refs
|
|
192
232
|
end
|
|
193
233
|
|
|
234
|
+
# QUERY expressions bind their variable inside the query; collect every
|
|
235
|
+
# bound variable name in the tree so the walk can skip them.
|
|
236
|
+
def query_variables(node)
|
|
237
|
+
found = []
|
|
238
|
+
walk(node) do |child|
|
|
239
|
+
if child.is_a?(Expressir::Model::Expressions::QueryExpression) &&
|
|
240
|
+
child.id
|
|
241
|
+
found << child.id.to_s.downcase
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
found
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Attribute accesses (var.attr, SELF\entity.attr) resolve against the
|
|
248
|
+
# base value's type, not the schema — neither the accessed attribute
|
|
249
|
+
# nor the base identifier is a schema-level reference.
|
|
250
|
+
def attribute_access_identifiers(node)
|
|
251
|
+
found = []
|
|
252
|
+
walk(node) do |child|
|
|
253
|
+
next unless child.is_a?(Expressir::Model::References::AttributeReference)
|
|
254
|
+
|
|
255
|
+
collect_access_identifier(found, child.ref)
|
|
256
|
+
collect_access_identifier(found, child.attribute)
|
|
257
|
+
end
|
|
258
|
+
found
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def collect_access_identifier(found, part)
|
|
262
|
+
return unless part.is_a?(Expressir::Model::References::SimpleReference)
|
|
263
|
+
return unless part.id
|
|
264
|
+
|
|
265
|
+
found << part.id.to_s.downcase
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Bare identifiers in an entity's expressions resolve against the
|
|
269
|
+
# entity's own attributes and those inherited through its supertype
|
|
270
|
+
# chain, following subtypes across schemas when the parent entity is
|
|
271
|
+
# in the corpus (name-keyed; EXPRESS entity names are schema-scoped,
|
|
272
|
+
# but cross-schema parents resolve through interface visibility, so
|
|
273
|
+
# the name is the identity the child's source references).
|
|
274
|
+
def attribute_scope(entity, entities, corpus_entities)
|
|
275
|
+
names = []
|
|
276
|
+
queue = [entity]
|
|
277
|
+
seen = {}
|
|
278
|
+
until queue.empty?
|
|
279
|
+
current = queue.shift
|
|
280
|
+
key = current.id.to_s.downcase
|
|
281
|
+
next if seen[key]
|
|
282
|
+
|
|
283
|
+
seen[key] = true
|
|
284
|
+
Array(current.attributes).each do |attribute|
|
|
285
|
+
names << attribute.id.downcase if attribute.id
|
|
286
|
+
end
|
|
287
|
+
supertype_references(current).each do |(kind, name)|
|
|
288
|
+
next unless kind == "subtype_of"
|
|
289
|
+
|
|
290
|
+
parent = entities[name.downcase] ||
|
|
291
|
+
corpus_entities[name.downcase]
|
|
292
|
+
queue << parent if parent
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
names
|
|
296
|
+
end
|
|
297
|
+
|
|
194
298
|
def simple_reference?(node)
|
|
195
299
|
node.is_a?(Expressir::Model::References::SimpleReference) &&
|
|
196
300
|
!node.id.nil?
|
data/lib/suma/corpus_graph.rb
CHANGED
|
@@ -13,6 +13,12 @@ module Suma
|
|
|
13
13
|
# change-impact analysis (S3) and dependency-driven builds (S6). Nodes are
|
|
14
14
|
# schemas and their declared elements; edges are USE/REFERENCE interfaces.
|
|
15
15
|
class CorpusGraph
|
|
16
|
+
# One USE/REFERENCE import: the target schema, its kind, and the named
|
|
17
|
+
# items (renames already applied — the visible name is +items+).
|
|
18
|
+
InterfaceEdge = Struct.new(:kind, :target, :items, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
EMPTY_VISIBLE = {}.freeze
|
|
21
|
+
|
|
16
22
|
attr_reader :schema_ids
|
|
17
23
|
|
|
18
24
|
def initialize(repositories)
|
|
@@ -24,6 +30,7 @@ module Suma
|
|
|
24
30
|
repo.schemas.each { |schema| add_schema(schema) }
|
|
25
31
|
end
|
|
26
32
|
@schema_ids = @schemas.keys
|
|
33
|
+
@visibility = build_visibility
|
|
27
34
|
freeze
|
|
28
35
|
end
|
|
29
36
|
|
|
@@ -47,20 +54,18 @@ module Suma
|
|
|
47
54
|
@schemas.key?(normalize(id))
|
|
48
55
|
end
|
|
49
56
|
|
|
50
|
-
# Does +schema_id+ (or
|
|
51
|
-
# EXPRESS resolves unqualified names against the local schema and
|
|
52
|
-
#
|
|
57
|
+
# Does +schema_id+ (or the schemas it imports) declare +element_id+?
|
|
58
|
+
# EXPRESS resolves unqualified names against the local schema and the
|
|
59
|
+
# schema's interface visibility, including USE-FROM cascades.
|
|
53
60
|
#
|
|
54
61
|
# @param schema_id [String]
|
|
55
62
|
# @param element_id [String]
|
|
56
63
|
# @return [Boolean]
|
|
57
64
|
def element_visible?(schema_id, element_id)
|
|
58
65
|
key = normalize(schema_id)
|
|
59
|
-
return false unless @
|
|
60
|
-
|
|
61
|
-
return true if @elements[key].key?(normalize(element_id))
|
|
66
|
+
return false unless @visibility.key?(key)
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
@visibility[key].key?(normalize(element_id))
|
|
64
69
|
end
|
|
65
70
|
|
|
66
71
|
# Schemas that +schema_id+ USEs FROM or REFERENCEs.
|
|
@@ -68,7 +73,7 @@ module Suma
|
|
|
68
73
|
# @param schema_id [String]
|
|
69
74
|
# @return [Array<String>] normalized ids of interface targets
|
|
70
75
|
def interface_targets(schema_id)
|
|
71
|
-
@interfaces[normalize(schema_id)] || []
|
|
76
|
+
(@interfaces[normalize(schema_id)] || []).map(&:target)
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
# Transitive interface closure: every schema +schema_id+ depends on,
|
|
@@ -110,11 +115,21 @@ module Suma
|
|
|
110
115
|
key = normalize(schema.id)
|
|
111
116
|
@schemas[key] = true
|
|
112
117
|
@elements[key] = declared_elements(schema)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
@interfaces[key] = Array(schema.interfaces).filter_map do |interface|
|
|
119
|
+
build_edge(interface)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def build_edge(interface)
|
|
124
|
+
target = interface.schema&.id
|
|
125
|
+
return nil unless target
|
|
126
|
+
|
|
127
|
+
items = Array(interface.items).filter_map do |item|
|
|
128
|
+
item.id || (item.ref.id if item.ref.respond_to?(:id))
|
|
129
|
+
end
|
|
130
|
+
InterfaceEdge.new(kind: interface.kind.to_s,
|
|
131
|
+
target: normalize(target),
|
|
132
|
+
items: items)
|
|
118
133
|
end
|
|
119
134
|
|
|
120
135
|
def declared_elements(schema)
|
|
@@ -128,9 +143,57 @@ module Suma
|
|
|
128
143
|
index
|
|
129
144
|
end
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
# EXPRESS interface visibility (ISO 10303-11 §13.3): a schema sees its
|
|
147
|
+
# declarations plus its imports. An unrestricted USE re-exports
|
|
148
|
+
# everything its target makes visible, so visibility cascades through
|
|
149
|
+
# USE chains; a named import exposes only the named (renamed) items;
|
|
150
|
+
# a REFERENCE exposes the target's declarations without cascading.
|
|
151
|
+
#
|
|
152
|
+
# WG12 corpora ship circular USE interfaces, so this is a monotone
|
|
153
|
+
# fixed-point iteration — sound under cycles, and each pass only adds.
|
|
154
|
+
def build_visibility
|
|
155
|
+
visible = {}
|
|
156
|
+
@schemas.each_key { |key| visible[key] = (@elements[key] || {}).dup }
|
|
157
|
+
|
|
158
|
+
changed = true
|
|
159
|
+
while changed
|
|
160
|
+
changed = false
|
|
161
|
+
@schemas.each_key do |key|
|
|
162
|
+
changed ||= apply_visibility_pass(key, visible)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
visible
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def apply_visibility_pass(key, visible)
|
|
169
|
+
names = visible[key]
|
|
170
|
+
changed = false
|
|
171
|
+
(@interfaces[key] || []).each do |edge|
|
|
172
|
+
next unless @schemas.key?(edge.target)
|
|
173
|
+
|
|
174
|
+
changed ||= import_edge_names(names, edge, visible)
|
|
175
|
+
end
|
|
176
|
+
changed
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def import_edge_names(names, edge, visible)
|
|
180
|
+
changed = false
|
|
181
|
+
imported_names(edge, visible).each_key do |name|
|
|
182
|
+
next if names.key?(name)
|
|
183
|
+
|
|
184
|
+
names[name] = true
|
|
185
|
+
changed = true
|
|
186
|
+
end
|
|
187
|
+
changed
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def imported_names(edge, visible)
|
|
191
|
+
if edge.items.any?
|
|
192
|
+
edge.items.to_h { |name| [normalize(name), true] }
|
|
193
|
+
elsif edge.kind == "USE"
|
|
194
|
+
visible[edge.target] || EMPTY_VISIBLE
|
|
195
|
+
else
|
|
196
|
+
@elements[edge.target] || EMPTY_VISIBLE
|
|
134
197
|
end
|
|
135
198
|
end
|
|
136
199
|
|