kumi-parser 0.0.2 → 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.
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'grammar'
4
+ require_relative 'transform'
5
+ require 'kumi/core/error_reporting'
6
+
7
+ module Kumi
8
+ module Parser
9
+ module TextParser
10
+ # Parslet-based parser with proper arithmetic operator precedence
11
+ class Parser
12
+ include Kumi::Core::ErrorReporting
13
+
14
+ def initialize
15
+ @grammar = Grammar.new
16
+ @transform = Transform.new
17
+ end
18
+
19
+ def parse(text_dsl, source_file: '<parslet_parser>')
20
+ # Parse with Parslet grammar
21
+ parse_tree = @grammar.parse(text_dsl)
22
+
23
+ # Transform to AST
24
+ ast = @transform.apply(parse_tree)
25
+
26
+ # Post-process to create final Root node if needed
27
+ post_process(ast)
28
+ rescue Parslet::ParseFailed => e
29
+ raise_syntax_error(
30
+ "Parse error: #{e.parse_failure_cause.ascii_tree}",
31
+ location: create_location(source_file, 1, 1)
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def post_process(ast)
38
+ # If it's already a Root node, return it
39
+ return ast if ast.is_a?(Syntax::Root)
40
+
41
+ # If it's a hash with input and declarations, convert it
42
+ if ast.is_a?(Hash) && ast[:input] && ast[:declarations]
43
+ input_decls = ast[:input][:declarations] || []
44
+ input_decls = [input_decls] unless input_decls.is_a?(Array)
45
+
46
+ # Process input declarations
47
+ processed_input_decls = input_decls.map do |input_decl|
48
+ process_input_declaration(input_decl)
49
+ end
50
+
51
+ other_decls = ast[:declarations] || []
52
+ other_decls = [other_decls] unless other_decls.is_a?(Array)
53
+
54
+ # Convert remaining hash values to proper nodes
55
+ values = []
56
+ traits = []
57
+
58
+ other_decls.each do |decl|
59
+ if decl.is_a?(Hash) && decl[:name] && decl[:expr]
60
+ expr = process_expression(decl[:expr])
61
+ values << Syntax::ValueDeclaration.new(decl[:name], expr, loc: create_location('<parslet>', 1, 1))
62
+ elsif decl.is_a?(Syntax::ValueDeclaration)
63
+ # Need to process the expression if it's still a hash
64
+ if decl.expression.is_a?(Hash)
65
+ processed_expr = process_expression(decl.expression)
66
+ values << Syntax::ValueDeclaration.new(decl.name, processed_expr, loc: decl.loc)
67
+ else
68
+ values << decl
69
+ end
70
+ elsif decl.is_a?(Syntax::TraitDeclaration)
71
+ traits << decl
72
+ end
73
+ end
74
+
75
+ Syntax::Root.new(processed_input_decls, values, traits, loc: create_location('<parslet>', 1, 1))
76
+ else
77
+ ast
78
+ end
79
+ end
80
+
81
+ def process_expression(expr)
82
+ # If it's already a proper AST node, return it
83
+ return expr unless expr.is_a?(Hash)
84
+
85
+ # Handle nested expression structures
86
+ if expr[:left] && expr[:ops]
87
+ left_expr = process_expression(expr[:left])
88
+ ops = expr[:ops] || []
89
+ ops = [ops] unless ops.is_a?(Array)
90
+
91
+ result = ops.inject(left_expr) do |left, op|
92
+ if op.is_a?(Hash) && op[:op] && op[:right]
93
+ op_name = op[:op].keys.first
94
+ right_expr = process_expression(op[:right])
95
+ Syntax::CallExpression.new(op_name, [left, right_expr], loc: create_location('<parslet>', 1, 1))
96
+ else
97
+ left
98
+ end
99
+ end
100
+
101
+ # Handle comparison
102
+ if expr[:comp] && expr[:comp][:op] && expr[:comp][:right]
103
+ comp = expr[:comp]
104
+ op_name = comp[:op].keys.first
105
+ right_expr = process_expression(comp[:right])
106
+ Syntax::CallExpression.new(op_name, [result, right_expr], loc: create_location('<parslet>', 1, 1))
107
+ else
108
+ result
109
+ end
110
+ else
111
+ expr
112
+ end
113
+ end
114
+
115
+ def process_input_declaration(input_decl)
116
+ return input_decl if input_decl.is_a?(Syntax::InputDeclaration)
117
+
118
+ if input_decl.is_a?(Hash)
119
+ if input_decl[:nested_fields]
120
+ # Array input declaration
121
+ nested_fields = input_decl[:nested_fields] || []
122
+ nested_fields = [nested_fields] unless nested_fields.is_a?(Array)
123
+
124
+ processed_fields = nested_fields.map do |field|
125
+ if field.is_a?(Hash) && field[:type] && field[:name]
126
+ Syntax::InputDeclaration.new(
127
+ field[:name],
128
+ field[:domain],
129
+ field[:type].to_sym,
130
+ [],
131
+ loc: create_location('<parslet>', 1, 1)
132
+ )
133
+ else
134
+ field
135
+ end
136
+ end
137
+
138
+ Syntax::InputDeclaration.new(
139
+ input_decl[:name],
140
+ nil,
141
+ :array,
142
+ processed_fields,
143
+ loc: create_location('<parslet>', 1, 1)
144
+ )
145
+ elsif input_decl[:type] && input_decl[:name]
146
+ # Simple input declaration
147
+ Syntax::InputDeclaration.new(
148
+ input_decl[:name],
149
+ input_decl[:domain],
150
+ input_decl[:type].to_sym,
151
+ [],
152
+ loc: create_location('<parslet>', 1, 1)
153
+ )
154
+ else
155
+ input_decl
156
+ end
157
+ else
158
+ input_decl
159
+ end
160
+ end
161
+
162
+ def create_location(file, line, column)
163
+ Syntax::Location.new(file: file, line: line, column: column)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parslet'
4
+ require 'kumi/syntax/node'
5
+ require 'kumi/syntax/root'
6
+ require 'kumi/syntax/input_declaration'
7
+ require 'kumi/syntax/value_declaration'
8
+ require 'kumi/syntax/trait_declaration'
9
+ require 'kumi/syntax/call_expression'
10
+ require 'kumi/syntax/input_reference'
11
+ require 'kumi/syntax/input_element_reference'
12
+ require 'kumi/syntax/declaration_reference'
13
+ require 'kumi/syntax/literal'
14
+
15
+ module Kumi
16
+ module Parser
17
+ module TextParser
18
+ class Transform < Parslet::Transform
19
+ LOC = Kumi::Syntax::Location.new(file: '<parslet_parser>', line: 1, column: 1)
20
+
21
+ # Literals
22
+ rule(integer: simple(:x)) { Kumi::Syntax::Literal.new(x.to_i, loc: LOC) }
23
+ rule(float: simple(:x)) { Kumi::Syntax::Literal.new(x.to_f, loc: LOC) }
24
+ rule(string: simple(:x)) { Kumi::Syntax::Literal.new(x.to_s, loc: LOC) }
25
+ rule(true: simple(:_)) { Kumi::Syntax::Literal.new(true, loc: LOC) }
26
+ rule(false: simple(:_)) { Kumi::Syntax::Literal.new(false, loc: LOC) }
27
+
28
+ # Symbols
29
+ rule(symbol: simple(:name)) { name.to_sym }
30
+
31
+ # Input and declaration references
32
+ rule(input_ref: simple(:path)) do
33
+ # Handle multi-level paths like "items.price"
34
+ path_parts = path.to_s.split('.')
35
+ if path_parts.length == 1
36
+ Kumi::Syntax::InputReference.new(path_parts[0].to_sym, loc: LOC)
37
+ else
38
+ Kumi::Syntax::InputElementReference.new(path_parts.map(&:to_sym), loc: LOC)
39
+ end
40
+ end
41
+
42
+ rule(decl_ref: simple(:name)) { Kumi::Syntax::DeclarationReference.new(name.to_sym, loc: LOC) }
43
+
44
+ # Function calls
45
+ rule(fn_name: simple(:name), args: sequence(:args)) do
46
+ Kumi::Syntax::CallExpression.new(name, args, loc: LOC)
47
+ end
48
+
49
+ rule(fn_name: simple(:name), args: []) do
50
+ Kumi::Syntax::CallExpression.new(name, [], loc: LOC)
51
+ end
52
+
53
+ # Arithmetic expressions with left-associativity
54
+ rule(left: simple(:l), ops: sequence(:operations)) do
55
+ operations.inject(l) do |left_expr, op|
56
+ op_name = op[:op].keys.first
57
+ Kumi::Syntax::CallExpression.new(op_name, [left_expr, op[:right]], loc: LOC)
58
+ end
59
+ end
60
+
61
+ rule(left: simple(:l), ops: []) { l }
62
+
63
+ # Comparison expressions
64
+ rule(left: simple(:l), comp: simple(:comparison)) do
65
+ if comparison && comparison[:op] && comparison[:right]
66
+ op_name = comparison[:op].keys.first
67
+ Kumi::Syntax::CallExpression.new(op_name, [l, comparison[:right]], loc: LOC)
68
+ else
69
+ l
70
+ end
71
+ end
72
+
73
+ rule(left: simple(:l), comp: nil) { l }
74
+
75
+ # Simple input declarations
76
+ rule(type: simple(:type), name: simple(:name)) do
77
+ Kumi::Syntax::InputDeclaration.new(name, nil, type.to_sym, [], loc: LOC)
78
+ end
79
+
80
+ # Nested array declarations
81
+ rule(name: simple(:name), nested_fields: sequence(:fields)) do
82
+ # Transform nested field hashes to InputDeclaration objects
83
+ transformed_fields = fields.map do |field|
84
+ if field.is_a?(Hash) && field[:type] && field[:name]
85
+ Kumi::Syntax::InputDeclaration.new(
86
+ field[:name],
87
+ field[:domain],
88
+ field[:type].to_sym,
89
+ [],
90
+ loc: LOC
91
+ )
92
+ else
93
+ field
94
+ end
95
+ end
96
+
97
+ # Create an array input declaration with nested fields
98
+ Kumi::Syntax::InputDeclaration.new(
99
+ name,
100
+ nil,
101
+ :array,
102
+ transformed_fields,
103
+ loc: LOC
104
+ )
105
+ end
106
+
107
+ rule(name: simple(:name), nested_fields: simple(:field)) do
108
+ # Single nested field case
109
+ transformed_field = if field.is_a?(Hash) && field[:type] && field[:name]
110
+ Kumi::Syntax::InputDeclaration.new(
111
+ field[:name],
112
+ field[:domain],
113
+ field[:type].to_sym,
114
+ [],
115
+ loc: LOC
116
+ )
117
+ else
118
+ field
119
+ end
120
+
121
+ Kumi::Syntax::InputDeclaration.new(
122
+ name,
123
+ nil,
124
+ :array,
125
+ [transformed_field],
126
+ loc: LOC
127
+ )
128
+ end
129
+
130
+ rule(type: simple(:type), name: simple(:name), expr: simple(:expr)) do
131
+ # Differentiate between value and trait declarations based on type
132
+ if type.to_s == 'value'
133
+ Kumi::Syntax::ValueDeclaration.new(name, expr, loc: LOC)
134
+ elsif type.to_s == 'trait'
135
+ Kumi::Syntax::TraitDeclaration.new(name, expr, loc: LOC)
136
+ else
137
+ # Fallback - shouldn't happen
138
+ Kumi::Syntax::ValueDeclaration.new(name, expr, loc: LOC)
139
+ end
140
+ end
141
+
142
+ # Handle the intermediate case before the expression gets fully transformed
143
+ rule(type: simple(:type), name: { symbol: simple(:name) }, expr: simple(:expr)) do
144
+ # Differentiate between value and trait declarations based on type
145
+ if type.to_s == 'value'
146
+ Kumi::Syntax::ValueDeclaration.new(name.to_sym, expr, loc: LOC)
147
+ elsif type.to_s == 'trait'
148
+ Kumi::Syntax::TraitDeclaration.new(name.to_sym, expr, loc: LOC)
149
+ else
150
+ # Fallback - shouldn't happen
151
+ Kumi::Syntax::ValueDeclaration.new(name.to_sym, expr, loc: LOC)
152
+ end
153
+ end
154
+
155
+ # Schema structure - convert the hash to Root node
156
+ rule(input: { declarations: sequence(:input_decls) }, declarations: sequence(:other_decls)) do
157
+ values = other_decls.select { |d| d.is_a?(Kumi::Syntax::ValueDeclaration) }
158
+ traits = other_decls.select { |d| d.is_a?(Kumi::Syntax::TraitDeclaration) }
159
+ Kumi::Syntax::Root.new(input_decls, values, traits, loc: LOC)
160
+ end
161
+
162
+ rule(input: { declarations: simple(:input_decl) }, declarations: sequence(:other_decls)) do
163
+ values = other_decls.select { |d| d.is_a?(Kumi::Syntax::ValueDeclaration) }
164
+ traits = other_decls.select { |d| d.is_a?(Kumi::Syntax::TraitDeclaration) }
165
+ Kumi::Syntax::Root.new([input_decl], values, traits, loc: LOC)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'syntax_validator'
4
+
5
+ module Kumi
6
+ module Parser
7
+ module TextParser
8
+ # TextParser module - all classes are autoloaded by Zeitwerk
9
+
10
+ class << self
11
+ # Check if text is syntactically valid
12
+ def valid?(text, source_file: '<input>')
13
+ validator.valid?(text, source_file: source_file)
14
+ end
15
+
16
+ # Validate text and return diagnostic collection
17
+ def validate(text, source_file: '<input>')
18
+ validator.validate(text, source_file: source_file)
19
+ end
20
+
21
+ # Get Monaco Editor format diagnostics
22
+ def diagnostics_for_monaco(text, source_file: '<input>')
23
+ validate(text, source_file: source_file).to_monaco
24
+ end
25
+
26
+ # Get CodeMirror format diagnostics
27
+ def diagnostics_for_codemirror(text, source_file: '<input>')
28
+ validate(text, source_file: source_file).to_codemirror
29
+ end
30
+
31
+ # Get JSON format diagnostics
32
+ def diagnostics_as_json(text, source_file: '<input>')
33
+ validate(text, source_file: source_file).to_json
34
+ end
35
+
36
+ # Parse text (compatibility method)
37
+ def parse(text, source_file: '<input>')
38
+ parser.parse(text, source_file: source_file)
39
+ end
40
+
41
+ private
42
+
43
+ def validator
44
+ @validator ||= SyntaxValidator.new
45
+ end
46
+
47
+ def parser
48
+ @parser ||= TextParser::Parser.new
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ VERSION = '0.0.3'
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ # This namespace will contain TextParser and related classes
6
+ # All classes will be autoloaded by Zeitwerk
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kumi'
4
+ require 'zeitwerk'
5
+ require 'parslet'
6
+
7
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
8
+ loader.ignore("#{__dir__}/kumi-parser.rb")
9
+ loader.ignore("#{__dir__}/kumi/parser/version.rb")
10
+ loader.setup
11
+
12
+ require_relative 'kumi/parser/version'
13
+
14
+ module Kumi
15
+ module Parser
16
+ # Parser extension for Kumi DSL
17
+ end
18
+ end
data/test_basic.rb ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ # Basic functionality test for kumi-parser
3
+
4
+ require_relative 'lib/kumi/parser'
5
+
6
+ schema_text = <<~SCHEMA
7
+ schema do
8
+ input do
9
+ integer :age
10
+ end
11
+ #{' '}
12
+ trait :adult, input.age >= 18
13
+ value :bonus, 100
14
+ end
15
+ SCHEMA
16
+
17
+ puts 'Testing kumi-parser...'
18
+ puts '=' * 40
19
+
20
+ begin
21
+ # Test validation
22
+ puts '1. Testing validation...'
23
+ diagnostics = Kumi::Parser::TextParser.validate(schema_text)
24
+ puts " ✅ Validation: #{diagnostics.empty? ? 'PASSED' : 'FAILED'}"
25
+
26
+ unless diagnostics.empty?
27
+ diagnostics.to_a.each do |d|
28
+ puts " Error: Line #{d.line}, Column #{d.column}: #{d.message}"
29
+ end
30
+ end
31
+
32
+ # Test parsing if validation passed
33
+ if diagnostics.empty?
34
+ puts '2. Testing parsing...'
35
+ ast = Kumi::Parser::TextParser.parse(schema_text)
36
+ puts " ✅ Parsing: #{ast ? 'PASSED' : 'FAILED'}"
37
+ puts " AST type: #{ast.class}"
38
+ end
39
+
40
+ puts "\n🎉 Basic functionality test completed!"
41
+ rescue StandardError => e
42
+ puts "❌ Error: #{e.message}"
43
+ puts e.backtrace.first(5)
44
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kumi-parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kumi Team
@@ -128,7 +128,29 @@ email:
128
128
  executables: []
129
129
  extensions: []
130
130
  extra_rdoc_files: []
131
- files: []
131
+ files:
132
+ - ".rspec"
133
+ - LICENSE
134
+ - README.md
135
+ - Rakefile
136
+ - examples/debug_text_parser.rb
137
+ - examples/debug_transform_rule.rb
138
+ - examples/text_parser_comprehensive_test.rb
139
+ - examples/text_parser_test_with_comments.rb
140
+ - kumi-parser.gemspec
141
+ - lib/kumi-parser.rb
142
+ - lib/kumi/parser.rb
143
+ - lib/kumi/parser/analyzer_diagnostic_converter.rb
144
+ - lib/kumi/parser/error_extractor.rb
145
+ - lib/kumi/parser/syntax_validator.rb
146
+ - lib/kumi/parser/text_parser.rb
147
+ - lib/kumi/parser/text_parser/api.rb
148
+ - lib/kumi/parser/text_parser/editor_diagnostic.rb
149
+ - lib/kumi/parser/text_parser/grammar.rb
150
+ - lib/kumi/parser/text_parser/parser.rb
151
+ - lib/kumi/parser/text_parser/transform.rb
152
+ - lib/kumi/parser/version.rb
153
+ - test_basic.rb
132
154
  homepage: https://github.com/amuta/kumi-parser
133
155
  licenses:
134
156
  - MIT