kumi-parser 0.0.2 → 0.0.4
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/.rspec +3 -0
- data/CLAUDE.md +120 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +10 -0
- data/examples/debug_text_parser.rb +41 -0
- data/examples/debug_transform_rule.rb +26 -0
- data/examples/text_parser_comprehensive_test.rb +333 -0
- data/examples/text_parser_test_with_comments.rb +146 -0
- data/kumi-parser.gemspec +45 -0
- data/lib/kumi/parser/base.rb +51 -0
- data/lib/kumi/parser/direct_parser.rb +502 -0
- data/lib/kumi/parser/error_extractor.rb +89 -0
- data/lib/kumi/parser/errors.rb +40 -0
- data/lib/kumi/parser/smart_tokenizer.rb +287 -0
- data/lib/kumi/parser/syntax_validator.rb +21 -0
- data/lib/kumi/parser/text_parser/api.rb +60 -0
- data/lib/kumi/parser/text_parser.rb +38 -0
- data/lib/kumi/parser/token.rb +84 -0
- data/lib/kumi/parser/token_metadata.rb +370 -0
- data/lib/kumi/parser/version.rb +7 -0
- data/lib/kumi/text_parser.rb +40 -0
- data/lib/kumi/text_schema.rb +31 -0
- data/lib/kumi-parser.rb +19 -0
- metadata +26 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0067c520152fae1c2285b2628f3b8b5d25eff145ae61780cf3d283310816c336
|
4
|
+
data.tar.gz: 4c604f36cd250a8d4672fef7c2acc667c7de498fe0ee088980bcbd8a744b8d5e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3d3342f43d5bb41b140f40d02d3eaac08abeb88509d4c4f4ce127e38ec3583e7566c68d8a96956d2877a2819b046663e2bf95aa246763bcf7556cf4df380934
|
7
|
+
data.tar.gz: f7891f6e388e224ebfd0da8d027d331c6794325e0d27715f8800f4aa9124dd3421fc36ca3afb1ac10e6f4e8c5db61f610a18e9db0a33d21c9292efbbd5fa45a7
|
data/.rspec
ADDED
data/CLAUDE.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# Kumi Parser - Technical Context
|
2
|
+
|
3
|
+
## Current Architecture (January 2025)
|
4
|
+
|
5
|
+
## Key Files
|
6
|
+
|
7
|
+
- `lib/kumi/parser/smart_tokenizer.rb` - Tokenizer with context tracking
|
8
|
+
- `lib/kumi/parser/direct_parser.rb` - Parser implementation (renamed from direct_ast_parser.rb)
|
9
|
+
- `lib/kumi/parser/token_metadata.rb` - Token types and metadata
|
10
|
+
- `lib/kumi/parser/text_parser.rb` - Public API maintaining compatibility
|
11
|
+
- `lib/kumi/parser/base.rb` - Core parsing interface
|
12
|
+
- `lib/kumi/parser/syntax_validator.rb` - Validation with proper diagnostics
|
13
|
+
- `lib/kumi/parser/errors.rb` - Custom error types
|
14
|
+
|
15
|
+
## Important Syntax Rules
|
16
|
+
|
17
|
+
- **Functions**: `fn(:symbol, args...)` only (no dot notation like `fn.max()`)
|
18
|
+
- **Operators**: Standard precedence (*/% > +- > comparisons > & > |)
|
19
|
+
- **Array access**: Uses `array[index]` syntax (converted to `:at` function internally)
|
20
|
+
- **Equality**: `==` and `!=` operators (converted from `:eq`/`:ne` tokens)
|
21
|
+
- **Multi-line expressions**: Parser skips newlines within expressions
|
22
|
+
- **Cascade**: `value :name do ... on condition, result ... base result ... end`
|
23
|
+
- **Constants**: Text parser cannot resolve Ruby constants - use inline values
|
24
|
+
|
25
|
+
## AST Structure & Compatibility
|
26
|
+
|
27
|
+
All nodes from `Kumi::Syntax::*` (defined in main kumi gem):
|
28
|
+
- `Root(inputs, attributes, traits)`
|
29
|
+
- `InputDeclaration(name, domain, type, children)`
|
30
|
+
- `ValueDeclaration(name, expression)`
|
31
|
+
- `TraitDeclaration(name, expression)`
|
32
|
+
- `CallExpression(fn_name, args)`
|
33
|
+
- `InputReference(name)` / `InputElementReference(path)`
|
34
|
+
- `DeclarationReference(name)`
|
35
|
+
- `Literal(value)`
|
36
|
+
- `CascadeExpression(cases)` / `CaseExpression(condition, result)`
|
37
|
+
- `ArrayExpression(elements)`
|
38
|
+
|
39
|
+
**Ruby DSL Compatibility**:
|
40
|
+
- Cascade conditions: Simple trait references wrapped in `all?([trait])` function calls
|
41
|
+
- Array access: `[index]` becomes `CallExpression(:at, [array, index])`
|
42
|
+
- Operators: `:eq` → `:==`, `:ne` → `:!=` for consistency
|
43
|
+
- Constants: Ruby constants resolved to values in DSL, remain as `DeclarationReference` in text parser
|
44
|
+
|
45
|
+
## Debugging & Testing
|
46
|
+
|
47
|
+
**View AST structure**:
|
48
|
+
```ruby
|
49
|
+
ast = Kumi::Parser::TextParser.parse(schema)
|
50
|
+
puts Kumi::Support::SExpressionPrinter.print(ast)
|
51
|
+
# => (Root
|
52
|
+
# inputs: [(InputDeclaration :income :float)]
|
53
|
+
# attributes: [(ValueDeclaration :tax (CallExpression :+ ...))]
|
54
|
+
# traits: [(TraitDeclaration :adult (CallExpression :>= ...))])
|
55
|
+
```
|
56
|
+
|
57
|
+
**Quick validation test**:
|
58
|
+
```ruby
|
59
|
+
ruby -r./lib/kumi/parser/text_parser -e "p Kumi::Parser::TextParser.valid?('schema do input do float :x end end')"
|
60
|
+
```
|
61
|
+
|
62
|
+
**Compare with Ruby DSL**:
|
63
|
+
```ruby
|
64
|
+
# Define schema in Ruby
|
65
|
+
module TestSchema
|
66
|
+
extend Kumi::Schema
|
67
|
+
schema do
|
68
|
+
input do
|
69
|
+
float :income
|
70
|
+
end
|
71
|
+
value :tax, fn(:calc, input.income)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parse equivalent text
|
76
|
+
text_ast = Kumi::Parser::TextParser.parse(<<~KUMI)
|
77
|
+
schema do
|
78
|
+
input do
|
79
|
+
float :income
|
80
|
+
end
|
81
|
+
value :tax, fn(:calc, input.income)
|
82
|
+
end
|
83
|
+
KUMI
|
84
|
+
|
85
|
+
# Compare ASTs
|
86
|
+
ruby_ast = TestSchema.__syntax_tree__
|
87
|
+
text_ast == ruby_ast # Should be true
|
88
|
+
```
|
89
|
+
|
90
|
+
- Tax schema in `spec/kumi/parser/text_parser_example tax_schema_spec.rb` is canonical test
|
91
|
+
- Run all tests: `rspec spec/kumi/parser/`
|
92
|
+
- Integration tests: `rspec spec/kumi/parser/text_parser_integration_spec.rb`
|
93
|
+
|
94
|
+
## Error Handling & Validation
|
95
|
+
|
96
|
+
- **Parse errors**: `Kumi::Parser::Errors::ParseError` (internal) → `Kumi::Errors::SyntaxError` (public API)
|
97
|
+
- **Tokenizer errors**: `Kumi::Parser::Errors::TokenizerError` with location info
|
98
|
+
- **Diagnostics**: Use `SyntaxValidator` for detailed error reporting with line/column info
|
99
|
+
- **Location tracking**: All tokens and AST nodes include `Kumi::Syntax::Location(file, line, column)`
|
100
|
+
|
101
|
+
## Test Status (January 2025)
|
102
|
+
|
103
|
+
✅ **All specs passing**: 32 examples, 0 failures, 1 pending
|
104
|
+
- ✅ Syntax validation with proper diagnostics
|
105
|
+
- ✅ AST compatibility with Ruby DSL (when constants aren't used)
|
106
|
+
- ✅ Integration with analyzer and compiler
|
107
|
+
- ✅ End-to-end execution testing
|
108
|
+
- ✅ Error type compatibility
|
109
|
+
|
110
|
+
## Known Limitations
|
111
|
+
|
112
|
+
- **Ruby constants**: Text parser cannot resolve Ruby constants like `CONST_NAME` - use inline values instead
|
113
|
+
- **Domain specification**: Parsing not fully implemented
|
114
|
+
- **Diagnostic APIs**: Monaco/CodeMirror/JSON format methods not implemented
|
115
|
+
|
116
|
+
## Performance
|
117
|
+
|
118
|
+
- Tokenization: <1ms for typical schemas
|
119
|
+
- Parsing: ~4ms for complete tax schema (21 values, 4 traits)
|
120
|
+
- Direct AST construction eliminates transformation overhead
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Kumi Team
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Kumi::Parser
|
2
|
+
|
3
|
+
Text parser for [Kumi](https://github.com/amuta/kumi) schemas. Direct tokenizer → AST construction with ~4ms parse time.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem 'kumi-parser'
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
require 'kumi/parser'
|
15
|
+
|
16
|
+
schema = <<~KUMI
|
17
|
+
schema do
|
18
|
+
input do
|
19
|
+
float :income
|
20
|
+
string :status
|
21
|
+
end
|
22
|
+
|
23
|
+
trait :adult, input.age >= 18
|
24
|
+
value :tax, fn(:calculate_tax, input.income)
|
25
|
+
end
|
26
|
+
KUMI
|
27
|
+
|
28
|
+
# Parse to AST
|
29
|
+
ast = Kumi::Parser::TextParser.parse(schema)
|
30
|
+
|
31
|
+
# Validate
|
32
|
+
Kumi::Parser::TextParser.valid?(schema) # => true
|
33
|
+
```
|
34
|
+
|
35
|
+
## API
|
36
|
+
|
37
|
+
- `parse(text)` → AST
|
38
|
+
- `valid?(text)` → Boolean
|
39
|
+
- `validate(text)` → Array of error hashes
|
40
|
+
|
41
|
+
## Syntax
|
42
|
+
|
43
|
+
```
|
44
|
+
schema do
|
45
|
+
input do
|
46
|
+
<type> :<name>[, domain: <spec>]
|
47
|
+
end
|
48
|
+
|
49
|
+
trait :<name>, <expression>
|
50
|
+
|
51
|
+
value :<name>, <expression>
|
52
|
+
value :<name> do
|
53
|
+
on <condition>, <result>
|
54
|
+
base <result>
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
**Function calls**: `fn(:name, arg1, arg2, ...)`
|
60
|
+
**Operators**: `+` `-` `*` `/` `%` `>` `<` `>=` `<=` `==` `!=` `&` `|`
|
61
|
+
**References**: `input.field`, `value_name`, `array[index]`
|
62
|
+
|
63
|
+
## Architecture
|
64
|
+
|
65
|
+
- `smart_tokenizer.rb` - Context-aware tokenization with embedded metadata
|
66
|
+
- `direct_ast_parser.rb` - Recursive descent parser, direct AST construction
|
67
|
+
- `token_metadata.rb` - Token types, precedence, and semantic hints
|
68
|
+
|
69
|
+
See `docs/` for technical details.
|
70
|
+
|
71
|
+
## License
|
72
|
+
|
73
|
+
MIT
|
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Debug the text parser transform
|
2
|
+
|
3
|
+
require_relative 'lib/kumi/text_parser'
|
4
|
+
|
5
|
+
schema_text = <<~SCHEMA
|
6
|
+
schema do
|
7
|
+
input do
|
8
|
+
integer :age
|
9
|
+
end
|
10
|
+
#{' '}
|
11
|
+
trait :adult, input.age >= 18
|
12
|
+
value :bonus, 100
|
13
|
+
end
|
14
|
+
SCHEMA
|
15
|
+
|
16
|
+
puts 'Debugging text parser...'
|
17
|
+
|
18
|
+
begin
|
19
|
+
# Test just the grammar parsing first
|
20
|
+
grammar = Kumi::TextParser::Grammar.new
|
21
|
+
parse_tree = grammar.parse(schema_text)
|
22
|
+
|
23
|
+
puts 'Raw parse tree:'
|
24
|
+
puts parse_tree.inspect
|
25
|
+
puts
|
26
|
+
|
27
|
+
# Now test the transform
|
28
|
+
transform = Kumi::TextParser::Transform.new
|
29
|
+
ast = transform.apply(parse_tree)
|
30
|
+
|
31
|
+
puts 'Transformed AST:'
|
32
|
+
puts ast.inspect
|
33
|
+
puts
|
34
|
+
|
35
|
+
puts 'AST structure:'
|
36
|
+
puts "- Values: #{ast.attributes.count} - #{ast.attributes.map(&:name)}"
|
37
|
+
puts "- Traits: #{ast.traits.count} - #{ast.traits.map(&:name)}"
|
38
|
+
rescue StandardError => e
|
39
|
+
puts "Error: #{e.message}"
|
40
|
+
puts e.backtrace.first(5)
|
41
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Debug specific transform rule
|
2
|
+
|
3
|
+
require_relative 'lib/kumi/text_parser'
|
4
|
+
|
5
|
+
# Test just the trait parsing
|
6
|
+
trait_text = 'trait :adult, input.age >= 18'
|
7
|
+
|
8
|
+
grammar = Kumi::TextParser::Grammar.new
|
9
|
+
transform = Kumi::TextParser::Transform.new
|
10
|
+
|
11
|
+
begin
|
12
|
+
# Parse just the trait declaration
|
13
|
+
parse_result = grammar.trait_declaration.parse(trait_text)
|
14
|
+
puts 'Trait parse result:'
|
15
|
+
puts parse_result.inspect
|
16
|
+
puts
|
17
|
+
|
18
|
+
# Try to transform it
|
19
|
+
transformed = transform.apply(parse_result)
|
20
|
+
puts 'Transformed result:'
|
21
|
+
puts transformed.inspect
|
22
|
+
puts "Class: #{transformed.class}"
|
23
|
+
rescue StandardError => e
|
24
|
+
puts "Error: #{e.message}"
|
25
|
+
puts e.backtrace.first(5)
|
26
|
+
end
|
@@ -0,0 +1,333 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../lib/kumi'
|
4
|
+
|
5
|
+
# Comprehensive test of all text parser supported features
|
6
|
+
# This example tests what the text parser ACTUALLY supports
|
7
|
+
# (not what the Ruby DSL supports)
|
8
|
+
|
9
|
+
module TextParserComprehensiveTest
|
10
|
+
extend Kumi::Schema
|
11
|
+
|
12
|
+
schema do
|
13
|
+
input do
|
14
|
+
# Basic type declarations
|
15
|
+
integer :age, domain: 18..65
|
16
|
+
float :score, domain: 0.0..100.0
|
17
|
+
string :status, domain: %w[active inactive suspended]
|
18
|
+
boolean :verified
|
19
|
+
any :metadata
|
20
|
+
|
21
|
+
# Nested array declarations
|
22
|
+
array :items do
|
23
|
+
string :name
|
24
|
+
float :price
|
25
|
+
integer :quantity
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# ============================================================
|
30
|
+
# ARITHMETIC OPERATIONS - All supported operators
|
31
|
+
# ============================================================
|
32
|
+
|
33
|
+
# Addition and subtraction
|
34
|
+
value :total_price, input.items.price + input.items.quantity
|
35
|
+
value :price_diff, input.items.price - 10.0
|
36
|
+
|
37
|
+
# Multiplication and division
|
38
|
+
value :scaled_score, input.score * 1.5
|
39
|
+
value :average_score, input.score / 2
|
40
|
+
|
41
|
+
# Modulo
|
42
|
+
value :remainder, input.age % 10
|
43
|
+
|
44
|
+
# Complex arithmetic with parentheses
|
45
|
+
value :complex_calc, (input.score * 2.5) + (input.age / 2) - 10
|
46
|
+
|
47
|
+
# ============================================================
|
48
|
+
# COMPARISON OPERATIONS - All comparison operators
|
49
|
+
# ============================================================
|
50
|
+
|
51
|
+
trait :adult, input.age >= 18
|
52
|
+
trait :senior, input.age > 60
|
53
|
+
trait :young_adult, input.age <= 25
|
54
|
+
trait :is_teen, input.age < 20
|
55
|
+
trait :perfect_score, input.score == 100.0
|
56
|
+
trait :not_perfect, input.score != 100.0
|
57
|
+
|
58
|
+
# String comparisons
|
59
|
+
trait :is_active, input.status == 'active'
|
60
|
+
trait :not_suspended, input.status != 'suspended'
|
61
|
+
|
62
|
+
# Boolean comparisons
|
63
|
+
trait :is_verified, input.verified == true
|
64
|
+
trait :not_verified, input.verified == false
|
65
|
+
|
66
|
+
# ============================================================
|
67
|
+
# LOGICAL OPERATIONS - AND and OR
|
68
|
+
# ============================================================
|
69
|
+
|
70
|
+
# AND operations using &
|
71
|
+
trait :eligible, (input.age >= 18) & (input.verified == true) & (input.status == 'active')
|
72
|
+
trait :premium_user, is_verified & is_active & (input.score > 80.0)
|
73
|
+
|
74
|
+
# OR operations using |
|
75
|
+
trait :needs_attention, (input.status == 'suspended') | (input.verified == false)
|
76
|
+
trait :special_case, senior | (input.score >= 95.0)
|
77
|
+
|
78
|
+
# Complex logical expressions
|
79
|
+
trait :complex_logic, (adult & is_verified) | (senior & is_active)
|
80
|
+
|
81
|
+
# ============================================================
|
82
|
+
# FUNCTION CALLS - fn(:name, args) syntax
|
83
|
+
# ============================================================
|
84
|
+
|
85
|
+
# Math functions
|
86
|
+
value :absolute_diff, fn(:abs, fn(:subtract, input.score, 50.0))
|
87
|
+
value :rounded_score, fn(:round, input.score)
|
88
|
+
value :clamped_score, fn(:clamp, input.score, 20.0, 80.0)
|
89
|
+
|
90
|
+
# String functions (note: no method syntax like .length)
|
91
|
+
value :name_length, fn(:string_length, input.status)
|
92
|
+
value :uppercase_status, fn(:upcase, input.status)
|
93
|
+
value :lowercase_status, fn(:downcase, input.status)
|
94
|
+
|
95
|
+
# Collection/aggregation functions on arrays
|
96
|
+
value :total_items, fn(:sum, input.items.quantity)
|
97
|
+
value :item_count, fn(:size, input.items)
|
98
|
+
value :max_price, fn(:max, input.items.price)
|
99
|
+
value :min_price, fn(:min, input.items.price)
|
100
|
+
|
101
|
+
# ============================================================
|
102
|
+
# REFERENCES - Both bare identifiers and ref() NOT supported in text parser
|
103
|
+
# ============================================================
|
104
|
+
|
105
|
+
# Using previously defined values/traits (bare identifiers)
|
106
|
+
trait :super_eligible, eligible & premium_user
|
107
|
+
value :bonus_points, fn(:multiply, total_price, 0.1)
|
108
|
+
|
109
|
+
# ============================================================
|
110
|
+
# CASCADE EXPRESSIONS - on/base syntax
|
111
|
+
# ============================================================
|
112
|
+
|
113
|
+
value :user_tier do
|
114
|
+
on premium_user, 'premium'
|
115
|
+
on eligible, 'standard'
|
116
|
+
on is_verified, 'basic'
|
117
|
+
base 'guest'
|
118
|
+
end
|
119
|
+
|
120
|
+
value :discount_rate do
|
121
|
+
on senior, 0.25
|
122
|
+
on premium_user, 0.15
|
123
|
+
on is_active, 0.05
|
124
|
+
base 0.0
|
125
|
+
end
|
126
|
+
|
127
|
+
# Cascades using complex conditions
|
128
|
+
value :risk_level do
|
129
|
+
on complex_logic, 'low'
|
130
|
+
on needs_attention, 'high'
|
131
|
+
on is_active, 'medium'
|
132
|
+
base 'unknown'
|
133
|
+
end
|
134
|
+
|
135
|
+
# ============================================================
|
136
|
+
# NESTED INPUT REFERENCES - Deep field access
|
137
|
+
# ============================================================
|
138
|
+
|
139
|
+
# Direct nested field access (parsed as multi-part input reference)
|
140
|
+
value :first_item_name, input.items.name
|
141
|
+
value :all_prices, input.items.price
|
142
|
+
|
143
|
+
# Operations on nested fields (broadcasting)
|
144
|
+
value :discounted_prices, input.items.price * 0.9
|
145
|
+
|
146
|
+
# ============================================================
|
147
|
+
# LITERALS - All supported literal types
|
148
|
+
# ============================================================
|
149
|
+
|
150
|
+
# Number literals (integers and floats)
|
151
|
+
value :constant_int, 42
|
152
|
+
value :constant_float, 3.14159
|
153
|
+
|
154
|
+
# String literals (double quotes only)
|
155
|
+
value :greeting, 'Hello, World!'
|
156
|
+
|
157
|
+
# Boolean literals
|
158
|
+
value :always_true, true
|
159
|
+
value :always_false, false
|
160
|
+
|
161
|
+
# ============================================================
|
162
|
+
# EDGE CASES AND LIMITATIONS
|
163
|
+
# ============================================================
|
164
|
+
|
165
|
+
# Complex parenthesized expressions
|
166
|
+
value :nested_parens, ((input.age + 10) * 2) / (input.score - 20)
|
167
|
+
|
168
|
+
# Multiple operators in sequence
|
169
|
+
value :operator_chain, input.age + 10 - 5 + 20 - 3
|
170
|
+
|
171
|
+
# Nested function calls
|
172
|
+
value :nested_functions, fn(:round, fn(:multiply, fn(:add, input.score, 10), 1.5))
|
173
|
+
|
174
|
+
# ============================================================
|
175
|
+
# NOT SUPPORTED (would fail in text parser)
|
176
|
+
# ============================================================
|
177
|
+
|
178
|
+
# These are commented out as they would cause parse errors:
|
179
|
+
|
180
|
+
# Array literals in expressions
|
181
|
+
# value :array_literal, [1, 2, 3, 4, 5]
|
182
|
+
# value :max_of_array, fn(:max, [input.age, 30, 50])
|
183
|
+
|
184
|
+
# Method-style function calls
|
185
|
+
# value :method_style, fn.add(input.age, 10)
|
186
|
+
|
187
|
+
# Sugar method calls on fields
|
188
|
+
# trait :long_status, input.status.length > 5
|
189
|
+
# value :name_chars, input.status.size
|
190
|
+
|
191
|
+
# Ternary/conditional expressions
|
192
|
+
# value :conditional, eligible ? 100 : 0
|
193
|
+
|
194
|
+
# Hash literals
|
195
|
+
# value :options, { min: 0, max: 100 }
|
196
|
+
|
197
|
+
# ref() syntax
|
198
|
+
# trait :ref_example, ref(:eligible) & ref(:premium_user)
|
199
|
+
|
200
|
+
# Symbol literals (except in fn() calls)
|
201
|
+
# value :symbol_value, :active
|
202
|
+
|
203
|
+
# Power operator
|
204
|
+
# value :squared, input.age ** 2
|
205
|
+
|
206
|
+
# Comments within the DSL
|
207
|
+
# value :test, 42 # this would fail
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# Test the schema with the text parser
|
212
|
+
if __FILE__ == $0
|
213
|
+
require_relative '../lib/kumi/text_parser'
|
214
|
+
|
215
|
+
# Create a clean schema without comments for text parser testing
|
216
|
+
schema_text = <<~SCHEMA
|
217
|
+
schema do
|
218
|
+
input do
|
219
|
+
integer :age, domain: 18..65
|
220
|
+
float :score, domain: 0.0..100.0
|
221
|
+
string :status, domain: %w[active inactive suspended]
|
222
|
+
boolean :verified
|
223
|
+
any :metadata
|
224
|
+
#{' '}
|
225
|
+
array :items do
|
226
|
+
string :name
|
227
|
+
float :price
|
228
|
+
integer :quantity
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
value :total_price, input.items.price + input.items.quantity
|
233
|
+
value :price_diff, input.items.price - 10.0
|
234
|
+
value :scaled_score, input.score * 1.5
|
235
|
+
value :average_score, input.score / 2
|
236
|
+
value :remainder, input.age % 10
|
237
|
+
value :complex_calc, (input.score * 2.5) + (input.age / 2) - 10
|
238
|
+
#{' '}
|
239
|
+
trait :adult, input.age >= 18
|
240
|
+
trait :senior, input.age > 60
|
241
|
+
trait :young_adult, input.age <= 25
|
242
|
+
trait :is_teen, input.age < 20
|
243
|
+
trait :perfect_score, input.score == 100.0
|
244
|
+
trait :not_perfect, input.score != 100.0
|
245
|
+
trait :is_active, input.status == "active"
|
246
|
+
trait :not_suspended, input.status != "suspended"
|
247
|
+
trait :is_verified, input.verified == true
|
248
|
+
trait :not_verified, input.verified == false
|
249
|
+
#{' '}
|
250
|
+
trait :eligible, (input.age >= 18) & (input.verified == true) & (input.status == "active")
|
251
|
+
trait :premium_user, is_verified & is_active & (input.score > 80.0)
|
252
|
+
trait :needs_attention, (input.status == "suspended") | (input.verified == false)
|
253
|
+
trait :special_case, senior | (input.score >= 95.0)
|
254
|
+
trait :complex_logic, (adult & is_verified) | (senior & is_active)
|
255
|
+
#{' '}
|
256
|
+
value :absolute_diff, fn(:abs, fn(:subtract, input.score, 50.0))
|
257
|
+
value :rounded_score, fn(:round, input.score)
|
258
|
+
value :clamped_score, fn(:clamp, input.score, 20.0, 80.0)
|
259
|
+
value :name_length, fn(:string_length, input.status)
|
260
|
+
value :uppercase_status, fn(:upcase, input.status)
|
261
|
+
value :lowercase_status, fn(:downcase, input.status)
|
262
|
+
value :total_items, fn(:sum, input.items.quantity)
|
263
|
+
value :item_count, fn(:size, input.items)
|
264
|
+
value :max_price, fn(:max, input.items.price)
|
265
|
+
value :min_price, fn(:min, input.items.price)
|
266
|
+
#{' '}
|
267
|
+
trait :super_eligible, eligible & premium_user
|
268
|
+
value :bonus_points, fn(:multiply, total_price, 0.1)
|
269
|
+
#{' '}
|
270
|
+
value :user_tier do
|
271
|
+
on premium_user, "premium"
|
272
|
+
on eligible, "standard"
|
273
|
+
on is_verified, "basic"
|
274
|
+
base "guest"
|
275
|
+
end
|
276
|
+
#{' '}
|
277
|
+
value :discount_rate do
|
278
|
+
on senior, 0.25
|
279
|
+
on premium_user, 0.15
|
280
|
+
on is_active, 0.05
|
281
|
+
base 0.0
|
282
|
+
end
|
283
|
+
#{' '}
|
284
|
+
value :risk_level do
|
285
|
+
on complex_logic, "low"
|
286
|
+
on needs_attention, "high"
|
287
|
+
on is_active, "medium"
|
288
|
+
base "unknown"
|
289
|
+
end
|
290
|
+
#{' '}
|
291
|
+
value :first_item_name, input.items.name
|
292
|
+
value :all_prices, input.items.price
|
293
|
+
value :discounted_prices, input.items.price * 0.9
|
294
|
+
#{' '}
|
295
|
+
value :constant_int, 42
|
296
|
+
value :constant_float, 3.14159
|
297
|
+
value :greeting, "Hello, World!"
|
298
|
+
value :always_true, true
|
299
|
+
value :always_false, false
|
300
|
+
#{' '}
|
301
|
+
value :nested_parens, ((input.age + 10) * 2) / (input.score - 20)
|
302
|
+
value :operator_chain, input.age + 10 - 5 + 20 - 3
|
303
|
+
value :nested_functions, fn(:round, fn(:multiply, fn(:add, input.score, 10), 1.5))
|
304
|
+
end
|
305
|
+
SCHEMA
|
306
|
+
|
307
|
+
puts 'Testing text parser with comprehensive DSL...'
|
308
|
+
puts '=' * 60
|
309
|
+
|
310
|
+
begin
|
311
|
+
# Validate the schema
|
312
|
+
diagnostics = Kumi::TextParser.validate(schema_text)
|
313
|
+
|
314
|
+
if diagnostics.empty?
|
315
|
+
puts '✅ Schema is valid!'
|
316
|
+
|
317
|
+
# Parse and show some info
|
318
|
+
ast = Kumi::TextParser.parse(schema_text)
|
319
|
+
puts "\nParsed successfully!"
|
320
|
+
puts "- Input fields: #{ast.inputs.map(&:name).join(', ')}"
|
321
|
+
puts "- Values: #{ast.attributes.count}"
|
322
|
+
puts "- Traits: #{ast.traits.count}"
|
323
|
+
else
|
324
|
+
puts '❌ Schema has errors:'
|
325
|
+
diagnostics.to_a.each do |diagnostic|
|
326
|
+
puts " Line #{diagnostic.line}, Column #{diagnostic.column}: #{diagnostic.message}"
|
327
|
+
end
|
328
|
+
end
|
329
|
+
rescue StandardError => e
|
330
|
+
puts "❌ Parser error: #{e.message}"
|
331
|
+
puts e.backtrace.first(5)
|
332
|
+
end
|
333
|
+
end
|