json_completer 1.0.0 → 1.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fe03f14437a3cfd88193b2cfc7a7f156e116632b1937a42ea6c4a1aefffa7c2
4
- data.tar.gz: b20c23c7843a3ff8f18f0110ed824b54d1306eb440309186efc00c71317d33ba
3
+ metadata.gz: acf870fc5a65bf4f4f1b586cfa61e012137fc96c0c48e3345d26a38da7d765bf
4
+ data.tar.gz: fe454f3e2485ae789840bb9c55c7fe72f3e8c9afffc9eccd0d13cc53ede8c05f
5
5
  SHA512:
6
- metadata.gz: 9815201cb51addf45defae03cb710502ed93091208ec13c404865fdbbd58be2b20773334e3528beef4d82bd93cb3de81d2de22e5ecad996aebdded6e3a138b87
7
- data.tar.gz: 261db1237466e85281eb969d90b7f6d90555c72955df728d001122db6f0d0cfd1546e399d3f7185d869d0e689108d49cb63347fffd5f5484dfe24c311e22d193
6
+ metadata.gz: 5cb2ad4d01e5f204cafc9b7fb4df996e319ba2c0dccdce6a16f23eacd2493cbdf1168c068c15586b7e4643863935ca70fe8e2628ed8e0994439fefa2151c60b7
7
+ data.tar.gz: 7dcd0cee8613e5a45f9cedbee90386234f31fe36ce832bb5abe160d4993b25a06d891340b9f17c0559026dd8e4b5bb4754a9a350ca480baed8791496f2837437
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # JsonCompleter
2
2
 
3
- A Ruby gem that converts partial JSON strings into valid JSON with high-performance incremental parsing. Efficiently processes streaming JSON with O(n) complexity for new data by maintaining parsing state between chunks. Handles truncated primitives, missing values, and unclosed structures without reprocessing previously parsed data.
3
+ A Ruby gem for incremental parsing of partial and incomplete JSON streams. It is built for streaming output from LLM providers such as OpenAI and Anthropic, and processes each new chunk in O(n) time by maintaining parser state between calls. Use `.parse` for parsed Ruby values and `.complete` when you specifically need completed JSON text.
4
4
 
5
5
  ## Installation
6
6
 
@@ -26,44 +26,58 @@ gem install json_completer
26
26
 
27
27
  ### Basic Usage
28
28
 
29
- Complete partial JSON strings in one call:
29
+ Use `.parse` when you want the current parsed Ruby value directly from a partial stream:
30
30
 
31
31
  ```ruby
32
32
  require 'json_completer'
33
33
 
34
- # Complete truncated JSON
35
- JsonCompleter.complete('{"name": "John", "age":')
36
- # => '{"name": "John", "age": null}'
34
+ # Parse partial JSON into Ruby objects
35
+ JsonCompleter.parse('{"name": "John", "age":')
36
+ # => {"name" => "John", "age" => nil}
37
37
 
38
38
  # Handle incomplete strings
39
- JsonCompleter.complete('{"message": "Hello wo')
40
- # => '{"message": "Hello wo"}'
39
+ JsonCompleter.parse('{"message": "Hello wo')
40
+ # => {"message" => "Hello wo"}
41
41
 
42
- # Fix unclosed structures
43
- JsonCompleter.complete('[1, 2, {"key": "value"')
44
- # => '[1, 2, {"key": "value"}]'
42
+ # Close unclosed structures
43
+ JsonCompleter.parse('[1, 2, {"key": "value"')
44
+ # => [1, 2, {"key" => "value"}]
45
45
  ```
46
46
 
47
47
  ### Incremental Processing
48
48
 
49
- For streaming scenarios where JSON arrives in chunks. Each call processes only new data (O(n) complexity) by maintaining parsing state, making it highly efficient for large streaming responses:
49
+ For streaming scenarios where JSON arrives in chunks. Each call processes only new data (O(n) complexity) by maintaining parsing state:
50
50
 
51
51
  ```ruby
52
52
  completer = JsonCompleter.new
53
53
 
54
54
  # Process first chunk
55
- result1 = completer.complete('{"users": [{"name": "')
56
- # => '{"users": [{"name": ""}]}'
55
+ result1 = completer.parse('{"users": [{"name": "')
56
+ # => {"users" => [{"name" => ""}]}
57
57
 
58
58
  # Process additional data
59
- result2 = completer.complete('{"users": [{"name": "Alice"}')
60
- # => '{"users": [{"name": "Alice"}]}'
59
+ result2 = completer.parse('{"users": [{"name": "Alice"}')
60
+ # => {"users" => [{"name" => "Alice"}]}
61
61
 
62
- # Final complete JSON
63
- result3 = completer.complete('{"users": [{"name": "Alice"}, {"name": "Bob"}]}')
64
- # => '{"users": [{"name": "Alice"}, {"name": "Bob"}]}'
62
+ # Final parsed value
63
+ result3 = completer.parse('{"users": [{"name": "Alice"}, {"name": "Bob"}]}')
64
+ # => {"users" => [{"name" => "Alice"}, {"name" => "Bob"}]}
65
65
  ```
66
66
 
67
+ ### String Output with `.complete`
68
+
69
+ Use `.complete` when you specifically need completed JSON text instead of parsed Ruby objects:
70
+
71
+ ```ruby
72
+ JsonCompleter.complete('{"name": "John", "age":')
73
+ # => '{"name": "John", "age": null}'
74
+
75
+ JsonCompleter.complete('[1, 2, {"key": "value"')
76
+ # => '[1, 2, {"key": "value"}]'
77
+ ```
78
+
79
+ This is the second-tier option when another layer expects JSON text and you want `json_completer` to materialize the current partial state as valid JSON.
80
+
67
81
  #### Performance Characteristics
68
82
 
69
83
  - **Zero reprocessing**: Maintains parsing state to avoid reparsing previously processed data
@@ -73,9 +87,9 @@ result3 = completer.complete('{"users": [{"name": "Alice"}, {"name": "Bob"}]}')
73
87
 
74
88
  ### Common Use Cases
75
89
 
76
- - **High-performance streaming JSON**: Process large JSON responses efficiently as data arrives over network connections
77
- - **Truncated API responses**: Complete JSON that was cut off due to size limits
78
- - **Log parsing**: Handle incomplete JSON entries in log files
90
+ - **LLM streaming output**: Parse partial JSON emitted token-by-token from providers such as OpenAI and Anthropic
91
+ - **Incremental structured output parsing**: Keep a live Ruby object while more JSON arrives
92
+ - **JSON text completion**: Produce valid JSON text snapshots for downstream consumers that require a string
79
93
 
80
94
  ## Contributing
81
95
 
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonCompleter
4
+ module CompletionEngine
5
+ def complete(partial_json)
6
+ input = partial_json
7
+
8
+ if @state.nil? || @state.input_length > input.length
9
+ @state = ParsingState.new
10
+ end
11
+
12
+ return input if input.empty?
13
+ return input if valid_json_primitive_or_document?(input)
14
+
15
+ if @state.input_length == input.length && !@state.output_tokens.empty?
16
+ return finalize_completion(@state.output_tokens.dup, @state.context_stack.dup, @state.incomplete_string_token)
17
+ end
18
+
19
+ output_tokens = @state.output_tokens.dup
20
+ context_stack = @state.context_stack.dup
21
+ index = @state.last_index
22
+ length = input.length
23
+ incomplete_string_token = @state.incomplete_string_token
24
+
25
+ if incomplete_string_token && output_tokens.last&.start_with?('"') && output_tokens.last.end_with?('"')
26
+ output_tokens.pop
27
+ end
28
+
29
+ while index < length
30
+ if incomplete_string_token && index == @state.last_index
31
+ index, status = Scanners.scan_string(input, index, incomplete_string_token)
32
+
33
+ break unless %i[terminated invalid_unicode].include?(status)
34
+
35
+ output_tokens << incomplete_string_token.buffer.string
36
+ incomplete_string_token = nil
37
+
38
+ next
39
+ end
40
+
41
+ char = input[index]
42
+ last_significant_char_in_output = get_last_significant_char(output_tokens)
43
+
44
+ case char
45
+ when '{'
46
+ ensure_comma_before_new_item(output_tokens, context_stack, last_significant_char_in_output)
47
+ ensure_colon_if_value_expected(output_tokens, context_stack, last_significant_char_in_output)
48
+ output_tokens << char
49
+ context_stack << '{'
50
+ index += 1
51
+ when '['
52
+ ensure_comma_before_new_item(output_tokens, context_stack, last_significant_char_in_output)
53
+ ensure_colon_if_value_expected(output_tokens, context_stack, last_significant_char_in_output)
54
+ output_tokens << char
55
+ context_stack << '['
56
+ index += 1
57
+ when '}'
58
+ remove_trailing_comma(output_tokens)
59
+ output_tokens << char
60
+ context_stack.pop if !context_stack.empty? && context_stack.last == '{'
61
+ index += 1
62
+ when ']'
63
+ output_tokens << char
64
+ context_stack.pop if !context_stack.empty? && context_stack.last == '['
65
+ index += 1
66
+ when '"'
67
+ ensure_comma_before_new_item(output_tokens, context_stack, last_significant_char_in_output)
68
+ ensure_colon_if_value_expected(output_tokens, context_stack, last_significant_char_in_output)
69
+
70
+ string_token = Scanners::CompletionStringToken.new
71
+ index, status = Scanners.scan_string(input, index + 1, string_token)
72
+
73
+ if %i[terminated invalid_unicode].include?(status)
74
+ output_tokens << string_token.buffer.string
75
+ else
76
+ incomplete_string_token = string_token
77
+ end
78
+ when ':'
79
+ remove_trailing_comma(output_tokens) if last_significant_char_in_output == ','
80
+ output_tokens << char
81
+ index += 1
82
+ when ','
83
+ remove_trailing_comma(output_tokens)
84
+ output_tokens << char
85
+ index += 1
86
+ when 't', 'f', 'n'
87
+ ensure_comma_before_new_item(output_tokens, context_stack, last_significant_char_in_output)
88
+ ensure_colon_if_value_expected(output_tokens, context_stack, last_significant_char_in_output)
89
+
90
+ keyword_val, consumed = Scanners.scan_keyword_literal(input, index, KEYWORD_MAP[char.downcase])
91
+ output_tokens << keyword_val
92
+ index += consumed
93
+ when '-', '0'..'9'
94
+ ensure_comma_before_new_item(output_tokens, context_stack, last_significant_char_in_output)
95
+ ensure_colon_if_value_expected(output_tokens, context_stack, last_significant_char_in_output)
96
+
97
+ num_str, consumed = Scanners.scan_number_literal(input, index)
98
+ output_tokens << num_str
99
+ index += consumed
100
+ when /\s/
101
+ output_tokens << char
102
+ index += 1
103
+ else
104
+ index += 1
105
+ end
106
+ end
107
+
108
+ @state = ParsingState.new(
109
+ output_tokens: output_tokens,
110
+ context_stack: context_stack,
111
+ last_index: index,
112
+ input_length: length,
113
+ incomplete_string_token: incomplete_string_token
114
+ )
115
+
116
+ finalize_completion(output_tokens.dup, context_stack.dup, incomplete_string_token)
117
+ end
118
+
119
+ private
120
+
121
+ def finalize_completion(output_tokens, context_stack, incomplete_string_token = nil)
122
+ output_tokens << incomplete_string_token.finalized_incomplete_value if incomplete_string_token
123
+
124
+ last_sig_char_final = get_last_significant_char(output_tokens)
125
+
126
+ unless context_stack.empty?
127
+ current_ctx = context_stack.last
128
+ if current_ctx == '{'
129
+ if last_sig_char_final == '"'
130
+ prev_sig_char = get_previous_significant_char(output_tokens)
131
+ output_tokens << ':' << 'null' if ['{', ','].include?(prev_sig_char)
132
+ elsif last_sig_char_final == ':'
133
+ output_tokens << 'null'
134
+ end
135
+ elsif current_ctx == '['
136
+ output_tokens << 'null' if last_sig_char_final == ','
137
+ end
138
+ end
139
+
140
+ until context_stack.empty?
141
+ opener = context_stack.pop
142
+ remove_trailing_comma(output_tokens)
143
+ output_tokens << (opener == '{' ? '}' : ']')
144
+ end
145
+
146
+ reassembled_json = output_tokens.join
147
+ return 'null' if reassembled_json.match?(/\A\s*[,:]\s*\z/)
148
+
149
+ reassembled_json
150
+ end
151
+
152
+ def get_last_significant_char(output_tokens)
153
+ (output_tokens.length - 1).downto(0) do |index|
154
+ stripped_token = output_tokens[index].strip
155
+ return stripped_token[-1] unless stripped_token.empty?
156
+ end
157
+
158
+ nil
159
+ end
160
+
161
+ def get_previous_significant_char(output_tokens)
162
+ significant_chars = []
163
+
164
+ (output_tokens.length - 1).downto(0) do |index|
165
+ stripped_token = output_tokens[index].strip
166
+ next if stripped_token.empty?
167
+
168
+ significant_chars << stripped_token[-1]
169
+ return significant_chars[1] if significant_chars.length >= 2
170
+ end
171
+
172
+ nil
173
+ end
174
+
175
+ def ensure_comma_before_new_item(output_tokens, context_stack, last_sig_char)
176
+ return if output_tokens.empty? || context_stack.empty? || last_sig_char.nil?
177
+ return if STRUCTURE_CHARS.include?(last_sig_char)
178
+ return unless context_stack.last == '[' || (context_stack.last == '{' && last_sig_char != ':')
179
+
180
+ output_tokens << ','
181
+ end
182
+
183
+ def ensure_colon_if_value_expected(output_tokens, context_stack, last_sig_char)
184
+ return if output_tokens.empty? || context_stack.empty? || last_sig_char.nil?
185
+ return unless context_stack.last == '{' && last_sig_char == '"'
186
+
187
+ output_tokens << ':'
188
+ end
189
+
190
+ def remove_trailing_comma(output_tokens)
191
+ last_token_idx = -1
192
+
193
+ (output_tokens.length - 1).downto(0) do |index|
194
+ next if output_tokens[index].strip.empty?
195
+
196
+ last_token_idx = index
197
+ break
198
+ end
199
+
200
+ return unless last_token_idx != -1 && output_tokens[last_token_idx].strip == ','
201
+
202
+ output_tokens.slice!(last_token_idx)
203
+
204
+ while last_token_idx.positive? && output_tokens[last_token_idx - 1].strip.empty?
205
+ output_tokens.slice!(last_token_idx - 1)
206
+ last_token_idx -= 1
207
+ end
208
+ end
209
+
210
+ def valid_json_primitive_or_document?(str)
211
+ return true if VALID_PRIMITIVES.include?(str)
212
+
213
+ if str.match?(/\A-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?\z/) &&
214
+ !str.end_with?('.') && !str.match?(/[eE][+-]?$/)
215
+ return true
216
+ end
217
+
218
+ str.match?(/\A"(?:[^"\\]|\\.)*"\z/)
219
+ end
220
+ end
221
+
222
+ include CompletionEngine
223
+ end
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ class JsonCompleter
4
+ module ParserEngine
5
+ def parse(partial_json)
6
+ input = partial_json
7
+
8
+ if @parse_state.nil? ||
9
+ @parse_state.input_length > input.length ||
10
+ (@parse_state.input_snapshot && !input.start_with?(@parse_state.input_snapshot))
11
+ @parse_state = self.class.new_parse_state
12
+ end
13
+
14
+ return nil if input.empty?
15
+
16
+ begin
17
+ if @parse_state.input_length == input.length
18
+ finalize_parse_result
19
+ return @parse_state.root
20
+ end
21
+
22
+ prepare_parse_state_for_incremental_input
23
+
24
+ index = @parse_state.last_index
25
+ while index < input.length
26
+ if @parse_state.token_state
27
+ index = continue_parse_token(input, index)
28
+ next
29
+ end
30
+
31
+ char = input[index]
32
+ if top_level_value_complete? && char !~ /\s/
33
+ raise ParseError, 'unexpected token after top-level value'
34
+ end
35
+
36
+ case char
37
+ when /\s/
38
+ index += 1
39
+ when '{'
40
+ start_parse_container({})
41
+ index += 1
42
+ when '['
43
+ start_parse_container([])
44
+ index += 1
45
+ when '}'
46
+ close_parse_object!
47
+ index += 1
48
+ when ']'
49
+ close_parse_array!
50
+ index += 1
51
+ when '"'
52
+ start_parse_string_token
53
+ index += 1
54
+ when ':'
55
+ parse_colon!
56
+ index += 1
57
+ when ','
58
+ parse_comma!
59
+ index += 1
60
+ when 't', 'f', 'n'
61
+ start_parse_keyword_token(char)
62
+ index += 1
63
+ when '-', '0'..'9'
64
+ start_parse_number_token(char)
65
+ index += 1
66
+ else
67
+ raise ParseError, "unexpected token #{char.inspect}"
68
+ end
69
+ end
70
+
71
+ @parse_state.last_index = index
72
+ @parse_state.input_length = input.length
73
+ @parse_state.input_snapshot = input
74
+ finalize_parse_result
75
+ @parse_state.root
76
+ rescue ParseError
77
+ @parse_state = self.class.new_parse_state
78
+ raise
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def prepare_parse_state_for_incremental_input
85
+ token = @parse_state.token_state
86
+ return unless token.is_a?(Scanners::ParsedStringToken) && token.role == :key && token.visible_key
87
+
88
+ restore_visible_key_placeholder(token)
89
+ end
90
+
91
+ def continue_parse_token(input, index)
92
+ token = @parse_state.token_state
93
+
94
+ case token
95
+ when Scanners::ParsedStringToken
96
+ continue_parse_string_token(input, index)
97
+ when Scanners::NumberToken
98
+ continue_parse_number_token(input, index)
99
+ when Scanners::KeywordToken
100
+ continue_parse_keyword_token(input, index)
101
+ else
102
+ raise ParseError, "unsupported token state: #{token.class}"
103
+ end
104
+ end
105
+
106
+ def start_parse_container(container)
107
+ slot = parse_value_slot!
108
+ assign_parse_slot(slot, container)
109
+ transition_after_parse_value(slot)
110
+
111
+ @parse_state.context_stack << if container.is_a?(Hash)
112
+ ObjectContext.new(container: container)
113
+ else
114
+ ArrayContext.new(container: container)
115
+ end
116
+ end
117
+
118
+ def close_parse_object!
119
+ context = @parse_state.context_stack.last
120
+ raise ParseError, 'unexpected object close' unless context.is_a?(ObjectContext)
121
+ raise ParseError, 'cannot close object while a key is incomplete' if context.mode == :key_in_progress
122
+ raise ParseError, 'cannot close object before a colon' if context.mode == :after_key
123
+ raise ParseError, 'cannot close object while a value is missing' if context.mode == :value
124
+
125
+ @parse_state.context_stack.pop
126
+ end
127
+
128
+ def close_parse_array!
129
+ context = @parse_state.context_stack.last
130
+ raise ParseError, 'unexpected array close' unless context.is_a?(ArrayContext)
131
+ raise ParseError, 'cannot close array while a value is missing' if context.provisional_index
132
+
133
+ @parse_state.context_stack.pop
134
+ end
135
+
136
+ def start_parse_string_token
137
+ context = @parse_state.context_stack.last
138
+
139
+ if context.is_a?(ObjectContext) && context.mode == :key_or_end
140
+ context.mode = :key_in_progress
141
+ @parse_state.token_state = Scanners::ParsedStringToken.new(role: :key, context: context)
142
+ return
143
+ end
144
+
145
+ slot = parse_value_slot!
146
+ token = Scanners::ParsedStringToken.new(role: :value, slot: slot)
147
+ assign_parse_slot(slot, token.buffer)
148
+ transition_after_parse_value(slot)
149
+ @parse_state.token_state = token
150
+ end
151
+
152
+ def continue_parse_string_token(input, index)
153
+ token = @parse_state.token_state
154
+ index, status = Scanners.scan_string(input, index, token)
155
+ raise ParseError, 'invalid string escape sequence' if status == :invalid_escape
156
+ raise ParseError, 'invalid unicode escape sequence' if status == :invalid_unicode
157
+ raise ParseError, 'invalid control character in string literal' if status == :invalid_control_character
158
+
159
+ finish_parse_string_token! if status == :terminated
160
+ index
161
+ end
162
+
163
+ def finish_parse_string_token!
164
+ token = @parse_state.token_state
165
+ return unless token
166
+
167
+ if token.role == :key
168
+ token.context.current_key = token.buffer.dup
169
+ token.context.mode = :after_key
170
+ end
171
+
172
+ @parse_state.token_state = nil
173
+ end
174
+
175
+ def start_parse_number_token(first_char)
176
+ slot = parse_value_slot!
177
+ token = Scanners::NumberToken.new(slot: slot)
178
+ token.append(first_char)
179
+ assign_parse_slot(slot, token.parsed_value)
180
+ transition_after_parse_value(slot)
181
+ @parse_state.token_state = token
182
+ end
183
+
184
+ def continue_parse_number_token(input, index)
185
+ token = @parse_state.token_state
186
+
187
+ while index < input.length && token.append(input[index])
188
+ assign_parse_slot(token.slot, token.parsed_value)
189
+ index += 1
190
+ end
191
+
192
+ raise ParseError, 'invalid number literal' if token.invalid?
193
+
194
+ @parse_state.token_state = nil if index < input.length
195
+ index
196
+ end
197
+
198
+ def start_parse_keyword_token(first_char)
199
+ slot = parse_value_slot!
200
+ token = Scanners::KeywordToken.new(slot: slot, target: KEYWORD_MAP[first_char], matched: 1)
201
+ assign_parse_slot(slot, token.parsed_value)
202
+ transition_after_parse_value(slot)
203
+ @parse_state.token_state = token
204
+ end
205
+
206
+ def continue_parse_keyword_token(input, index)
207
+ token = @parse_state.token_state
208
+
209
+ while index < input.length && token.matched < token.target.length && token.append(input[index])
210
+ index += 1
211
+ end
212
+
213
+ raise ParseError, 'invalid keyword literal' if token.matched < token.target.length && index < input.length
214
+
215
+ @parse_state.token_state = nil if index < input.length || token.matched == token.target.length
216
+ index
217
+ end
218
+
219
+ def parse_colon!
220
+ context = @parse_state.context_stack.last
221
+ raise ParseError, 'unexpected colon' unless context.is_a?(ObjectContext) && context.mode == :after_key
222
+
223
+ context.mode = :value
224
+ end
225
+
226
+ def parse_comma!
227
+ context = @parse_state.context_stack.last
228
+ raise ParseError, 'unexpected comma' unless context
229
+
230
+ case context
231
+ when ArrayContext
232
+ raise ParseError, 'cannot add a comma while an array value is missing' unless context.mode == :after_value
233
+
234
+ context.mode = :value_or_end
235
+ context.provisional_index = context.container.length
236
+ when ObjectContext
237
+ raise ParseError, 'cannot add a comma while an object entry is incomplete' unless context.mode == :after_value
238
+
239
+ context.mode = :key_or_end
240
+ context.current_key = nil
241
+
242
+ end
243
+ end
244
+
245
+ def parse_value_slot!
246
+ context = @parse_state.context_stack.last
247
+
248
+ unless context
249
+ raise ParseError, 'unexpected token after top-level value' if @parse_state.root_assigned
250
+
251
+ return ParseSlot.new(root: true)
252
+ end
253
+
254
+ case context
255
+ when ArrayContext
256
+ raise ParseError, 'expected comma before next array value' if context.mode == :after_value
257
+ raise ParseError, 'cannot parse array value here' unless context.mode == :value_or_end
258
+
259
+ index = context.provisional_index || context.container.length
260
+ context.provisional_index = nil
261
+ ParseSlot.new(container: context.container, key: index, root: false)
262
+ when ObjectContext
263
+ raise ParseError, 'expected colon before object value' if context.mode == :after_key
264
+ raise ParseError, 'expected comma before next object entry' if context.mode == :after_value
265
+ raise ParseError, 'expected object key' unless context.mode == :value
266
+
267
+ ParseSlot.new(container: context.container, key: context.current_key, root: false)
268
+ end
269
+ end
270
+
271
+ def top_level_value_complete?
272
+ @parse_state.root_assigned &&
273
+ @parse_state.context_stack.empty? &&
274
+ @parse_state.token_state.nil?
275
+ end
276
+
277
+ def assign_parse_slot(slot, value)
278
+ if slot.root
279
+ @parse_state.root = value
280
+ @parse_state.root_assigned = true
281
+ else
282
+ slot.container[slot.key] = value
283
+ end
284
+ end
285
+
286
+ def transition_after_parse_value(slot)
287
+ context = @parse_state.context_stack.last
288
+
289
+ case context
290
+ when ArrayContext
291
+ context.mode = :after_value
292
+ when ObjectContext
293
+ context.mode = :after_value if slot.root || !context.current_key.nil?
294
+ end
295
+ end
296
+
297
+ def finalize_parse_result
298
+ token = @parse_state.token_state
299
+
300
+ if token.is_a?(Scanners::ParsedStringToken) && token.role == :key
301
+ update_visible_key_placeholder(token)
302
+ return
303
+ end
304
+
305
+ @parse_state.context_stack.each do |context|
306
+ case context
307
+ when ObjectContext
308
+ next unless %i[after_key value].include?(context.mode) && context.current_key
309
+
310
+ context.container[context.current_key] = nil
311
+ when ArrayContext
312
+ next unless context.provisional_index
313
+
314
+ context.container[context.provisional_index] = nil
315
+ end
316
+ end
317
+ end
318
+
319
+ def restore_visible_key_placeholder(token)
320
+ if token.visible_key_replaced_present
321
+ token.context.container[token.visible_key] = token.visible_key_replaced_value
322
+ else
323
+ token.context.container.delete(token.visible_key)
324
+ end
325
+
326
+ token.visible_key = nil
327
+ token.visible_key_replaced_value = nil
328
+ token.visible_key_replaced_present = false
329
+ end
330
+
331
+ def update_visible_key_placeholder(token)
332
+ current_key = token.buffer.dup
333
+ return if token.visible_key == current_key
334
+
335
+ restore_visible_key_placeholder(token) if token.visible_key
336
+
337
+ token.visible_key = current_key
338
+ token.visible_key_replaced_present = token.context.container.key?(current_key)
339
+ token.visible_key_replaced_value = token.context.container[current_key]
340
+ token.context.container[current_key] = nil
341
+ end
342
+ end
343
+
344
+ include ParserEngine
345
+ end