kumi 0.0.6 → 0.0.7
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/CLAUDE.md +33 -176
- data/README.md +33 -2
- data/docs/SYNTAX.md +2 -7
- data/docs/features/array-broadcasting.md +1 -1
- data/docs/schema_metadata/broadcasts.md +53 -0
- data/docs/schema_metadata/cascades.md +45 -0
- data/docs/schema_metadata/declarations.md +54 -0
- data/docs/schema_metadata/dependencies.md +57 -0
- data/docs/schema_metadata/evaluation_order.md +29 -0
- data/docs/schema_metadata/examples.md +95 -0
- data/docs/schema_metadata/inferred_types.md +46 -0
- data/docs/schema_metadata/inputs.md +86 -0
- data/docs/schema_metadata.md +108 -0
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +52 -57
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +8 -8
- data/lib/kumi/analyzer/passes/input_collector.rb +2 -2
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +15 -16
- data/lib/kumi/analyzer/passes/toposorter.rb +23 -23
- data/lib/kumi/analyzer/passes/type_checker.rb +7 -9
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/analyzer/passes/type_inferencer.rb +24 -24
- data/lib/kumi/analyzer/passes/unsat_detector.rb +11 -13
- data/lib/kumi/analyzer.rb +5 -5
- data/lib/kumi/compiler.rb +39 -45
- data/lib/kumi/error_reporting.rb +1 -1
- data/lib/kumi/explain.rb +12 -0
- data/lib/kumi/export/node_registry.rb +2 -2
- data/lib/kumi/json_schema/generator.rb +63 -0
- data/lib/kumi/json_schema/validator.rb +25 -0
- data/lib/kumi/json_schema.rb +14 -0
- data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/declaration_reference_proxy.rb +3 -3
- data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +2 -2
- data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +14 -14
- data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_builder.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_field_proxy.rb +4 -4
- data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/nested_input.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/sugar.rb +1 -1
- data/lib/kumi/ruby_parser.rb +10 -0
- data/lib/kumi/schema.rb +10 -4
- data/lib/kumi/schema_instance.rb +6 -6
- data/lib/kumi/schema_metadata.rb +524 -0
- data/lib/kumi/vectorization_metadata.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +14 -0
- metadata +28 -15
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
@@ -0,0 +1,524 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
# Primary interface for extracting structured metadata from analyzed Kumi schemas.
|
5
|
+
#
|
6
|
+
# SchemaMetadata provides both processed semantic metadata (inputs, values, traits, functions)
|
7
|
+
# and raw analyzer state for advanced use cases. This interface is designed for building
|
8
|
+
# external tools like form generators, documentation systems, and schema analysis utilities.
|
9
|
+
#
|
10
|
+
# @example Basic usage
|
11
|
+
# class MySchema
|
12
|
+
# extend Kumi::Schema
|
13
|
+
# schema do
|
14
|
+
# input do
|
15
|
+
# integer :age, domain: 18..65
|
16
|
+
# string :name
|
17
|
+
# end
|
18
|
+
# trait :adult, (input.age >= 18)
|
19
|
+
# value :greeting, "Hello " + input.name
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# metadata = MySchema.schema_metadata
|
24
|
+
# puts metadata.inputs[:age][:domain] # => { type: :range, min: 18, max: 65, ... }
|
25
|
+
# puts metadata.traits[:adult][:condition] # => ">=(input.age, 18)"
|
26
|
+
#
|
27
|
+
# @example Tool integration
|
28
|
+
# def generate_form_fields(schema_class)
|
29
|
+
# metadata = schema_class.schema_metadata
|
30
|
+
# metadata.inputs.map do |field_name, field_info|
|
31
|
+
# create_input_field(field_name, field_info[:type], field_info[:domain])
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
class SchemaMetadata
|
35
|
+
# @param state_hash [Hash] Raw analyzer state from multi-pass analysis
|
36
|
+
# @param syntax_tree [Syntax::Root] Parsed AST of the schema definition
|
37
|
+
def initialize(state_hash, syntax_tree)
|
38
|
+
@state = state_hash
|
39
|
+
@syntax_tree = syntax_tree
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns processed input field metadata with normalized types and domains.
|
43
|
+
#
|
44
|
+
# Transforms raw input metadata from the analyzer into a clean, tool-friendly format
|
45
|
+
# with consistent type representation and domain constraint normalization.
|
46
|
+
#
|
47
|
+
# @return [Hash<Symbol, Hash>] Input field metadata keyed by field name
|
48
|
+
# @example
|
49
|
+
# metadata.inputs
|
50
|
+
# # => {
|
51
|
+
# # :age => { type: :integer, domain: { type: :range, min: 18, max: 65 }, required: true },
|
52
|
+
# # :name => { type: :string, required: true },
|
53
|
+
# # :items => { type: :array, required: true }
|
54
|
+
# # }
|
55
|
+
def inputs
|
56
|
+
@inputs ||= extract_inputs
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns processed value declaration metadata with dependencies and expressions.
|
60
|
+
#
|
61
|
+
# Extracts computed value information including type inference results, dependency
|
62
|
+
# relationships, and expression representations. Cascade expressions are expanded
|
63
|
+
# into structured condition/result pairs.
|
64
|
+
#
|
65
|
+
# @return [Hash<Symbol, Hash>] Value metadata keyed by declaration name
|
66
|
+
# @example
|
67
|
+
# metadata.values
|
68
|
+
# # => {
|
69
|
+
# # :tax_amount => {
|
70
|
+
# # type: :float,
|
71
|
+
# # dependencies: [:income, :tax_rate],
|
72
|
+
# # computed: true,
|
73
|
+
# # expression: "multiply(input.income, tax_rate)"
|
74
|
+
# # },
|
75
|
+
# # :status => {
|
76
|
+
# # type: :string,
|
77
|
+
# # dependencies: [:adult, :verified],
|
78
|
+
# # computed: true,
|
79
|
+
# # cascade: { conditions: [...], base: "default" }
|
80
|
+
# # }
|
81
|
+
# # }
|
82
|
+
def values
|
83
|
+
@values ||= extract_values
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns processed trait metadata with conditions and dependencies.
|
87
|
+
#
|
88
|
+
# Extracts boolean trait information including dependency relationships and
|
89
|
+
# human-readable condition expressions for documentation and analysis.
|
90
|
+
#
|
91
|
+
# @return [Hash<Symbol, Hash>] Trait metadata keyed by trait name
|
92
|
+
# @example
|
93
|
+
# metadata.traits
|
94
|
+
# # => {
|
95
|
+
# # :adult => {
|
96
|
+
# # type: :boolean,
|
97
|
+
# # dependencies: [:age],
|
98
|
+
# # condition: ">=(input.age, 18)"
|
99
|
+
# # },
|
100
|
+
# # :eligible => {
|
101
|
+
# # type: :boolean,
|
102
|
+
# # dependencies: [:adult, :verified, :score],
|
103
|
+
# # condition: "and(adult, verified, >(input.score, 80))"
|
104
|
+
# # }
|
105
|
+
# # }
|
106
|
+
def traits
|
107
|
+
@traits ||= extract_traits
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns function registry information for functions used in the schema.
|
111
|
+
#
|
112
|
+
# Analyzes all expressions in the schema to identify function calls and extracts
|
113
|
+
# their signatures from the function registry. Useful for documentation generation
|
114
|
+
# and validation tooling.
|
115
|
+
#
|
116
|
+
# @return [Hash<Symbol, Hash>] Function metadata keyed by function name
|
117
|
+
# @example
|
118
|
+
# metadata.functions
|
119
|
+
# # => {
|
120
|
+
# # :multiply => {
|
121
|
+
# # param_types: [:float, :float],
|
122
|
+
# # return_type: :float,
|
123
|
+
# # arity: 2,
|
124
|
+
# # description: "Multiplies two numbers"
|
125
|
+
# # },
|
126
|
+
# # :sum => {
|
127
|
+
# # param_types: [{ array: :float }],
|
128
|
+
# # return_type: :float,
|
129
|
+
# # arity: 1,
|
130
|
+
# # description: "Sums array elements"
|
131
|
+
# # }
|
132
|
+
# # }
|
133
|
+
def functions
|
134
|
+
@functions ||= extract_functions
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns serializable processed metadata as a hash.
|
138
|
+
#
|
139
|
+
# Combines all processed metadata (inputs, values, traits, functions) into a single
|
140
|
+
# hash suitable for JSON serialization, API responses, and external tool integration.
|
141
|
+
# Does not include raw AST nodes or analyzer state.
|
142
|
+
#
|
143
|
+
# @return [Hash<Symbol, Hash>] Serializable metadata hash
|
144
|
+
# @example
|
145
|
+
# metadata.to_h
|
146
|
+
# # => {
|
147
|
+
# # inputs: { :age => { type: :integer, ... }, ... },
|
148
|
+
# # values: { :tax_amount => { type: :float, ... }, ... },
|
149
|
+
# # traits: { :adult => { type: :boolean, ... }, ... },
|
150
|
+
# # functions: { :multiply => { param_types: [...], ... }, ... }
|
151
|
+
# # }
|
152
|
+
def to_h
|
153
|
+
{
|
154
|
+
inputs: inputs,
|
155
|
+
values: values,
|
156
|
+
traits: traits,
|
157
|
+
functions: functions
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
alias to_hash to_h
|
162
|
+
|
163
|
+
# Returns raw analyzer state including AST nodes.
|
164
|
+
#
|
165
|
+
# Provides access to the complete analyzer state hash for advanced use cases
|
166
|
+
# requiring direct AST manipulation or detailed analysis. Contains non-serializable
|
167
|
+
# AST node objects and should not be used for JSON export.
|
168
|
+
#
|
169
|
+
# @return [Hash] Complete analyzer state with all keys and AST nodes
|
170
|
+
# @example
|
171
|
+
# raw_state = metadata.analyzer_state
|
172
|
+
# declarations = raw_state[:declarations] # AST nodes
|
173
|
+
# dependency_graph = raw_state[:dependencies] # Edge objects
|
174
|
+
def analyzer_state
|
175
|
+
@state.dup
|
176
|
+
end
|
177
|
+
|
178
|
+
# @deprecated Use to_h instead for processed metadata
|
179
|
+
attr_reader :state
|
180
|
+
|
181
|
+
# Returns JSON representation of processed metadata.
|
182
|
+
#
|
183
|
+
# Serializes the processed metadata (inputs, values, traits, functions) to JSON.
|
184
|
+
# Excludes raw analyzer state and AST nodes for clean serialization.
|
185
|
+
#
|
186
|
+
# @param args [Array] Arguments passed to Hash#to_json
|
187
|
+
# @return [String] JSON representation
|
188
|
+
def to_json(*args)
|
189
|
+
require "json"
|
190
|
+
to_h.to_json(*args)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns JSON Schema representation of input fields.
|
194
|
+
#
|
195
|
+
# Generates a JSON Schema document describing the expected input structure,
|
196
|
+
# including type constraints, domain validation, and Kumi-specific extensions
|
197
|
+
# for computed values and traits.
|
198
|
+
#
|
199
|
+
# @return [Hash] JSON Schema document
|
200
|
+
# @example
|
201
|
+
# schema = metadata.to_json_schema
|
202
|
+
# # => {
|
203
|
+
# # type: "object",
|
204
|
+
# # properties: {
|
205
|
+
# # age: { type: "integer", minimum: 18, maximum: 65 },
|
206
|
+
# # name: { type: "string" }
|
207
|
+
# # },
|
208
|
+
# # required: [:age, :name],
|
209
|
+
# # "x-kumi-values": { ... },
|
210
|
+
# # "x-kumi-traits": { ... }
|
211
|
+
# # }
|
212
|
+
def to_json_schema
|
213
|
+
JsonSchema::Generator.new(self).generate
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns processed declaration metadata.
|
217
|
+
#
|
218
|
+
# Transforms raw AST declaration nodes into clean, serializable metadata showing
|
219
|
+
# declaration types and basic information. For raw AST nodes, use analyzer_state.
|
220
|
+
#
|
221
|
+
# @return [Hash<Symbol, Hash>] Declaration metadata keyed by name
|
222
|
+
# @example
|
223
|
+
# metadata.declarations
|
224
|
+
# # => {
|
225
|
+
# # :adult => { type: :trait, expression: ">=(input.age, 18)" },
|
226
|
+
# # :tax_amount => { type: :value, expression: "multiply(input.income, tax_rate)" }
|
227
|
+
# # }
|
228
|
+
def declarations
|
229
|
+
@declarations ||= extract_declarations
|
230
|
+
end
|
231
|
+
|
232
|
+
# Returns processed dependency information.
|
233
|
+
#
|
234
|
+
# Transforms internal Edge objects into clean, serializable dependency data
|
235
|
+
# showing relationships between declarations. For raw Edge objects, use analyzer_state.
|
236
|
+
#
|
237
|
+
# @return [Hash<Symbol, Array<Hash>>] Dependencies with plain data
|
238
|
+
# @example
|
239
|
+
# metadata.dependencies
|
240
|
+
# # => {
|
241
|
+
# # :tax_amount => [
|
242
|
+
# # { to: :income, conditional: false },
|
243
|
+
# # { to: :tax_rate, conditional: true, cascade_owner: :status }
|
244
|
+
# # ]
|
245
|
+
# # }
|
246
|
+
def dependencies
|
247
|
+
@dependencies ||= extract_dependencies
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns reverse dependency lookup (dependents).
|
251
|
+
#
|
252
|
+
# Shows which declarations depend on each declaration. Useful for impact analysis
|
253
|
+
# and understanding how changes to input fields or computed values affect other
|
254
|
+
# parts of the schema.
|
255
|
+
#
|
256
|
+
# @return [Hash<Symbol, Array<Symbol>>] Dependent names keyed by declaration name
|
257
|
+
def dependents
|
258
|
+
@state[:dependents] || {}
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns leaf node categorization.
|
262
|
+
#
|
263
|
+
# Identifies declarations with no dependencies, categorized by type (trait/value).
|
264
|
+
# Useful for understanding schema structure and identifying independent computations.
|
265
|
+
#
|
266
|
+
# @return [Hash<Symbol, Array<Symbol>>] Leaf declarations by category
|
267
|
+
def leaves
|
268
|
+
@state[:leaves] || {}
|
269
|
+
end
|
270
|
+
|
271
|
+
# Returns topologically sorted evaluation order.
|
272
|
+
#
|
273
|
+
# Provides the dependency-safe evaluation order for all declarations. Computed by
|
274
|
+
# topological sort of the dependency graph. Critical for correct evaluation sequence
|
275
|
+
# in runners and compilers.
|
276
|
+
#
|
277
|
+
# @return [Array<Symbol>] Declaration names in evaluation order
|
278
|
+
def evaluation_order
|
279
|
+
@state[:evaluation_order] || []
|
280
|
+
end
|
281
|
+
|
282
|
+
# Returns type inference results for all declarations.
|
283
|
+
#
|
284
|
+
# Maps declaration names to their inferred types based on expression analysis.
|
285
|
+
# Includes both simple types (:boolean, :string, :float) and complex types
|
286
|
+
# for array operations and structured data.
|
287
|
+
#
|
288
|
+
# @return [Hash<Symbol, Object>] Inferred types keyed by declaration name
|
289
|
+
# @example
|
290
|
+
# types = metadata.inferred_types
|
291
|
+
# # => { :adult => :boolean, :tax_amount => :float, :totals => { array: :float } }
|
292
|
+
def inferred_types
|
293
|
+
@state[:inferred_types] || {}
|
294
|
+
end
|
295
|
+
|
296
|
+
# Returns cascade mutual exclusion analysis.
|
297
|
+
#
|
298
|
+
# Provides analysis results for cascade expressions including mutual exclusion
|
299
|
+
# detection and satisfiability analysis. Used internally for optimization and
|
300
|
+
# error detection in cascade logic.
|
301
|
+
#
|
302
|
+
# @return [Hash] Cascade analysis results
|
303
|
+
def cascades
|
304
|
+
@state[:cascades] || {}
|
305
|
+
end
|
306
|
+
|
307
|
+
# Returns array broadcasting operation metadata.
|
308
|
+
#
|
309
|
+
# Contains analysis of vectorized operations on array inputs, including element
|
310
|
+
# access paths and broadcasting behavior. Used for generating efficient compiled
|
311
|
+
# code for array operations.
|
312
|
+
#
|
313
|
+
# @return [Hash] Broadcasting operation metadata
|
314
|
+
def broadcasts
|
315
|
+
@state[:broadcasts] || {}
|
316
|
+
end
|
317
|
+
|
318
|
+
private
|
319
|
+
|
320
|
+
def extract_declarations
|
321
|
+
return {} unless @state[:declarations]
|
322
|
+
|
323
|
+
@state[:declarations].transform_values do |node|
|
324
|
+
{
|
325
|
+
type: node.is_a?(Syntax::TraitDeclaration) ? :trait : :value,
|
326
|
+
expression: expression_to_string(node.expression)
|
327
|
+
}
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def extract_dependencies
|
332
|
+
return {} unless @state[:dependencies]
|
333
|
+
|
334
|
+
@state[:dependencies].transform_values do |edges|
|
335
|
+
edges.map do |edge|
|
336
|
+
result = { to: edge.to, conditional: edge.conditional }
|
337
|
+
result[:cascade_owner] = edge.cascade_owner if edge.cascade_owner
|
338
|
+
result
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def extract_inputs
|
344
|
+
return {} unless @state[:inputs]
|
345
|
+
|
346
|
+
@state[:inputs].transform_values do |field_info|
|
347
|
+
{
|
348
|
+
type: normalize_type(field_info[:type]),
|
349
|
+
domain: normalize_domain(field_info[:domain]),
|
350
|
+
required: true
|
351
|
+
}.compact
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def extract_values
|
356
|
+
return {} unless @state[:dependencies]
|
357
|
+
|
358
|
+
value_nodes = (@syntax_tree.values || []).flatten.select { |node| node.is_a?(Syntax::ValueDeclaration) }
|
359
|
+
dependency_graph = @state[:dependencies] || {}
|
360
|
+
inferred_types = @state[:inferred_types] || {}
|
361
|
+
|
362
|
+
value_nodes.each_with_object({}) do |node, result|
|
363
|
+
name = node.name
|
364
|
+
dependency_edges = dependency_graph[name] || []
|
365
|
+
dependencies = dependency_edges.map(&:to)
|
366
|
+
|
367
|
+
result[name] = {
|
368
|
+
type: inferred_types[name],
|
369
|
+
dependencies: dependencies,
|
370
|
+
computed: true
|
371
|
+
}.tap do |spec|
|
372
|
+
if node.expression.is_a?(Syntax::CascadeExpression)
|
373
|
+
spec[:cascade] = extract_cascade_info(node.expression)
|
374
|
+
else
|
375
|
+
spec[:expression] = expression_to_string(node.expression)
|
376
|
+
end
|
377
|
+
end.compact
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def extract_traits
|
382
|
+
return {} unless @state[:dependencies]
|
383
|
+
|
384
|
+
trait_nodes = (@syntax_tree.traits || []).flatten.select { |node| node.is_a?(Syntax::TraitDeclaration) }
|
385
|
+
dependency_graph = @state[:dependencies] || {}
|
386
|
+
|
387
|
+
trait_nodes.each_with_object({}) do |node, result|
|
388
|
+
name = node.name
|
389
|
+
dependency_edges = dependency_graph[name] || []
|
390
|
+
dependencies = dependency_edges.map(&:to)
|
391
|
+
|
392
|
+
result[name] = {
|
393
|
+
type: :boolean,
|
394
|
+
dependencies: dependencies,
|
395
|
+
condition: expression_to_string(node.expression)
|
396
|
+
}.compact
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
def extract_functions
|
401
|
+
function_calls = Set.new
|
402
|
+
|
403
|
+
value_nodes = (@syntax_tree.values || []).flatten.select { |node| node.is_a?(Syntax::ValueDeclaration) }
|
404
|
+
trait_nodes = (@syntax_tree.traits || []).flatten.select { |node| node.is_a?(Syntax::TraitDeclaration) }
|
405
|
+
|
406
|
+
value_nodes.each do |node|
|
407
|
+
collect_function_calls(node.expression, function_calls)
|
408
|
+
end
|
409
|
+
|
410
|
+
trait_nodes.each do |node|
|
411
|
+
collect_function_calls(node.expression, function_calls)
|
412
|
+
end
|
413
|
+
|
414
|
+
function_calls.each_with_object({}) do |func_name, result|
|
415
|
+
next unless Kumi::FunctionRegistry.supported?(func_name)
|
416
|
+
|
417
|
+
function_info = Kumi::FunctionRegistry.signature(func_name)
|
418
|
+
result[func_name] = {
|
419
|
+
param_types: function_info[:param_types],
|
420
|
+
return_type: function_info[:return_type],
|
421
|
+
arity: function_info[:arity],
|
422
|
+
description: function_info[:description]
|
423
|
+
}.compact
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def normalize_type(type_spec)
|
428
|
+
case type_spec
|
429
|
+
when Hash
|
430
|
+
if type_spec.key?(:hash)
|
431
|
+
:hash
|
432
|
+
elsif type_spec.key?(:array)
|
433
|
+
:array
|
434
|
+
else
|
435
|
+
type_spec
|
436
|
+
end
|
437
|
+
else
|
438
|
+
type_spec
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def normalize_domain(domain_spec)
|
443
|
+
case domain_spec
|
444
|
+
when Range
|
445
|
+
{
|
446
|
+
type: :range,
|
447
|
+
min: domain_spec.begin,
|
448
|
+
max: domain_spec.end,
|
449
|
+
exclusive_end: domain_spec.exclude_end?
|
450
|
+
}
|
451
|
+
when Array
|
452
|
+
{ type: :enum, values: domain_spec }
|
453
|
+
when Proc
|
454
|
+
{ type: :custom, description: "custom validation function" }
|
455
|
+
when Hash
|
456
|
+
domain_spec
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
def extract_cascade_info(cascade_expr)
|
461
|
+
cases = cascade_expr.cases || []
|
462
|
+
|
463
|
+
conditions = []
|
464
|
+
base_case = nil
|
465
|
+
|
466
|
+
cases.each do |case_expr|
|
467
|
+
if case_expr.condition
|
468
|
+
conditions << {
|
469
|
+
when: [expression_to_string(case_expr.condition)],
|
470
|
+
then: literal_value(case_expr.result)
|
471
|
+
}
|
472
|
+
else
|
473
|
+
base_case = literal_value(case_expr.result)
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
result = { conditions: conditions }
|
478
|
+
result[:base] = base_case if base_case
|
479
|
+
result
|
480
|
+
end
|
481
|
+
|
482
|
+
def expression_to_string(expr)
|
483
|
+
case expr
|
484
|
+
when Syntax::Literal
|
485
|
+
expr.value.inspect
|
486
|
+
when Syntax::InputReference
|
487
|
+
"input.#{expr.name}"
|
488
|
+
when Syntax::DeclarationReference
|
489
|
+
expr.name.to_s
|
490
|
+
when Syntax::CallExpression
|
491
|
+
args = expr.args.map { |arg| expression_to_string(arg) }.join(", ")
|
492
|
+
"#{expr.fn_name}(#{args})"
|
493
|
+
when Syntax::CascadeExpression
|
494
|
+
"cascade"
|
495
|
+
else
|
496
|
+
expr.class.name.split("::").last
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
def literal_value(expr)
|
501
|
+
expr.is_a?(Syntax::Literal) ? expr.value : expression_to_string(expr)
|
502
|
+
end
|
503
|
+
|
504
|
+
def collect_function_calls(expr, function_calls)
|
505
|
+
case expr
|
506
|
+
when Syntax::CallExpression
|
507
|
+
function_calls << expr.fn_name
|
508
|
+
expr.args.each { |arg| collect_function_calls(arg, function_calls) }
|
509
|
+
when Syntax::CascadeExpression
|
510
|
+
expr.cases.each do |case_expr|
|
511
|
+
collect_function_calls(case_expr.condition, function_calls) if case_expr.condition
|
512
|
+
collect_function_calls(case_expr.result, function_calls)
|
513
|
+
end
|
514
|
+
when Syntax::ArrayExpression
|
515
|
+
expr.elements.each { |elem| collect_function_calls(elem, function_calls) }
|
516
|
+
when Syntax::HashExpression
|
517
|
+
expr.pairs.each do |pair|
|
518
|
+
collect_function_calls(pair.key, function_calls)
|
519
|
+
collect_function_calls(pair.value, function_calls)
|
520
|
+
end
|
521
|
+
end
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
@@ -46,7 +46,7 @@ module Kumi
|
|
46
46
|
|
47
47
|
# Check if a function call should be treated as a reducer
|
48
48
|
def reducer_function?(fn_name, args)
|
49
|
-
REDUCER_FUNCTIONS.include?(fn_name) &&
|
49
|
+
REDUCER_FUNCTIONS.include?(fn_name) &&
|
50
50
|
args.any? { |arg| vectorized_expression?(arg) }
|
51
51
|
end
|
52
52
|
|
@@ -62,7 +62,7 @@ module Kumi
|
|
62
62
|
|
63
63
|
def vectorized_element_reference?(elem_ref)
|
64
64
|
return false unless elem_ref.path.size >= 2
|
65
|
-
|
65
|
+
|
66
66
|
array_name, _field_name = elem_ref.path
|
67
67
|
@array_tracker.array_declaration?(array_name)
|
68
68
|
end
|
@@ -76,7 +76,7 @@ module Kumi
|
|
76
76
|
|
77
77
|
def initialize
|
78
78
|
@vectorized_values = Set.new
|
79
|
-
@reducer_values = Set.new
|
79
|
+
@reducer_values = Set.new
|
80
80
|
@scalar_values = Set.new
|
81
81
|
end
|
82
82
|
|
@@ -105,4 +105,4 @@ module Kumi
|
|
105
105
|
end
|
106
106
|
end
|
107
107
|
end
|
108
|
-
end
|
108
|
+
end
|
data/lib/kumi/version.rb
CHANGED
data/lib/kumi.rb
CHANGED
@@ -9,4 +9,18 @@ loader.setup
|
|
9
9
|
|
10
10
|
module Kumi
|
11
11
|
extend Schema
|
12
|
+
|
13
|
+
def self.inspector_from_schema
|
14
|
+
Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.reset!
|
18
|
+
@__syntax_tree__ = nil
|
19
|
+
@__analyzer_result__ = nil
|
20
|
+
@__compiled_schema__ = nil
|
21
|
+
@__schema_metadata__ = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reset on require to avoid state leakage in tests
|
25
|
+
reset!
|
12
26
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- André Muta
|
@@ -48,13 +48,21 @@ files:
|
|
48
48
|
- docs/features/array-broadcasting.md
|
49
49
|
- docs/features/input-declaration-system.md
|
50
50
|
- docs/features/performance.md
|
51
|
+
- docs/schema_metadata.md
|
52
|
+
- docs/schema_metadata/broadcasts.md
|
53
|
+
- docs/schema_metadata/cascades.md
|
54
|
+
- docs/schema_metadata/declarations.md
|
55
|
+
- docs/schema_metadata/dependencies.md
|
56
|
+
- docs/schema_metadata/evaluation_order.md
|
57
|
+
- docs/schema_metadata/examples.md
|
58
|
+
- docs/schema_metadata/inferred_types.md
|
59
|
+
- docs/schema_metadata/inputs.md
|
51
60
|
- examples/deep_schema_compilation_and_evaluation_benchmark.rb
|
52
61
|
- examples/federal_tax_calculator_2024.rb
|
53
62
|
- examples/game_of_life.rb
|
54
63
|
- examples/simple_rpg_game.rb
|
55
64
|
- examples/static_analysis_errors.rb
|
56
65
|
- examples/wide_schema_compilation_and_evaluation_benchmark.rb
|
57
|
-
- lib/generators/trait_engine/templates/schema_spec.rb.erb
|
58
66
|
- lib/kumi.rb
|
59
67
|
- lib/kumi/analyzer.rb
|
60
68
|
- lib/kumi/analyzer/analysis_state.rb
|
@@ -105,21 +113,26 @@ files:
|
|
105
113
|
- lib/kumi/input/type_matcher.rb
|
106
114
|
- lib/kumi/input/validator.rb
|
107
115
|
- lib/kumi/input/violation_creator.rb
|
108
|
-
- lib/kumi/
|
109
|
-
- lib/kumi/
|
110
|
-
- lib/kumi/
|
111
|
-
- lib/kumi/
|
112
|
-
- lib/kumi/
|
113
|
-
- lib/kumi/
|
114
|
-
- lib/kumi/
|
115
|
-
- lib/kumi/
|
116
|
-
- lib/kumi/
|
117
|
-
- lib/kumi/
|
118
|
-
- lib/kumi/
|
119
|
-
- lib/kumi/
|
120
|
-
- lib/kumi/
|
116
|
+
- lib/kumi/json_schema.rb
|
117
|
+
- lib/kumi/json_schema/generator.rb
|
118
|
+
- lib/kumi/json_schema/validator.rb
|
119
|
+
- lib/kumi/ruby_parser.rb
|
120
|
+
- lib/kumi/ruby_parser/build_context.rb
|
121
|
+
- lib/kumi/ruby_parser/declaration_reference_proxy.rb
|
122
|
+
- lib/kumi/ruby_parser/dsl.rb
|
123
|
+
- lib/kumi/ruby_parser/dsl_cascade_builder.rb
|
124
|
+
- lib/kumi/ruby_parser/expression_converter.rb
|
125
|
+
- lib/kumi/ruby_parser/guard_rails.rb
|
126
|
+
- lib/kumi/ruby_parser/input_builder.rb
|
127
|
+
- lib/kumi/ruby_parser/input_field_proxy.rb
|
128
|
+
- lib/kumi/ruby_parser/input_proxy.rb
|
129
|
+
- lib/kumi/ruby_parser/nested_input.rb
|
130
|
+
- lib/kumi/ruby_parser/parser.rb
|
131
|
+
- lib/kumi/ruby_parser/schema_builder.rb
|
132
|
+
- lib/kumi/ruby_parser/sugar.rb
|
121
133
|
- lib/kumi/schema.rb
|
122
134
|
- lib/kumi/schema_instance.rb
|
135
|
+
- lib/kumi/schema_metadata.rb
|
123
136
|
- lib/kumi/syntax/array_expression.rb
|
124
137
|
- lib/kumi/syntax/call_expression.rb
|
125
138
|
- lib/kumi/syntax/cascade_expression.rb
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
require "rails_helper"
|
3
|
-
|
4
|
-
RSpec.describe <%= schema_constant %> do
|
5
|
-
# Single shared instance for every example
|
6
|
-
let(:schema) { described_class }
|
7
|
-
|
8
|
-
# Minimal dummy context so every binding can run
|
9
|
-
let(:ctx) do
|
10
|
-
{
|
11
|
-
<% leaf_keys.each do |k| -%>
|
12
|
-
<%= "#{k}:" %> nil,
|
13
|
-
<% end -%>
|
14
|
-
}
|
15
|
-
end
|
16
|
-
|
17
|
-
<% expose_names.each do |name| -%>
|
18
|
-
describe "<%= name %>" do
|
19
|
-
it "evaluates without raising" do
|
20
|
-
expect {
|
21
|
-
schema.evaluate_binding(:<%= name %>, ctx)
|
22
|
-
}.not_to raise_error
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
<% end -%>
|
27
|
-
end
|