json-projection 0.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 +7 -0
- data/lib/json-projection/parser/buffer.rb +126 -0
- data/lib/json-projection/parser/errors.rb +7 -0
- data/lib/json-projection/parser/events.rb +81 -0
- data/lib/json-projection/parser/fifo.rb +53 -0
- data/lib/json-projection/parser.rb +594 -0
- data/lib/json-projection.rb +6 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ed7412ad6cf14e0b9a5d5c7ae2667289d7e407b3
|
4
|
+
data.tar.gz: 0912f5a454752fe11c589a65d08ab1f0cb291668
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9d5517cd1c7e2f69a08d0cada96df5cbd20f3a20d4152bea642e3b7920a81454b7cd8889399b1d538ffd5b74209249278336d34cf8253be810a86c52ef5ff992
|
7
|
+
data.tar.gz: 57e8cb50913047a8801a30a5fbf9259098a367e5ae38c500e1cc6be41096be6c4b730bab7c5330db81eebb220420db888be3ed33987b2643040eb61049bd3d50
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# Based on JSON::Stream::Buffer from https://github.com/dgraham/json-stream
|
4
|
+
# license preserved below.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2010-2014 David Graham
|
7
|
+
#
|
8
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
# of this software and associated documentation files (the "Software"), to deal
|
10
|
+
# in the Software without restriction, including without limitation the rights
|
11
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
# copies of the Software, and to permit persons to whom the Software is
|
13
|
+
# furnished to do so, subject to the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be included in
|
16
|
+
# all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
24
|
+
# THE SOFTWARE.
|
25
|
+
|
26
|
+
module JsonProjection
|
27
|
+
|
28
|
+
# A character buffer that expects a UTF-8 encoded stream of bytes.
|
29
|
+
# This handles truncated multi-byte characters properly so we can just
|
30
|
+
# feed it binary data and receive a properly formatted UTF-8 String as
|
31
|
+
# output.
|
32
|
+
#
|
33
|
+
# More UTF-8 parsing details are available at:
|
34
|
+
#
|
35
|
+
# http://en.wikipedia.org/wiki/UTF-8
|
36
|
+
# http://tools.ietf.org/html/rfc3629#section-3
|
37
|
+
class Buffer
|
38
|
+
def initialize
|
39
|
+
@state = :start
|
40
|
+
@buffer = []
|
41
|
+
@need = 0
|
42
|
+
end
|
43
|
+
|
44
|
+
# Fill the buffer with a String of binary UTF-8 encoded bytes. Returns
|
45
|
+
# as much of the data in a UTF-8 String as we have. Truncated multi-byte
|
46
|
+
# characters are saved in the buffer until the next call to this method
|
47
|
+
# where we expect to receive the rest of the multi-byte character.
|
48
|
+
#
|
49
|
+
# data - The partial binary encoded String data.
|
50
|
+
#
|
51
|
+
# Raises JSON::Stream::ParserError if the UTF-8 byte sequence is malformed.
|
52
|
+
#
|
53
|
+
# Returns a UTF-8 encoded String.
|
54
|
+
def <<(data)
|
55
|
+
# Avoid state machine for complete UTF-8.
|
56
|
+
if @buffer.empty?
|
57
|
+
data.force_encoding(Encoding::UTF_8)
|
58
|
+
return data if data.valid_encoding?
|
59
|
+
end
|
60
|
+
|
61
|
+
bytes = []
|
62
|
+
data.each_byte do |byte|
|
63
|
+
case @state
|
64
|
+
when :start
|
65
|
+
if byte < 128
|
66
|
+
bytes << byte
|
67
|
+
elsif byte >= 192
|
68
|
+
@state = :multi_byte
|
69
|
+
@buffer << byte
|
70
|
+
@need =
|
71
|
+
case
|
72
|
+
when byte >= 240 then 4
|
73
|
+
when byte >= 224 then 3
|
74
|
+
when byte >= 192 then 2
|
75
|
+
end
|
76
|
+
else
|
77
|
+
error('Expected start of multi-byte or single byte char')
|
78
|
+
end
|
79
|
+
when :multi_byte
|
80
|
+
if byte > 127 && byte < 192
|
81
|
+
@buffer << byte
|
82
|
+
if @buffer.size == @need
|
83
|
+
bytes += @buffer.slice!(0, @buffer.size)
|
84
|
+
@state = :start
|
85
|
+
end
|
86
|
+
else
|
87
|
+
error('Expected continuation byte')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Build UTF-8 encoded string from completed codepoints.
|
93
|
+
bytes.pack('C*').force_encoding(Encoding::UTF_8).tap do |text|
|
94
|
+
error('Invalid UTF-8 byte sequence') unless text.valid_encoding?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Determine if the buffer contains partial UTF-8 continuation bytes that
|
99
|
+
# are waiting on subsequent completion bytes before a full codepoint is
|
100
|
+
# formed.
|
101
|
+
#
|
102
|
+
# Examples
|
103
|
+
#
|
104
|
+
# bytes = "é".bytes
|
105
|
+
#
|
106
|
+
# buffer << bytes[0]
|
107
|
+
# buffer.empty?
|
108
|
+
# # => false
|
109
|
+
#
|
110
|
+
# buffer << bytes[1]
|
111
|
+
# buffer.empty?
|
112
|
+
# # => true
|
113
|
+
#
|
114
|
+
# Returns true if the buffer is empty.
|
115
|
+
def empty?
|
116
|
+
@buffer.empty?
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def error(message)
|
122
|
+
raise ParseError, message
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module JsonProjection
|
2
|
+
|
3
|
+
# Sum type
|
4
|
+
class StreamEvent
|
5
|
+
|
6
|
+
def self.empty
|
7
|
+
@empty ||= new
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
other.is_a?(self.class)
|
12
|
+
end
|
13
|
+
|
14
|
+
def hash
|
15
|
+
self.class.hash
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
class StartDocument < StreamEvent
|
21
|
+
end
|
22
|
+
|
23
|
+
class EndDocument < StreamEvent
|
24
|
+
end
|
25
|
+
|
26
|
+
class StartObject < StreamEvent
|
27
|
+
end
|
28
|
+
|
29
|
+
class EndObject < StreamEvent
|
30
|
+
end
|
31
|
+
|
32
|
+
class StartArray < StreamEvent
|
33
|
+
end
|
34
|
+
|
35
|
+
class EndArray < StreamEvent
|
36
|
+
end
|
37
|
+
|
38
|
+
class Key < StreamEvent
|
39
|
+
attr_reader :key
|
40
|
+
def initialize(key)
|
41
|
+
@key = key
|
42
|
+
end
|
43
|
+
|
44
|
+
def ==(other)
|
45
|
+
return false unless super(other)
|
46
|
+
return key == other.key
|
47
|
+
end
|
48
|
+
|
49
|
+
def hash
|
50
|
+
key.hash
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Value < StreamEvent
|
55
|
+
attr_reader :value
|
56
|
+
def initialize(value)
|
57
|
+
@value = value
|
58
|
+
end
|
59
|
+
|
60
|
+
def ==(other)
|
61
|
+
return false unless super(other)
|
62
|
+
return value == other.value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class String < Value
|
67
|
+
end
|
68
|
+
|
69
|
+
class Number < Value
|
70
|
+
end
|
71
|
+
|
72
|
+
class Boolean < Value
|
73
|
+
end
|
74
|
+
|
75
|
+
class Null < Value
|
76
|
+
def self.empty
|
77
|
+
new(nil)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module JsonProjection
|
2
|
+
class Fifo
|
3
|
+
|
4
|
+
def self.empty
|
5
|
+
@empty ||= self.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.pure(val)
|
9
|
+
Fifo.new([val])
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(stack = [])
|
13
|
+
@stack = stack
|
14
|
+
end
|
15
|
+
|
16
|
+
def push(val)
|
17
|
+
Fifo.new(@stack.dup.insert(0, val))
|
18
|
+
end
|
19
|
+
|
20
|
+
def pop()
|
21
|
+
return self, nil if empty?
|
22
|
+
|
23
|
+
init = @stack.slice(0, @stack.size - 1)
|
24
|
+
last = @stack.last
|
25
|
+
|
26
|
+
return Fifo.new(init), last
|
27
|
+
end
|
28
|
+
|
29
|
+
def empty?
|
30
|
+
@stack.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def ==(other)
|
34
|
+
return false unless other.is_a?(Fifo)
|
35
|
+
return stack == other.stack
|
36
|
+
end
|
37
|
+
|
38
|
+
def hash
|
39
|
+
stack.hash
|
40
|
+
end
|
41
|
+
|
42
|
+
def append(fifo)
|
43
|
+
return self if fifo.empty?
|
44
|
+
return fifo if self.empty?
|
45
|
+
Fifo.new(@stack.concat(fifo.stack))
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
attr_reader :stack
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,594 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# Based on JSON::Stream::Parser from https://github.com/dgraham/json-stream
|
4
|
+
# license preserved below.
|
5
|
+
#
|
6
|
+
# Copyright (c) 2010-2014 David Graham
|
7
|
+
#
|
8
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
9
|
+
# of this software and associated documentation files (the "Software"), to deal
|
10
|
+
# in the Software without restriction, including without limitation the rights
|
11
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
12
|
+
# copies of the Software, and to permit persons to whom the Software is
|
13
|
+
# furnished to do so, subject to the following conditions:
|
14
|
+
#
|
15
|
+
# The above copyright notice and this permission notice shall be included in
|
16
|
+
# all copies or substantial portions of the Software.
|
17
|
+
#
|
18
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
24
|
+
# THE SOFTWARE.
|
25
|
+
|
26
|
+
require_relative 'parser/buffer'
|
27
|
+
require_relative 'parser/events'
|
28
|
+
require_relative 'parser/errors'
|
29
|
+
require_relative 'parser/fifo'
|
30
|
+
|
31
|
+
module JsonProjection
|
32
|
+
|
33
|
+
# A streaming JSON parser that generates SAX-like events for state changes.
|
34
|
+
# Use the json gem for small documents. Use this for huge documents that
|
35
|
+
# won't fit in memory.
|
36
|
+
class Parser
|
37
|
+
BUF_SIZE = 4096
|
38
|
+
CONTROL = /[\x00-\x1F]/
|
39
|
+
WS = /[ \n\t\r]/
|
40
|
+
HEX = /[0-9a-fA-F]/
|
41
|
+
DIGIT = /[0-9]/
|
42
|
+
DIGIT_1_9 = /[1-9]/
|
43
|
+
DIGIT_END = /\d$/
|
44
|
+
TRUE_RE = /[rue]/
|
45
|
+
FALSE_RE = /[alse]/
|
46
|
+
NULL_RE = /[ul]/
|
47
|
+
TRUE_KEYWORD = 'true'
|
48
|
+
FALSE_KEYWORD = 'false'
|
49
|
+
NULL_KEYWORD = 'null'
|
50
|
+
LEFT_BRACE = '{'
|
51
|
+
RIGHT_BRACE = '}'
|
52
|
+
LEFT_BRACKET = '['
|
53
|
+
RIGHT_BRACKET = ']'
|
54
|
+
BACKSLASH = '\\'
|
55
|
+
SLASH = '/'
|
56
|
+
QUOTE = '"'
|
57
|
+
COMMA = ','
|
58
|
+
COLON = ':'
|
59
|
+
ZERO = '0'
|
60
|
+
MINUS = '-'
|
61
|
+
PLUS = '+'
|
62
|
+
POINT = '.'
|
63
|
+
EXPONENT = /[eE]/
|
64
|
+
B,F,N,R,T,U = %w[b f n r t u]
|
65
|
+
|
66
|
+
# Initialize a new parser with a stream. The stream cursor is advanced as
|
67
|
+
# events are drawn from the parser. The parser maintains a small data cache
|
68
|
+
# of bytes read from the stream.
|
69
|
+
#
|
70
|
+
# stream :: IO
|
71
|
+
# IO stream to read data from.
|
72
|
+
#
|
73
|
+
# Returns nothing.
|
74
|
+
def initialize(stream)
|
75
|
+
@stream = stream
|
76
|
+
|
77
|
+
@event_buffer = Fifo.new
|
78
|
+
|
79
|
+
@bytes_buffer = Buffer.new
|
80
|
+
@bytes = nil
|
81
|
+
|
82
|
+
@pos = -1
|
83
|
+
@state = :start_document
|
84
|
+
@stack = []
|
85
|
+
|
86
|
+
@value_buffer = ""
|
87
|
+
@unicode = ""
|
88
|
+
end
|
89
|
+
|
90
|
+
# Draw bytes from the stream until an event can be constructed. May raise
|
91
|
+
# IO errors.
|
92
|
+
#
|
93
|
+
# Returns a JsonProject::StreamEvent subclass or raises StandardError.
|
94
|
+
def next_event()
|
95
|
+
# Are there any already read events, return the oldest
|
96
|
+
@event_buffer, event = @event_buffer.pop
|
97
|
+
return event unless event.nil?
|
98
|
+
|
99
|
+
if @state == :end_document
|
100
|
+
error("already EOF, no more events")
|
101
|
+
end
|
102
|
+
|
103
|
+
while true do
|
104
|
+
if @bytes.nil? || @bytes.empty?
|
105
|
+
data = stream.read(BUF_SIZE)
|
106
|
+
if data == nil # hit EOF
|
107
|
+
error("unexpected EOF")
|
108
|
+
end
|
109
|
+
|
110
|
+
@bytes = @bytes_buffer.<<(data).each_char.to_a
|
111
|
+
end
|
112
|
+
|
113
|
+
head = @bytes.first
|
114
|
+
tail = @bytes.slice!(1, @bytes.size - 1)
|
115
|
+
|
116
|
+
@bytes = tail
|
117
|
+
@pos += 1
|
118
|
+
|
119
|
+
new_state, events = handle_character(@state, head)
|
120
|
+
|
121
|
+
@state = new_state
|
122
|
+
@event_buffer = events.append(@event_buffer)
|
123
|
+
|
124
|
+
unless @event_buffer.empty?
|
125
|
+
@event_buffer, event = @event_buffer.pop
|
126
|
+
return event
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def stream
|
134
|
+
@stream
|
135
|
+
end
|
136
|
+
|
137
|
+
# Given a state and new character, return a new state and fifo of events to
|
138
|
+
# yield to pull callers.
|
139
|
+
#
|
140
|
+
# state :: Symbol
|
141
|
+
#
|
142
|
+
# ch :: String
|
143
|
+
#
|
144
|
+
# Returns a tuple of (Symbol, Fifo<Event>) or raises StandardError.
|
145
|
+
def handle_character(state, ch)
|
146
|
+
case state
|
147
|
+
when :start_document
|
148
|
+
case ch
|
149
|
+
when WS
|
150
|
+
return :start_document, Fifo.empty
|
151
|
+
when LEFT_BRACE
|
152
|
+
@stack.push(:object)
|
153
|
+
|
154
|
+
events = Fifo.pure(StartDocument.empty).push(StartObject.empty)
|
155
|
+
|
156
|
+
return :start_object, events
|
157
|
+
when LEFT_BRACKET
|
158
|
+
@stack.push(:array)
|
159
|
+
|
160
|
+
events = Fifo.pure(StartDocument.empty).push(StartArray.empty)
|
161
|
+
|
162
|
+
return :start_array, events
|
163
|
+
end
|
164
|
+
|
165
|
+
error('Expected whitespace, object `{` or array `[` start token')
|
166
|
+
|
167
|
+
when :start_object
|
168
|
+
case ch
|
169
|
+
when WS
|
170
|
+
return :start_object, Fifo.empty
|
171
|
+
when QUOTE
|
172
|
+
@stack.push(:key)
|
173
|
+
return :start_string, Fifo.empty
|
174
|
+
when RIGHT_BRACE
|
175
|
+
return end_container(:object)
|
176
|
+
end
|
177
|
+
|
178
|
+
error('Expected object key `"` start')
|
179
|
+
|
180
|
+
when :start_string
|
181
|
+
case ch
|
182
|
+
when QUOTE
|
183
|
+
if @stack.pop == :string
|
184
|
+
events = Fifo.pure(end_value(@value_buffer.dup))
|
185
|
+
@value_buffer.clear
|
186
|
+
|
187
|
+
return :end_value, events
|
188
|
+
else # :key
|
189
|
+
events = Fifo.pure(Key.new(@value_buffer.dup))
|
190
|
+
@value_buffer.clear
|
191
|
+
|
192
|
+
return :end_key, events
|
193
|
+
end
|
194
|
+
when BACKSLASH
|
195
|
+
return :start_escape, Fifo.empty
|
196
|
+
when CONTROL
|
197
|
+
error('Control characters must be escaped')
|
198
|
+
else
|
199
|
+
@value_buffer << ch
|
200
|
+
return :start_string, Fifo.empty
|
201
|
+
end
|
202
|
+
|
203
|
+
when :start_escape
|
204
|
+
case ch
|
205
|
+
when QUOTE, BACKSLASH, SLASH
|
206
|
+
@value_buffer << ch
|
207
|
+
return :start_string, Fifo.empty
|
208
|
+
when B
|
209
|
+
@value_buffer << "\b"
|
210
|
+
return :start_string, Fifo.empty
|
211
|
+
when F
|
212
|
+
@value_buffer << "\f"
|
213
|
+
return :start_string, Fifo.empty
|
214
|
+
when N
|
215
|
+
@value_buffer << "\n"
|
216
|
+
return :start_string, Fifo.empty
|
217
|
+
when R
|
218
|
+
@value_buffer << "\r"
|
219
|
+
return :start_string, Fifo.empty
|
220
|
+
when T
|
221
|
+
@value_buffer << "\t"
|
222
|
+
return :start_string, Fifo.empty
|
223
|
+
when U
|
224
|
+
return :unicode_escape, Fifo.empty
|
225
|
+
else
|
226
|
+
error('Expected escaped character')
|
227
|
+
end
|
228
|
+
|
229
|
+
when :unicode_escape
|
230
|
+
case ch
|
231
|
+
when HEX
|
232
|
+
@unicode << ch
|
233
|
+
if @unicode.size == 4
|
234
|
+
codepoint = @unicode.slice!(0, 4).hex
|
235
|
+
if codepoint >= 0xD800 && codepoint <= 0xDBFF
|
236
|
+
error('Expected low surrogate pair half') if @stack[-1].is_a?(Fixnum)
|
237
|
+
@stack.push(codepoint)
|
238
|
+
return :start_surrogate_pair, Fifo.empty
|
239
|
+
elsif codepoint >= 0xDC00 && codepoint <= 0xDFFF
|
240
|
+
high = @stack.pop
|
241
|
+
error('Expected high surrogate pair half') unless high.is_a?(Fixnum)
|
242
|
+
pair = ((high - 0xD800) * 0x400) + (codepoint - 0xDC00) + 0x10000
|
243
|
+
@value_buffer << pair
|
244
|
+
return :start_string, Fifo.empty
|
245
|
+
else
|
246
|
+
@value_buffer << codepoint
|
247
|
+
return :start_string, Fifo.empty
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
return :unicode_escape, Fifo.empty
|
252
|
+
else
|
253
|
+
error('Expected unicode escape hex digit')
|
254
|
+
end
|
255
|
+
|
256
|
+
when :start_surrogate_pair
|
257
|
+
case ch
|
258
|
+
when BACKSLASH
|
259
|
+
return :start_surrogate_pair_u, Fifo.empty
|
260
|
+
else
|
261
|
+
error('Expected low surrogate pair half')
|
262
|
+
end
|
263
|
+
|
264
|
+
when :start_surrogate_pair_u
|
265
|
+
case ch
|
266
|
+
when U
|
267
|
+
return :unicode_escape, Fifo.empty
|
268
|
+
else
|
269
|
+
error('Expected low surrogate pair half')
|
270
|
+
end
|
271
|
+
|
272
|
+
when :start_negative_number
|
273
|
+
case ch
|
274
|
+
when ZERO
|
275
|
+
@value_buffer << ch
|
276
|
+
return :start_zero, Fifo.empty
|
277
|
+
when DIGIT_1_9
|
278
|
+
@value_buffer << ch
|
279
|
+
return :start_int, Fifo.empty
|
280
|
+
else
|
281
|
+
error('Expected 0-9 digit')
|
282
|
+
end
|
283
|
+
|
284
|
+
when :start_zero
|
285
|
+
case ch
|
286
|
+
when POINT
|
287
|
+
@value_buffer << ch
|
288
|
+
return :start_float, Fifo.empty
|
289
|
+
when EXPONENT
|
290
|
+
@value_buffer << ch
|
291
|
+
return :start_exponent, Fifo.empty
|
292
|
+
else
|
293
|
+
events = Fifo.pure(end_value(@value_buffer.to_i))
|
294
|
+
@value_buffer.clear
|
295
|
+
|
296
|
+
state = :end_value
|
297
|
+
|
298
|
+
state, new_events = handle_character(state, ch)
|
299
|
+
|
300
|
+
return state, new_events.append(events)
|
301
|
+
end
|
302
|
+
|
303
|
+
when :start_float
|
304
|
+
case ch
|
305
|
+
when DIGIT
|
306
|
+
@value_buffer << ch
|
307
|
+
return :in_float, Fifo.empty
|
308
|
+
end
|
309
|
+
|
310
|
+
error('Expected 0-9 digit')
|
311
|
+
|
312
|
+
when :in_float
|
313
|
+
case ch
|
314
|
+
when DIGIT
|
315
|
+
@value_buffer << ch
|
316
|
+
return :in_float, Fifo.empty
|
317
|
+
when EXPONENT
|
318
|
+
@value_buffer << ch
|
319
|
+
return :start_exponent, Fifo.empty
|
320
|
+
else
|
321
|
+
events = Fifo.pure(end_value(@value_buffer.to_f))
|
322
|
+
@value_buffer.clear
|
323
|
+
|
324
|
+
state = :end_value
|
325
|
+
|
326
|
+
state, new_events = handle_character(state, ch)
|
327
|
+
|
328
|
+
return state, new_events.append(events)
|
329
|
+
end
|
330
|
+
|
331
|
+
when :start_exponent
|
332
|
+
case ch
|
333
|
+
when MINUS, PLUS, DIGIT
|
334
|
+
@value_buffer << ch
|
335
|
+
return :in_exponent, Fifo.empty
|
336
|
+
end
|
337
|
+
|
338
|
+
error('Expected +, -, or 0-9 digit')
|
339
|
+
|
340
|
+
when :in_exponent
|
341
|
+
case ch
|
342
|
+
when DIGIT
|
343
|
+
@value_buffer << ch
|
344
|
+
return :in_exponent, Fifo.empty
|
345
|
+
else
|
346
|
+
error('Expected 0-9 digit') unless @value_buffer =~ DIGIT_END
|
347
|
+
|
348
|
+
events = Fifo.pure(end_value(@value_buffer.to_f))
|
349
|
+
@value_buffer.clear
|
350
|
+
|
351
|
+
state = :end_value
|
352
|
+
|
353
|
+
state, new_events = handle_character(state, ch)
|
354
|
+
|
355
|
+
return state, new_events.append(events)
|
356
|
+
end
|
357
|
+
|
358
|
+
when :start_int
|
359
|
+
case ch
|
360
|
+
when DIGIT
|
361
|
+
@value_buffer << ch
|
362
|
+
return :start_int, Fifo.empty
|
363
|
+
when POINT
|
364
|
+
@value_buffer << ch
|
365
|
+
return :start_float, Fifo.empty
|
366
|
+
when EXPONENT
|
367
|
+
@value_buffer << ch
|
368
|
+
return :start_exponent, Fifo.empty
|
369
|
+
else
|
370
|
+
events = Fifo.pure(end_value(@value_buffer.to_i))
|
371
|
+
@value_buffer.clear
|
372
|
+
|
373
|
+
state = :end_value
|
374
|
+
|
375
|
+
state, new_events = handle_character(state, ch)
|
376
|
+
|
377
|
+
return state, new_events.append(events)
|
378
|
+
end
|
379
|
+
|
380
|
+
when :start_true
|
381
|
+
state, event = keyword(TRUE_KEYWORD, true, TRUE_RE, ch)
|
382
|
+
if state.nil?
|
383
|
+
return :start_true, Fifo.empty
|
384
|
+
end
|
385
|
+
|
386
|
+
return state, Fifo.pure(event)
|
387
|
+
when :start_false
|
388
|
+
state, event = keyword(FALSE_KEYWORD, false, FALSE_RE, ch)
|
389
|
+
if state.nil?
|
390
|
+
return :start_false, Fifo.empty
|
391
|
+
end
|
392
|
+
|
393
|
+
return state, Fifo.pure(event)
|
394
|
+
when :start_null
|
395
|
+
state, event = keyword(NULL_KEYWORD, nil, NULL_RE, ch)
|
396
|
+
if state.nil?
|
397
|
+
return :start_null, Fifo.empty
|
398
|
+
end
|
399
|
+
|
400
|
+
return state, Fifo.pure(event)
|
401
|
+
|
402
|
+
when :end_key
|
403
|
+
case ch
|
404
|
+
when WS
|
405
|
+
return :end_key, Fifo.empty
|
406
|
+
when COLON
|
407
|
+
return :key_sep, Fifo.empty
|
408
|
+
end
|
409
|
+
|
410
|
+
error('Expected colon key separator')
|
411
|
+
|
412
|
+
when :key_sep
|
413
|
+
case ch
|
414
|
+
when WS
|
415
|
+
return :key_sep, Fifo.empty
|
416
|
+
else
|
417
|
+
return start_value(ch)
|
418
|
+
end
|
419
|
+
|
420
|
+
when :start_array
|
421
|
+
case ch
|
422
|
+
when WS
|
423
|
+
return :start_array, Fifo.empty
|
424
|
+
when RIGHT_BRACKET
|
425
|
+
return end_container(:array)
|
426
|
+
else
|
427
|
+
return start_value(ch)
|
428
|
+
end
|
429
|
+
|
430
|
+
when :end_value
|
431
|
+
case ch
|
432
|
+
when WS
|
433
|
+
return :end_value, Fifo.empty
|
434
|
+
when COMMA
|
435
|
+
return :value_sep, Fifo.empty
|
436
|
+
when RIGHT_BRACE
|
437
|
+
return end_container(:object)
|
438
|
+
when RIGHT_BRACKET
|
439
|
+
return end_container(:array)
|
440
|
+
end
|
441
|
+
|
442
|
+
error('Expected comma `,` object `}` or array `]` close')
|
443
|
+
|
444
|
+
when :value_sep
|
445
|
+
if @stack[-1] == :object
|
446
|
+
case ch
|
447
|
+
when WS
|
448
|
+
return :value_sep, Fifo.empty
|
449
|
+
when QUOTE
|
450
|
+
@stack.push(:key)
|
451
|
+
return :start_string, Fifo.empty
|
452
|
+
end
|
453
|
+
|
454
|
+
error('Expected whitespace or object key start `"`')
|
455
|
+
end
|
456
|
+
|
457
|
+
case ch
|
458
|
+
when WS
|
459
|
+
return :value_sep, Fifo.empty
|
460
|
+
else
|
461
|
+
return start_value(ch)
|
462
|
+
end
|
463
|
+
|
464
|
+
when :end_document
|
465
|
+
error('Unexpected data') unless ch =~ WS
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
# Complete an object or array container value type.
|
470
|
+
#
|
471
|
+
# type - The Symbol, :object or :array, of the expected type.
|
472
|
+
#
|
473
|
+
# Raises a JSON::Stream::ParserError if the expected container type
|
474
|
+
# was not completed.
|
475
|
+
#
|
476
|
+
# Returns a tuple of (Symbol, Fifo<Event>) instance or raises a
|
477
|
+
# JsonProjection::ParseError if the character does not signal the start of
|
478
|
+
# a value.
|
479
|
+
def end_container(type)
|
480
|
+
state = :end_value
|
481
|
+
events = Fifo.empty
|
482
|
+
|
483
|
+
if @stack.pop == type
|
484
|
+
case type
|
485
|
+
when :object then
|
486
|
+
events = events.push(EndObject.empty)
|
487
|
+
when :array then
|
488
|
+
events = events.push(EndArray.empty)
|
489
|
+
end
|
490
|
+
else
|
491
|
+
error("Expected end of #{type}")
|
492
|
+
end
|
493
|
+
|
494
|
+
if @stack.empty?
|
495
|
+
state = :end_document
|
496
|
+
events = events.push(EndDocument.empty)
|
497
|
+
end
|
498
|
+
|
499
|
+
return state, events
|
500
|
+
end
|
501
|
+
|
502
|
+
# Parse one of the three allowed keywords: true, false, null.
|
503
|
+
#
|
504
|
+
# word - The String keyword ('true', 'false', 'null').
|
505
|
+
# value - The Ruby value (true, false, nil).
|
506
|
+
# re - The Regexp of allowed keyword characters.
|
507
|
+
# ch - The current String character being parsed.
|
508
|
+
#
|
509
|
+
# Raises a JSON::Stream::ParserError if the character does not belong
|
510
|
+
# in the expected keyword.
|
511
|
+
#
|
512
|
+
# Returns a JsonProjection::StreamEvent? instance or raises.
|
513
|
+
def keyword(word, value, re, ch)
|
514
|
+
if ch =~ re
|
515
|
+
@value_buffer << ch
|
516
|
+
else
|
517
|
+
error("Expected #{word} keyword")
|
518
|
+
end
|
519
|
+
|
520
|
+
if @value_buffer.size != word.size
|
521
|
+
return nil
|
522
|
+
elsif @value_buffer == word
|
523
|
+
event = end_value(value)
|
524
|
+
@value_buffer.clear
|
525
|
+
|
526
|
+
return :end_value, event
|
527
|
+
else
|
528
|
+
error("Expected #{word} keyword")
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Process the first character of one of the seven possible JSON
|
533
|
+
# values: object, array, string, true, false, null, number.
|
534
|
+
#
|
535
|
+
# ch :: String
|
536
|
+
# The current character String.
|
537
|
+
#
|
538
|
+
# Returns a JsonProjection::StreamEvent? subclass.
|
539
|
+
def start_value(ch)
|
540
|
+
case ch
|
541
|
+
when LEFT_BRACE
|
542
|
+
@stack.push(:object)
|
543
|
+
return :start_object, Fifo.pure(StartObject.empty)
|
544
|
+
when LEFT_BRACKET
|
545
|
+
@stack.push(:array)
|
546
|
+
return :start_array, Fifo.pure(StartArray.empty)
|
547
|
+
when QUOTE
|
548
|
+
@stack.push(:string)
|
549
|
+
return :start_string, Fifo.empty
|
550
|
+
when T
|
551
|
+
@value_buffer << ch
|
552
|
+
return :start_true, Fifo.empty
|
553
|
+
when F
|
554
|
+
@value_buffer << ch
|
555
|
+
return :start_false, Fifo.empty
|
556
|
+
when N
|
557
|
+
@value_buffer << ch
|
558
|
+
return :start_null, Fifo.empty
|
559
|
+
when MINUS
|
560
|
+
@value_buffer << ch
|
561
|
+
return :start_negative_number, Fifo.empty
|
562
|
+
when ZERO
|
563
|
+
@value_buffer << ch
|
564
|
+
return :start_zero, Fifo.empty
|
565
|
+
when DIGIT_1_9
|
566
|
+
@value_buffer << ch
|
567
|
+
return :start_int, Fifo.empty
|
568
|
+
end
|
569
|
+
|
570
|
+
error('Expected value')
|
571
|
+
end
|
572
|
+
|
573
|
+
# Advance the state machine and construct the event for the value just read.
|
574
|
+
#
|
575
|
+
# Returns a JsonProjection::StreamEvent subclass.
|
576
|
+
def end_value(value)
|
577
|
+
case value
|
578
|
+
when TrueClass, FalseClass
|
579
|
+
Boolean.new(value)
|
580
|
+
when Numeric
|
581
|
+
Number.new(value)
|
582
|
+
when ::String
|
583
|
+
JsonProjection::String.new(value)
|
584
|
+
when NilClass
|
585
|
+
Null.empty
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
def error(message)
|
590
|
+
raise ParseError, "#{message}: char #{@pos}"
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: json-projection
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Keith Duncan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-08-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 11.2.2
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 11.2.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 5.0.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 5.0.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest-focus
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.1.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.1.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 5.9.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 5.9.0
|
69
|
+
description: Iteratively parse a stream of JSON data and project it into a smaller
|
70
|
+
version which can be held in memory
|
71
|
+
email: keith.duncan@github.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- lib/json-projection.rb
|
77
|
+
- lib/json-projection/parser.rb
|
78
|
+
- lib/json-projection/parser/buffer.rb
|
79
|
+
- lib/json-projection/parser/errors.rb
|
80
|
+
- lib/json-projection/parser/events.rb
|
81
|
+
- lib/json-projection/parser/fifo.rb
|
82
|
+
homepage: https://github.com/keithduncan/json-projection
|
83
|
+
licenses:
|
84
|
+
- MIT
|
85
|
+
metadata: {}
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 2.0.14.1
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: JSON structure preserving transform
|
106
|
+
test_files: []
|