drjson 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in drjson.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Matthias Luedtke - http://github.com/mat
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Dr. JSON
2
+
3
+ Closes abruptly cut-off JSON strings.
4
+
5
+ ## Installation
6
+
7
+ Install the gem:
8
+
9
+ $ gem install drjson
10
+
11
+ Or add this line to your application's Gemfile:
12
+
13
+ gem 'drjson'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ ## Usage
20
+
21
+ You can hand over your poor JSON over command line
22
+
23
+ $ echo '{"foo":nul' | drjson
24
+ {"foo":null}
25
+
26
+ $ echo -n '[7, [42' | drjson
27
+ [7, [42]]
28
+
29
+ or via file
30
+
31
+ $ echo -n '{"foo": {"bar"' > my_file.json
32
+ $ drjson my_file.json
33
+ {"foo": {"bar":null}}
34
+
35
+ ## Contributing
36
+
37
+ 1. Fork it
38
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
39
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
40
+ 4. Push to the branch (`git push origin my-new-feature`)
41
+ 5. Create new Pull Request
42
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/drjson ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'drjson'
4
+
5
+ if filename = ARGV[0]
6
+ json_like = File.read(filename)
7
+ else
8
+ json_like = $stdin.read
9
+ end
10
+
11
+ json = DrJson.new.repair(json_like)
12
+ puts json
13
+
data/drjson.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'drjson/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "drjson"
8
+ gem.version = Drjson::VERSION
9
+ gem.authors = ["Matthias Luedtke"]
10
+ gem.email = ["github@matthias.luedtke.de"]
11
+ gem.description = %q{Closes abruptly cut-off JSON strings.}
12
+ gem.summary = gem.description
13
+ gem.homepage = "http://drjson.github.com/"
14
+
15
+ gem.files = (`git ls-files`.split($/)).reject{ |path| path =~ /fixtures/}
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
data/lib/drjson.rb ADDED
@@ -0,0 +1,217 @@
1
+
2
+ # DrJson closes abruptly cut-off JSON strings.
3
+ #
4
+ # $ echo '{"foo":nul' | drjson
5
+ # {"foo":null}
6
+ #
7
+ # Regular, well-formed JSON is passed through untouched.
8
+ #
9
+ # Install with rubygems
10
+ #
11
+ # $ gem install drjson
12
+ #
13
+ # Find the [source on GitHub][so].
14
+ #
15
+ # [so]: https://github.com/mat/drjson/
16
+
17
+ require 'strscan'
18
+
19
+ class DrJson
20
+
21
+ ### repair(json)
22
+
23
+ # Parses and repairs the possbily cut-off input `json_str`
24
+ # in recursive descent fashion. Returns `json_str` with
25
+ # necesarry closing tags appended.
26
+ def repair(json_str)
27
+ @input = StringScanner.new(json_str)
28
+ @result = ""
29
+
30
+ begin
31
+ # Real world JSON has arrays as top level elements, yep.
32
+ parse_object || parse_array
33
+ spaces
34
+ rescue UnexpectedTokenError => e
35
+ raise e if debug
36
+ end
37
+ result
38
+ end
39
+ class UnexpectedTokenError < StandardError ; end
40
+
41
+ def initialize(options = {})
42
+ @debug = options.fetch(:debug, false)
43
+ end
44
+
45
+ private
46
+ attr_reader :input, :result, :debug
47
+
48
+ #### Non-Terminal Symbols
49
+
50
+ def parse_object
51
+ spaces
52
+ if next_is "{"
53
+ spaces
54
+ begin
55
+ parse_members
56
+ ensure # and recursively auto close
57
+ must_see "}"
58
+ end
59
+ spaces
60
+ end
61
+ end
62
+
63
+ def parse_members
64
+ parse_pair
65
+ spaces
66
+ if next_is ","
67
+ parse_members
68
+ end
69
+ end
70
+
71
+ # An object's pair is the strictest of all
72
+ # elements: It has to have all syntax elements
73
+ # that's why we `&&`` them.
74
+ def parse_pair
75
+ parse_string &&
76
+ spaces &&
77
+ must_see(":") &&
78
+ spaces &&
79
+ # We may have to fill in a rhs *null*.
80
+ (parse_value || append('null'))
81
+ end
82
+
83
+ def parse_array
84
+ spaces
85
+ if next_is "["
86
+ spaces
87
+ begin
88
+ parse_elements || true
89
+ ensure # and recursively auto close
90
+ must_see "]"
91
+ end
92
+ end
93
+ end
94
+
95
+ def parse_elements
96
+ spaces
97
+ value_found = parse_value
98
+ spaces
99
+ while next_is ","
100
+ spaces
101
+ parse_value || append('null')
102
+ spaces
103
+ end
104
+
105
+ value_found
106
+ end
107
+
108
+ def parse_value
109
+ spaces
110
+ parse_string || parse_number || parse_object || parse_array || consume(TRUE) || consume(FALSE) || consume(NULL)
111
+ end
112
+ NULL = /null/
113
+ FALSE = /false/
114
+ TRUE = /true/
115
+
116
+ #### Terminal Symbols
117
+
118
+ def parse_string
119
+ spaces
120
+ if next_is "\""
121
+ consume CHAR_SEQUENCE
122
+
123
+ # We don't need to `ensure` here because we
124
+ # don't need to close strings recursively;
125
+ # a string cannot contain another string.
126
+
127
+ must_see "\""
128
+ end
129
+ end
130
+ CHAR_SEQUENCE = /([^\"\\\n]|\\([tbrfn\"\/\\])|\\u[0-9a-f]{4,4})*/i
131
+
132
+ def parse_number
133
+ spaces
134
+ number_found = parse_int
135
+ parse_frac
136
+ parse_exp
137
+
138
+ number_found
139
+ end
140
+
141
+ def parse_int
142
+ consume MINUS
143
+ consume DIGITS
144
+ end
145
+
146
+ def parse_frac
147
+ if next_is "."
148
+ consume DIGITS
149
+ end
150
+ end
151
+
152
+ def parse_exp
153
+ if next_is("e") || next_is("E")
154
+ consume(MINUS) || consume(PLUS)
155
+ consume DIGITS
156
+ end
157
+ end
158
+ MINUS = /-/
159
+ PLUS = /[+]/
160
+ DIGITS = /\d+/
161
+
162
+ #### Helpers
163
+
164
+ def spaces
165
+ consume ANY_WHITESPACE
166
+ end
167
+ ANY_WHITESPACE = /\s*/
168
+
169
+ # `must_see` makes sure we add the necessary
170
+ # amount of (closing) tags to the output
171
+ def must_see(char)
172
+ spaces
173
+ if next_is char
174
+ # All good, input as expected.
175
+ char
176
+ elsif input.eos?
177
+ # Finally, the case we've been waiting for:
178
+ # Input exhausted, but still elements to close.
179
+ append char
180
+ else
181
+ append char
182
+ # The magic trick:
183
+ # We implicitly use the stack to count how many closing tags we have/need.
184
+ # We pair with exception's `ensure` mechanism to actually write them out.
185
+ if debug
186
+ msg = "Seen so far: %s" % result.clone
187
+ raise UnexpectedTokenError, msg
188
+ else
189
+ raise UnexpectedTokenError
190
+ end
191
+ end
192
+ end
193
+
194
+ # Checks whether the next char is `exptected_char`
195
+ #
196
+ # If it is, `expected_char` is consumed, `appended` and true is returned.
197
+ # If not, this call is a no op and false is returned.
198
+ def next_is(expected_char)
199
+ next_char = input.peek(1)
200
+ if next_char == expected_char
201
+ append input.getch
202
+ end
203
+ end
204
+
205
+ # `consume` is `next_is`'s sibling for arbitrary patterns
206
+ # not single chars. Same behavior.
207
+ def consume(pattern)
208
+ if input.match?(pattern)
209
+ append input.scan(pattern)
210
+ end
211
+ end
212
+
213
+ def append(str)
214
+ result << str
215
+ end
216
+ end
217
+
@@ -0,0 +1,3 @@
1
+ module Drjson
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,178 @@
1
+
2
+ require 'rspec'
3
+ require 'json'
4
+ require 'yajl'
5
+ require 'oj'
6
+
7
+ require 'drjson'
8
+
9
+ describe DrJson do
10
+
11
+ def doctor
12
+ DrJson.new(:debug => true)
13
+ end
14
+
15
+ describe "completion" do
16
+ it "works" do
17
+ doctor.repair("{").should == "{}"
18
+ end
19
+
20
+ it "insert the object value if absent" do
21
+ doctor.repair('{"foo": ').should == '{"foo": null}'
22
+ end
23
+
24
+ it "closes array brackets" do
25
+ doctor.repair("[42").should == "[42]"
26
+ end
27
+ it "inserts missing trailing array elements" do
28
+ doctor.repair("[42,").should == "[42,null]"
29
+ doctor.repair("[42,7,").should == "[42,7,null]"
30
+ end
31
+
32
+ it "works" do
33
+ doctor.repair('{"foo": [42 ').should == '{"foo": [42 ]}'
34
+ doctor.repair('{"foo": [ ').should == '{"foo": [ ]}'
35
+ end
36
+ it "works" do
37
+ doctor.repair('{"foo": "bar').should == '{"foo": "bar"}'
38
+ doctor.repair('{"foo": "ba').should == '{"foo": "ba"}'
39
+
40
+ doctor.repair('{"foo": "').should == '{"foo": ""}'
41
+ doctor.repair('{"foo": ').should == '{"foo": null}'
42
+ doctor.repair('{"foo" ').should == '{"foo" :null}'
43
+ doctor.repair('{"foo ').should == '{"foo ":null}'
44
+ doctor.repair('{"foo').should == '{"foo":null}'
45
+
46
+ doctor.repair('{"f').should == '{"f":null}'
47
+ doctor.repair('{"').should == '{"":null}'
48
+ doctor.repair('{').should == '{}'
49
+ end
50
+ end #completion
51
+
52
+ ##################
53
+ it "works" do
54
+ doctor.repair("[]").should == "[]"
55
+ doctor.repair("[42]").should == "[42]"
56
+ doctor.repair('{"foo": "bar"}').should == '{"foo": "bar"}'
57
+ end
58
+ it "empty strings" do
59
+ doctor.repair('{"":""}').should == '{"":""}'
60
+ end
61
+ it "works" do
62
+ doctor.repair('{"foo": 42 }').should == '{"foo": 42 }'
63
+ doctor.repair('{"foo": 42}').should == '{"foo": 42}'
64
+ end
65
+ it "multiline completion" do
66
+ doctor.repair('{"foo": "bar').should == '{"foo": "bar"}'
67
+ end
68
+ it "works" do
69
+ doctor.repair('{"foo": 42, "bar": 7 }').should == '{"foo": 42, "bar": 7 }'
70
+ end
71
+ it "works" do
72
+ doctor.repair('{"foo": {"bar" : "baz"} }').should == '{"foo": {"bar" : "baz"} }'
73
+ doctor.repair('{"foo": {"bar" : "baz').should == '{"foo": {"bar" : "baz"}}'
74
+ end
75
+ it "works" do
76
+ doctor.repair('{"foo": [] }').should == '{"foo": [] }'
77
+ doctor.repair('{"foo": [42] }').should == '{"foo": [42] }'
78
+ end
79
+ it "handles inclomplete (null|false|true) tokens" do
80
+ DrJson.new.repair('[tru').should == '[]'
81
+ DrJson.new.repair('[fals').should == '[]'
82
+ DrJson.new.repair('[nul').should == '[]'
83
+ end
84
+ it "handles inclomplete (null|false|true) tokens" do
85
+ DrJson.new.repair('[[tru').should == '[[]]'
86
+ DrJson.new.repair('[[[fal').should == '[[[]]]'
87
+ DrJson.new.repair('[[[[nul').should == '[[[[]]]]'
88
+ end
89
+ it "handles inclomplete (null|false|true) values" do
90
+ DrJson.new.repair('{"foo":tru').should == '{"foo":null}'
91
+ DrJson.new.repair('{"foo":{"bar":tru').should == '{"foo":{"bar":null}}'
92
+ end
93
+ it "handles inclomplete keys in pairs" do
94
+ DrJson.new.repair('{"foo').should == '{"foo":null}'
95
+ end
96
+ it "works" do
97
+ doctor.repair('{"foo": [42,7] }').should == '{"foo": [42,7] }'
98
+ doctor.repair('{"foo": [42, 7] }').should == '{"foo": [42, 7] }'
99
+ doctor.repair('{"foo": [42 ,7] }').should == '{"foo": [42 ,7] }'
100
+ doctor.repair('{"foo": [42 , 7] }').should == '{"foo": [42 , 7] }'
101
+ doctor.repair('{"foo": [42 , 7, 4711] }').should == '{"foo": [42 , 7, 4711] }'
102
+ end
103
+
104
+ it "supports all the null|false|true terminal symbols" do
105
+ doctor.repair('[null]').should == '[null]'
106
+ doctor.repair('[true]').should == '[true]'
107
+ doctor.repair('[false]').should == '[false]'
108
+ end
109
+
110
+ context "numbers" do
111
+ it "supports -" do
112
+ doctor.repair('{"foo": -42 }').should == '{"foo": -42 }'
113
+ end
114
+ it "supports floats" do
115
+ doctor.repair('{"foo": 7.42 }').should == '{"foo": 7.42 }'
116
+ end
117
+ it "supports exponents" do
118
+ doctor.repair('{"foo": 1e-4 }').should == '{"foo": 1e-4 }'
119
+ doctor.repair('{"foo": 1E-4 }').should == '{"foo": 1E-4 }'
120
+ doctor.repair('{"foo": 1e+4 }').should == '{"foo": 1e+4 }'
121
+ doctor.repair('{"foo": 1E+4 }').should == '{"foo": 1E+4 }'
122
+ doctor.repair('{"foo": 1e4 }').should == '{"foo": 1e4 }'
123
+ doctor.repair('{"foo": 1E4 }').should == '{"foo": 1E4 }'
124
+ end
125
+ end
126
+
127
+ it "knows escape sequences" do
128
+ doctor.repair('{"foo": "\"" }').should == '{"foo": "\"" }'
129
+ doctor.repair('{"foo": "\\\\" }').should == '{"foo": "\\\\" }'
130
+ doctor.repair('{"foo": "\/" }').should == '{"foo": "\/" }'
131
+ doctor.repair('{"foo": "\b" }').should == '{"foo": "\b" }'
132
+ doctor.repair('{"foo": "\f" }').should == '{"foo": "\f" }'
133
+ doctor.repair('{"foo": "\n" }').should == '{"foo": "\n" }'
134
+ doctor.repair('{"foo": "\r" }').should == '{"foo": "\r" }'
135
+ doctor.repair('{"foo": "\t" }').should == '{"foo": "\t" }'
136
+ doctor.repair('{"foo": "\u4711" }').should == '{"foo": "\u4711" }'
137
+ doctor.repair('{"foo": "\ubeef" }').should == '{"foo": "\ubeef" }'
138
+ end
139
+
140
+ it "does not break on unexpected input, it tries to fix it" do
141
+ broken_json ='{"foo": ["beef" {][] senseless'
142
+ repaired_json = '{"foo": ["beef" ]}'
143
+ DrJson.new.repair(broken_json).should == repaired_json
144
+ end
145
+
146
+ it "indicates unexpected input in debug mode" do
147
+ broken_json ='{"foo": "beef" {'
148
+ lambda {DrJson.new(:debug => true).repair(broken_json)}.should raise_error DrJson::UnexpectedTokenError
149
+ end
150
+
151
+ context "pass-through behavior for real life files" do
152
+ json_files = Dir.glob("spec/fixtures/yajl-ruby/*.json")
153
+
154
+ json_files.each do |file|
155
+ describe "#{file}" do
156
+ #it "is parseable with JSON: #{file}" do
157
+ # JSON.parse(File.read(file))
158
+ #end
159
+ it "is parseable with Oj: #{file}" do
160
+ Oj.load(File.read(file))
161
+ end
162
+ #it "is parseable with Yajl: #{file}" do
163
+ # Yajl::Parser.parse(File.read(file))
164
+ #end
165
+ it "does pass through correct, real life file #{file}" do
166
+ test_file(file)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ def test_file(file_name)
173
+ json_str = File.read (file_name)
174
+ repaired = doctor.repair(json_str)
175
+ repaired.should == json_str
176
+ end
177
+ end
178
+
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: drjson
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matthias Luedtke
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-23 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Closes abruptly cut-off JSON strings.
15
+ email:
16
+ - github@matthias.luedtke.de
17
+ executables:
18
+ - drjson
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - Gemfile
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - bin/drjson
28
+ - drjson.gemspec
29
+ - lib/drjson.rb
30
+ - lib/drjson/version.rb
31
+ - spec/drjson_spec.rb
32
+ homepage: http://drjson.github.com/
33
+ licenses: []
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubyforge_project:
52
+ rubygems_version: 1.8.24
53
+ signing_key:
54
+ specification_version: 3
55
+ summary: Closes abruptly cut-off JSON strings.
56
+ test_files:
57
+ - spec/drjson_spec.rb