expressir 2.1.29 → 2.1.31

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +98 -0
  3. data/.github/workflows/links.yml +100 -0
  4. data/.github/workflows/rake.yml +4 -0
  5. data/.github/workflows/release.yml +5 -0
  6. data/.github/workflows/validate_schemas.yml +1 -1
  7. data/.gitignore +3 -0
  8. data/.rubocop.yml +1 -1
  9. data/.rubocop_todo.yml +209 -55
  10. data/Gemfile +2 -1
  11. data/README.adoc +650 -83
  12. data/docs/Gemfile +12 -0
  13. data/docs/_config.yml +141 -0
  14. data/docs/_guides/changes/changes-format.adoc +778 -0
  15. data/docs/_guides/changes/importing-eengine.adoc +898 -0
  16. data/docs/_guides/changes/index.adoc +396 -0
  17. data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
  18. data/docs/_guides/changes/validating-changes.adoc +681 -0
  19. data/docs/_guides/cli/benchmark-performance.adoc +834 -0
  20. data/docs/_guides/cli/coverage-analysis.adoc +921 -0
  21. data/docs/_guides/cli/format-schemas.adoc +547 -0
  22. data/docs/_guides/cli/index.adoc +8 -0
  23. data/docs/_guides/cli/managing-changes.adoc +927 -0
  24. data/docs/_guides/cli/validate-ascii.adoc +645 -0
  25. data/docs/_guides/cli/validate-schemas.adoc +534 -0
  26. data/docs/_guides/index.adoc +165 -0
  27. data/docs/_guides/ler/creating-packages.adoc +664 -0
  28. data/docs/_guides/ler/index.adoc +305 -0
  29. data/docs/_guides/ler/loading-packages.adoc +707 -0
  30. data/docs/_guides/ler/package-formats.adoc +748 -0
  31. data/docs/_guides/ler/querying-packages.adoc +826 -0
  32. data/docs/_guides/ler/validating-packages.adoc +750 -0
  33. data/docs/_guides/liquid/basic-templates.adoc +813 -0
  34. data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
  35. data/docs/_guides/liquid/drops-reference.adoc +829 -0
  36. data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
  37. data/docs/_guides/liquid/index.adoc +468 -0
  38. data/docs/_guides/manifests/creating-manifests.adoc +483 -0
  39. data/docs/_guides/manifests/index.adoc +307 -0
  40. data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
  41. data/docs/_guides/manifests/validating-manifests.adoc +713 -0
  42. data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
  43. data/docs/_guides/ruby-api/index.adoc +257 -0
  44. data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
  45. data/docs/_guides/ruby-api/search-engine.adoc +609 -0
  46. data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
  47. data/docs/_pages/data-model.adoc +665 -0
  48. data/docs/_pages/express-language.adoc +506 -0
  49. data/docs/_pages/getting-started.adoc +414 -0
  50. data/docs/_pages/index.adoc +116 -0
  51. data/docs/_pages/introduction.adoc +256 -0
  52. data/docs/_pages/ler-packages.adoc +837 -0
  53. data/docs/_pages/parsers.adoc +683 -0
  54. data/docs/_pages/schema-manifests.adoc +431 -0
  55. data/docs/_references/index.adoc +228 -0
  56. data/docs/_tutorials/creating-ler-package.adoc +735 -0
  57. data/docs/_tutorials/documentation-coverage.adoc +795 -0
  58. data/docs/_tutorials/index.adoc +221 -0
  59. data/docs/_tutorials/liquid-templates.adoc +806 -0
  60. data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
  61. data/docs/_tutorials/querying-schemas.adoc +751 -0
  62. data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
  63. data/docs/index.adoc +242 -0
  64. data/docs/lychee.toml +84 -0
  65. data/examples/demo_ler_usage.sh +86 -0
  66. data/examples/ler/README.md +111 -0
  67. data/examples/ler/simple_example.ler +0 -0
  68. data/examples/ler/simple_schema.exp +33 -0
  69. data/examples/ler_build.rb +75 -0
  70. data/examples/ler_cli.rb +79 -0
  71. data/examples/ler_demo_complete.rb +276 -0
  72. data/examples/ler_query.rb +91 -0
  73. data/examples/ler_query_examples.rb +305 -0
  74. data/examples/ler_stats.rb +81 -0
  75. data/examples/phase3_demo.rb +159 -0
  76. data/examples/query_demo_simple.rb +131 -0
  77. data/expressir.gemspec +2 -0
  78. data/lib/expressir/changes/schema_change.rb +32 -22
  79. data/lib/expressir/changes/{edition_change.rb → version_change.rb} +3 -3
  80. data/lib/expressir/cli.rb +12 -4
  81. data/lib/expressir/commands/changes_import_eengine.rb +2 -2
  82. data/lib/expressir/commands/changes_validate.rb +1 -1
  83. data/lib/expressir/commands/manifest.rb +427 -0
  84. data/lib/expressir/commands/package.rb +1274 -0
  85. data/lib/expressir/commands/validate.rb +70 -37
  86. data/lib/expressir/commands/validate_ascii.rb +607 -0
  87. data/lib/expressir/commands/validate_load.rb +88 -0
  88. data/lib/expressir/express/formatter.rb +5 -1
  89. data/lib/expressir/express/formatters/remark_item_formatter.rb +25 -0
  90. data/lib/expressir/express/parser.rb +33 -0
  91. data/lib/expressir/manifest/resolver.rb +213 -0
  92. data/lib/expressir/manifest/validator.rb +195 -0
  93. data/lib/expressir/model/declarations/entity.rb +6 -0
  94. data/lib/expressir/model/dependency_resolver.rb +270 -0
  95. data/lib/expressir/model/indexes/entity_index.rb +103 -0
  96. data/lib/expressir/model/indexes/reference_index.rb +148 -0
  97. data/lib/expressir/model/indexes/type_index.rb +149 -0
  98. data/lib/expressir/model/interface_validator.rb +384 -0
  99. data/lib/expressir/model/repository.rb +400 -5
  100. data/lib/expressir/model/repository_validator.rb +295 -0
  101. data/lib/expressir/model/search_engine.rb +525 -0
  102. data/lib/expressir/model.rb +4 -94
  103. data/lib/expressir/package/builder.rb +200 -0
  104. data/lib/expressir/package/metadata.rb +81 -0
  105. data/lib/expressir/package/reader.rb +165 -0
  106. data/lib/expressir/schema_manifest.rb +11 -1
  107. data/lib/expressir/version.rb +1 -1
  108. data/lib/expressir.rb +16 -3
  109. metadata +115 -5
  110. data/docs/benchmarking.adoc +0 -107
  111. data/docs/liquid_drops.adoc +0 -1547
@@ -0,0 +1,384 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expressir
4
+ module Model
5
+ # Validates EXPRESS interface statements (USE FROM, REFERENCE FROM)
6
+ # Single responsibility: Interface-specific validation only
7
+ # Handles schema reference validation, interface item validation, and type compatibility checks
8
+ class InterfaceValidator
9
+ # Error types returned by validation
10
+ ERROR_TYPES = {
11
+ missing_schema: :missing_schema_reference,
12
+ missing_item: :missing_interface_item,
13
+ type_mismatch: :interface_type_mismatch,
14
+ duplicate_alias: :duplicate_interface_alias,
15
+ self_reference: :self_reference_interface,
16
+ empty_items: :empty_interface_items,
17
+ }.freeze
18
+
19
+ attr_reader :repository, :schemas
20
+
21
+ # Initialize validator with a repository
22
+ # @param repository [Repository] Repository containing schemas to validate
23
+ def initialize(repository)
24
+ @repository = repository
25
+ @schemas = repository.schemas
26
+ end
27
+
28
+ # Main validation entry point
29
+ # @param options [Hash] Validation options
30
+ # @option options [Boolean] :detailed Show detailed reports with fix suggestions
31
+ # @option options [Boolean] :check_duplicates Check for duplicate aliases
32
+ # @option options [Boolean] :check_self_references Check for self-referencing interfaces
33
+ # @option options [Boolean] :strict Enable strict validation (warnings become errors)
34
+ # @return [Hash] Validation results with :valid?, :errors, :warnings, :interface_report
35
+ def validate(options = {})
36
+ errors = []
37
+ warnings = []
38
+ stats = { use_from: 0, reference_from: 0, total_items: 0 }
39
+
40
+ schemas.each do |schema|
41
+ validate_schema_interfaces(schema, errors, warnings, stats, options)
42
+ end
43
+
44
+ result = {
45
+ valid?: errors.empty?,
46
+ errors: errors,
47
+ warnings: warnings,
48
+ stats: stats,
49
+ }
50
+
51
+ if options[:detailed]
52
+ result[:interface_report] =
53
+ build_detailed_report(errors, warnings, stats)
54
+ end
55
+
56
+ result
57
+ end
58
+
59
+ # Validate interfaces in a specific schema
60
+ # @param schema_name [String] Schema name
61
+ # @param options [Hash] Validation options
62
+ # @return [Hash] Validation results for the schema
63
+ def validate_schema(schema_name, options = {})
64
+ schema = find_schema(schema_name)
65
+ unless schema
66
+ return { valid?: false,
67
+ errors: [{ type: :schema_not_found,
68
+ message: "Schema '#{schema_name}' not found" }] }
69
+ end
70
+
71
+ errors = []
72
+ warnings = []
73
+ stats = { use_from: 0, reference_from: 0, total_items: 0 }
74
+
75
+ validate_schema_interfaces(schema, errors, warnings, stats, options)
76
+
77
+ {
78
+ valid?: errors.empty?,
79
+ errors: errors,
80
+ warnings: warnings,
81
+ stats: stats,
82
+ }
83
+ end
84
+
85
+ private
86
+
87
+ # Validate all interfaces in a schema
88
+ # @param schema [Declarations::Schema] Schema to validate
89
+ # @param errors [Array] Accumulator for errors
90
+ # @param warnings [Array] Accumulator for warnings
91
+ # @param stats [Hash] Statistics accumulator
92
+ # @param options [Hash] Validation options
93
+ # @return [void]
94
+ def validate_schema_interfaces(schema, errors, warnings, stats, options)
95
+ return unless schema.interfaces
96
+
97
+ schema.interfaces.each do |interface|
98
+ case interface.kind
99
+ when Declarations::Interface::USE
100
+ stats[:use_from] += 1
101
+ validate_use_from(schema, interface, errors, warnings, options)
102
+ when Declarations::Interface::REFERENCE
103
+ stats[:reference_from] += 1
104
+ validate_reference_from(schema, interface, errors, warnings,
105
+ options)
106
+ end
107
+
108
+ stats[:total_items] += interface.items.size
109
+ end
110
+
111
+ # Additional validations if enabled
112
+ if options[:check_duplicates]
113
+ check_duplicate_aliases(schema, errors)
114
+ end
115
+
116
+ if options[:check_self_references]
117
+ check_self_references(schema, warnings)
118
+ end
119
+ end
120
+
121
+ # Validate USE FROM interface
122
+ # @param schema [Declarations::Schema] Source schema
123
+ # @param interface [Declarations::Interface] Interface to validate
124
+ # @param errors [Array] Accumulator for errors
125
+ # @param warnings [Array] Accumulator for warnings
126
+ # @param options [Hash] Validation options
127
+ # @return [void]
128
+ def validate_use_from(schema, interface, errors, warnings, _options)
129
+ referenced_schema = find_schema(interface.schema.id)
130
+
131
+ if referenced_schema.nil?
132
+ errors << build_error(
133
+ type: ERROR_TYPES[:missing_schema],
134
+ schema: schema.id,
135
+ interface_type: "USE FROM",
136
+ referenced_schema: interface.schema.id,
137
+ message: "Schema '#{schema.id}' uses non-existent schema '#{interface.schema.id}'",
138
+ fix_suggestion: "Ensure schema '#{interface.schema.id}' exists in the repository or remove the USE FROM statement",
139
+ )
140
+ return
141
+ end
142
+
143
+ # Validate interface items
144
+ validate_interface_items(schema, interface, referenced_schema, errors,
145
+ warnings, "USE FROM")
146
+
147
+ # Warn if USE FROM has no items (imports everything)
148
+ if interface.items.empty?
149
+ warnings << {
150
+ type: ERROR_TYPES[:empty_items],
151
+ schema: schema.id,
152
+ interface_type: "USE FROM",
153
+ referenced_schema: interface.schema.id,
154
+ message: "USE FROM imports all items from '#{interface.schema.id}' (consider specifying explicit items)",
155
+ fix_suggestion: "Add explicit item list: USE FROM #{interface.schema.id} (item1, item2, ...);",
156
+ }
157
+ end
158
+ end
159
+
160
+ # Validate REFERENCE FROM interface
161
+ # @param schema [Declarations::Schema] Source schema
162
+ # @param interface [Declarations::Interface] Interface to validate
163
+ # @param errors [Array] Accumulator for errors
164
+ # @param warnings [Array] Accumulator for warnings
165
+ # @param options [Hash] Validation options
166
+ # @return [void]
167
+ def validate_reference_from(schema, interface, errors, warnings, _options)
168
+ referenced_schema = find_schema(interface.schema.id)
169
+
170
+ if referenced_schema.nil?
171
+ errors << build_error(
172
+ type: ERROR_TYPES[:missing_schema],
173
+ schema: schema.id,
174
+ interface_type: "REFERENCE FROM",
175
+ referenced_schema: interface.schema.id,
176
+ message: "Schema '#{schema.id}' references non-existent schema '#{interface.schema.id}'",
177
+ fix_suggestion: "Ensure schema '#{interface.schema.id}' exists in the repository or remove the REFERENCE FROM statement",
178
+ )
179
+ return
180
+ end
181
+
182
+ # Validate interface items
183
+ validate_interface_items(schema, interface, referenced_schema, errors,
184
+ warnings, "REFERENCE FROM")
185
+
186
+ # REFERENCE FROM should always have explicit items
187
+ if interface.items.empty?
188
+ errors << build_error(
189
+ type: ERROR_TYPES[:empty_items],
190
+ schema: schema.id,
191
+ interface_type: "REFERENCE FROM",
192
+ referenced_schema: interface.schema.id,
193
+ message: "REFERENCE FROM requires explicit item list",
194
+ fix_suggestion: "Add item list: REFERENCE FROM #{interface.schema.id} (item1, item2, ...);",
195
+ )
196
+ end
197
+ end
198
+
199
+ # Validate interface items exist in referenced schema
200
+ # @param schema [Declarations::Schema] Source schema
201
+ # @param interface [Declarations::Interface] Interface to validate
202
+ # @param referenced_schema [Declarations::Schema] Referenced schema
203
+ # @param errors [Array] Accumulator for errors
204
+ # @param warnings [Array] Accumulator for warnings
205
+ # @param interface_type [String] Type of interface (USE FROM or REFERENCE FROM)
206
+ # @return [void]
207
+ def validate_interface_items(schema, interface, referenced_schema,
208
+ errors, _warnings, interface_type)
209
+ interface.items.each do |item|
210
+ ref_id = item.ref.id.safe_downcase
211
+ item_type = find_item_type(referenced_schema, ref_id)
212
+
213
+ unless item_type
214
+ errors << build_error(
215
+ type: ERROR_TYPES[:missing_item],
216
+ schema: schema.id,
217
+ interface_type: interface_type,
218
+ referenced_schema: interface.schema.id,
219
+ item: item.ref.id,
220
+ message: "Interface item '#{item.ref.id}' not found in schema '#{interface.schema.id}'",
221
+ fix_suggestion: "Remove '#{item.ref.id}' from interface or check spelling. Available items in '#{interface.schema.id}': #{list_available_items(referenced_schema)}",
222
+ )
223
+ end
224
+ end
225
+ end
226
+
227
+ # Check for duplicate interface aliases
228
+ # @param schema [Declarations::Schema] Schema to check
229
+ # @param errors [Array] Accumulator for errors
230
+ # @return [void]
231
+ def check_duplicate_aliases(schema, errors)
232
+ return unless schema.interfaces
233
+
234
+ aliases = {}
235
+ schema.interfaces.each do |interface|
236
+ interface.items.each do |item|
237
+ alias_id = (item.id || item.ref.id).safe_downcase
238
+ if aliases[alias_id]
239
+ errors << build_error(
240
+ type: ERROR_TYPES[:duplicate_alias],
241
+ schema: schema.id,
242
+ alias: alias_id,
243
+ message: "Duplicate interface alias '#{item.id || item.ref.id}' in schema '#{schema.id}'",
244
+ fix_suggestion: "Use unique aliases: USE FROM schema (item AS unique_name)",
245
+ )
246
+ else
247
+ aliases[alias_id] = true
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ # Check for self-referencing interfaces
254
+ # @param schema [Declarations::Schema] Schema to check
255
+ # @param warnings [Array] Accumulator for warnings
256
+ # @return [void]
257
+ def check_self_references(schema, warnings)
258
+ return unless schema.interfaces
259
+
260
+ schema.interfaces.each do |interface|
261
+ if interface.schema.id.safe_downcase == schema.id.safe_downcase
262
+ warnings << {
263
+ type: ERROR_TYPES[:self_reference],
264
+ schema: schema.id,
265
+ message: "Schema '#{schema.id}' references itself (likely unnecessary)",
266
+ fix_suggestion: "Remove self-referencing interface statement",
267
+ }
268
+ end
269
+ end
270
+ end
271
+
272
+ # Find schema by name
273
+ # @param schema_name [String] Schema name
274
+ # @return [Declarations::Schema, nil] Found schema or nil
275
+ def find_schema(schema_name)
276
+ schemas.find { |s| s.id.safe_downcase == schema_name.safe_downcase }
277
+ end
278
+
279
+ # Find item type in schema
280
+ # @param schema [Declarations::Schema] Schema to search
281
+ # @param ref_id [String] Item reference ID (normalized)
282
+ # @return [Symbol, nil] Item type or nil if not found
283
+ def find_item_type(schema, ref_id)
284
+ return :entity if schema.entities&.any? do |e|
285
+ e.id.safe_downcase == ref_id
286
+ end
287
+ return :type if schema.types&.any? { |t| t.id.safe_downcase == ref_id }
288
+ return :function if schema.functions&.any? do |f|
289
+ f.id.safe_downcase == ref_id
290
+ end
291
+ return :procedure if schema.procedures&.any? do |p|
292
+ p.id.safe_downcase == ref_id
293
+ end
294
+ return :constant if schema.constants&.any? do |c|
295
+ c.id.safe_downcase == ref_id
296
+ end
297
+ return :rule if schema.rules&.any? { |r| r.id.safe_downcase == ref_id }
298
+
299
+ nil
300
+ end
301
+
302
+ # List available items in schema
303
+ # @param schema [Declarations::Schema] Schema to list items from
304
+ # @param limit [Integer] Maximum number of items to list
305
+ # @return [String] Comma-separated list of items
306
+ def list_available_items(schema, limit = 10)
307
+ items = []
308
+ items.concat(schema.entities.map(&:id)) if schema.entities
309
+ items.concat(schema.types.map(&:id)) if schema.types
310
+ items.concat(schema.functions.map(&:id)) if schema.functions
311
+ items.concat(schema.procedures.map(&:id)) if schema.procedures
312
+ items.concat(schema.constants.map(&:id)) if schema.constants
313
+
314
+ items = items.take(limit)
315
+ result = items.join(", ")
316
+ result += ", ..." if items.size == limit
317
+ result
318
+ end
319
+
320
+ # Build error hash with consistent structure
321
+ # @param params [Hash] Error parameters
322
+ # @return [Hash] Error hash
323
+ def build_error(**params)
324
+ params
325
+ end
326
+
327
+ # Build detailed report from errors and warnings
328
+ # @param errors [Array<Hash>] Validation errors
329
+ # @param warnings [Array<Hash>] Validation warnings
330
+ # @param stats [Hash] Statistics
331
+ # @return [String] Formatted report
332
+ def build_detailed_report(errors, warnings, stats)
333
+ report = []
334
+ report << ("=" * 80)
335
+ report << "Interface Validation Report"
336
+ report << ("=" * 80)
337
+ report << ""
338
+
339
+ # Statistics
340
+ report << "Statistics:"
341
+ report << " Total USE FROM statements: #{stats[:use_from]}"
342
+ report << " Total REFERENCE FROM statements: #{stats[:reference_from]}"
343
+ report << " Total interface items: #{stats[:total_items]}"
344
+ report << ""
345
+
346
+ # Errors
347
+ if errors.any?
348
+ report << "Errors (#{errors.size}):"
349
+ report << ("-" * 80)
350
+ errors.each_with_index do |error, i|
351
+ report << "#{i + 1}. [#{error[:type]}] #{error[:message]}"
352
+ report << " Schema: #{error[:schema]}" if error[:schema]
353
+ report << " Referenced Schema: #{error[:referenced_schema]}" if error[:referenced_schema]
354
+ report << " Item: #{error[:item]}" if error[:item]
355
+ report << " Fix: #{error[:fix_suggestion]}" if error[:fix_suggestion]
356
+ report << ""
357
+ end
358
+ else
359
+ report << "✓ No errors found"
360
+ report << ""
361
+ end
362
+
363
+ # Warnings
364
+ if warnings.any?
365
+ report << "Warnings (#{warnings.size}):"
366
+ report << ("-" * 80)
367
+ warnings.each_with_index do |warning, i|
368
+ report << "#{i + 1}. [#{warning[:type]}] #{warning[:message]}"
369
+ report << " Schema: #{warning[:schema]}" if warning[:schema]
370
+ report << " Referenced Schema: #{warning[:referenced_schema]}" if warning[:referenced_schema]
371
+ report << " Fix: #{warning[:fix_suggestion]}" if warning[:fix_suggestion]
372
+ report << ""
373
+ end
374
+ else
375
+ report << "✓ No warnings"
376
+ report << ""
377
+ end
378
+
379
+ report << ("=" * 80)
380
+ report.join("\n")
381
+ end
382
+ end
383
+ end
384
+ end