kumi 0.0.0 → 0.0.3

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/.rubocop.yml +113 -3
  3. data/CHANGELOG.md +21 -1
  4. data/CLAUDE.md +387 -0
  5. data/README.md +257 -20
  6. data/docs/development/README.md +120 -0
  7. data/docs/development/error-reporting.md +361 -0
  8. data/documents/AST.md +126 -0
  9. data/documents/DSL.md +154 -0
  10. data/documents/FUNCTIONS.md +132 -0
  11. data/documents/SYNTAX.md +367 -0
  12. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
  13. data/examples/federal_tax_calculator_2024.rb +112 -0
  14. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
  15. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
  16. data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
  17. data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
  18. data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
  19. data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
  20. data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
  21. data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
  22. data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
  23. data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
  24. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
  25. data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
  26. data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
  27. data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
  28. data/lib/kumi/analyzer.rb +54 -0
  29. data/lib/kumi/atom_unsat_solver.rb +349 -0
  30. data/lib/kumi/compiled_schema.rb +41 -0
  31. data/lib/kumi/compiler.rb +127 -0
  32. data/lib/kumi/domain/enum_analyzer.rb +53 -0
  33. data/lib/kumi/domain/range_analyzer.rb +83 -0
  34. data/lib/kumi/domain/validator.rb +84 -0
  35. data/lib/kumi/domain/violation_formatter.rb +40 -0
  36. data/lib/kumi/domain.rb +8 -0
  37. data/lib/kumi/error_reporter.rb +164 -0
  38. data/lib/kumi/error_reporting.rb +95 -0
  39. data/lib/kumi/errors.rb +116 -0
  40. data/lib/kumi/evaluation_wrapper.rb +20 -0
  41. data/lib/kumi/explain.rb +282 -0
  42. data/lib/kumi/export/deserializer.rb +39 -0
  43. data/lib/kumi/export/errors.rb +12 -0
  44. data/lib/kumi/export/node_builders.rb +140 -0
  45. data/lib/kumi/export/node_registry.rb +38 -0
  46. data/lib/kumi/export/node_serializers.rb +156 -0
  47. data/lib/kumi/export/serializer.rb +23 -0
  48. data/lib/kumi/export.rb +33 -0
  49. data/lib/kumi/function_registry/collection_functions.rb +92 -0
  50. data/lib/kumi/function_registry/comparison_functions.rb +31 -0
  51. data/lib/kumi/function_registry/conditional_functions.rb +36 -0
  52. data/lib/kumi/function_registry/function_builder.rb +92 -0
  53. data/lib/kumi/function_registry/logical_functions.rb +42 -0
  54. data/lib/kumi/function_registry/math_functions.rb +72 -0
  55. data/lib/kumi/function_registry/string_functions.rb +54 -0
  56. data/lib/kumi/function_registry/type_functions.rb +51 -0
  57. data/lib/kumi/function_registry.rb +138 -0
  58. data/lib/kumi/input/type_matcher.rb +92 -0
  59. data/lib/kumi/input/validator.rb +52 -0
  60. data/lib/kumi/input/violation_creator.rb +50 -0
  61. data/lib/kumi/input.rb +8 -0
  62. data/lib/kumi/parser/build_context.rb +25 -0
  63. data/lib/kumi/parser/dsl.rb +12 -0
  64. data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
  65. data/lib/kumi/parser/expression_converter.rb +58 -0
  66. data/lib/kumi/parser/guard_rails.rb +43 -0
  67. data/lib/kumi/parser/input_builder.rb +94 -0
  68. data/lib/kumi/parser/input_proxy.rb +29 -0
  69. data/lib/kumi/parser/parser.rb +66 -0
  70. data/lib/kumi/parser/schema_builder.rb +172 -0
  71. data/lib/kumi/parser/sugar.rb +108 -0
  72. data/lib/kumi/schema.rb +49 -0
  73. data/lib/kumi/schema_instance.rb +43 -0
  74. data/lib/kumi/syntax/declarations.rb +23 -0
  75. data/lib/kumi/syntax/expressions.rb +30 -0
  76. data/lib/kumi/syntax/node.rb +46 -0
  77. data/lib/kumi/syntax/root.rb +12 -0
  78. data/lib/kumi/syntax/terminal_expressions.rb +27 -0
  79. data/lib/kumi/syntax.rb +9 -0
  80. data/lib/kumi/types/builder.rb +21 -0
  81. data/lib/kumi/types/compatibility.rb +86 -0
  82. data/lib/kumi/types/formatter.rb +24 -0
  83. data/lib/kumi/types/inference.rb +40 -0
  84. data/lib/kumi/types/normalizer.rb +70 -0
  85. data/lib/kumi/types/validator.rb +35 -0
  86. data/lib/kumi/types.rb +64 -0
  87. data/lib/kumi/version.rb +1 -1
  88. data/lib/kumi.rb +7 -3
  89. data/scripts/generate_function_docs.rb +59 -0
  90. data/test_impossible_cascade.rb +51 -0
  91. metadata +93 -10
  92. data/sig/kumi.rbs +0 -4
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Kumi
6
+ module Export
7
+ # Core interface - only depends on Syntax::Root
8
+ def self.to_json(syntax_root, **options)
9
+ Serializer.new(**options).serialize(syntax_root)
10
+ end
11
+
12
+ def self.from_json(json_string, **options)
13
+ Deserializer.new(**options).deserialize(json_string)
14
+ end
15
+
16
+ # Convenience methods
17
+ def self.to_file(syntax_root, filepath, **options)
18
+ File.write(filepath, to_json(syntax_root, **options))
19
+ end
20
+
21
+ def self.from_file(filepath, **options)
22
+ from_json(File.read(filepath), **options)
23
+ end
24
+
25
+ # Validation without import
26
+ def self.valid?(json_string)
27
+ from_json(json_string)
28
+ true
29
+ rescue StandardError
30
+ false
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # Collection manipulation and query functions
6
+ module CollectionFunctions
7
+ def self.definitions
8
+ {
9
+ # Collection queries
10
+ empty?: FunctionBuilder.collection_unary(:empty?, "Check if collection is empty", :empty?),
11
+ size: FunctionBuilder.collection_unary(:size, "Get collection size", :size, return_type: :integer),
12
+ length: FunctionBuilder.collection_unary(:length, "Get collection length", :length, return_type: :integer),
13
+
14
+ # Element access
15
+ first: FunctionBuilder::Entry.new(
16
+ fn: lambda(&:first),
17
+ arity: 1,
18
+ param_types: [Kumi::Types.array(:any)],
19
+ return_type: :any,
20
+ description: "Get first element of collection"
21
+ ),
22
+
23
+ last: FunctionBuilder::Entry.new(
24
+ fn: lambda(&:last),
25
+ arity: 1,
26
+ param_types: [Kumi::Types.array(:any)],
27
+ return_type: :any,
28
+ description: "Get last element of collection"
29
+ ),
30
+
31
+ # Mathematical operations on collections
32
+ sum: FunctionBuilder::Entry.new(
33
+ fn: lambda(&:sum),
34
+ arity: 1,
35
+ param_types: [Kumi::Types.array(:float)],
36
+ return_type: :float,
37
+ description: "Sum all numeric elements in collection"
38
+ ),
39
+
40
+ min: FunctionBuilder::Entry.new(
41
+ fn: lambda(&:min),
42
+ arity: 1,
43
+ param_types: [Kumi::Types.array(:float)],
44
+ return_type: :float,
45
+ description: "Find minimum value in numeric collection"
46
+ ),
47
+
48
+ max: FunctionBuilder::Entry.new(
49
+ fn: lambda(&:max),
50
+ arity: 1,
51
+ param_types: [Kumi::Types.array(:float)],
52
+ return_type: :float,
53
+ description: "Find maximum value in numeric collection"
54
+ ),
55
+
56
+ # Collection operations
57
+ include?: FunctionBuilder::Entry.new(
58
+ fn: ->(collection, element) { collection.include?(element) },
59
+ arity: 2,
60
+ param_types: [Kumi::Types.array(:any), :any],
61
+ return_type: :boolean,
62
+ description: "Check if collection includes element"
63
+ ),
64
+
65
+ reverse: FunctionBuilder::Entry.new(
66
+ fn: lambda(&:reverse),
67
+ arity: 1,
68
+ param_types: [Kumi::Types.array(:any)],
69
+ return_type: Kumi::Types.array(:any),
70
+ description: "Reverse collection order"
71
+ ),
72
+
73
+ sort: FunctionBuilder::Entry.new(
74
+ fn: lambda(&:sort),
75
+ arity: 1,
76
+ param_types: [Kumi::Types.array(:any)],
77
+ return_type: Kumi::Types.array(:any),
78
+ description: "Sort collection"
79
+ ),
80
+
81
+ unique: FunctionBuilder::Entry.new(
82
+ fn: lambda(&:uniq),
83
+ arity: 1,
84
+ param_types: [Kumi::Types.array(:any)],
85
+ return_type: Kumi::Types.array(:any),
86
+ description: "Remove duplicate elements from collection"
87
+ )
88
+ }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # Comparison and equality functions
6
+ module ComparisonFunctions
7
+ def self.definitions
8
+ {
9
+ # Equality operators
10
+ :== => FunctionBuilder.equality(:==, "Equality comparison", :==),
11
+ :!= => FunctionBuilder.equality(:!=, "Inequality comparison", :!=),
12
+
13
+ # Comparison operators
14
+ :> => FunctionBuilder.comparison(:>, "Greater than comparison", :>),
15
+ :< => FunctionBuilder.comparison(:<, "Less than comparison", :<),
16
+ :>= => FunctionBuilder.comparison(:>=, "Greater than or equal comparison", :>=),
17
+ :<= => FunctionBuilder.comparison(:<=, "Less than or equal comparison", :<=),
18
+
19
+ # Range comparison
20
+ :between? => FunctionBuilder::Entry.new(
21
+ fn: ->(value, min, max) { value.between?(min, max) },
22
+ arity: 3,
23
+ param_types: %i[float float float],
24
+ return_type: :boolean,
25
+ description: "Check if value is between min and max"
26
+ )
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # Conditional and control flow functions
6
+ module ConditionalFunctions
7
+ def self.definitions
8
+ {
9
+ conditional: FunctionBuilder::Entry.new(
10
+ fn: ->(condition, true_value, false_value) { condition ? true_value : false_value },
11
+ arity: 3,
12
+ param_types: %i[boolean any any],
13
+ return_type: :any,
14
+ description: "Ternary conditional operator"
15
+ ),
16
+
17
+ if: FunctionBuilder::Entry.new(
18
+ fn: ->(condition, true_value, false_value = nil) { condition ? true_value : false_value },
19
+ arity: -1, # Variable arity (2 or 3)
20
+ param_types: %i[boolean any any],
21
+ return_type: :any,
22
+ description: "If-then-else conditional"
23
+ ),
24
+
25
+ coalesce: FunctionBuilder::Entry.new(
26
+ fn: ->(*values) { values.find { |v| !v.nil? } },
27
+ arity: -1, # Variable arity
28
+ param_types: [:any],
29
+ return_type: :any,
30
+ description: "Return first non-nil value"
31
+ )
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # Utility class to reduce repetition in function definitions
6
+ class FunctionBuilder
7
+ Entry = Struct.new(:fn, :arity, :param_types, :return_type, :description, :inverse, keyword_init: true)
8
+
9
+ def self.comparison(_name, description, operation)
10
+ Entry.new(
11
+ fn: ->(a, b) { a.public_send(operation, b) },
12
+ arity: 2,
13
+ param_types: %i[float float],
14
+ return_type: :boolean,
15
+ description: description
16
+ )
17
+ end
18
+
19
+ def self.equality(_name, description, operation)
20
+ Entry.new(
21
+ fn: ->(a, b) { a.public_send(operation, b) },
22
+ arity: 2,
23
+ param_types: %i[any any],
24
+ return_type: :boolean,
25
+ description: description
26
+ )
27
+ end
28
+
29
+ def self.math_binary(_name, description, operation, return_type: :float)
30
+ Entry.new(
31
+ fn: lambda { |a, b|
32
+ a.public_send(operation, b)
33
+ },
34
+ arity: 2,
35
+ param_types: %i[float float],
36
+ return_type: return_type,
37
+ description: description
38
+ )
39
+ end
40
+
41
+ def self.math_unary(_name, description, operation, return_type: :float)
42
+ Entry.new(
43
+ fn: proc(&operation),
44
+ arity: 1,
45
+ param_types: [:float],
46
+ return_type: return_type,
47
+ description: description
48
+ )
49
+ end
50
+
51
+ def self.string_unary(_name, description, operation)
52
+ Entry.new(
53
+ fn: ->(str) { str.to_s.public_send(operation) },
54
+ arity: 1,
55
+ param_types: [:string],
56
+ return_type: :string,
57
+ description: description
58
+ )
59
+ end
60
+
61
+ def self.string_binary(_name, description, operation, return_type: :string)
62
+ Entry.new(
63
+ fn: ->(str, arg) { str.to_s.public_send(operation, arg.to_s) },
64
+ arity: 2,
65
+ param_types: %i[string string],
66
+ return_type: return_type,
67
+ description: description
68
+ )
69
+ end
70
+
71
+ def self.logical_variadic(_name, description, operation)
72
+ Entry.new(
73
+ fn: ->(conditions) { conditions.public_send(operation) },
74
+ arity: -1,
75
+ param_types: [:boolean],
76
+ return_type: :boolean,
77
+ description: description
78
+ )
79
+ end
80
+
81
+ def self.collection_unary(_name, description, operation, return_type: :boolean)
82
+ Entry.new(
83
+ fn: proc(&operation),
84
+ arity: 1,
85
+ param_types: [Kumi::Types.array(:any)],
86
+ return_type: return_type,
87
+ description: description
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # Logical operations and boolean functions
6
+ module LogicalFunctions
7
+ def self.definitions
8
+ {
9
+ # Basic logical operations
10
+ and: FunctionBuilder::Entry.new(
11
+ fn: ->(*conditions) { conditions.all? },
12
+ arity: -1,
13
+ param_types: [:boolean],
14
+ return_type: :boolean,
15
+ description: "Logical AND of multiple conditions"
16
+ ),
17
+
18
+ or: FunctionBuilder::Entry.new(
19
+ fn: ->(*conditions) { conditions.any? },
20
+ arity: -1,
21
+ param_types: [:boolean],
22
+ return_type: :boolean,
23
+ description: "Logical OR of multiple conditions"
24
+ ),
25
+
26
+ not: FunctionBuilder::Entry.new(
27
+ fn: lambda(&:!),
28
+ arity: 1,
29
+ param_types: [:boolean],
30
+ return_type: :boolean,
31
+ description: "Logical NOT"
32
+ ),
33
+
34
+ # Collection logical operations
35
+ all?: FunctionBuilder.collection_unary(:all?, "Check if all elements in collection are truthy", :all?),
36
+ any?: FunctionBuilder.collection_unary(:any?, "Check if any element in collection is truthy", :any?),
37
+ none?: FunctionBuilder.collection_unary(:none?, "Check if no elements in collection are truthy", :none?)
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # Mathematical operations
6
+ module MathFunctions
7
+ def self.definitions
8
+ {
9
+ # Basic arithmetic
10
+ add: FunctionBuilder.math_binary(:add, "Add two numbers", :+),
11
+ subtract: FunctionBuilder.math_binary(:subtract, "Subtract second number from first", :-),
12
+ multiply: FunctionBuilder.math_binary(:multiply, "Multiply two numbers", :*),
13
+ divide: FunctionBuilder.math_binary(:divide, "Divide first number by second", :/),
14
+ modulo: FunctionBuilder.math_binary(:modulo, "Modulo operation", :%),
15
+ power: FunctionBuilder.math_binary(:power, "Raise first number to power of second", :**),
16
+
17
+ # Unary operations
18
+ abs: FunctionBuilder.math_unary(:abs, "Absolute value", :abs),
19
+ floor: FunctionBuilder.math_unary(:floor, "Floor of number", :floor, return_type: :integer),
20
+ ceil: FunctionBuilder.math_unary(:ceil, "Ceiling of number", :ceil, return_type: :integer),
21
+
22
+ # Special operations
23
+ round: FunctionBuilder::Entry.new(
24
+ fn: ->(a, precision = 0) { a.round(precision) },
25
+ arity: -1,
26
+ param_types: [:float],
27
+ return_type: :float,
28
+ description: "Round number to specified precision"
29
+ ),
30
+
31
+ clamp: FunctionBuilder::Entry.new(
32
+ fn: ->(value, min, max) { value.clamp(min, max) },
33
+ arity: 3,
34
+ param_types: %i[float float float],
35
+ return_type: :float,
36
+ description: "Clamp value between min and max"
37
+ ),
38
+ piecewise_sum: FunctionBuilder::Entry.new(
39
+ # Tiered / piece‑wise accumulator ­­­­­­­­­­­­­­­­­­­­­­­­­
40
+ fn: lambda do |value, breaks, rates|
41
+ raise ArgumentError, "breaks & rates size mismatch" unless breaks.size == rates.size
42
+
43
+ acc = 0.0
44
+ previous = 0.0
45
+ marginal = rates.last
46
+
47
+ breaks.zip(rates).each do |upper, rate|
48
+ if value <= upper
49
+ marginal = rate
50
+ acc += (value - previous) * rate
51
+ break
52
+ else
53
+ acc += (upper - previous) * rate
54
+ previous = upper
55
+ end
56
+ end
57
+ [acc, marginal] # => [sum, marginal_rate]
58
+ end,
59
+ arity: 3,
60
+ param_types: [
61
+ :float,
62
+ Kumi::Types.array(:float), # breaks
63
+ Kumi::Types.array(:float) # rates
64
+ ],
65
+ return_type: Kumi::Types.array(:float), # 2‑element [sum, marginal]
66
+ description: "Accumulate over tiered ranges; returns [sum, marginal_rate]"
67
+ )
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # String manipulation functions
6
+ module StringFunctions
7
+ def self.definitions
8
+ {
9
+ # String transformations
10
+ upcase: FunctionBuilder.string_unary(:upcase, "Convert string to uppercase", :upcase),
11
+ downcase: FunctionBuilder.string_unary(:downcase, "Convert string to lowercase", :downcase),
12
+ capitalize: FunctionBuilder.string_unary(:capitalize, "Capitalize first letter of string", :capitalize),
13
+ strip: FunctionBuilder.string_unary(:strip, "Remove leading and trailing whitespace", :strip),
14
+
15
+ # String queries
16
+ string_length: FunctionBuilder::Entry.new(
17
+ fn: ->(str) { str.to_s.length },
18
+ arity: 1,
19
+ param_types: [:string],
20
+ return_type: :integer,
21
+ description: "Get string length"
22
+ ),
23
+
24
+ # Keep the original length for backward compatibility, but it will be overridden
25
+ length: FunctionBuilder::Entry.new(
26
+ fn: ->(str) { str.to_s.length },
27
+ arity: 1,
28
+ param_types: [:string],
29
+ return_type: :integer,
30
+ description: "Get string length"
31
+ ),
32
+
33
+ # String inclusion using different name to avoid conflict with collection include?
34
+ string_include?: FunctionBuilder.string_binary(:include?, "Check if string contains substring", :include?, return_type: :boolean),
35
+ includes?: FunctionBuilder.string_binary(:include?, "Check if string contains substring", :include?, return_type: :boolean),
36
+ contains?: FunctionBuilder.string_binary(:include?, "Check if string contains substring", :include?, return_type: :boolean),
37
+
38
+ start_with?: FunctionBuilder.string_binary(:start_with?, "Check if string starts with prefix", :start_with?,
39
+ return_type: :boolean),
40
+ end_with?: FunctionBuilder.string_binary(:end_with?, "Check if string ends with suffix", :end_with?, return_type: :boolean),
41
+
42
+ # String building
43
+ concat: FunctionBuilder::Entry.new(
44
+ fn: ->(*strings) { strings.join },
45
+ arity: -1,
46
+ param_types: [:string],
47
+ return_type: :string,
48
+ description: "Concatenate multiple strings"
49
+ )
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module FunctionRegistry
5
+ # Type checking and conversion functions
6
+ module TypeFunctions
7
+ def self.definitions
8
+ {
9
+ fetch: FunctionBuilder::Entry.new(
10
+ fn: ->(hash, key, default = nil) { hash.fetch(key, default) },
11
+ arity: -1, # Variable arity (2 or 3)
12
+ param_types: [Kumi::Types.hash(:any, :any), :any, :any],
13
+ return_type: :any,
14
+ description: "Fetch value from hash with optional default"
15
+ ),
16
+
17
+ has_key?: FunctionBuilder::Entry.new(
18
+ fn: ->(hash, key) { hash.key?(key) },
19
+ arity: 2,
20
+ param_types: [Kumi::Types.hash(:any, :any), :any],
21
+ return_type: :boolean,
22
+ description: "Check if hash has the given key"
23
+ ),
24
+
25
+ keys: FunctionBuilder::Entry.new(
26
+ fn: lambda(&:keys),
27
+ arity: 1,
28
+ param_types: [Kumi::Types.hash(:any, :any)],
29
+ return_type: Kumi::Types.array(:any),
30
+ description: "Get all keys from hash"
31
+ ),
32
+
33
+ values: FunctionBuilder::Entry.new(
34
+ fn: lambda(&:values),
35
+ arity: 1,
36
+ param_types: [Kumi::Types.hash(:any, :any)],
37
+ return_type: Kumi::Types.array(:any),
38
+ description: "Get all values from hash"
39
+ ),
40
+ at: FunctionBuilder::Entry.new(
41
+ fn: ->(array, index) { array[index] },
42
+ arity: 2,
43
+ param_types: [Kumi::Types.array(:any), :integer],
44
+ return_type: :any,
45
+ description: "Get element at index from array"
46
+ )
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ # Registry for functions that can be used in Kumi schemas
5
+ # This is the public interface for registering custom functions
6
+ module FunctionRegistry
7
+ class UnknownFunction < StandardError; end
8
+
9
+ # Re-export the Entry struct from FunctionBuilder for compatibility
10
+ Entry = FunctionBuilder::Entry
11
+
12
+ # Core operators that are always available
13
+ CORE_OPERATORS = %i[== > < >= <= != between?].freeze
14
+
15
+ # Build the complete function registry by combining all categories
16
+ CORE_OPERATIONS = {}.tap do |registry|
17
+ registry.merge!(ComparisonFunctions.definitions)
18
+ registry.merge!(MathFunctions.definitions)
19
+ registry.merge!(StringFunctions.definitions)
20
+ registry.merge!(LogicalFunctions.definitions)
21
+ registry.merge!(CollectionFunctions.definitions)
22
+ registry.merge!(ConditionalFunctions.definitions)
23
+ registry.merge!(TypeFunctions.definitions)
24
+ end.freeze
25
+
26
+ @functions = CORE_OPERATIONS.dup
27
+
28
+ class << self
29
+ # Public interface for registering custom functions
30
+ def register(name, &block)
31
+ raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
32
+
33
+ fn_lambda = block.is_a?(Proc) ? block : ->(*args) { yield(*args) }
34
+ register_with_metadata(name, fn_lambda, arity: fn_lambda.arity, param_types: [:any], return_type: :any)
35
+ end
36
+
37
+ # Register with custom metadata
38
+ def register_with_metadata(name, fn_lambda, arity:, param_types: [:any], return_type: :any, description: nil,
39
+ inverse: nil)
40
+ raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
41
+
42
+ @functions[name] = Entry.new(
43
+ fn: fn_lambda,
44
+ arity: arity,
45
+ param_types: param_types,
46
+ return_type: return_type,
47
+ description: description,
48
+ inverse: inverse
49
+ )
50
+ end
51
+
52
+ # Auto-register functions from modules
53
+ def auto_register(*modules)
54
+ modules.each do |mod|
55
+ mod.public_instance_methods(false).each do |method_name|
56
+ next if supported?(method_name)
57
+
58
+ register(method_name) do |*args|
59
+ mod.new.public_send(method_name, *args)
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # Query interface
66
+ def supported?(name)
67
+ @functions.key?(name)
68
+ end
69
+
70
+ def operator?(name)
71
+ return false unless name.is_a?(Symbol)
72
+
73
+ @functions.key?(name) && CORE_OPERATORS.include?(name)
74
+ end
75
+
76
+ def fetch(name)
77
+ @functions.fetch(name) { raise UnknownFunction, "Unknown function: #{name}" }.fn
78
+ end
79
+
80
+ def signature(name)
81
+ entry = @functions.fetch(name) { raise UnknownFunction, "Unknown function: #{name}" }
82
+ {
83
+ arity: entry.arity,
84
+ param_types: entry.param_types,
85
+ return_type: entry.return_type,
86
+ description: entry.description
87
+ }
88
+ end
89
+
90
+ def all_functions
91
+ @functions.keys
92
+ end
93
+
94
+ # Alias for compatibility
95
+ def all
96
+ @functions.keys
97
+ end
98
+
99
+ # Category accessors for introspection
100
+ def comparison_operators
101
+ ComparisonFunctions.definitions.keys
102
+ end
103
+
104
+ def math_operations
105
+ MathFunctions.definitions.keys
106
+ end
107
+
108
+ def string_operations
109
+ StringFunctions.definitions.keys
110
+ end
111
+
112
+ def logical_operations
113
+ LogicalFunctions.definitions.keys
114
+ end
115
+
116
+ def collection_operations
117
+ CollectionFunctions.definitions.keys
118
+ end
119
+
120
+ def conditional_operations
121
+ ConditionalFunctions.definitions.keys
122
+ end
123
+
124
+ def type_operations
125
+ TypeFunctions.definitions.keys
126
+ end
127
+
128
+ # Development helpers
129
+ def reset!
130
+ @functions = CORE_OPERATIONS.dup
131
+ end
132
+
133
+ def freeze!
134
+ @functions.freeze
135
+ end
136
+ end
137
+ end
138
+ end