yajl-ffi 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/LICENSE +19 -0
- data/README.md +180 -0
- data/Rakefile +22 -0
- data/lib/yajl/ffi.rb +54 -0
- data/lib/yajl/ffi/builder.rb +70 -0
- data/lib/yajl/ffi/parser.rb +316 -0
- data/lib/yajl/ffi/tasks/benchmark.rake +61 -0
- data/lib/yajl/ffi/version.rb +5 -0
- data/spec/builder_spec.rb +155 -0
- data/spec/fixtures/repository.json +107 -0
- data/spec/parser_spec.rb +795 -0
- data/yajl-ffi.gemspec +22 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 1b714671d249f07a8fd35a4f13a236a572a57f17
|
4
|
+
data.tar.gz: 8449424c54943747b93f9927e5bacc24969099d9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d1c3aaeeb4c9d8af71712bfbe420ec93c3ef9a664f59227eb619f1bc561c926b07ca82953317d015240ff594e61a61bf281d54ce8ba1b6a22829b5d453577bcf
|
7
|
+
data.tar.gz: da1ea8c2301ea0020361de9cc56acde0fbe3bf13baeef046123c2d350cfe00a490b1e07f27cf8aa27909aef25c7d5033d3b5315f1a0a5c9e58d18de67bc3ce48
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2014 David Graham
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# Yajl::FFI
|
2
|
+
|
3
|
+
Yajl::FFI is a [JSON](http://json.org) parser, based on
|
4
|
+
[FFI](https://github.com/ffi/ffi) bindings into the native
|
5
|
+
[YAJL](https://github.com/lloyd/yajl) library, that generates
|
6
|
+
events for each state change. This allows streaming both the JSON document into
|
7
|
+
memory and the parsed object graph out of memory to some other process.
|
8
|
+
|
9
|
+
This is similar to an XML SAX parser that generates events during parsing. There
|
10
|
+
is no requirement for the document, or the object graph, to be fully buffered in
|
11
|
+
memory. Yajl::FFI is best suited for huge JSON documents that won't fit in memory.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
The simplest way to parse is to read the full JSON document into memory
|
16
|
+
and then parse it into a full object graph. This is fine for small documents
|
17
|
+
because we have room for both the text and parsed object in memory.
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require 'yajl/ffi'
|
21
|
+
json = File.read('/tmp/test.json')
|
22
|
+
obj = Yajl::FFI::Parser.parse(json)
|
23
|
+
```
|
24
|
+
|
25
|
+
While it's possible to do this with Yajl::FFI, we should really use the
|
26
|
+
standard library's [json]([json](https://github.com/flori/json)
|
27
|
+
gem for documents like this. It's faster because it doesn't need to generate
|
28
|
+
events and notify observers each time the parser changes state. It parses and
|
29
|
+
builds the Ruby object entirely in native code and hands it back to us, fully
|
30
|
+
formed.
|
31
|
+
|
32
|
+
For larger documents, we can use an IO object to stream it into the parser.
|
33
|
+
We still need room for the parsed object, but the document itself is never
|
34
|
+
fully read into memory.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
require 'yajl/ffi'
|
38
|
+
stream = File.open('/tmp/test.json')
|
39
|
+
obj = Yajl::FFI::Parser.parse(stream)
|
40
|
+
```
|
41
|
+
|
42
|
+
However, when streaming small documents from disk, or over the network, the
|
43
|
+
[yajl-ruby](https://github.com/brianmario/yajl-ruby) gem will give us the best
|
44
|
+
performance.
|
45
|
+
|
46
|
+
Huge documents arriving over the network in small chunks to an
|
47
|
+
[EventMachine](https://github.com/eventmachine/eventmachine)
|
48
|
+
`receive_data` loop is where Yajl::FFI is uniquely suited. Inside an
|
49
|
+
`EventMachine::Connection` subclass we might have:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
def post_init
|
53
|
+
@parser = Yajl::FFI::Parser.new
|
54
|
+
@parser.start_document { puts "start document" }
|
55
|
+
@parser.end_document { puts "end document" }
|
56
|
+
@parser.start_object { puts "start object" }
|
57
|
+
@parser.end_object { puts "end object" }
|
58
|
+
@parser.start_array { puts "start array" }
|
59
|
+
@parser.end_array { puts "end array" }
|
60
|
+
@parser.key {|k| puts "key: #{k}" }
|
61
|
+
@parser.value {|v| puts "value: #{v}" }
|
62
|
+
end
|
63
|
+
|
64
|
+
def receive_data(data)
|
65
|
+
begin
|
66
|
+
@parser << data
|
67
|
+
rescue Yajl::FFI::ParserError => e
|
68
|
+
close_connection
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
The parser accepts chunks of the JSON document and parses up to the end of the
|
74
|
+
available buffer. Passing in more data resumes the parse from the prior state.
|
75
|
+
When an interesting state change happens, the parser notifies all registered
|
76
|
+
callback procs of the event.
|
77
|
+
|
78
|
+
The event callback is where we can do interesting data filtering and passing
|
79
|
+
to other processes. The above example simply prints state changes, but the
|
80
|
+
callbacks might look for an array named `rows` and process sets of these row
|
81
|
+
objects in small batches. Millions of rows, streaming over the network, can be
|
82
|
+
processed in constant memory space this way.
|
83
|
+
|
84
|
+
## Dependencies
|
85
|
+
|
86
|
+
* [libyajl2](https://github.com/lloyd/yajl)
|
87
|
+
* ruby >= 1.9.3
|
88
|
+
* jruby >= 1.7
|
89
|
+
|
90
|
+
## Library loading
|
91
|
+
|
92
|
+
FFI uses the the `dlopen` system call to dynamically load the libyajl library
|
93
|
+
into memory at runtime. It searches the usual directories for the library file,
|
94
|
+
like `/usr/lib` and `/usr/local/lib`, and raises an error if it's not found.
|
95
|
+
If libyajl is installed in an unusual directory, we can tell `dlopen` where to
|
96
|
+
look by setting the `LD_LIBRARY_PATH` environment variable.
|
97
|
+
|
98
|
+
```sh
|
99
|
+
# test normal library load
|
100
|
+
$ ruby -r 'yajl/ffi' -e 'puts Yajl::FFI::VERSION'
|
101
|
+
|
102
|
+
# if it fails, specify the search path
|
103
|
+
$ LD_LIBRARY_PATH=/somewhere/yajl/lib \
|
104
|
+
ruby -r 'yajl/ffi' -e 'puts Yajl::FFI::VERSION'
|
105
|
+
```
|
106
|
+
|
107
|
+
## Installation
|
108
|
+
|
109
|
+
The libyajl library needs to be installed before this gem can bind to it.
|
110
|
+
|
111
|
+
### OS X
|
112
|
+
|
113
|
+
Use [Homebrew](http://brew.sh) or compile from source below.
|
114
|
+
|
115
|
+
```
|
116
|
+
$ brew install yajl
|
117
|
+
```
|
118
|
+
|
119
|
+
### Fedora
|
120
|
+
|
121
|
+
Fedora 20 provides libyajl2 in a package. Older versions might need to compile
|
122
|
+
the latest yajl version from source.
|
123
|
+
|
124
|
+
```
|
125
|
+
$ sudo yum install yajl
|
126
|
+
```
|
127
|
+
|
128
|
+
### Ubuntu
|
129
|
+
|
130
|
+
Ubuntu 14.04 provides a libyajl2 package. Older versions might also need to
|
131
|
+
compile yajl from source.
|
132
|
+
|
133
|
+
```
|
134
|
+
$ sudo apt-get install libyajl2
|
135
|
+
```
|
136
|
+
|
137
|
+
### Source
|
138
|
+
|
139
|
+
By default, this compiles and installs to `/usr/local`. Use
|
140
|
+
`./configure -p /tmp/somewhere` to install to a different directory.
|
141
|
+
Setting `LD_LIBRARY_PATH` will be required in that case.
|
142
|
+
|
143
|
+
```
|
144
|
+
$ git clone https://github.com/lloyd/yajl
|
145
|
+
$ cd yajl
|
146
|
+
$ ./configure
|
147
|
+
$ make && make install
|
148
|
+
```
|
149
|
+
|
150
|
+
## Alternatives
|
151
|
+
|
152
|
+
* [json](https://github.com/flori/json)
|
153
|
+
* [yajl-ruby](https://github.com/brianmario/yajl-ruby)
|
154
|
+
* [json-stream](https://github.com/dgraham/json-stream)
|
155
|
+
|
156
|
+
This gem provides a benchmark script to test the relative performance of
|
157
|
+
several parsers. Here's a sample run.
|
158
|
+
|
159
|
+
```
|
160
|
+
$ rake benchmark
|
161
|
+
user system total real
|
162
|
+
json 0.130000 0.010000 0.140000 ( 0.136225)
|
163
|
+
yajl-ruby 0.130000 0.010000 0.140000 ( 0.133872)
|
164
|
+
yajl-ffi 0.800000 0.010000 0.810000 ( 0.812818)
|
165
|
+
json-stream 9.500000 0.080000 9.580000 ( 9.580571)
|
166
|
+
```
|
167
|
+
|
168
|
+
Yajl::FFI is about 6x slower than the pure native parsers. JSON::Stream is a
|
169
|
+
pure Ruby parser, and it performs accordingly. But it's useful in cases where
|
170
|
+
you're unable to use native bindings or when the limiting factor is the
|
171
|
+
network, rather than processor speed.
|
172
|
+
|
173
|
+
So if you need to parse many small JSON documents, the json and yajl-ruby gems
|
174
|
+
are the best options. If you need to stream, and incrementally parse, pieces of a
|
175
|
+
large document in constant memory space, yajl-ffi and json-stream are good
|
176
|
+
choices.
|
177
|
+
|
178
|
+
## License
|
179
|
+
|
180
|
+
Yajl::FFI is released under the MIT license. Check the LICENSE file for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
load './lib/yajl/ffi/tasks/benchmark.rake'
|
6
|
+
|
7
|
+
CLOBBER.include('pkg')
|
8
|
+
|
9
|
+
directory 'pkg'
|
10
|
+
|
11
|
+
desc 'Build distributable packages'
|
12
|
+
task :build => [:pkg] do
|
13
|
+
system 'gem build yajl-ffi.gemspec && mv yajl-ffi-*.gem pkg/'
|
14
|
+
end
|
15
|
+
|
16
|
+
Rake::TestTask.new(:test) do |test|
|
17
|
+
test.libs << 'test'
|
18
|
+
test.pattern = 'spec/**/*_spec.rb'
|
19
|
+
test.warning = true
|
20
|
+
end
|
21
|
+
|
22
|
+
task :default => [:clobber, :test, :build]
|
data/lib/yajl/ffi.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
require 'stringio'
|
3
|
+
require 'yajl/ffi/builder'
|
4
|
+
require 'yajl/ffi/parser'
|
5
|
+
require 'yajl/ffi/version'
|
6
|
+
|
7
|
+
module Yajl
|
8
|
+
module FFI
|
9
|
+
extend ::FFI::Library
|
10
|
+
|
11
|
+
ffi_lib 'yajl'
|
12
|
+
|
13
|
+
enum :status, [
|
14
|
+
:ok,
|
15
|
+
:client_canceled,
|
16
|
+
:error
|
17
|
+
]
|
18
|
+
|
19
|
+
enum :options, [
|
20
|
+
:allow_comments, 0x01,
|
21
|
+
:allow_invalid_utf8, 0x02,
|
22
|
+
:allow_trailing_garbage, 0x04,
|
23
|
+
:allow_multiple_values, 0x08,
|
24
|
+
:allow_partial_values, 0x10
|
25
|
+
]
|
26
|
+
|
27
|
+
class Callbacks < ::FFI::Struct
|
28
|
+
layout \
|
29
|
+
:on_null, :pointer,
|
30
|
+
:on_boolean, :pointer,
|
31
|
+
:on_integer, :pointer,
|
32
|
+
:on_double, :pointer,
|
33
|
+
:on_number, :pointer,
|
34
|
+
:on_string, :pointer,
|
35
|
+
:on_start_object, :pointer,
|
36
|
+
:on_key, :pointer,
|
37
|
+
:on_end_object, :pointer,
|
38
|
+
:on_start_array, :pointer,
|
39
|
+
:on_end_array, :pointer
|
40
|
+
end
|
41
|
+
|
42
|
+
typedef :pointer, :handle
|
43
|
+
|
44
|
+
attach_function :alloc, :yajl_alloc, [:pointer, :pointer, :pointer], :handle
|
45
|
+
attach_function :free, :yajl_free, [:handle], :void
|
46
|
+
attach_function :config, :yajl_config, [:handle, :options, :varargs], :int
|
47
|
+
attach_function :parse, :yajl_parse, [:handle, :pointer, :size_t], :status
|
48
|
+
attach_function :complete_parse, :yajl_complete_parse, [:handle], :status
|
49
|
+
attach_function :get_error, :yajl_get_error, [:handle, :int, :pointer, :size_t], :pointer
|
50
|
+
attach_function :free_error, :yajl_free_error, [:handle, :pointer], :void
|
51
|
+
attach_function :get_bytes_consumed, :yajl_get_bytes_consumed, [:handle], :size_t
|
52
|
+
attach_function :status_to_string, :yajl_status_to_string, [:status], :string
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Yajl
|
2
|
+
module FFI
|
3
|
+
# A parser listener that builds a full, in memory, object from a JSON
|
4
|
+
# document. This is similar to using the json gem's `JSON.parse` method.
|
5
|
+
#
|
6
|
+
# Examples
|
7
|
+
#
|
8
|
+
# parser = Yajl::FFI::Parser.new
|
9
|
+
# builder = Yajl::FFI::Builder.new(parser)
|
10
|
+
# parser << '{"answer": 42, "question": false}'
|
11
|
+
# obj = builder.result
|
12
|
+
class Builder
|
13
|
+
METHODS = %w[start_document end_document start_object end_object start_array end_array key value]
|
14
|
+
|
15
|
+
attr_reader :result
|
16
|
+
|
17
|
+
def initialize(parser)
|
18
|
+
METHODS.each do |name|
|
19
|
+
parser.send(name, &method(name))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def start_document
|
24
|
+
@stack = []
|
25
|
+
@keys = []
|
26
|
+
@result = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def end_document
|
30
|
+
@result = @stack.pop
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_object
|
34
|
+
@stack.push({})
|
35
|
+
end
|
36
|
+
|
37
|
+
def end_object
|
38
|
+
return if @stack.size == 1
|
39
|
+
node = @stack.pop
|
40
|
+
|
41
|
+
case @stack.last
|
42
|
+
when Hash
|
43
|
+
@stack.last[@keys.pop] = node
|
44
|
+
when Array
|
45
|
+
@stack.last << node
|
46
|
+
end
|
47
|
+
end
|
48
|
+
alias :end_array :end_object
|
49
|
+
|
50
|
+
def start_array
|
51
|
+
@stack.push([])
|
52
|
+
end
|
53
|
+
|
54
|
+
def key(key)
|
55
|
+
@keys << key
|
56
|
+
end
|
57
|
+
|
58
|
+
def value(value)
|
59
|
+
case @stack.last
|
60
|
+
when Hash
|
61
|
+
@stack.last[@keys.pop] = value
|
62
|
+
when Array
|
63
|
+
@stack.last << value
|
64
|
+
else
|
65
|
+
@stack << value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
module Yajl
|
2
|
+
module FFI
|
3
|
+
# Raised on any invalid JSON text.
|
4
|
+
ParserError = Class.new(RuntimeError)
|
5
|
+
|
6
|
+
# A streaming JSON parser that generates SAX-like events for state changes.
|
7
|
+
#
|
8
|
+
# Examples
|
9
|
+
#
|
10
|
+
# parser = Yajl::FFI::Parser.new
|
11
|
+
# parser.key {|key| puts key }
|
12
|
+
# parser.value {|value| puts value }
|
13
|
+
# parser << '{"answer":'
|
14
|
+
# parser << ' 42}'
|
15
|
+
class Parser
|
16
|
+
BUF_SIZE = 4096
|
17
|
+
CONTINUE_PARSE = 1
|
18
|
+
FLOAT = /[\.eE]/
|
19
|
+
|
20
|
+
# Parses a full JSON document from a String or an IO stream and returns
|
21
|
+
# the parsed object graph. For parsing small JSON documents with small
|
22
|
+
# memory requirements, use the json gem's faster JSON.parse method instead.
|
23
|
+
#
|
24
|
+
# json - The String or IO containing JSON data.
|
25
|
+
#
|
26
|
+
# Examples
|
27
|
+
#
|
28
|
+
# Yajl::FFI::Parser.parse('{"hello": "world"}')
|
29
|
+
# # => {"hello": "world"}
|
30
|
+
#
|
31
|
+
# Raises a Yajl::FFI::ParserError if the JSON data is malformed.
|
32
|
+
#
|
33
|
+
# Returns a Hash.
|
34
|
+
def self.parse(json)
|
35
|
+
stream = json.is_a?(String) ? StringIO.new(json) : json
|
36
|
+
parser = Parser.new
|
37
|
+
builder = Builder.new(parser)
|
38
|
+
while (buffer = stream.read(BUF_SIZE)) != nil
|
39
|
+
parser << buffer
|
40
|
+
end
|
41
|
+
parser.finish
|
42
|
+
builder.result
|
43
|
+
ensure
|
44
|
+
stream.close
|
45
|
+
end
|
46
|
+
|
47
|
+
# Create a new parser with an optional initialization block where
|
48
|
+
# we can register event callbacks.
|
49
|
+
#
|
50
|
+
# Examples
|
51
|
+
#
|
52
|
+
# parser = Yajl::FFI::Parser.new do
|
53
|
+
# start_document { puts "start document" }
|
54
|
+
# end_document { puts "end document" }
|
55
|
+
# start_object { puts "start object" }
|
56
|
+
# end_object { puts "end object" }
|
57
|
+
# start_array { puts "start array" }
|
58
|
+
# end_array { puts "end array" }
|
59
|
+
# key {|k| puts "key: #{k}" }
|
60
|
+
# value {|v| puts "value: #{v}" }
|
61
|
+
# end
|
62
|
+
def initialize(&block)
|
63
|
+
@listeners = {
|
64
|
+
start_document: [],
|
65
|
+
end_document: [],
|
66
|
+
start_object: [],
|
67
|
+
end_object: [],
|
68
|
+
start_array: [],
|
69
|
+
end_array: [],
|
70
|
+
key: [],
|
71
|
+
value: []
|
72
|
+
}
|
73
|
+
|
74
|
+
# Track parse stack.
|
75
|
+
@depth = 0
|
76
|
+
@started = false
|
77
|
+
|
78
|
+
# Allocate native memory.
|
79
|
+
@callbacks = callbacks
|
80
|
+
@handle = Yajl::FFI.alloc(@callbacks.to_ptr, nil, nil)
|
81
|
+
@handle = ::FFI::AutoPointer.new(@handle, method(:release))
|
82
|
+
|
83
|
+
# Register any observers in the block.
|
84
|
+
instance_eval(&block) if block_given?
|
85
|
+
end
|
86
|
+
|
87
|
+
def start_document(&block)
|
88
|
+
@listeners[:start_document] << block
|
89
|
+
end
|
90
|
+
|
91
|
+
def end_document(&block)
|
92
|
+
@listeners[:end_document] << block
|
93
|
+
end
|
94
|
+
|
95
|
+
def start_object(&block)
|
96
|
+
@listeners[:start_object] << block
|
97
|
+
end
|
98
|
+
|
99
|
+
def end_object(&block)
|
100
|
+
@listeners[:end_object] << block
|
101
|
+
end
|
102
|
+
|
103
|
+
def start_array(&block)
|
104
|
+
@listeners[:start_array] << block
|
105
|
+
end
|
106
|
+
|
107
|
+
def end_array(&block)
|
108
|
+
@listeners[:end_array] << block
|
109
|
+
end
|
110
|
+
|
111
|
+
def key(&block)
|
112
|
+
@listeners[:key] << block
|
113
|
+
end
|
114
|
+
|
115
|
+
def value(&block)
|
116
|
+
@listeners[:value] << block
|
117
|
+
end
|
118
|
+
|
119
|
+
# Pass data into the parser to advance the state machine and
|
120
|
+
# generate callback events. This is well suited for an EventMachine
|
121
|
+
# receive_data loop.
|
122
|
+
#
|
123
|
+
# data - The String of partial JSON data to parse.
|
124
|
+
#
|
125
|
+
# Raises a Yajl::FFI::ParserError if the JSON data is malformed.
|
126
|
+
#
|
127
|
+
# Returns nothing.
|
128
|
+
def <<(data)
|
129
|
+
result = Yajl::FFI.parse(@handle, data, data.bytesize)
|
130
|
+
error(data) if result == :error
|
131
|
+
if @started && @depth == 0
|
132
|
+
result = Yajl::FFI.complete_parse(@handle)
|
133
|
+
error(data) if result == :error
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Drain any remaining buffered characters into the parser to complete
|
138
|
+
# the parsing of the document.
|
139
|
+
#
|
140
|
+
# This is only required when parsing a document containing a single
|
141
|
+
# numeric value, integer or float. The parser has no other way to
|
142
|
+
# detect when it should no longer expect additional characters with
|
143
|
+
# which to complete the parse, so it must be signaled by a call to
|
144
|
+
# this method.
|
145
|
+
#
|
146
|
+
# If you're parsing more typical object or array documents, there's no
|
147
|
+
# need to call `finish` because the parse will complete when the final
|
148
|
+
# closing `]` or `}` character is scanned.
|
149
|
+
#
|
150
|
+
# Raises a Yajl::FFI::ParserError if the JSON data is malformed.
|
151
|
+
#
|
152
|
+
# Returns nothing.
|
153
|
+
def finish
|
154
|
+
result = Yajl::FFI.complete_parse(@handle)
|
155
|
+
error('') if result == :error
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
# Raise a ParserError for the malformed JSON data sent to the parser.
|
161
|
+
#
|
162
|
+
# data - The malformed JSON String that the yajl parser rejected.
|
163
|
+
#
|
164
|
+
# Returns nothing.
|
165
|
+
def error(data)
|
166
|
+
pointer = Yajl::FFI.get_error(@handle, 1, data, data.bytesize)
|
167
|
+
message = pointer.read_string
|
168
|
+
Yajl::FFI.free_error(@handle, pointer)
|
169
|
+
raise ParserError, message
|
170
|
+
end
|
171
|
+
|
172
|
+
# Invoke all registered observer procs for the event type.
|
173
|
+
#
|
174
|
+
# type - The Symbol listener name.
|
175
|
+
# args - The argument list to pass into the observer procs.
|
176
|
+
#
|
177
|
+
# Examples
|
178
|
+
#
|
179
|
+
# # broadcast events for {"answer": 42}
|
180
|
+
# notify(:start_object)
|
181
|
+
# notify(:key, "answer")
|
182
|
+
# notify(:value, 42)
|
183
|
+
# notify(:end_object)
|
184
|
+
#
|
185
|
+
# Returns nothing.
|
186
|
+
def notify(type, *args)
|
187
|
+
@started = true
|
188
|
+
@listeners[type].each do |block|
|
189
|
+
block.call(*args)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Build a native Callbacks struct that broadcasts parser state change
|
194
|
+
# events to registered observers.
|
195
|
+
#
|
196
|
+
# The functions registered in the struct are invoked by the native yajl
|
197
|
+
# parser. They convert the yajl callback data into the expected Ruby
|
198
|
+
# objects and invoke observers registered on the parser with
|
199
|
+
# `start_object`, `key`, `value`, and so on.
|
200
|
+
#
|
201
|
+
# The struct instance returned from this method must be stored in an
|
202
|
+
# instance variable. This prevents the FFI::Function objects from being
|
203
|
+
# garbage collected while the parser is still in use. The native function
|
204
|
+
# bindings need to be collected at the same time as the Parser instance.
|
205
|
+
#
|
206
|
+
# Returns a Yajl::FFI::Callbacks struct.
|
207
|
+
def callbacks
|
208
|
+
callbacks = Yajl::FFI::Callbacks.new
|
209
|
+
|
210
|
+
callbacks[:on_null] = ::FFI::Function.new(:int, [:pointer]) do |ctx|
|
211
|
+
notify(:start_document) if @depth == 0
|
212
|
+
notify(:value, nil)
|
213
|
+
notify(:end_document) if @depth == 0
|
214
|
+
CONTINUE_PARSE
|
215
|
+
end
|
216
|
+
|
217
|
+
callbacks[:on_boolean] = ::FFI::Function.new(:int, [:pointer, :int]) do |ctx, value|
|
218
|
+
notify(:start_document) if @depth == 0
|
219
|
+
notify(:value, value == 1)
|
220
|
+
notify(:end_document) if @depth == 0
|
221
|
+
CONTINUE_PARSE
|
222
|
+
end
|
223
|
+
|
224
|
+
# yajl only calls on_number
|
225
|
+
callbacks[:on_integer] = nil
|
226
|
+
callbacks[:on_double] = nil
|
227
|
+
|
228
|
+
callbacks[:on_number] = ::FFI::Function.new(:int, [:pointer, :string, :size_t]) do |ctx, value, length|
|
229
|
+
notify(:start_document) if @depth == 0
|
230
|
+
value = value.slice(0, length)
|
231
|
+
number = (value =~ FLOAT) ? value.to_f : value.to_i
|
232
|
+
notify(:value, number)
|
233
|
+
notify(:end_document) if @depth == 0
|
234
|
+
CONTINUE_PARSE
|
235
|
+
end
|
236
|
+
|
237
|
+
callbacks[:on_string] = ::FFI::Function.new(:int, [:pointer, :pointer, :size_t]) do |ctx, value, length|
|
238
|
+
notify(:start_document) if @depth == 0
|
239
|
+
notify(:value, extract(value, length))
|
240
|
+
notify(:end_document) if @depth == 0
|
241
|
+
CONTINUE_PARSE
|
242
|
+
end
|
243
|
+
|
244
|
+
callbacks[:on_start_object] = ::FFI::Function.new(:int, [:pointer]) do |ctx|
|
245
|
+
@depth += 1
|
246
|
+
notify(:start_document) if @depth == 1
|
247
|
+
notify(:start_object)
|
248
|
+
CONTINUE_PARSE
|
249
|
+
end
|
250
|
+
|
251
|
+
callbacks[:on_key] = ::FFI::Function.new(:int, [:pointer, :pointer, :size_t]) do |ctx, key, length|
|
252
|
+
notify(:key, extract(key, length))
|
253
|
+
CONTINUE_PARSE
|
254
|
+
end
|
255
|
+
|
256
|
+
callbacks[:on_end_object] = ::FFI::Function.new(:int, [:pointer]) do |ctx|
|
257
|
+
@depth -= 1
|
258
|
+
notify(:end_object)
|
259
|
+
notify(:end_document) if @depth == 0
|
260
|
+
CONTINUE_PARSE
|
261
|
+
end
|
262
|
+
|
263
|
+
callbacks[:on_start_array] = ::FFI::Function.new(:int, [:pointer]) do |ctx|
|
264
|
+
@depth += 1
|
265
|
+
notify(:start_document) if @depth == 1
|
266
|
+
notify(:start_array)
|
267
|
+
CONTINUE_PARSE
|
268
|
+
end
|
269
|
+
|
270
|
+
callbacks[:on_end_array] = ::FFI::Function.new(:int, [:pointer]) do |ctx|
|
271
|
+
@depth -= 1
|
272
|
+
notify(:end_array)
|
273
|
+
notify(:end_document) if @depth == 0
|
274
|
+
CONTINUE_PARSE
|
275
|
+
end
|
276
|
+
|
277
|
+
callbacks
|
278
|
+
end
|
279
|
+
|
280
|
+
# Convert the binary encoded string data passed out of the yajl parser
|
281
|
+
# into a UTF-8 encoded string.
|
282
|
+
#
|
283
|
+
# pointer - The FFI::Pointer containing the ASCII-8BIT encoded String.
|
284
|
+
# length - The Fixnum number of characters to extract from `pointer`.
|
285
|
+
#
|
286
|
+
# Raises a ParserError if the data contains malformed UTF-8 bytes.
|
287
|
+
#
|
288
|
+
# Returns a String.
|
289
|
+
def extract(pointer, length)
|
290
|
+
string = pointer.read_string(length)
|
291
|
+
string.force_encoding(Encoding::UTF_8)
|
292
|
+
unless string.valid_encoding?
|
293
|
+
raise ParserError, 'Invalid UTF-8 byte sequence'
|
294
|
+
end
|
295
|
+
string
|
296
|
+
end
|
297
|
+
|
298
|
+
# Free the memory held by a yajl parser handle previously allocated
|
299
|
+
# with Yajl::FFI.alloc.
|
300
|
+
#
|
301
|
+
# It's not sufficient to just allow the handle pointer to be freed
|
302
|
+
# normally because it contains pointers that must also be freed. The
|
303
|
+
# native yajl API provides a `yajl_free` function for this purpose.
|
304
|
+
#
|
305
|
+
# This method is invoked by the FFI::AutoPointer, wrapping the yajl
|
306
|
+
# parser handle, when it's garbage collected by Ruby.
|
307
|
+
#
|
308
|
+
# pointer - The FFI::Pointer that references the native yajl parser.
|
309
|
+
#
|
310
|
+
# Returns nothing.
|
311
|
+
def release(pointer)
|
312
|
+
Yajl::FFI.free(pointer)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|