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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +34 -0
  3. data/.gitignore +3 -0
  4. data/Gemfile +1 -1
  5. data/TODO.eengine/00-overview.md +39 -0
  6. data/TODO.eengine/01-resolution-diagnostics.md +30 -0
  7. data/TODO.eengine/02-check-catalog-completion.md +53 -0
  8. data/TODO.eengine/03-entity-computations.md +22 -0
  9. data/TODO.eengine/04-shtolo-rule-audit.md +32 -0
  10. data/TODO.eengine/05-shtolo-longform-flatten.md +12 -0
  11. data/TODO.eengine/06-population-p21.md +34 -0
  12. data/TODO.eengine/07-schema-compare-patch.md +25 -0
  13. data/TODO.eengine/08-official-corpus-gate.md +7 -0
  14. data/TODO.eengine/09-mim-mapping-parser.md +9 -0
  15. data/TODO.parity-ee/00-overview.md +1 -1
  16. data/TODO.sota/00-overview.md +57 -0
  17. data/TODO.sota/01-type-checking.md +115 -0
  18. data/TODO.sota/02-evaluator.md +86 -0
  19. data/TODO.sota/03-conformance.md +53 -0
  20. data/TODO.sota/04-lsp.md +70 -0
  21. data/TODO.sota/05-interop.md +55 -0
  22. data/TODO.sota/06-dialects.md +57 -0
  23. data/TODO.sota/07-reference-artifacts.md +55 -0
  24. data/TODO.suma-improvements/07-part28-xml.md +10 -13
  25. data/docs/_guides/interop/index.adoc +25 -0
  26. data/docs/_guides/interop/json-schema.adoc +88 -0
  27. data/docs/_guides/interop/owl-rdf.adoc +87 -0
  28. data/docs/_guides/reference/checker-notes.adoc +55 -0
  29. data/docs/_guides/reference/checker.adoc +36 -0
  30. data/docs/_guides/reference/evaluator.adoc +40 -0
  31. data/docs/_guides/reference/grammar-crosswalk.adoc +299 -0
  32. data/docs/_guides/reference/index.adoc +22 -0
  33. data/docs/_guides/reference/lsp.adoc +41 -0
  34. data/docs/_guides/reference/p21.adoc +30 -0
  35. data/docs/_guides/reference/shtolo.adoc +36 -0
  36. data/exe/expressir-lsp +6 -0
  37. data/expressir.gemspec +2 -2
  38. data/lib/expressir/3.2/expressir_core.so +0 -0
  39. data/lib/expressir/3.3/expressir_core.so +0 -0
  40. data/lib/expressir/3.4/expressir_core.so +0 -0
  41. data/lib/expressir/4.0/expressir_core.so +0 -0
  42. data/lib/expressir/cli.rb +25 -0
  43. data/lib/expressir/commands/check.rb +3 -1
  44. data/lib/expressir/commands/export_json_schema.rb +29 -0
  45. data/lib/expressir/commands/export_owl.rb +26 -0
  46. data/lib/expressir/commands/smrl_xml_index.rb +25 -0
  47. data/lib/expressir/commands/validate.rb +3 -0
  48. data/lib/expressir/commands.rb +3 -0
  49. data/lib/expressir/conformance/trend.rb +100 -0
  50. data/lib/expressir/conformance.rb +8 -0
  51. data/lib/expressir/eval/environment.rb +83 -0
  52. data/lib/expressir/eval/expression.rb +610 -0
  53. data/lib/expressir/eval/interpreter.rb +316 -0
  54. data/lib/expressir/eval/rules.rb +88 -0
  55. data/lib/expressir/eval/values.rb +74 -0
  56. data/lib/expressir/eval.rb +17 -0
  57. data/lib/expressir/express/builder.rb +17 -5
  58. data/lib/expressir/express/builders/entity_decl_builder.rb +17 -5
  59. data/lib/expressir/express/builders/expression_builder.rb +9 -1
  60. data/lib/expressir/express/checker.rb +970 -5
  61. data/lib/expressir/express/dialect.rb +44 -0
  62. data/lib/expressir/express/formatters/declarations_formatter.rb +21 -14
  63. data/lib/expressir/express/formatters/expressions_formatter.rb +4 -1
  64. data/lib/expressir/express/parser.rb +9 -4
  65. data/lib/expressir/express/pretty_formatter.rb +9 -14
  66. data/lib/expressir/express/schema_compare.rb +194 -0
  67. data/lib/expressir/express/shtolo.rb +194 -63
  68. data/lib/expressir/express.rb +3 -0
  69. data/lib/expressir/interop/json_schema.rb +195 -0
  70. data/lib/expressir/interop/owl.rb +216 -0
  71. data/lib/expressir/interop.rb +13 -0
  72. data/lib/expressir/lsp/document.rb +226 -0
  73. data/lib/expressir/lsp/server.rb +184 -0
  74. data/lib/expressir/lsp/workspace.rb +402 -0
  75. data/lib/expressir/lsp.rb +11 -0
  76. data/lib/expressir/model/declarations/entity.rb +132 -0
  77. data/lib/expressir/model/declarations/schema.rb +37 -6
  78. data/lib/expressir/p21/builder.rb +92 -0
  79. data/lib/expressir/p21/grammar.rb +107 -0
  80. data/lib/expressir/p21/parser.rb +16 -0
  81. data/lib/expressir/p21/population.rb +35 -0
  82. data/lib/expressir/p21/validator.rb +213 -0
  83. data/lib/expressir/p21.rb +17 -0
  84. data/lib/expressir/reference/grammar_crosswalk.rb +122 -0
  85. data/lib/expressir/reference/grammar_crosswalk.yml +800 -0
  86. data/lib/expressir/reference/note_catalog.rb +54 -0
  87. data/lib/expressir/reference.rb +10 -0
  88. data/lib/expressir/version.rb +1 -1
  89. data/lib/expressir.rb +5 -0
  90. data/lib/tasks/conformance.rake +27 -0
  91. data/lib/tasks/reference.rake +25 -0
  92. 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 closure[original]&.entities&.find { |e| e.id == original }
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 << build_dependent_rule if @reference_entities.any?
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
- seen_first = {} # downcase id => owning schema (first occurrence)
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 seen_first.key?(key)
179
- final = "#{schema.id.safe_downcase}_dot_#{decl.id}"
180
- own[key] = final
181
- global[key] = final
182
- else
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
- def build_dependent_rule
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
- src = <<~EXP
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 |t|
482
- t.underlying_type.is_a?(Model::DataTypes::Select) && t.underlying_type.extensible
483
- end.each do |base|
484
- sel = base.underlying_type
485
-
486
- # all-extenders for GENERIC_ENTITY selects (WG12 requirement,
487
- # #32): the extenders are the entity types themselves — fold in
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
- extensions = schema.types.select do |t|
500
- t.underlying_type.is_a?(Model::DataTypes::Select) &&
501
- !t.underlying_type.extensible &&
502
- t.underlying_type.based_on&.id&.safe_downcase == base.id.safe_downcase
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
- merged_items = sel.items +
505
- extensions.flat_map { |x| x.underlying_type.items }
506
- seen_ids = {}
507
- uniq = merged_items.select do |item|
508
- key = item.id.safe_downcase
509
- next false if seen_ids.key?(key) || key.empty?
510
-
511
- seen_ids[key] = true
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
- sel.items = uniq
514
- sel.extensible = false
515
- sel.based_on = nil
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
- extensions.each do |ext|
518
- keep = ext.underlying_type.items.map { |i| i.id.upcase }
519
- exclusions = uniq.reject { |i| keep.include?(i.id.upcase) }
520
- base_ref = "#{@longform_name.upcase}.#{base.id.upcase}"
521
- ext.underlying_type = Model::DataTypes::Select.new(
522
- items: sel.items.map { |i| Model::References::SimpleReference.new(id: i.id) },
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 entity.abstract
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}"
@@ -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