suma 0.5.7 → 0.5.8
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/nightly-benchmark.yml +25 -0
- data/.github/workflows/release-provenance.yml +38 -0
- data/.rubocop_todo.yml +84 -0
- data/CLAUDE.md +44 -0
- data/Rakefile +13 -1
- data/bin/benchmark_corpus +138 -0
- data/bin/release_provenance +40 -0
- data/lib/suma/build_plan.rb +71 -0
- data/lib/suma/change_classification.rb +37 -0
- data/lib/suma/change_impact.rb +61 -0
- data/lib/suma/change_manifest.rb +147 -0
- data/lib/suma/cli/build.rb +23 -0
- data/lib/suma/cli/compare.rb +49 -2
- data/lib/suma/cli/core.rb +83 -0
- data/lib/suma/cli/export.rb +140 -10
- data/lib/suma/cli/extract_terms.rb +11 -0
- data/lib/suma/cli/generate_register.rb +12 -0
- data/lib/suma/cli/resolve.rb +45 -0
- data/lib/suma/cli/validate.rb +90 -0
- data/lib/suma/cli.rb +1 -0
- data/lib/suma/closure_validator.rb +317 -0
- data/lib/suma/corpus_graph.rb +141 -0
- data/lib/suma/express_schema.rb +22 -28
- data/lib/suma/null_cache.rb +4 -0
- data/lib/suma/owl_projection.rb +611 -0
- data/lib/suma/processor.rb +55 -6
- data/lib/suma/provenance.rb +159 -0
- data/lib/suma/publication.rb +108 -0
- data/lib/suma/register_manifest_generator.rb +76 -8
- data/lib/suma/schema_cache.rb +72 -13
- data/lib/suma/schema_category.rb +31 -2
- data/lib/suma/schema_collection.rb +58 -5
- data/lib/suma/schema_exporter.rb +5 -5
- data/lib/suma/schema_naming.rb +29 -33
- data/lib/suma/schema_profile.rb +134 -0
- data/lib/suma/term_classification.rb +9 -2
- data/lib/suma/term_extractor.rb +76 -35
- data/lib/suma/urn_resolver.rb +318 -0
- data/lib/suma/version.rb +1 -1
- data/lib/suma/worker_pool.rb +62 -0
- data/lib/suma.rb +12 -0
- metadata +19 -2
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "expressir"
|
|
4
|
+
# Expressir::SchemaManifest references Pathname without requiring it
|
|
5
|
+
# (expressir upstream gap); load it before any manifest parsing.
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
module Suma
|
|
9
|
+
# S1 referential closure: extracts every EXPRESS-internal reference from
|
|
10
|
+
# a schema collection and resolves it against the {CorpusGraph}.
|
|
11
|
+
#
|
|
12
|
+
# Reference classes covered (one predicate, many extractors):
|
|
13
|
+
# attribute domains, SELECT/AGGREGATE branches, where-rule expressions,
|
|
14
|
+
# subtype constraints, supertype/subtype lists, rule applies-to targets,
|
|
15
|
+
# and inter-schema interfaces.
|
|
16
|
+
#
|
|
17
|
+
# A reference resolves against, in order: the enclosing schema's declared
|
|
18
|
+
# elements, elements of schemas it interfaces to, the enclosing entity's
|
|
19
|
+
# own attributes (for entity-scoped expressions), the enclosing rule's
|
|
20
|
+
# applies-to attributes and variables, and the EXPRESS builtin vocabulary.
|
|
21
|
+
#
|
|
22
|
+
# The predicate is zero-tolerance: any unresolved reference is a
|
|
23
|
+
# violation, and `violations.empty?` is the corpus well-formedness gate.
|
|
24
|
+
class ClosureValidator
|
|
25
|
+
UnresolvedReference = Struct.new(:file, :line, :schema_id, :owner_id,
|
|
26
|
+
:kind, :reference, :reason,
|
|
27
|
+
keyword_init: true)
|
|
28
|
+
|
|
29
|
+
# Attributes that hold resolved links rather than syntactic children.
|
|
30
|
+
# Traversing them would walk the whole resolved graph (and its cycles)
|
|
31
|
+
# instead of the expression/type tree.
|
|
32
|
+
RESOLUTION_LINK_ATTRIBUTES = %i[ref entity].freeze
|
|
33
|
+
|
|
34
|
+
BUILTIN_IDENTIFIERS = %w[
|
|
35
|
+
abs acos asin atan atan2 bexists blength const_e cos exists exp
|
|
36
|
+
format hibound hiindex length lobound loindex log log2 log10 nvl odd
|
|
37
|
+
roles_of sin sizeof sqrt tan typeof usedin value value_in
|
|
38
|
+
value_range value_list_by unique
|
|
39
|
+
self and or xor not in like equal instanceof
|
|
40
|
+
e pi
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
attr_reader :graph
|
|
44
|
+
|
|
45
|
+
# @param graph [CorpusGraph]
|
|
46
|
+
def initialize(graph)
|
|
47
|
+
@graph = graph
|
|
48
|
+
@source_lines = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Validate every schema of the manifest.
|
|
52
|
+
#
|
|
53
|
+
# @param manifest_file [String] schema manifest path
|
|
54
|
+
# @return [Array<UnresolvedReference>]
|
|
55
|
+
def validate_manifest(manifest_file)
|
|
56
|
+
manifest = Expressir::SchemaManifest.from_file(
|
|
57
|
+
File.expand_path(manifest_file),
|
|
58
|
+
)
|
|
59
|
+
manifest.schemas.flat_map do |entry|
|
|
60
|
+
repo = Expressir::Express::Parser.from_file(entry.path.to_s)
|
|
61
|
+
repo.schemas.flat_map do |schema|
|
|
62
|
+
validate_schema(entry.path.to_s, schema)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# All unresolved references in one schema.
|
|
68
|
+
#
|
|
69
|
+
# @param file [String] source file path (for diagnostics)
|
|
70
|
+
# @param schema [Expressir::Model::Declarations::Schema]
|
|
71
|
+
# @return [Array<UnresolvedReference>]
|
|
72
|
+
def validate_schema(file, schema)
|
|
73
|
+
violations = []
|
|
74
|
+
scope = SchemaScope.new(schema, graph)
|
|
75
|
+
|
|
76
|
+
schema.interfaces.each do |interface|
|
|
77
|
+
next if graph.schema_exists?(interface.schema.id)
|
|
78
|
+
|
|
79
|
+
violations << unresolved(file, schema.id, schema.id, "interface",
|
|
80
|
+
interface.schema.id,
|
|
81
|
+
"interface target not declared")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
schema.children.each do |owner|
|
|
85
|
+
violations.concat(declared_element_references(file, schema.id, owner,
|
|
86
|
+
scope))
|
|
87
|
+
end
|
|
88
|
+
violations
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Type- and structure-position references of one declared element.
|
|
94
|
+
def declared_element_references(file, schema_id, owner, scope)
|
|
95
|
+
refs = typed_references(owner)
|
|
96
|
+
refs.filter_map do |(kind, identifier)|
|
|
97
|
+
if resolvable?(identifier, scope)
|
|
98
|
+
nil
|
|
99
|
+
else
|
|
100
|
+
unresolved(
|
|
101
|
+
file, schema_id, owner_id_of(owner), kind, identifier,
|
|
102
|
+
"'#{identifier}' is not declared in this schema, its " \
|
|
103
|
+
"interfaces, or the EXPRESS builtins"
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# (kind, identifier) pairs from every position of one declaration.
|
|
110
|
+
def typed_references(owner)
|
|
111
|
+
case owner
|
|
112
|
+
when Expressir::Model::Declarations::Entity then entity_references(owner)
|
|
113
|
+
when Expressir::Model::Declarations::Type then type_references(owner)
|
|
114
|
+
when Expressir::Model::Declarations::Rule then rule_references(owner)
|
|
115
|
+
when Expressir::Model::Declarations::Constant
|
|
116
|
+
datatype_references(owner.type, "constant")
|
|
117
|
+
when Expressir::Model::Declarations::Function
|
|
118
|
+
function_references(owner)
|
|
119
|
+
when Expressir::Model::Declarations::SubtypeConstraint
|
|
120
|
+
subtype_constraint_references(owner)
|
|
121
|
+
else []
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def entity_references(entity)
|
|
126
|
+
refs = (entity.where_rules || []).flat_map do |rule|
|
|
127
|
+
expression_references(rule.expression, "where")
|
|
128
|
+
end
|
|
129
|
+
entity.attributes.each do |attribute|
|
|
130
|
+
refs.concat(datatype_references(attribute.type, "attribute"))
|
|
131
|
+
end
|
|
132
|
+
refs.concat(supertype_references(entity))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def type_references(type)
|
|
136
|
+
refs = (type.where_rules || []).flat_map do |rule|
|
|
137
|
+
expression_references(rule.expression, "where")
|
|
138
|
+
end
|
|
139
|
+
refs.concat(datatype_references(type.underlying_type, "underlying"))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def rule_references(rule)
|
|
143
|
+
refs = rule.applies_to.map { |ref| ["rule_applies_to", ref.id] }
|
|
144
|
+
refs += (rule.where_rules || []).flat_map do |where|
|
|
145
|
+
expression_references(where.expression, "where")
|
|
146
|
+
end
|
|
147
|
+
refs += (rule.variables || []).flat_map do |variable|
|
|
148
|
+
datatype_references(variable.type, "variable")
|
|
149
|
+
end
|
|
150
|
+
refs
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def function_references(function)
|
|
154
|
+
refs = (function.parameters || []).flat_map do |parameter|
|
|
155
|
+
datatype_references(parameter.type, "parameter")
|
|
156
|
+
end
|
|
157
|
+
if function.return_type
|
|
158
|
+
refs.concat(datatype_references(function.return_type, "return"))
|
|
159
|
+
end
|
|
160
|
+
refs
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def subtype_constraint_references(constraint)
|
|
164
|
+
refs = []
|
|
165
|
+
refs << ["subtype_constraint", constraint.entity.id] if constraint.entity
|
|
166
|
+
refs.concat(supertype_references(constraint))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def supertype_references(owner)
|
|
170
|
+
refs = []
|
|
171
|
+
(owner.subtype_of || []).each do |ref|
|
|
172
|
+
refs << ["subtype_of", ref.id] if ref.respond_to?(:id) && ref.id
|
|
173
|
+
end
|
|
174
|
+
if owner.respond_to?(:supertype_expression) && owner.supertype_expression
|
|
175
|
+
refs.concat(expression_references(owner.supertype_expression,
|
|
176
|
+
"supertype"))
|
|
177
|
+
end
|
|
178
|
+
refs
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# (kind, identifier) pairs from a datatype or expression tree.
|
|
182
|
+
def datatype_references(node, kind)
|
|
183
|
+
expression_references(node, kind)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def expression_references(node, kind)
|
|
187
|
+
refs = []
|
|
188
|
+
walk(node) do |child|
|
|
189
|
+
refs << [kind, child.id] if simple_reference?(child)
|
|
190
|
+
end
|
|
191
|
+
refs
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def simple_reference?(node)
|
|
195
|
+
node.is_a?(Expressir::Model::References::SimpleReference) &&
|
|
196
|
+
!node.id.nil?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Depth-first walk over the syntactic tree of a model element.
|
|
200
|
+
# Expressir only implements `children` on some classes, so traversal
|
|
201
|
+
# goes through the lutaml-model attribute definitions: every attribute
|
|
202
|
+
# whose value is a ModelElement (or a collection of them) is a child.
|
|
203
|
+
# The model graph has reference cycles, so visited nodes are tracked
|
|
204
|
+
# by identity.
|
|
205
|
+
def walk(node, seen = {}.compare_by_identity, &block)
|
|
206
|
+
return unless node.is_a?(Expressir::Model::ModelElement)
|
|
207
|
+
return if seen.key?(node)
|
|
208
|
+
|
|
209
|
+
seen[node] = true
|
|
210
|
+
yield node
|
|
211
|
+
each_model_child(node) { |child| walk(child, seen, &block) }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def each_model_child(node)
|
|
215
|
+
node.class.attributes.each_key do |name|
|
|
216
|
+
next if RESOLUTION_LINK_ATTRIBUTES.include?(name)
|
|
217
|
+
|
|
218
|
+
value = begin
|
|
219
|
+
node.public_send(name)
|
|
220
|
+
rescue StandardError
|
|
221
|
+
next
|
|
222
|
+
end
|
|
223
|
+
values = value.is_a?(Array) ? value : [value]
|
|
224
|
+
values.each do |child|
|
|
225
|
+
yield child if child.is_a?(Expressir::Model::ModelElement)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# A reference resolves when its root identifier is declared in the
|
|
231
|
+
# schema, visible through an interface, or is an EXPRESS builtin.
|
|
232
|
+
def resolvable?(identifier, scope)
|
|
233
|
+
root = identifier.split(".").first.downcase
|
|
234
|
+
return true if BUILTIN_IDENTIFIERS.include?(root)
|
|
235
|
+
|
|
236
|
+
scope.visible?(root)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def unresolved(file, schema_id, owner_id, kind, reference, reason)
|
|
240
|
+
UnresolvedReference.new(
|
|
241
|
+
file: file,
|
|
242
|
+
line: approximate_line(file, reference),
|
|
243
|
+
schema_id: schema_id,
|
|
244
|
+
owner_id: owner_id,
|
|
245
|
+
kind: kind,
|
|
246
|
+
reference: reference,
|
|
247
|
+
reason: reason,
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Expressir's Ruby model carries no source positions, so diagnostics
|
|
252
|
+
# approximate the line by the first token occurrence in the file.
|
|
253
|
+
def approximate_line(file, identifier)
|
|
254
|
+
lines = (@source_lines[file] ||= read_source(file))
|
|
255
|
+
token = identifier.split(".").first
|
|
256
|
+
pattern = /\b#{Regexp.escape(token)}\b/i
|
|
257
|
+
index = lines.index { |line| line.match?(pattern) }
|
|
258
|
+
index ? index + 1 : nil
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def read_source(file)
|
|
262
|
+
File.readlines(file)
|
|
263
|
+
rescue StandardError
|
|
264
|
+
[]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def owner_id_of(owner)
|
|
268
|
+
owner.respond_to?(:id) && owner.id ? owner.id : "(anonymous)"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Names visible for expression resolution inside one schema: declared
|
|
272
|
+
# elements, the attributes of every entity, rule applies-to entity
|
|
273
|
+
# attributes, rule variables, constants, and function parameters.
|
|
274
|
+
# Only declarations are collected — expression contents are the very
|
|
275
|
+
# references being validated and must never enter the scope.
|
|
276
|
+
class SchemaScope
|
|
277
|
+
def initialize(schema, graph)
|
|
278
|
+
@names = {}
|
|
279
|
+
@graph = graph
|
|
280
|
+
@schema_id = schema.id
|
|
281
|
+
|
|
282
|
+
schema.children.each { |declaration| add(declaration) }
|
|
283
|
+
schema.entities.each do |entity|
|
|
284
|
+
entity.attributes.each { |attribute| add(attribute) }
|
|
285
|
+
end
|
|
286
|
+
schema.rules.each do |rule|
|
|
287
|
+
rule.variables.each { |variable| add(variable) }
|
|
288
|
+
rule.applies_to.each do |ref|
|
|
289
|
+
entity = schema.entities.find do |candidate|
|
|
290
|
+
candidate.id&.downcase == ref.id&.downcase
|
|
291
|
+
end
|
|
292
|
+
next unless entity
|
|
293
|
+
|
|
294
|
+
entity.attributes.each { |attribute| add(attribute) }
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
schema.constants.each { |constant| add(constant) }
|
|
298
|
+
schema.functions.each do |function|
|
|
299
|
+
(function.parameters || []).each { |parameter| add(parameter) }
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def visible?(name)
|
|
304
|
+
@names[name.downcase] ||
|
|
305
|
+
@graph.element_visible?(@schema_id, name)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private
|
|
309
|
+
|
|
310
|
+
def add(node)
|
|
311
|
+
return unless node.respond_to?(:id) && node.id
|
|
312
|
+
|
|
313
|
+
@names[node.id.downcase] = true
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "expressir"
|
|
4
|
+
# Expressir::SchemaManifest references Pathname without requiring it
|
|
5
|
+
# (expressir upstream gap); load it before any manifest parsing.
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
module Suma
|
|
9
|
+
# The declared-element graph of a schema collection: which schemas exist,
|
|
10
|
+
# which elements each declares, and which schemas interface to which.
|
|
11
|
+
#
|
|
12
|
+
# This is the resolution substrate for S1 referential closure and, later,
|
|
13
|
+
# change-impact analysis (S3) and dependency-driven builds (S6). Nodes are
|
|
14
|
+
# schemas and their declared elements; edges are USE/REFERENCE interfaces.
|
|
15
|
+
class CorpusGraph
|
|
16
|
+
attr_reader :schema_ids
|
|
17
|
+
|
|
18
|
+
def initialize(repositories)
|
|
19
|
+
@schemas = {}
|
|
20
|
+
@elements = {}
|
|
21
|
+
@interfaces = {}
|
|
22
|
+
|
|
23
|
+
repositories.each do |repo|
|
|
24
|
+
repo.schemas.each { |schema| add_schema(schema) }
|
|
25
|
+
end
|
|
26
|
+
@schema_ids = @schemas.keys
|
|
27
|
+
freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Parse every schema in a manifest and build the graph.
|
|
31
|
+
#
|
|
32
|
+
# @param manifest_file [String] path to the schema manifest
|
|
33
|
+
# @return [CorpusGraph]
|
|
34
|
+
def self.from_manifest(manifest_file)
|
|
35
|
+
manifest = Expressir::SchemaManifest.from_file(
|
|
36
|
+
File.expand_path(manifest_file),
|
|
37
|
+
)
|
|
38
|
+
repositories = manifest.schemas.map do |entry|
|
|
39
|
+
Expressir::Express::Parser.from_file(entry.path.to_s)
|
|
40
|
+
end
|
|
41
|
+
new(repositories)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param id [String]
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def schema_exists?(id)
|
|
47
|
+
@schemas.key?(normalize(id))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Does +schema_id+ (or a schema it interfaces to) declare +element_id+?
|
|
51
|
+
# EXPRESS resolves unqualified names against the local schema and
|
|
52
|
+
# implicitly-used schemas.
|
|
53
|
+
#
|
|
54
|
+
# @param schema_id [String]
|
|
55
|
+
# @param element_id [String]
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
def element_visible?(schema_id, element_id)
|
|
58
|
+
key = normalize(schema_id)
|
|
59
|
+
return false unless @elements.key?(key)
|
|
60
|
+
|
|
61
|
+
return true if @elements[key].key?(normalize(element_id))
|
|
62
|
+
|
|
63
|
+
visible_from_interfaces(key).include?(normalize(element_id))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Schemas that +schema_id+ USEs FROM or REFERENCEs.
|
|
67
|
+
#
|
|
68
|
+
# @param schema_id [String]
|
|
69
|
+
# @return [Array<String>] normalized ids of interface targets
|
|
70
|
+
def interface_targets(schema_id)
|
|
71
|
+
@interfaces[normalize(schema_id)] || []
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Transitive interface closure: every schema +schema_id+ depends on,
|
|
75
|
+
# directly or through other schemas.
|
|
76
|
+
#
|
|
77
|
+
# @param schema_id [String]
|
|
78
|
+
# @return [Array<String>] normalized ids (excluding +schema_id+)
|
|
79
|
+
def dependency_closure(schema_id)
|
|
80
|
+
seen = {}
|
|
81
|
+
queue = interface_targets(schema_id).dup
|
|
82
|
+
until queue.empty?
|
|
83
|
+
id = queue.shift
|
|
84
|
+
next if seen.key?(id)
|
|
85
|
+
|
|
86
|
+
seen[id] = true
|
|
87
|
+
queue.concat(interface_targets(id))
|
|
88
|
+
end
|
|
89
|
+
seen.keys
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Schemas that transitively depend on +schema_id+ — the reverse of
|
|
93
|
+
# {#dependency_closure}; used by change-impact analysis (S3).
|
|
94
|
+
#
|
|
95
|
+
# @param schema_id [String]
|
|
96
|
+
# @return [Array<String>] normalized ids, sorted
|
|
97
|
+
def dependents_of(schema_id)
|
|
98
|
+
target = normalize(schema_id)
|
|
99
|
+
@schemas.keys
|
|
100
|
+
.reject { |candidate| candidate == target }
|
|
101
|
+
.select do |candidate|
|
|
102
|
+
dependency_closure(candidate).include?(target)
|
|
103
|
+
end
|
|
104
|
+
.sort
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def add_schema(schema)
|
|
110
|
+
key = normalize(schema.id)
|
|
111
|
+
@schemas[key] = true
|
|
112
|
+
@elements[key] = declared_elements(schema)
|
|
113
|
+
targets = (schema.interfaces || [])
|
|
114
|
+
.filter_map { |i| i.schema&.id }
|
|
115
|
+
.map { |id| normalize(id) }
|
|
116
|
+
.uniq
|
|
117
|
+
@interfaces[key] = targets
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def declared_elements(schema)
|
|
121
|
+
index = {}
|
|
122
|
+
schema.children.each do |element|
|
|
123
|
+
if element.respond_to?(:id) && element.id
|
|
124
|
+
index[normalize(element.id)] =
|
|
125
|
+
true
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
index
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def visible_from_interfaces(key)
|
|
132
|
+
(@interfaces[key] || []).flat_map do |target|
|
|
133
|
+
@elements[target]&.keys || []
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def normalize(id)
|
|
138
|
+
id&.downcase
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
data/lib/suma/express_schema.rb
CHANGED
|
@@ -7,47 +7,34 @@ module Suma
|
|
|
7
7
|
class ExpressSchema
|
|
8
8
|
module Type
|
|
9
9
|
RESOURCE = :resource
|
|
10
|
+
APPLICATION_PROTOCOL = :application_protocol
|
|
10
11
|
MODULE_ARM = :module_arm
|
|
11
12
|
MODULE_MIM = :module_mim
|
|
12
13
|
BUSINESS_OBJECT_MODEL = :business_object_model
|
|
13
14
|
CORE_MODEL = :core_model
|
|
14
15
|
STANDALONE = :standalone
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"/modules/" => :MODULE_ARM,
|
|
25
|
-
"/core_model/" => :CORE_MODEL,
|
|
26
|
-
}.freeze
|
|
27
|
-
|
|
28
|
-
def self.classify(id:, path:)
|
|
29
|
-
name = id&.downcase || ""
|
|
30
|
-
|
|
31
|
-
ID_SUFFIXES.each do |suffix, type|
|
|
32
|
-
return const_get(type) if name.end_with?(suffix)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
path_str = path.to_s
|
|
36
|
-
PATH_SEGMENTS.each do |segment, type|
|
|
37
|
-
return const_get(type) if path_str.include?(segment)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
STANDALONE
|
|
17
|
+
# Classify a schema under a schema profile (default: ISO 10303).
|
|
18
|
+
#
|
|
19
|
+
# @param id [String, nil] schema identifier
|
|
20
|
+
# @param path [String, Pathname, nil] schema file path
|
|
21
|
+
# @param profile [SchemaProfile, Symbol, String, nil] schema profile
|
|
22
|
+
# @return [Symbol] a Type constant value
|
|
23
|
+
def self.classify(id:, path:, profile: nil)
|
|
24
|
+
SchemaProfile.fetch(profile).classify(id: id, path: path)
|
|
41
25
|
end
|
|
42
26
|
end
|
|
43
27
|
|
|
44
28
|
attr_accessor :path, :id, :parsed, :output_path, :is_standalone_file
|
|
45
29
|
|
|
46
|
-
def initialize(id:, path:, output_path:, is_standalone_file: false
|
|
30
|
+
def initialize(id:, path:, output_path:, is_standalone_file: false,
|
|
31
|
+
profile: nil, cache: nil)
|
|
47
32
|
@path = Pathname.new(path).expand_path
|
|
48
33
|
@id = id
|
|
49
34
|
@output_path = output_path
|
|
50
35
|
@is_standalone_file = is_standalone_file
|
|
36
|
+
@profile = profile
|
|
37
|
+
@cache = cache
|
|
51
38
|
end
|
|
52
39
|
|
|
53
40
|
def type
|
|
@@ -57,7 +44,8 @@ module Suma
|
|
|
57
44
|
def parsed
|
|
58
45
|
return @parsed if @parsed
|
|
59
46
|
|
|
60
|
-
|
|
47
|
+
source = File.read(@path.to_s, encoding: "UTF-8")
|
|
48
|
+
@parsed = @cache&.fetch_parsed(source) || parse_and_cache(source)
|
|
61
49
|
Utils.log "Loaded EXPRESS schema: #{path}"
|
|
62
50
|
@id = @parsed.schemas.first.id
|
|
63
51
|
@parsed
|
|
@@ -98,8 +86,14 @@ module Suma
|
|
|
98
86
|
|
|
99
87
|
private
|
|
100
88
|
|
|
89
|
+
def parse_and_cache(source)
|
|
90
|
+
repository = Expressir::Express::Parser.from_file(@path.to_s)
|
|
91
|
+
@cache&.store_parsed(source, repository)
|
|
92
|
+
repository
|
|
93
|
+
end
|
|
94
|
+
|
|
101
95
|
def classify
|
|
102
|
-
Type.classify(id: @id, path: @path)
|
|
96
|
+
Type.classify(id: @id, path: @path, profile: @profile)
|
|
103
97
|
end
|
|
104
98
|
end
|
|
105
99
|
end
|