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 +4 -4
- data/README.md +35 -21
- data/lib/json_completer/completion_engine.rb +223 -0
- data/lib/json_completer/parser_engine.rb +345 -0
- data/lib/json_completer/scanners.rb +402 -0
- data/lib/json_completer.rb +36 -688
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acf870fc5a65bf4f4f1b586cfa61e012137fc96c0c48e3345d26a38da7d765bf
|
|
4
|
+
data.tar.gz: fe454f3e2485ae789840bb9c55c7fe72f3e8c9afffc9eccd0d13cc53ede8c05f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
35
|
-
JsonCompleter.
|
|
36
|
-
# =>
|
|
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.
|
|
40
|
-
# =>
|
|
39
|
+
JsonCompleter.parse('{"message": "Hello wo')
|
|
40
|
+
# => {"message" => "Hello wo"}
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
JsonCompleter.
|
|
44
|
-
# =>
|
|
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
|
|
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.
|
|
56
|
-
# =>
|
|
55
|
+
result1 = completer.parse('{"users": [{"name": "')
|
|
56
|
+
# => {"users" => [{"name" => ""}]}
|
|
57
57
|
|
|
58
58
|
# Process additional data
|
|
59
|
-
result2 = completer.
|
|
60
|
-
# =>
|
|
59
|
+
result2 = completer.parse('{"users": [{"name": "Alice"}')
|
|
60
|
+
# => {"users" => [{"name" => "Alice"}]}
|
|
61
61
|
|
|
62
|
-
# Final
|
|
63
|
-
result3 = completer.
|
|
64
|
-
# =>
|
|
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
|
-
- **
|
|
77
|
-
- **
|
|
78
|
-
- **
|
|
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
|