drjson 1.0.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.
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