parsanol 1.0.1-aarch64-linux

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 (101) hide show
  1. checksums.yaml +7 -0
  2. data/HISTORY.txt +12 -0
  3. data/LICENSE +23 -0
  4. data/README.adoc +487 -0
  5. data/Rakefile +135 -0
  6. data/lib/parsanol/3.2/parsanol_native.so +0 -0
  7. data/lib/parsanol/3.3/parsanol_native.so +0 -0
  8. data/lib/parsanol/3.4/parsanol_native.so +0 -0
  9. data/lib/parsanol/4.0/parsanol_native.so +0 -0
  10. data/lib/parsanol/ast_visitor.rb +122 -0
  11. data/lib/parsanol/atoms/alternative.rb +122 -0
  12. data/lib/parsanol/atoms/base.rb +202 -0
  13. data/lib/parsanol/atoms/can_flatten.rb +194 -0
  14. data/lib/parsanol/atoms/capture.rb +38 -0
  15. data/lib/parsanol/atoms/context.rb +334 -0
  16. data/lib/parsanol/atoms/context_optimized.rb +38 -0
  17. data/lib/parsanol/atoms/custom.rb +110 -0
  18. data/lib/parsanol/atoms/cut.rb +66 -0
  19. data/lib/parsanol/atoms/dsl.rb +96 -0
  20. data/lib/parsanol/atoms/dynamic.rb +39 -0
  21. data/lib/parsanol/atoms/entity.rb +75 -0
  22. data/lib/parsanol/atoms/ignored.rb +37 -0
  23. data/lib/parsanol/atoms/infix.rb +162 -0
  24. data/lib/parsanol/atoms/lookahead.rb +82 -0
  25. data/lib/parsanol/atoms/named.rb +74 -0
  26. data/lib/parsanol/atoms/re.rb +83 -0
  27. data/lib/parsanol/atoms/repetition.rb +259 -0
  28. data/lib/parsanol/atoms/scope.rb +35 -0
  29. data/lib/parsanol/atoms/sequence.rb +194 -0
  30. data/lib/parsanol/atoms/str.rb +103 -0
  31. data/lib/parsanol/atoms/visitor.rb +91 -0
  32. data/lib/parsanol/atoms.rb +46 -0
  33. data/lib/parsanol/buffer.rb +133 -0
  34. data/lib/parsanol/builder_callbacks.rb +353 -0
  35. data/lib/parsanol/cause.rb +122 -0
  36. data/lib/parsanol/context.rb +39 -0
  37. data/lib/parsanol/convenience.rb +36 -0
  38. data/lib/parsanol/edit_tracker.rb +111 -0
  39. data/lib/parsanol/error_reporter/contextual.rb +99 -0
  40. data/lib/parsanol/error_reporter/deepest.rb +120 -0
  41. data/lib/parsanol/error_reporter/tree.rb +63 -0
  42. data/lib/parsanol/error_reporter.rb +100 -0
  43. data/lib/parsanol/expression/treetop.rb +154 -0
  44. data/lib/parsanol/expression.rb +106 -0
  45. data/lib/parsanol/fast_mode.rb +149 -0
  46. data/lib/parsanol/first_set.rb +79 -0
  47. data/lib/parsanol/grammar_builder.rb +177 -0
  48. data/lib/parsanol/incremental_parser.rb +177 -0
  49. data/lib/parsanol/interval_tree.rb +217 -0
  50. data/lib/parsanol/lazy_result.rb +179 -0
  51. data/lib/parsanol/lexer.rb +144 -0
  52. data/lib/parsanol/mermaid.rb +139 -0
  53. data/lib/parsanol/native/parser.rb +612 -0
  54. data/lib/parsanol/native/serializer.rb +248 -0
  55. data/lib/parsanol/native/transformer.rb +435 -0
  56. data/lib/parsanol/native/types.rb +42 -0
  57. data/lib/parsanol/native.rb +217 -0
  58. data/lib/parsanol/optimizer.rb +85 -0
  59. data/lib/parsanol/optimizers/choice_optimizer.rb +78 -0
  60. data/lib/parsanol/optimizers/cut_inserter.rb +179 -0
  61. data/lib/parsanol/optimizers/lookahead_optimizer.rb +50 -0
  62. data/lib/parsanol/optimizers/quantifier_optimizer.rb +60 -0
  63. data/lib/parsanol/optimizers/sequence_optimizer.rb +97 -0
  64. data/lib/parsanol/options/ruby_transform.rb +107 -0
  65. data/lib/parsanol/options/serialized.rb +94 -0
  66. data/lib/parsanol/options/zero_copy.rb +128 -0
  67. data/lib/parsanol/options.rb +20 -0
  68. data/lib/parsanol/parallel.rb +133 -0
  69. data/lib/parsanol/parser.rb +182 -0
  70. data/lib/parsanol/parslet.rb +151 -0
  71. data/lib/parsanol/pattern/binding.rb +91 -0
  72. data/lib/parsanol/pattern.rb +159 -0
  73. data/lib/parsanol/pool.rb +219 -0
  74. data/lib/parsanol/pools/array_pool.rb +75 -0
  75. data/lib/parsanol/pools/buffer_pool.rb +175 -0
  76. data/lib/parsanol/pools/position_pool.rb +92 -0
  77. data/lib/parsanol/pools/slice_pool.rb +64 -0
  78. data/lib/parsanol/position.rb +94 -0
  79. data/lib/parsanol/resettable.rb +29 -0
  80. data/lib/parsanol/result.rb +46 -0
  81. data/lib/parsanol/result_builder.rb +208 -0
  82. data/lib/parsanol/result_stream.rb +261 -0
  83. data/lib/parsanol/rig/rspec.rb +71 -0
  84. data/lib/parsanol/rope.rb +81 -0
  85. data/lib/parsanol/scope.rb +104 -0
  86. data/lib/parsanol/slice.rb +146 -0
  87. data/lib/parsanol/source/line_cache.rb +109 -0
  88. data/lib/parsanol/source.rb +180 -0
  89. data/lib/parsanol/source_location.rb +167 -0
  90. data/lib/parsanol/streaming_parser.rb +124 -0
  91. data/lib/parsanol/string_view.rb +195 -0
  92. data/lib/parsanol/transform.rb +226 -0
  93. data/lib/parsanol/version.rb +5 -0
  94. data/lib/parsanol/wasm/README.md +80 -0
  95. data/lib/parsanol/wasm/package.json +51 -0
  96. data/lib/parsanol/wasm/parsanol.js +252 -0
  97. data/lib/parsanol/wasm/parslet.d.ts +129 -0
  98. data/lib/parsanol/wasm_parser.rb +240 -0
  99. data/lib/parsanol.rb +280 -0
  100. data/parsanol-ruby.gemspec +67 -0
  101. metadata +280 -0
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Encapsules the concept of a position inside a string.
4
+ #
5
+ module Parsanol
6
+ class Position
7
+ include Parsanol::Resettable
8
+
9
+ # Changed to accessor to support pooling
10
+ attr_accessor :bytepos
11
+ attr_accessor :string, :charpos
12
+
13
+ include Comparable
14
+
15
+ def initialize(string, bytepos, charpos = nil)
16
+ @string = string
17
+ @bytepos = bytepos
18
+ @charpos = charpos
19
+ end
20
+
21
+ # Reset the position for reuse in object pooling.
22
+ # This allows the position to be reinitialized with new values for efficient reuse.
23
+ #
24
+ # @param string [String] Source string for position tracking
25
+ # @param bytepos [Integer] New byte position
26
+ # @param charpos [Integer, nil] Optional character position
27
+ # @return [self] Returns self for method chaining
28
+ #
29
+ def reset!(string, bytepos, charpos = nil)
30
+ @string = string
31
+ @bytepos = bytepos
32
+ @charpos = charpos
33
+ self
34
+ end
35
+
36
+ def charpos
37
+ # If charpos was provided during initialization, use it
38
+ return @charpos if @charpos
39
+
40
+ # Cache the calculated charpos to avoid repeated calculations
41
+ @charpos ||= calculate_charpos
42
+ end
43
+
44
+ private
45
+
46
+ def calculate_charpos
47
+ # Calculate it based on platform
48
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'opal'
49
+ # In Opal, convert byte position to character position.
50
+ # We need to calculate how many characters occupy the first @bytepos bytes.
51
+ `
52
+ var str = #{@string};
53
+ var bytePos = #{@bytepos};
54
+ var chars = Array.from(str);
55
+ var byteCount = 0;
56
+ var charCount = 0;
57
+
58
+ for (var i = 0; i < chars.length; i++) {
59
+ if (byteCount >= bytePos) break;
60
+
61
+ var char = chars[i];
62
+ var codePoint = char.codePointAt(0);
63
+
64
+ // Calculate UTF-8 byte length for this character
65
+ if (codePoint < 0x80) {
66
+ byteCount += 1;
67
+ } else if (codePoint < 0x800) {
68
+ byteCount += 2;
69
+ } else if (codePoint < 0x10000) {
70
+ byteCount += 3;
71
+ } else {
72
+ byteCount += 4;
73
+ }
74
+
75
+ if (byteCount <= bytePos) {
76
+ charCount++;
77
+ }
78
+ }
79
+
80
+ return charCount;
81
+ `
82
+ else
83
+ # Ruby: Use standard byteslice which handles Unicode correctly
84
+ @string.byteslice(0, @bytepos).size
85
+ end
86
+ end
87
+
88
+ public
89
+
90
+ def <=>(other)
91
+ bytepos <=> other.bytepos
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # Module for objects that can be reset for object pool reuse.
5
+ #
6
+ # Including this module signals that an object supports the reset!
7
+ # method for pooling purposes. This provides an explicit contract
8
+ # instead of duck-typing with respond_to?.
9
+ #
10
+ # @example
11
+ # class MyPooledObject
12
+ # include Parsanol::Resettable
13
+ #
14
+ # def reset!
15
+ # @state = nil
16
+ # self
17
+ # end
18
+ # end
19
+ #
20
+ module Resettable
21
+ # Reset object state for reuse in object pool.
22
+ #
23
+ # @return [self] for method chaining
24
+ # @raise [NotImplementedError] if not implemented by including class
25
+ def reset!
26
+ raise NotImplementedError, "#{self.class} must implement #reset!"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Phase 58: Result wrapper to replace [success, value] arrays
4
+ #
5
+ # This class wraps parse results to eliminate array allocations.
6
+ # Instead of [true, value] or [false, cause], we use Result objects.
7
+ #
8
+ # Benefits:
9
+ # - Eliminates array allocations (40% reduction)
10
+ # - Cleaner API with success? method
11
+ # - Can be optimized further (object pooling, etc.)
12
+ #
13
+ module Parsanol
14
+ class Result
15
+ attr_reader :value
16
+
17
+ def initialize(success, value)
18
+ @success = success
19
+ @value = value
20
+ end
21
+
22
+ def success?
23
+ @success
24
+ end
25
+
26
+ def error?
27
+ !@success
28
+ end
29
+
30
+ # Compatibility: Allow destructuring like arrays
31
+ # This enables gradual migration: result.success?, result.value
32
+ # or: success, value = result (array-like)
33
+ def to_ary
34
+ [@success, @value]
35
+ end
36
+
37
+ # Factory methods for common cases
38
+ def self.success(value)
39
+ new(true, value)
40
+ end
41
+
42
+ def self.error(cause)
43
+ new(false, cause)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # Base class for efficient result construction.
5
+ #
6
+ # ResultBuilder provides specialized construction patterns that avoid
7
+ # intermediate array allocations by building results directly.
8
+ #
9
+ # == Usage
10
+ #
11
+ # builder = ResultBuilder.for(:repetition, context, estimated_size: 10)
12
+ # builder.add_element(value1)
13
+ # builder.add_element(value2)
14
+ # result = builder.build # Returns LazyResult
15
+ #
16
+ # == Builders
17
+ #
18
+ # - RepetitionBuilder: For repetition results
19
+ # - SequenceBuilder: For sequence results
20
+ # - HashBuilder: For named capture results
21
+ #
22
+ class ResultBuilder
23
+ # Factory method to create appropriate builder.
24
+ #
25
+ # @param type [Symbol] Builder type (:repetition, :sequence, :hash)
26
+ # @param context [Context] Parse context
27
+ # @param options [Hash] Builder options
28
+ # @return [ResultBuilder] Appropriate builder instance
29
+ #
30
+ def self.for(type, context, **options)
31
+ case type
32
+ when :repetition
33
+ RepetitionBuilder.new(context, **options)
34
+ when :sequence
35
+ SequenceBuilder.new(context, **options)
36
+ when :hash
37
+ HashBuilder.new(context, **options)
38
+ else
39
+ raise ArgumentError, "Unknown builder type: #{type}"
40
+ end
41
+ end
42
+
43
+ # Initialize builder.
44
+ #
45
+ # @param context [Context] Parse context for buffer access
46
+ #
47
+ def initialize(context)
48
+ @context = context
49
+ end
50
+
51
+ # Add element to result (subclasses implement).
52
+ #
53
+ # @param value [Object] Value to add
54
+ # @return [self] For method chaining
55
+ #
56
+ def add_element(value)
57
+ raise NotImplementedError
58
+ end
59
+
60
+ # Build final result (subclasses implement).
61
+ #
62
+ # @return [Object] Constructed result
63
+ #
64
+ def build
65
+ raise NotImplementedError
66
+ end
67
+
68
+ # Release resources (subclasses implement).
69
+ #
70
+ # @return [void]
71
+ #
72
+ def release
73
+ # Default: no-op
74
+ end
75
+ end
76
+
77
+ # Builder for repetition results.
78
+ #
79
+ # Constructs [:repetition, ...] arrays efficiently.
80
+ #
81
+ class RepetitionBuilder < ResultBuilder
82
+ # Initialize repetition builder.
83
+ #
84
+ # @param context [Context] Parse context
85
+ # @param tag [Symbol] Tag to use (default: :repetition)
86
+ # @param estimated_size [Integer] Estimated element count
87
+ #
88
+ def initialize(context, tag: :repetition, estimated_size: 10)
89
+ super(context)
90
+ @tag = tag
91
+ @buffer = context.acquire_buffer(size: estimated_size + 1)
92
+ @buffer.push(@tag)
93
+ end
94
+
95
+ # Add element to repetition.
96
+ #
97
+ # @param value [Object] Element to add
98
+ # @return [self]
99
+ #
100
+ def add_element(value)
101
+ @buffer.push(value)
102
+ self
103
+ end
104
+
105
+ # Build LazyResult.
106
+ #
107
+ # @return [LazyResult] Lazy repetition result
108
+ #
109
+ def build
110
+ Parsanol::LazyResult.new(@buffer, @context)
111
+ end
112
+
113
+ # Release buffer on failure.
114
+ #
115
+ # @return [void]
116
+ #
117
+ def release
118
+ @context.release_buffer(@buffer) if @buffer
119
+ @buffer = nil
120
+ end
121
+ end
122
+
123
+ # Builder for sequence results.
124
+ #
125
+ # Constructs [:sequence, ...] arrays efficiently.
126
+ #
127
+ class SequenceBuilder < ResultBuilder
128
+ # Initialize sequence builder.
129
+ #
130
+ # @param context [Context] Parse context
131
+ # @param size [Integer] Expected sequence length
132
+ #
133
+ def initialize(context, size: 5)
134
+ super(context)
135
+ @buffer = context.acquire_buffer(size: size + 1)
136
+ @buffer.push(:sequence)
137
+ end
138
+
139
+ # Add element to sequence.
140
+ #
141
+ # @param value [Object] Element to add
142
+ # @return [self]
143
+ #
144
+ def add_element(value)
145
+ @buffer.push(value) if value # Skip nil values
146
+ self
147
+ end
148
+
149
+ # Build LazyResult.
150
+ #
151
+ # @return [LazyResult] Lazy sequence result
152
+ #
153
+ def build
154
+ Parsanol::LazyResult.new(@buffer, @context)
155
+ end
156
+
157
+ # Release buffer on failure.
158
+ #
159
+ # @return [void]
160
+ #
161
+ def release
162
+ @context.release_buffer(@buffer) if @buffer
163
+ @buffer = nil
164
+ end
165
+ end
166
+
167
+ # Builder for hash results (named captures).
168
+ #
169
+ # Constructs hashes directly without intermediate arrays.
170
+ #
171
+ class HashBuilder < ResultBuilder
172
+ # Initialize hash builder.
173
+ #
174
+ # @param context [Context] Parse context
175
+ #
176
+ def initialize(context)
177
+ super
178
+ @hash = {}
179
+ end
180
+
181
+ # Add key-value pair.
182
+ #
183
+ # @param key [Symbol] Hash key
184
+ # @param value [Object] Hash value
185
+ # @return [self]
186
+ #
187
+ def add_pair(key, value)
188
+ @hash[key] = value
189
+ self
190
+ end
191
+
192
+ # Build hash result.
193
+ #
194
+ # @return [Hash] Constructed hash
195
+ #
196
+ def build
197
+ @hash
198
+ end
199
+
200
+ # Release resources (hash cleanup).
201
+ #
202
+ # @return [void]
203
+ #
204
+ def release
205
+ @hash = nil
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Parsanol
4
+ # Streaming result iterator for memory-efficient parsing.
5
+ #
6
+ # Provides an Enumerable interface over parse results, allowing
7
+ # incremental processing without materializing the entire tree.
8
+ # Uses depth-first traversal to minimize memory usage.
9
+ #
10
+ # == Motivation
11
+ #
12
+ # Traditional parsing materializes the entire parse tree in memory:
13
+ #
14
+ # results = parser.parse(large_input) # Full tree in memory
15
+ # results.each { |node| process(node) }
16
+ #
17
+ # For large inputs, this can consume significant memory. ResultStream
18
+ # provides lazy iteration without full tree materialization:
19
+ #
20
+ # stream = ResultStream.new(parser.parse(input))
21
+ # stream.each { |node| process(node) } # Processes incrementally
22
+ #
23
+ # == Usage
24
+ #
25
+ # Basic iteration:
26
+ #
27
+ # stream = ResultStream.new(parse_tree)
28
+ # stream.each { |node| puts node }
29
+ #
30
+ # Filtering (leverages Enumerable):
31
+ #
32
+ # stream.select { |node| node.is_a?(Hash) }.each { |hash| process(hash) }
33
+ #
34
+ # Mapping:
35
+ #
36
+ # transformed = stream.map { |node| transform(node) }
37
+ #
38
+ # == Performance Characteristics
39
+ #
40
+ # - Memory: O(tree depth) instead of O(tree size)
41
+ # - Speed: Minimal overhead (~1-2% vs direct iteration)
42
+ # - Lazy evaluation: Nodes processed on-demand
43
+ #
44
+ # == Integration with Parser
45
+ #
46
+ # Can be used directly with parse results:
47
+ #
48
+ # parser = MyParser.new
49
+ # result = parser.parse(input)
50
+ # stream = ResultStream.new(result)
51
+ #
52
+ # Or through the optional stream method on Base:
53
+ #
54
+ # stream = parser.stream(input) # If available
55
+ #
56
+ class ResultStream
57
+ include Enumerable
58
+
59
+ # Creates a new result stream.
60
+ #
61
+ # @param tree [Object] Parse tree (Hash, Array, or scalar)
62
+ def initialize(tree)
63
+ @tree = tree
64
+ end
65
+
66
+ # Iterates over all nodes in the parse tree.
67
+ # Uses depth-first traversal to minimize memory usage.
68
+ #
69
+ # Traversal order:
70
+ # 1. Current node (pre-order)
71
+ # 2. Child nodes (recursive)
72
+ #
73
+ # This ensures that:
74
+ # - Only the current path is kept in memory (stack)
75
+ # - Parent nodes are yielded before children
76
+ # - Natural processing order for most use cases
77
+ #
78
+ # @yield [node] Each node in the tree
79
+ # @yieldparam node [Object] Current node (Hash, Array, or scalar)
80
+ # @return [Enumerator] if no block given
81
+ #
82
+ # @example Basic iteration
83
+ # stream.each { |node| puts node.class }
84
+ #
85
+ # @example Lazy enumeration
86
+ # enum = stream.each # Returns Enumerator
87
+ # enum.next # Get next node
88
+ #
89
+ def each(&block)
90
+ return enum_for(:each) unless block_given?
91
+
92
+ traverse(@tree, &block)
93
+ self
94
+ end
95
+
96
+ # Filters nodes by type.
97
+ #
98
+ # @param klass [Class] Class to filter by
99
+ # @return [Enumerator] Filtered nodes
100
+ #
101
+ # @example Get all hash nodes
102
+ # stream.nodes_of_type(Hash)
103
+ #
104
+ def nodes_of_type(klass)
105
+ grep(klass)
106
+ end
107
+
108
+ # Returns all hash nodes in the tree.
109
+ #
110
+ # @return [Enumerator] Hash nodes
111
+ #
112
+ # @example
113
+ # stream.hashes.each { |h| puts h.keys }
114
+ #
115
+ def hashes
116
+ nodes_of_type(Hash)
117
+ end
118
+
119
+ # Returns all array nodes in the tree.
120
+ #
121
+ # @return [Enumerator] Array nodes
122
+ #
123
+ # @example
124
+ # stream.arrays.each { |a| puts a.size }
125
+ #
126
+ def arrays
127
+ nodes_of_type(Array)
128
+ end
129
+
130
+ # Returns all scalar nodes (non-Hash, non-Array).
131
+ #
132
+ # @return [Enumerator] Scalar nodes
133
+ #
134
+ # @example
135
+ # stream.scalars.each { |s| puts s }
136
+ #
137
+ def scalars
138
+ select { |node| !node.is_a?(Hash) && !node.is_a?(Array) }
139
+ end
140
+
141
+ # Returns nodes matching a predicate at a specific depth.
142
+ #
143
+ # @param depth [Integer] Tree depth (0 = root)
144
+ # @yield [node] Predicate to test each node
145
+ # @return [Enumerator] Matching nodes
146
+ #
147
+ # @example Get all nodes at depth 2
148
+ # stream.at_depth(2) { true }
149
+ #
150
+ def at_depth(target_depth, &predicate)
151
+ predicate ||= proc { true }
152
+ depth_traverse(@tree, 0, target_depth, &predicate)
153
+ end
154
+
155
+ # Counts total nodes in the tree.
156
+ #
157
+ # @return [Integer] Total node count
158
+ #
159
+ # @example
160
+ # stream.count # => 42
161
+ #
162
+ def count
163
+ counter = 0
164
+ each { counter += 1 }
165
+ counter
166
+ end
167
+
168
+ # Returns maximum depth of the tree.
169
+ #
170
+ # @return [Integer] Maximum depth
171
+ #
172
+ # @example
173
+ # stream.max_depth # => 5
174
+ #
175
+ def max_depth
176
+ find_max_depth(@tree, 0)
177
+ end
178
+
179
+ private
180
+
181
+ # Depth-first tree traversal with pre-order visiting.
182
+ #
183
+ # @param node [Object] Current node
184
+ # @yield [node] Each visited node
185
+ #
186
+ def traverse(node, &block)
187
+ # Yield current node first (pre-order)
188
+ yield node
189
+
190
+ # Recursively traverse children
191
+ case node
192
+ when Array
193
+ node.each { |item| traverse(item, &block) }
194
+ when Hash
195
+ node.each_value { |value| traverse(value, &block) }
196
+ end
197
+ # Scalars have no children, stop here
198
+ end
199
+
200
+ # Depth-aware traversal for filtering by level.
201
+ #
202
+ # @param node [Object] Current node
203
+ # @param current_depth [Integer] Current depth in tree
204
+ # @param target_depth [Integer] Depth to match
205
+ # @yield [node] Matching nodes at target depth
206
+ # @return [Enumerator]
207
+ #
208
+ def depth_traverse(node, current_depth, target_depth, &block)
209
+ return enum_for(:depth_traverse, node, current_depth, target_depth) unless block_given?
210
+
211
+ # Check if we're at target depth
212
+ return [node].to_enum if current_depth == target_depth && yield(node)
213
+
214
+ # Recurse to children if not at target depth yet
215
+ results = []
216
+ if current_depth < target_depth
217
+ case node
218
+ when Array
219
+ node.each do |item|
220
+ depth_traverse(item, current_depth + 1, target_depth, &block).each do |result|
221
+ results << result
222
+ end
223
+ end
224
+ when Hash
225
+ node.each_value do |value|
226
+ depth_traverse(value, current_depth + 1, target_depth, &block).each do |result|
227
+ results << result
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ results.to_enum
234
+ end
235
+
236
+ # Find maximum depth of tree recursively.
237
+ #
238
+ # @param node [Object] Current node
239
+ # @param current_depth [Integer] Current depth
240
+ # @return [Integer] Maximum depth from this node
241
+ #
242
+ def find_max_depth(node, current_depth)
243
+ max = current_depth
244
+
245
+ case node
246
+ when Array
247
+ node.each do |item|
248
+ depth = find_max_depth(item, current_depth + 1)
249
+ max = depth if depth > max
250
+ end
251
+ when Hash
252
+ node.each_value do |value|
253
+ depth = find_max_depth(value, current_depth + 1)
254
+ max = depth if depth > max
255
+ end
256
+ end
257
+
258
+ max
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher for parsing expectations. Provides a fluent DSL for
4
+ # specifying parsing behavior in tests.
5
+ #
6
+ # @example Basic usage
7
+ # expect(parser).to parse("input")
8
+ #
9
+ # @example With expected output
10
+ # expect(parser).to parse("123").as(123)
11
+ #
12
+ # @example With block validation
13
+ # expect(parser).to parse("input").as { |result| result.size > 0 }
14
+ #
15
+ # Inspired by RSpec matcher patterns and Parslet's testing utilities.
16
+ #
17
+ RSpec::Matchers.define(:parse) do |input_text, options|
18
+ expected_output = nil
19
+ validator_block = nil
20
+ actual_result = nil
21
+ error_trace = nil
22
+
23
+ match do |parser_instance|
24
+ actual_result = parser_instance.parse(input_text)
25
+ if validator_block
26
+ validator_block.call(actual_result)
27
+ else
28
+ expected_output.nil? || expected_output == actual_result
29
+ end
30
+ rescue Parsanol::ParseFailed => e
31
+ error_trace = e.parse_failure_cause.ascii_tree if options && options[:trace]
32
+ false
33
+ end
34
+
35
+ failure_message do |parser_instance|
36
+ if validator_block
37
+ "expected output of parsing #{input_text.inspect} with " \
38
+ "#{parser_instance.inspect} to meet block conditions, but it didn't"
39
+ else
40
+ msg = if expected_output
41
+ "expected output of parsing #{input_text.inspect} with " \
42
+ "#{parser_instance.inspect} to equal #{expected_output.inspect}, " \
43
+ "but was #{actual_result.inspect}"
44
+ else
45
+ "expected #{parser_instance.inspect} to be able to parse " \
46
+ "#{input_text.inspect}"
47
+ end
48
+ msg += "\n#{error_trace}" if error_trace
49
+ msg
50
+ end
51
+ end
52
+
53
+ failure_message_when_negated do |parser_instance|
54
+ if validator_block
55
+ "expected output of parsing #{input_text.inspect} with " \
56
+ "#{parser_instance.inspect} not to meet block conditions, but it did"
57
+ elsif expected_output
58
+ "expected output of parsing #{input_text.inspect} with " \
59
+ "#{parser_instance.inspect} not to equal #{expected_output.inspect}"
60
+ else
61
+ "expected #{parser_instance.inspect} to not parse " \
62
+ "#{input_text.inspect}, but it did"
63
+ end
64
+ end
65
+
66
+ # Chain method for specifying expected output or validation block
67
+ chain :as do |expected = nil, &block|
68
+ expected_output = expected
69
+ validator_block = block
70
+ end
71
+ end