expressir 2.4.31-aarch64-linux → 2.4.36-aarch64-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
|
@@ -130,16 +130,20 @@ module Expressir
|
|
|
130
130
|
schema.interfaces.each do |iface|
|
|
131
131
|
next unless iface.kind == Model::Declarations::Interface::REFERENCE
|
|
132
132
|
|
|
133
|
+
iface_schema = iface.schema.is_a?(String) ? iface.schema : iface.schema&.id
|
|
134
|
+
target = closure[iface_schema&.safe_downcase]
|
|
133
135
|
iface.items.each do |item|
|
|
134
136
|
original = original_name(item, schema)
|
|
135
|
-
next unless
|
|
137
|
+
next unless target&.entities&.find do |e|
|
|
138
|
+
e.id.safe_downcase == original.safe_downcase
|
|
139
|
+
end
|
|
136
140
|
|
|
137
141
|
@reference_entities << original.safe_downcase
|
|
138
142
|
end
|
|
139
143
|
end
|
|
140
144
|
end
|
|
141
145
|
|
|
142
|
-
declarations
|
|
146
|
+
declarations.concat(build_dependent_declarations) if @reference_entities.any?
|
|
143
147
|
|
|
144
148
|
Model::Declarations::Schema.new(
|
|
145
149
|
id: @longform_name,
|
|
@@ -166,7 +170,18 @@ module Expressir
|
|
|
166
170
|
# OWN schema's id, and the global map redirects the bare name so
|
|
167
171
|
# declarations from third schemas resolve to the first occurrence.
|
|
168
172
|
def plan_ids(schemas)
|
|
169
|
-
|
|
173
|
+
# G.NM.1: EVERY clashing declaration is prefixed (the annex
|
|
174
|
+
# example munges farming_dot_dog AND pet_shows_dot_dog), so the
|
|
175
|
+
# clash set is computed over the whole population first.
|
|
176
|
+
counts = Hash.new(0)
|
|
177
|
+
schemas.each do |schema|
|
|
178
|
+
all_decls(schema).each do |decl|
|
|
179
|
+
next unless decl.respond_to?(:id) && decl.id
|
|
180
|
+
|
|
181
|
+
counts[decl.id.safe_downcase] += 1
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
170
185
|
global = {} # downcase id => munged final
|
|
171
186
|
own_by_schema = {} # schema => {downcase id => final id}
|
|
172
187
|
schemas.each do |schema|
|
|
@@ -175,14 +190,11 @@ module Expressir
|
|
|
175
190
|
next unless decl.respond_to?(:id) && decl.id
|
|
176
191
|
|
|
177
192
|
key = decl.id.safe_downcase
|
|
178
|
-
if
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
seen_first[key] = schema
|
|
184
|
-
own[key] = decl.id
|
|
185
|
-
end
|
|
193
|
+
own[key] = if counts[key] > 1
|
|
194
|
+
global[key] = "#{schema.id.safe_downcase}_dot_#{decl.id}"
|
|
195
|
+
else
|
|
196
|
+
decl.id
|
|
197
|
+
end
|
|
186
198
|
end
|
|
187
199
|
own_by_schema[schema] = own
|
|
188
200
|
end
|
|
@@ -253,11 +265,20 @@ module Expressir
|
|
|
253
265
|
current == current.upcase ? new.upcase : new
|
|
254
266
|
end
|
|
255
267
|
|
|
256
|
-
# G.1.6: parse-templates keep the annex rule/function verbatim
|
|
257
|
-
|
|
268
|
+
# G.1.6: parse-templates keep the annex rule/function verbatim;
|
|
269
|
+
# the pair is emitted together (the rule's WHERE calls the
|
|
270
|
+
# function, and the prune pass would drop an uncalled function).
|
|
271
|
+
def build_dependent_declarations
|
|
272
|
+
snippet = parse_schema_snippet(dependent_template_src)
|
|
273
|
+
rule = snippet.schemas.first.rules.first
|
|
274
|
+
fn = snippet.schemas.first.functions.first
|
|
275
|
+
[rule, fn].compact
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def dependent_template_src
|
|
258
279
|
list = @reference_entities.map(&:upcase).join(", ")
|
|
259
280
|
union = @reference_entities.join("+")
|
|
260
|
-
|
|
281
|
+
<<~EXP
|
|
261
282
|
SCHEMA __dependent__;
|
|
262
283
|
RULE #{DEPENDENT_RULE_NAME} FOR (#{list});
|
|
263
284
|
LOCAL
|
|
@@ -270,9 +291,62 @@ module Expressir
|
|
|
270
291
|
WHERE
|
|
271
292
|
wr1: dependently_instantiated(all_instances, set_of_input_types, previous_in_chain);
|
|
272
293
|
END_RULE;
|
|
294
|
+
|
|
295
|
+
FUNCTION dependently_instantiated(
|
|
296
|
+
set_of_input_instances : SET OF GENERIC:igen;
|
|
297
|
+
set_of_input_types : SET OF STRING;
|
|
298
|
+
previous_in_chain : LIST OF GENERIC:cgen): BOOLEAN;
|
|
299
|
+
LOCAL
|
|
300
|
+
number_of_input_instances : INTEGER;
|
|
301
|
+
number_of_referring_instances : INTEGER;
|
|
302
|
+
bag_of_referring_instances : BAG OF GENERIC:igen := [];
|
|
303
|
+
dependently_instantiated_flag : BOOLEAN;
|
|
304
|
+
previous_in_chain_plus : LIST OF GENERIC:cgen := [];
|
|
305
|
+
result : BOOLEAN := true;
|
|
306
|
+
set_of_types : SET OF STRING := [];
|
|
307
|
+
END_LOCAL;
|
|
308
|
+
IF EXISTS(set_of_input_instances) THEN
|
|
309
|
+
number_of_input_instances := SIZEOF(set_of_input_instances);
|
|
310
|
+
set_of_input_types := set_of_input_types + 'GENERIC';
|
|
311
|
+
REPEAT i := 1 TO number_of_input_instances;
|
|
312
|
+
bag_of_referring_instances := USEDIN(set_of_input_instances[i], '');
|
|
313
|
+
IF EXISTS(bag_of_referring_instances) THEN
|
|
314
|
+
number_of_referring_instances := SIZEOF(bag_of_referring_instances);
|
|
315
|
+
dependently_instantiated_flag := false;
|
|
316
|
+
REPEAT j := 1 TO number_of_referring_instances;
|
|
317
|
+
set_of_types := TYPEOF(bag_of_referring_instances[j]);
|
|
318
|
+
IF set_of_types <= set_of_input_types THEN
|
|
319
|
+
IF NOT (bag_of_referring_instances[j] IN previous_in_chain) THEN
|
|
320
|
+
previous_in_chain_plus := previous_in_chain +
|
|
321
|
+
set_of_input_instances[i];
|
|
322
|
+
IF dependently_instantiated([bag_of_referring_instances[j]],
|
|
323
|
+
set_of_input_types,
|
|
324
|
+
previous_in_chain_plus) THEN
|
|
325
|
+
dependently_instantiated_flag := true;
|
|
326
|
+
ESCAPE;
|
|
327
|
+
ELSE
|
|
328
|
+
SKIP;
|
|
329
|
+
END_IF;
|
|
330
|
+
END_IF;
|
|
331
|
+
ELSE
|
|
332
|
+
dependently_instantiated_flag := true;
|
|
333
|
+
ESCAPE;
|
|
334
|
+
END_IF;
|
|
335
|
+
END_REPEAT;
|
|
336
|
+
IF NOT dependently_instantiated_flag THEN
|
|
337
|
+
RETURN(false);
|
|
338
|
+
END_IF;
|
|
339
|
+
ELSE
|
|
340
|
+
RETURN(false);
|
|
341
|
+
END_IF;
|
|
342
|
+
END_REPEAT;
|
|
343
|
+
ELSE
|
|
344
|
+
RETURN(false);
|
|
345
|
+
END_IF;
|
|
346
|
+
RETURN(true);
|
|
347
|
+
END_FUNCTION;
|
|
273
348
|
END_SCHEMA;
|
|
274
349
|
EXP
|
|
275
|
-
parse_schema_snippet(src).schemas.first.rules.first
|
|
276
350
|
end
|
|
277
351
|
|
|
278
352
|
def parse_schema_snippet(src)
|
|
@@ -295,10 +369,32 @@ module Expressir
|
|
|
295
369
|
convert_generic_entities!(artifact)
|
|
296
370
|
convert_renamed_attributes!(artifact)
|
|
297
371
|
rewrite_qualified_strings!(artifact)
|
|
372
|
+
add_provenance_remark!(artifact)
|
|
298
373
|
|
|
299
374
|
artifact
|
|
300
375
|
end
|
|
301
376
|
|
|
377
|
+
# G.1.10: optional embedded provenance remark — if any schema
|
|
378
|
+
# name is kept in the longform, ALL contributing schemas are
|
|
379
|
+
# recorded (`schema = <id> [schema_version_id = '<v>'];`, order
|
|
380
|
+
# insignificant).
|
|
381
|
+
def add_provenance_remark!(schema)
|
|
382
|
+
names = closure.values.map(&:id).sort
|
|
383
|
+
return if names.empty?
|
|
384
|
+
|
|
385
|
+
remarks = ["Original 2003 schemas:"] + names.map do |id|
|
|
386
|
+
contributing = closure.values.find { |s| s.id == id }
|
|
387
|
+
version = contributing&.version&.id
|
|
388
|
+
line = "schema = #{id};"
|
|
389
|
+
line = "schema = #{id} schema_version_id = '#{version}';" if version
|
|
390
|
+
line
|
|
391
|
+
end
|
|
392
|
+
schema.remark_items = [
|
|
393
|
+
Model::Declarations::RemarkItem.new(id: "provenance", remarks: remarks),
|
|
394
|
+
*Array(schema.remark_items),
|
|
395
|
+
]
|
|
396
|
+
end
|
|
397
|
+
|
|
302
398
|
# G.1.9 prune pass: the artifact keeps only what stays visible
|
|
303
399
|
# and reachable. Runs before the stage-2 rewrites so they never
|
|
304
400
|
# see declarations the longform would drop.
|
|
@@ -477,57 +573,95 @@ module Expressir
|
|
|
477
573
|
end
|
|
478
574
|
end
|
|
479
575
|
|
|
576
|
+
# G.2.2/G.2.4: every extensible/extending select becomes a
|
|
577
|
+
# non-extensible select whose items are the full domain in
|
|
578
|
+
# longform scope. Chains fold in dependency order (a node whose
|
|
579
|
+
# own BASED_ON is still pending is deferred), so an extending
|
|
580
|
+
# type that is itself extensible resolves against its base's
|
|
581
|
+
# folded domain.
|
|
582
|
+
# G.2.2/G.2.4: every extensible/extending select becomes a
|
|
583
|
+
# non-extensible select whose items are the full domain in
|
|
584
|
+
# longform scope. Chains fold in dependency order (a node whose
|
|
585
|
+
# own BASED_ON is still pending is deferred), so an extending
|
|
586
|
+
# type that is itself extensible resolves against its base's
|
|
587
|
+
# folded domain.
|
|
480
588
|
def resolve_extensible_selects!(schema)
|
|
481
|
-
schema.types.select do |
|
|
482
|
-
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
# every entity the artifact carries, so the longform select is
|
|
489
|
-
# complete against the closure. Flags stay as declared: the
|
|
490
|
-
# extensible/generic-entity markers are part of the shortform's
|
|
491
|
-
# semantics and are not expressir's to drop.
|
|
492
|
-
if sel.generic_entity
|
|
493
|
-
sel.items = schema.entities.map do |e|
|
|
494
|
-
Model::References::SimpleReference.new(id: e.id)
|
|
495
|
-
end
|
|
496
|
-
next
|
|
497
|
-
end
|
|
589
|
+
select_types = schema.types.select do |ty|
|
|
590
|
+
ty.underlying_type.is_a?(Model::DataTypes::Select)
|
|
591
|
+
end
|
|
592
|
+
select_types.select { |ty| sel_pending?(ty) }.each do |base|
|
|
593
|
+
fold_select_node(schema, select_types, base)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
498
596
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
597
|
+
def sel_pending?(select_type)
|
|
598
|
+
sel = select_type.underlying_type
|
|
599
|
+
sel.extensible || sel.based_on
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def fold_select_node(schema, select_types, base_type)
|
|
603
|
+
sel = base_type.underlying_type
|
|
604
|
+
return unless sel_pending?(base_type)
|
|
605
|
+
|
|
606
|
+
# all-extenders for GENERIC_ENTITY selects (WG12 requirement,
|
|
607
|
+
# #32): the extenders are the entity types themselves — fold in
|
|
608
|
+
# every entity the artifact carries, so the longform select is
|
|
609
|
+
# complete against the closure. Flags stay as declared: the
|
|
610
|
+
# extensible/generic-entity markers are part of the shortform's
|
|
611
|
+
# semantics and are not expressir's to drop.
|
|
612
|
+
if sel.generic_entity && !sel.based_on
|
|
613
|
+
sel.items = schema.entities.map do |e|
|
|
614
|
+
Model::References::SimpleReference.new(id: e.id)
|
|
503
615
|
end
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
616
|
+
return
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
own_items = Array(sel.items)
|
|
620
|
+
inherited = []
|
|
621
|
+
if sel.based_on
|
|
622
|
+
base_ty = select_types.find do |ty|
|
|
623
|
+
ty.id&.safe_downcase == sel.based_on&.id&.safe_downcase
|
|
512
624
|
end
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
625
|
+
fold_select_node(schema, select_types, base_ty) if base_ty && sel_pending?(base_ty)
|
|
626
|
+
inherited = base_ty ? Array(base_ty.underlying_type.items) : []
|
|
627
|
+
end
|
|
516
628
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
629
|
+
extenders = select_types.select do |ty|
|
|
630
|
+
!ty.equal?(base_type) &&
|
|
631
|
+
ty.underlying_type.based_on&.id&.safe_downcase == base_type.id&.safe_downcase
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
merged = inherited + own_items +
|
|
635
|
+
extenders.flat_map { |ty| Array(ty.underlying_type.items) }
|
|
636
|
+
seen = {}
|
|
637
|
+
uniq = merged.select do |item|
|
|
638
|
+
key = item.id.safe_downcase
|
|
639
|
+
next false if key.empty? || seen.key?(key)
|
|
640
|
+
|
|
641
|
+
seen[key] = true
|
|
642
|
+
end
|
|
643
|
+
sel.items = uniq
|
|
644
|
+
sel.extensible = false
|
|
645
|
+
sel.based_on = nil
|
|
646
|
+
|
|
647
|
+
extenders.each do |ty|
|
|
648
|
+
ext_sel = ty.underlying_type
|
|
649
|
+
keep = Array(ext_sel.items).map { |i| i.id.upcase }
|
|
650
|
+
base_ref = "#{@longform_name.upcase}.#{base_type.id.upcase}"
|
|
651
|
+
ty.underlying_type = Model::DataTypes::Select.new(
|
|
652
|
+
items: uniq.map { |i| Model::References::SimpleReference.new(id: i.id) },
|
|
653
|
+
)
|
|
654
|
+
ext_sel_items = ty.underlying_type.items
|
|
655
|
+
exclusions = uniq.reject { |i| keep.include?(i.id.upcase) }
|
|
656
|
+
ty.where_rules = exclusions.each_with_index.map do |item, i|
|
|
657
|
+
Model::Declarations::WhereRule.new(
|
|
658
|
+
id: "wr#{i + 1}",
|
|
659
|
+
expression: parse_expression(
|
|
660
|
+
"NOT ('#{base_ref}.#{item.id.upcase}' IN TYPEOF(SELF));",
|
|
661
|
+
),
|
|
523
662
|
)
|
|
524
|
-
ext.where_rules = exclusions.each_with_index.map do |item, i|
|
|
525
|
-
Model::Declarations::WhereRule.new(
|
|
526
|
-
id: "wr#{i + 1}",
|
|
527
|
-
expression: parse_expression("NOT ('#{base_ref}.#{item.id.upcase}' IN TYPEOF(SELF));"),
|
|
528
|
-
)
|
|
529
|
-
end
|
|
530
663
|
end
|
|
664
|
+
ext_sel_items
|
|
531
665
|
end
|
|
532
666
|
end
|
|
533
667
|
|
|
@@ -543,10 +677,7 @@ module Expressir
|
|
|
543
677
|
entity.supertype_expression =
|
|
544
678
|
entity.supertype_expression ? combine_andor(entity.supertype_expression, expr) : expr
|
|
545
679
|
end
|
|
546
|
-
if
|
|
547
|
-
# G.2.6: abstract entity ⇒ ABSTRACT SUPERTYPE (formatter
|
|
548
|
-
# renders from abstract + supertype status).
|
|
549
|
-
end
|
|
680
|
+
entity.abstract = true if sc.abstract
|
|
550
681
|
next if Array(sc.total_over).empty?
|
|
551
682
|
|
|
552
683
|
rule_name = "total_over_#{sc.id}"
|
data/lib/expressir/express.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Expressir
|
|
|
15
15
|
autoload :Checker, "#{__dir__}/express/checker"
|
|
16
16
|
autoload :Concatenator, "#{__dir__}/express/concatenator"
|
|
17
17
|
autoload :Core, "#{__dir__}/express/core"
|
|
18
|
+
autoload :Dialect, "#{__dir__}/express/dialect"
|
|
18
19
|
autoload :RemarkOverlay, "#{__dir__}/express/remark_overlay"
|
|
19
20
|
autoload :ModelTraversal, "#{__dir__}/express/model_traversal"
|
|
20
21
|
autoload :RefsOverlay, "#{__dir__}/express/refs_overlay"
|
|
@@ -43,6 +44,8 @@ module Expressir
|
|
|
43
44
|
autoload :SchemaPlainSourceFormatter,
|
|
44
45
|
"#{__dir__}/express/schema_plain_source_formatter"
|
|
45
46
|
autoload :SchemaSourceFormatter, "#{__dir__}/express/schema_source_formatter"
|
|
47
|
+
autoload :SchemaCompare, "#{__dir__}/express/schema_compare"
|
|
48
|
+
autoload :SchemaPatch, "#{__dir__}/express/schema_compare"
|
|
46
49
|
autoload :ScopeResolver, "#{__dir__}/express/scope_resolver"
|
|
47
50
|
autoload :LazyRepository, "#{__dir__}/express/lazy_repository"
|
|
48
51
|
autoload :SmrlXml, "#{__dir__}/express/smrl_xml"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Expressir
|
|
4
|
+
module Interop
|
|
5
|
+
# JSON Schema 2020-12 rendering of an EXPRESS schema. Decisions
|
|
6
|
+
# (docs/interop/json-schema.md): entities become object definitions
|
|
7
|
+
# composed by allOf over their subtype_of supertypes; explicit
|
|
8
|
+
# attributes are properties in declaration order; OPTIONAL
|
|
9
|
+
# attributes leave `required` and admit null; aggregates become
|
|
10
|
+
# arrays with bounds; SELECT becomes anyOf; ENUMERATION becomes a
|
|
11
|
+
# string enum; derived and inverse attributes are not exported
|
|
12
|
+
# (they are not part of the Part 21 exchange structure).
|
|
13
|
+
class JsonSchema
|
|
14
|
+
DRAFT = "https://json-schema.org/draft/2020-12/schema"
|
|
15
|
+
BASE_IRI = "https://expressir.org/json-schema"
|
|
16
|
+
|
|
17
|
+
def self.export(schema)
|
|
18
|
+
new(schema).export
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :schema
|
|
22
|
+
|
|
23
|
+
def initialize(schema)
|
|
24
|
+
@schema = schema
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def export
|
|
28
|
+
result = {
|
|
29
|
+
"$schema" => DRAFT,
|
|
30
|
+
"$id" => "#{BASE_IRI}/#{schema.id}",
|
|
31
|
+
"title" => schema.id.to_s,
|
|
32
|
+
"$defs" => definitions,
|
|
33
|
+
}
|
|
34
|
+
entities = Array(schema.entities)
|
|
35
|
+
result["anyOf"] = entities.map { |entity| reference(entity.id) } unless entities.empty?
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_json(pretty: true)
|
|
40
|
+
pretty ? JSON.pretty_generate(export) : JSON.generate(export)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def definitions
|
|
46
|
+
result = {}
|
|
47
|
+
Array(schema.types).each do |type|
|
|
48
|
+
result[type.id] = defined_type_schema(type)
|
|
49
|
+
end
|
|
50
|
+
Array(schema.entities).each do |entity|
|
|
51
|
+
result[entity.id] = entity_schema(entity)
|
|
52
|
+
end
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def defined_type_schema(type)
|
|
57
|
+
case type.underlying_type
|
|
58
|
+
when Model::DataTypes::Enumeration
|
|
59
|
+
enumeration_schema(type.underlying_type)
|
|
60
|
+
when Model::DataTypes::Select
|
|
61
|
+
select_schema(type.underlying_type)
|
|
62
|
+
else
|
|
63
|
+
type_schema(type.underlying_type)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def entity_schema(entity)
|
|
68
|
+
own = {
|
|
69
|
+
"type" => "object",
|
|
70
|
+
"properties" => explicit_attributes(entity).to_h do |attribute|
|
|
71
|
+
[attribute.id, attribute_schema(attribute)]
|
|
72
|
+
end,
|
|
73
|
+
}
|
|
74
|
+
required = explicit_attributes(entity).reject(&:optional)
|
|
75
|
+
.map(&:id)
|
|
76
|
+
own["required"] = required unless required.empty?
|
|
77
|
+
|
|
78
|
+
supertypes = Array(entity.subtype_of).map do |supertype|
|
|
79
|
+
reference(supertype.id)
|
|
80
|
+
end
|
|
81
|
+
return own.merge("title" => entity.id) if supertypes.empty?
|
|
82
|
+
|
|
83
|
+
{ "title" => entity.id, "allOf" => [*supertypes, own] }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def explicit_attributes(entity)
|
|
87
|
+
Array(entity.attributes)
|
|
88
|
+
.select { |attribute| attribute.instance_of?(Model::Declarations::Attribute) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def attribute_schema(attribute)
|
|
92
|
+
base = type_schema(attribute.type)
|
|
93
|
+
return base unless attribute.optional
|
|
94
|
+
|
|
95
|
+
{ "anyOf" => [base, { "type" => "null" }] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def type_schema(type)
|
|
99
|
+
case type
|
|
100
|
+
when Model::DataTypes::Integer then { "type" => "integer" }
|
|
101
|
+
when Model::DataTypes::Real, Model::DataTypes::Number
|
|
102
|
+
{ "type" => "number" }
|
|
103
|
+
when Model::DataTypes::String then { "type" => "string" }
|
|
104
|
+
when Model::DataTypes::Boolean then { "type" => "boolean" }
|
|
105
|
+
when Model::DataTypes::Logical
|
|
106
|
+
{ "enum" => [true, false, nil] }
|
|
107
|
+
when Model::DataTypes::Binary
|
|
108
|
+
{ "type" => "string", "pattern" => "\\A[01]*\\z" }
|
|
109
|
+
when Model::DataTypes::Enumeration
|
|
110
|
+
enumeration_schema(type)
|
|
111
|
+
when Model::DataTypes::Select
|
|
112
|
+
select_schema(type)
|
|
113
|
+
when Model::DataTypes::List, Model::DataTypes::Array,
|
|
114
|
+
Model::DataTypes::Set, Model::DataTypes::Bag
|
|
115
|
+
aggregate_schema(type)
|
|
116
|
+
when Model::References::SimpleReference then reference(type.id)
|
|
117
|
+
when nil then {}
|
|
118
|
+
else { "type" => "object" }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def enumeration_schema(enumeration)
|
|
123
|
+
{ "type" => "string",
|
|
124
|
+
"enum" => enumeration_values(enumeration).map(&:to_s) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def enumeration_values(enumeration, seen = [])
|
|
128
|
+
return [] if enumeration.nil? || seen.include?(enumeration.object_id)
|
|
129
|
+
|
|
130
|
+
seen += [enumeration.object_id]
|
|
131
|
+
values = []
|
|
132
|
+
base_type = resolve_reference(enumeration.based_on)
|
|
133
|
+
if base_type&.underlying_type.is_a?(Model::DataTypes::Enumeration)
|
|
134
|
+
values.concat(enumeration_values(base_type.underlying_type, seen))
|
|
135
|
+
end
|
|
136
|
+
values.concat(Array(enumeration.items).filter_map(&:id))
|
|
137
|
+
values.uniq
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def select_schema(select)
|
|
141
|
+
{ "anyOf" => select_schemas(select) }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def select_schemas(select, seen = [])
|
|
145
|
+
return [] if select.nil? || seen.include?(select.object_id)
|
|
146
|
+
|
|
147
|
+
seen += [select.object_id]
|
|
148
|
+
refs = []
|
|
149
|
+
base_type = resolve_reference(select.based_on)
|
|
150
|
+
if base_type&.underlying_type.is_a?(Model::DataTypes::Select)
|
|
151
|
+
refs.concat(select_schemas(base_type.underlying_type, seen))
|
|
152
|
+
end
|
|
153
|
+
refs.concat(Array(select.items).map { |item| type_schema(item) })
|
|
154
|
+
refs.uniq
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def aggregate_schema(type)
|
|
158
|
+
result = { "type" => "array",
|
|
159
|
+
"items" => type_schema(type.base_type) }
|
|
160
|
+
lower, upper = aggregate_bounds(type)
|
|
161
|
+
result["minItems"] = lower if lower
|
|
162
|
+
result["maxItems"] = upper if upper
|
|
163
|
+
result["uniqueItems"] = true if unique?(type)
|
|
164
|
+
result
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def aggregate_bounds(type)
|
|
168
|
+
[bound_value(type.bound1), bound_value(type.bound2)]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def bound_value(bound)
|
|
172
|
+
return nil unless bound.is_a?(Model::Literals::Integer)
|
|
173
|
+
|
|
174
|
+
bound.value.to_i
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def unique?(type)
|
|
178
|
+
type.is_a?(Model::DataTypes::Set) ||
|
|
179
|
+
(type.is_a?(Model::DataTypes::List) && type.unique)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def resolve_reference(reference)
|
|
183
|
+
return nil unless reference.respond_to?(:id) && reference.id
|
|
184
|
+
|
|
185
|
+
schema.find(reference.id)
|
|
186
|
+
rescue StandardError
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def reference(id)
|
|
191
|
+
{ "$ref" => "#/$defs/#{id}" }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|