tidy_json 0.1.1 → 0.2.3
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/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.md +78 -29
- data/Rakefile +2 -0
- data/lib/tidy_json.rb +259 -137
- data/lib/tidy_json/dedication.rb +12 -9
- data/lib/tidy_json/version.rb +3 -1
- data/test/JsonableObject.json +1 -0
- data/test/codecov_runner.rb +16 -0
- data/test/test_tidy_json.rb +107 -14
- data/tidy_json.gemspec +9 -5
- metadata +45 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 595bfb60293652c3aec843420229bfe1c55c84a8387c7f60e5b4d5a62f47a9df
|
4
|
+
data.tar.gz: 5ac9762a852f6753ff1bb8c52a2eac9224a55455bd14c9cf817377138fe21233
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e1bbd8d6caf301d83b83793188a0d4d8620876ed7930a4d38103aea5e5e9b79106762bb1b4caf0b03828d8e4713a0e1703bebe17dd12551f6d094d389865df3
|
7
|
+
data.tar.gz: dfac6e1f21f516cd8311d3777497c0f2ab1563d4303b719c5faf2b3b7250d16ac4e4ffd429fe6abde5b67b78908dc1c3f6651854269253b747cb3b1ef10399b7
|
data/Gemfile
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,24 +1,16 @@
|
|
1
|
-
#
|
1
|
+
# tidy_json
|
2
2
|
|
3
|
-
[![
|
3
|
+
[![Travis CI][travis_build_status_badge]][travis_build_status] [![Circle CI][cci_build_status_badge]][cci_build_status] [![codecov][codecov_badge]][codecov_status] [![Gem Version][gem_version_badge]][gem_version]
|
4
4
|
|
5
5
|
A mixin providing (recursive) JSON serialization and pretty printing.
|
6
6
|
|
7
7
|
### Installation
|
8
8
|
|
9
|
-
#### Minimal
|
10
|
-
|
11
|
-
```bash
|
12
|
-
$ gem install tidy_json
|
13
|
-
```
|
14
|
-
|
15
|
-
#### Development (tests, YARD docs)
|
16
|
-
|
17
9
|
```bash
|
18
|
-
|
10
|
+
$ gem install tidy_json
|
19
11
|
```
|
20
12
|
|
21
|
-
Or,
|
13
|
+
Or, in your `Gemfile`:
|
22
14
|
|
23
15
|
```ruby
|
24
16
|
source 'https://rubygems.org'
|
@@ -32,21 +24,71 @@ gem 'tidy_json'
|
|
32
24
|
```ruby
|
33
25
|
require 'tidy_json'
|
34
26
|
|
35
|
-
|
36
|
-
|
27
|
+
class Jsonable
|
28
|
+
attr_reader :a, :b
|
29
|
+
def initialize
|
30
|
+
@a = { a: 'uno', f: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', { 'ichi': "\u{4e00}", 'ni': "\u{4e8c}", 'san': "\u{4e09}", 'yon': "\u{56db}" }]], c: {}, b: 'dos', e: [[]] }
|
31
|
+
@b = { z: { iv: 4, ii: 'duos', iii: 3, i: 'one' }, b: ['two', 3, '<abbr title="four">IV</abbr>'], a: 1, g: [{ none: [] }], f: %w[x y z] }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
my_jsonable = Jsonable.new
|
36
|
+
# => #<Jsonable:0x0055b2aa0ff660 @a={:a=>"uno", :f=>["I", "II", "III", ["i.", "ii.", "iii.", {:ichi=>"一", :ni=>"二", :san=>"三", :yon=>"四"}]], :c=>{}, :b=>"dos", :e=>[[]]}, @b={:z=>{:iv=>4, :ii=>"duos", :iii=>3, :i=>"one"}, :b=>["two", 3, "<abbr title=\"four\">IV</abbr>"], :a=>1, :g=>[{:none=>[]}], :f=>["x", "y", "z"]}>
|
37
37
|
|
38
|
-
|
39
|
-
# => {
|
38
|
+
JSON.parse my_jsonable.stringify
|
39
|
+
# => {"class"=>"Jsonable", "a"=>{"a"=>"uno", "f"=>["I", "II", "III", ["i.", "ii.", "iii.", {"ichi"=>"一", "ni"=>"二", "san"=>"三", "yon"=>"四"}]], "c"=>{}, "b"=>"dos", "e"=>[[]]}, "b"=>{"z"=>{"iv"=>4, "ii"=>"duos", "iii"=>3, "i"=>"one"}, "b"=>["two", 3, "<abbr title=\"four\">IV</abbr>"], "a"=>1, "g"=>[{"none"=>[]}], "f"=>["x", "y", "z"]}}
|
40
40
|
|
41
|
-
puts
|
41
|
+
puts my_jsonable.to_tidy_json(indent: 4, sort: true)
|
42
42
|
# {
|
43
|
-
#
|
44
|
-
# "
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
43
|
+
# "a": {
|
44
|
+
# "a": "uno",
|
45
|
+
# "b": "dos",
|
46
|
+
# "c": {},
|
47
|
+
# "e": [
|
48
|
+
# []
|
49
|
+
# ],
|
50
|
+
# "f": [
|
51
|
+
# "I",
|
52
|
+
# "II",
|
53
|
+
# "III",
|
54
|
+
# [
|
55
|
+
# "i.",
|
56
|
+
# "ii.",
|
57
|
+
# "iii.",
|
58
|
+
# {
|
59
|
+
# "ichi": "一",
|
60
|
+
# "ni": "二",
|
61
|
+
# "san": "三",
|
62
|
+
# "yon": "四"
|
63
|
+
# }
|
64
|
+
# ]
|
65
|
+
# ]
|
66
|
+
# },
|
67
|
+
# "b": {
|
68
|
+
# "a": 1,
|
69
|
+
# "b": [
|
70
|
+
# "two",
|
71
|
+
# 3,
|
72
|
+
# "<abbr title=\"four\">IV</abbr>"
|
73
|
+
# ],
|
74
|
+
# "f": [
|
75
|
+
# "x",
|
76
|
+
# "y",
|
77
|
+
# "z"
|
78
|
+
# ],
|
79
|
+
# "g": [
|
80
|
+
# {
|
81
|
+
# "none": []
|
82
|
+
# }
|
83
|
+
# ],
|
84
|
+
# "z": {
|
85
|
+
# "i": "one",
|
86
|
+
# "ii": "duos",
|
87
|
+
# "iii": 3,
|
88
|
+
# "iv": 4
|
89
|
+
# }
|
90
|
+
# },
|
91
|
+
# "class": "Jsonable"
|
50
92
|
# }
|
51
93
|
# => nil
|
52
94
|
```
|
@@ -57,12 +99,19 @@ puts complex_object.to_tidy_json
|
|
57
99
|
- [json](https://rubygems.org/gems/json) ~> 2.2
|
58
100
|
|
59
101
|
#### Building
|
60
|
-
- [
|
61
|
-
- [minitest](https://rubygems.org/gems/minitest) ~> 5.0
|
102
|
+
- [test-unit](https://rubygems.org/gems/test-unit) ~> 3.3
|
62
103
|
- [yard](https://rubygems.org/gems/yard) ~> 0.9
|
63
104
|
|
64
105
|
### License
|
65
|
-
[MIT](https://
|
106
|
+
[MIT](https://github.com/rdipardo/tidy_json/blob/master/LICENSE)
|
107
|
+
|
108
|
+
|
109
|
+
[travis_build_status]: https://travis-ci.com/rdipardo/tidy_json
|
110
|
+
[travis_build_status_badge]: https://travis-ci.com/rdipardo/tidy_json.svg?branch=master
|
111
|
+
[cci_build_status]: https://circleci.com/gh/rdipardo/tidy_json/tree/master
|
112
|
+
[cci_build_status_badge]: https://circleci.com/gh/rdipardo/tidy_json.svg?style=svg
|
113
|
+
[codecov_status]: https://codecov.io/gh/rdipardo/tidy_json
|
114
|
+
[codecov_badge]: https://codecov.io/gh/rdipardo/tidy_json/badge.svg
|
115
|
+
[gem_version]: https://badge.fury.io/rb/tidy_json
|
116
|
+
[gem_version_badge]: https://badge.fury.io/rb/tidy_json.svg
|
66
117
|
|
67
|
-
### Author
|
68
|
-
[Robert Di Pardo](mailto:rdipardo0520@conestogac.on.ca)
|
data/Rakefile
CHANGED
data/lib/tidy_json.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require_relative 'tidy_json/version'
|
3
5
|
|
@@ -9,16 +11,20 @@ module TidyJson
|
|
9
11
|
# Emits a pretty-printed JSON representation of the given +obj+.
|
10
12
|
#
|
11
13
|
# @param obj [Object] A Ruby object that can be parsed as JSON.
|
14
|
+
# @param opts [Hash] Output format options.
|
15
|
+
# @option (see Formatter#initialize)
|
12
16
|
# @return [String] A pretty-printed JSON string.
|
13
|
-
def self.tidy(obj = {})
|
17
|
+
def self.tidy(obj = {}, opts = {})
|
18
|
+
formatter = Formatter.new(opts)
|
19
|
+
obj = sort_keys(obj) if formatter.sorted
|
14
20
|
str = ''
|
15
21
|
|
16
22
|
if obj.instance_of?(Hash)
|
17
23
|
str << "{\n"
|
18
24
|
|
19
25
|
obj.each do |k, v|
|
20
|
-
str << "\"#{k}\": "
|
21
|
-
str <<
|
26
|
+
str << formatter.indent << "\"#{k}\": "
|
27
|
+
str << formatter.format_node(v, obj)
|
22
28
|
end
|
23
29
|
|
24
30
|
str << "}\n"
|
@@ -27,29 +33,72 @@ module TidyJson
|
|
27
33
|
str << "[\n"
|
28
34
|
|
29
35
|
obj.each do |v|
|
30
|
-
str <<
|
36
|
+
str << formatter.indent
|
37
|
+
str << formatter.format_node(v, obj)
|
31
38
|
end
|
32
39
|
|
33
40
|
str << "]\n"
|
34
41
|
end
|
35
42
|
|
36
|
-
str
|
43
|
+
formatter.trim str
|
37
44
|
end
|
38
45
|
|
39
46
|
##
|
40
|
-
#
|
47
|
+
# Returns the given +obj+ with keys in ascending order to a maximum depth of
|
48
|
+
# 2.
|
41
49
|
#
|
42
|
-
# @param
|
43
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
50
|
+
# @param obj [Hash, Array<Hash>] A dictionary-like object or collection
|
51
|
+
# thereof.
|
52
|
+
# @return [Hash, Array<Hash>, Object] A copy of the given +obj+ with top- and
|
53
|
+
# second-level keys in ascending order, or else an identical copy of +obj+.
|
54
|
+
# @note +obj+ is returned unchanged if: 1) it's not iterable; 2) it's an
|
55
|
+
# empty collection; 3) any one of its elements is not hashable (and +obj+
|
56
|
+
# is an array).
|
57
|
+
def self.sort_keys(obj = {})
|
58
|
+
return obj if !obj.respond_to?(:each) || obj.empty? ||
|
59
|
+
(obj.instance_of?(Array) &&
|
60
|
+
!obj.all? { |e| e.respond_to? :keys })
|
61
|
+
|
62
|
+
sorted = {}
|
63
|
+
sorter = lambda { |data, ret_val|
|
64
|
+
data.keys.sort.each do |k|
|
65
|
+
ret_val[k.to_sym] = if data[k].instance_of? Hash
|
66
|
+
sorter.call(data[k], {})
|
67
|
+
else
|
68
|
+
data[k]
|
69
|
+
end
|
48
70
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
71
|
+
|
72
|
+
return ret_val
|
73
|
+
}
|
74
|
+
|
75
|
+
if obj.instance_of? Array
|
76
|
+
temp = {}
|
77
|
+
sorted = []
|
78
|
+
|
79
|
+
(obj.sort_by { |h| h.keys.first }).each_with_index do |h, idx|
|
80
|
+
temp[idx] = sorter.call(h, {})
|
52
81
|
end
|
82
|
+
|
83
|
+
temp.keys.each { |k| sorted << temp[k] }
|
84
|
+
else
|
85
|
+
sorted = sorter.call(obj, {})
|
86
|
+
end
|
87
|
+
|
88
|
+
sorted
|
89
|
+
end
|
90
|
+
|
91
|
+
##
|
92
|
+
# Like +TidyJson::tidy+, but callable by the sender object.
|
93
|
+
#
|
94
|
+
# @param opts [Hash] Output format options.
|
95
|
+
# @option (see Formatter#initialize)
|
96
|
+
# @return [String] A pretty-printed JSON string.
|
97
|
+
def to_tidy_json(opts = {})
|
98
|
+
if instance_variables.empty?
|
99
|
+
TidyJson.tidy(self, opts)
|
100
|
+
else
|
101
|
+
TidyJson.tidy(JSON.parse(stringify), opts)
|
53
102
|
end
|
54
103
|
end
|
55
104
|
|
@@ -70,20 +119,29 @@ module TidyJson
|
|
70
119
|
end
|
71
120
|
|
72
121
|
##
|
73
|
-
# Writes a
|
122
|
+
# Writes a JSON representation of the sender object to the file specified by
|
123
|
+
# +out+.
|
74
124
|
#
|
75
|
-
# @param
|
76
|
-
# @param
|
125
|
+
# @param out [String] The destination filename.
|
126
|
+
# @param opts [Hash] Output format options.
|
127
|
+
# @option (see Formatter#initialize)
|
128
|
+
# @option opts [Boolean] :tidy (false) Whether or not the output should be
|
129
|
+
# pretty-printed.
|
77
130
|
# @return [String, nil] The path to the written output file, if successful.
|
78
|
-
def write_json(
|
131
|
+
def write_json(out = "#{self.class.name}_#{Time.now.to_i}",
|
132
|
+
opts = { tidy: false })
|
79
133
|
path = nil
|
80
134
|
|
81
135
|
File.open("#{out}.json", 'w') do |f|
|
82
|
-
path =
|
136
|
+
path =
|
137
|
+
f << if opts[:tidy] then to_tidy_json(opts)
|
138
|
+
elsif instance_variables.empty? then to_json
|
139
|
+
else stringify
|
140
|
+
end
|
83
141
|
end
|
84
142
|
|
85
|
-
path
|
86
|
-
rescue IOError, RuntimeError, NoMethodError => e
|
143
|
+
path&.path
|
144
|
+
rescue Errno::ENOENT, Errno::EACCES, IOError, RuntimeError, NoMethodError => e
|
87
145
|
warn "#{__FILE__}.#{__LINE__}: #{e.message}"
|
88
146
|
end
|
89
147
|
|
@@ -93,17 +151,8 @@ module TidyJson
|
|
93
151
|
# @api private
|
94
152
|
class Serializer
|
95
153
|
##
|
96
|
-
#
|
97
|
-
#
|
98
|
-
@margins_to_backspace = 0
|
99
|
-
|
100
|
-
##
|
101
|
-
# True if printing a nested array
|
102
|
-
@should_backspace = false
|
103
|
-
|
104
|
-
##
|
105
|
-
# Searches +obj+ to a *maximum* depth of 2 for readable attributes,
|
106
|
-
# storing them as key-value pairs in +json_hash+.
|
154
|
+
# Searches +obj+ to a maximum depth of 2 for readable attributes, storing
|
155
|
+
# them as key-value pairs in +json_hash+.
|
107
156
|
#
|
108
157
|
# @param obj [Object] A Ruby object that can be parsed as JSON.
|
109
158
|
# @param json_hash [{String,Symbol => #to_s}] Accumulator.
|
@@ -127,7 +176,7 @@ module TidyJson
|
|
127
176
|
nested = nil
|
128
177
|
|
129
178
|
val.each.any? do |k, v|
|
130
|
-
|
179
|
+
unless v.instance_variables.empty?
|
131
180
|
nested_key = k
|
132
181
|
nested = v
|
133
182
|
end
|
@@ -138,7 +187,8 @@ module TidyJson
|
|
138
187
|
if nested
|
139
188
|
pos = val.keys.select { |k| k === nested_key }.first.to_sym
|
140
189
|
nested.instance_variables.each do
|
141
|
-
json_hash[key][pos] = serialize(nested,
|
190
|
+
json_hash[key][pos] = serialize(nested,
|
191
|
+
class: nested.class.name)
|
142
192
|
end
|
143
193
|
end
|
144
194
|
|
@@ -149,51 +199,48 @@ module TidyJson
|
|
149
199
|
val.each do |elem|
|
150
200
|
i = val.index(elem)
|
151
201
|
|
152
|
-
# multi-dimensional
|
153
|
-
if elem.
|
202
|
+
# member is a multi-dimensional collection
|
203
|
+
if elem.respond_to?(:each)
|
154
204
|
nested = []
|
155
205
|
elem.each do |e|
|
156
|
-
j = elem.
|
206
|
+
j = if elem.respond_to?(:key)
|
207
|
+
elem.key(e)
|
208
|
+
else elem.index(e)
|
209
|
+
end
|
157
210
|
|
158
|
-
# nested
|
159
|
-
if e.instance_variables.
|
211
|
+
# nested element is a class object
|
212
|
+
if !e.instance_variables.empty?
|
160
213
|
json_hash[key][j] = { class: e.class.name }
|
161
214
|
|
162
215
|
# recur over the contained object
|
163
216
|
serialize(e, json_hash[key][j])
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
217
|
+
|
218
|
+
# some kind of collection?
|
219
|
+
elsif e.respond_to?(:each)
|
220
|
+
temp = []
|
221
|
+
e.each do |el|
|
222
|
+
temp << if el.instance_variables.empty? then el
|
223
|
+
else JSON.parse(el.stringify)
|
224
|
+
end
|
171
225
|
end
|
226
|
+
|
227
|
+
nested << temp
|
228
|
+
|
229
|
+
# scalar type
|
230
|
+
else nested << e
|
172
231
|
end
|
173
232
|
end
|
174
233
|
# ~iteration of nested array elements
|
175
234
|
|
176
235
|
json_hash[key] << nested
|
177
236
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
# leverage 1:1 mapping of Hash:object
|
186
|
-
if elem.instance_of?(Hash) then json_hash[key] = val
|
187
|
-
else
|
188
|
-
# some kind of collection
|
189
|
-
if elem.respond_to? :each
|
190
|
-
temp = []
|
191
|
-
elem.each { |e| temp << e }
|
192
|
-
json_hash[key] << temp
|
193
|
-
else json_hash[key] << elem
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
237
|
+
# member is a flat array
|
238
|
+
elsif !elem.instance_variables.empty? # class object?
|
239
|
+
json_hash[key] << { class: elem.class.name }
|
240
|
+
serialize(elem, json_hash[key][i])
|
241
|
+
|
242
|
+
# scalar type
|
243
|
+
else json_hash[key] << elem
|
197
244
|
end
|
198
245
|
end
|
199
246
|
# ~iteration of top-level array elements
|
@@ -201,7 +248,7 @@ module TidyJson
|
|
201
248
|
# process any nested class members, i.e., handle a recursive call
|
202
249
|
# to Serializer.serialize
|
203
250
|
elsif obj.index(val) || json_hash.key?(key)
|
204
|
-
if val.instance_variables.
|
251
|
+
if !val.instance_variables.empty?
|
205
252
|
class_elem = { class: val.class.name }
|
206
253
|
json_hash[key] << class_elem
|
207
254
|
k = json_hash[key].index(class_elem)
|
@@ -210,23 +257,20 @@ module TidyJson
|
|
210
257
|
json_hash[key] << val
|
211
258
|
end
|
212
259
|
|
213
|
-
# process uncollected
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
json_hash[key] = val
|
228
|
-
end
|
229
|
-
end
|
260
|
+
# process uncollected class members
|
261
|
+
elsif !val.instance_variables.empty? # member is a class object
|
262
|
+
json_hash[key] = { class: val.class.name }
|
263
|
+
serialize(val, json_hash[key])
|
264
|
+
|
265
|
+
# member belongs to a contained object
|
266
|
+
elsif json_hash.key?(key) &&
|
267
|
+
!json_hash[key].has_val?(val) &&
|
268
|
+
json_hash[key].instance_of?(Hash)
|
269
|
+
|
270
|
+
json_hash[key][key] = val
|
271
|
+
|
272
|
+
# scalar member
|
273
|
+
else json_hash[key] = val
|
230
274
|
end
|
231
275
|
rescue NoMethodError
|
232
276
|
# we expected an array to behave like a hash, or vice-versa
|
@@ -238,143 +282,221 @@ module TidyJson
|
|
238
282
|
json_hash
|
239
283
|
end
|
240
284
|
# ~Serializer.serialize
|
285
|
+
end
|
286
|
+
# ~Serializer
|
287
|
+
|
288
|
+
##
|
289
|
+
# A purpose-built JSON formatter.
|
290
|
+
#
|
291
|
+
# @api private
|
292
|
+
class Formatter
|
293
|
+
attr_reader :indent, :sorted
|
294
|
+
# @!attribute indent
|
295
|
+
# @return [String] the string of white space used by this +Formatter+ to
|
296
|
+
# indent object members.
|
297
|
+
|
298
|
+
# @!attribute sorted
|
299
|
+
# @return [Boolean] whether or not this +Formatter+ will sort object
|
300
|
+
# members by key name.
|
301
|
+
|
302
|
+
##
|
303
|
+
# @param opts [Hash] Formatting options.
|
304
|
+
# @option opts [[2,4,6,8,10,12]] :indent (2) An even number of white spaces
|
305
|
+
# to indent each object member.
|
306
|
+
# @option opts [Boolean] :sort (false) Whether or not object members should
|
307
|
+
# be sorted by key.
|
308
|
+
def initialize(opts = {})
|
309
|
+
# The number of times to reduce the left indent of a nested array's
|
310
|
+
# opening bracket
|
311
|
+
@left_bracket_offset = 0
|
312
|
+
|
313
|
+
# True if printing a nested array
|
314
|
+
@need_offset = false
|
315
|
+
|
316
|
+
# don't test for the more explicit :integer? method because it's defined
|
317
|
+
# for floating point numbers also
|
318
|
+
valid_width = opts[:indent].positive? \
|
319
|
+
if opts[:indent].respond_to?(:times) &&
|
320
|
+
(2..12).step(2).include?(opts[:indent])
|
321
|
+
@indent = "\s" * (valid_width ? opts[:indent] : 2)
|
322
|
+
@sorted = opts[:sort] || false
|
323
|
+
end
|
324
|
+
# ~Formatter#initialize
|
241
325
|
|
242
326
|
##
|
243
327
|
# Returns the given +node+ as pretty-printed JSON.
|
244
328
|
#
|
245
329
|
# @param node [#to_s] A visible attribute of +obj+.
|
246
|
-
# @param obj [{Object =>
|
330
|
+
# @param obj [{Object => #to_s}, <#to_s>] The enumerable object
|
331
|
+
# containing +node+.
|
247
332
|
# @return [String] A formatted string representation of +node+.
|
248
|
-
def
|
333
|
+
def format_node(node, obj)
|
249
334
|
str = ''
|
335
|
+
indent = @indent
|
336
|
+
|
337
|
+
is_last = (obj.length <= 1) ||
|
338
|
+
(obj.length > 1 &&
|
339
|
+
(obj.instance_of?(Array) &&
|
340
|
+
!(node === obj.first) &&
|
341
|
+
(obj.size.pred == obj.rindex(node))))
|
250
342
|
|
251
343
|
if node.instance_of?(Array)
|
252
|
-
str <<
|
344
|
+
str << '['
|
345
|
+
str << "\n" unless node.empty?
|
253
346
|
|
347
|
+
# format array elements
|
254
348
|
node.each do |elem|
|
255
349
|
if elem.instance_of?(Hash)
|
256
|
-
str << "
|
350
|
+
str << "#{indent * 2}{"
|
351
|
+
str << "\n" unless elem.empty?
|
257
352
|
|
258
353
|
elem.each_with_index do |inner_h, h_idx|
|
259
|
-
str << "\
|
260
|
-
str << node_to_str(inner_h.last)
|
261
|
-
str << ', ' unless h_idx ==
|
354
|
+
str << "#{indent * 3}\"#{inner_h.first}\": "
|
355
|
+
str << node_to_str(inner_h.last, 4)
|
356
|
+
str << ', ' unless h_idx == elem.to_a.length.pred
|
262
357
|
str << "\n"
|
263
358
|
end
|
264
359
|
|
265
|
-
str <<
|
266
|
-
str << '
|
267
|
-
str <<
|
360
|
+
str << (indent * 2).to_s unless elem.empty?
|
361
|
+
str << '}'
|
362
|
+
str << ',' unless node.index(elem) == node.length.pred
|
363
|
+
str << "\n" unless node.index(elem) == node.length.pred
|
268
364
|
|
365
|
+
# element a scalar, or a nested array
|
269
366
|
else
|
270
|
-
|
271
|
-
|
272
|
-
|
367
|
+
is_nested_array = elem.instance_of?(Array) &&
|
368
|
+
elem.any? { |e| e.instance_of?(Array) }
|
369
|
+
if is_nested_array
|
370
|
+
@left_bracket_offset = \
|
371
|
+
elem.take_while { |e| e.instance_of?(Array) }.size
|
273
372
|
end
|
274
373
|
|
275
|
-
str <<
|
276
|
-
str <<
|
277
|
-
str << ",\n" unless node.index(elem) == (node.length - 1)
|
374
|
+
str << (indent * 2) << node_to_str(elem)
|
375
|
+
str << ",\n" unless node.index(elem) == node.length.pred
|
278
376
|
end
|
279
377
|
end
|
280
378
|
|
281
|
-
str << "\n
|
379
|
+
str << "\n#{indent}" unless node.empty?
|
380
|
+
str << "]\n"
|
282
381
|
|
283
382
|
elsif node.instance_of?(Hash)
|
284
|
-
str <<
|
383
|
+
str << '{'
|
384
|
+
str << "\n" unless node.empty?
|
285
385
|
|
386
|
+
# format elements as key-value pairs
|
286
387
|
node.each_with_index do |h, idx|
|
388
|
+
# format values which are hashes themselves
|
287
389
|
if h.last.instance_of?(Hash)
|
288
390
|
key = if h.first.eql? ''
|
289
|
-
"\
|
391
|
+
"#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
|
290
392
|
else
|
291
|
-
"\
|
393
|
+
"#{indent * 2}\"#{h.first}\": "
|
292
394
|
end
|
293
|
-
|
294
|
-
str <<
|
395
|
+
|
396
|
+
str << key << '{'
|
397
|
+
str << "\n" unless h.last.empty?
|
295
398
|
|
296
399
|
h.last.each_with_index do |inner_h, inner_h_idx|
|
297
|
-
str << "\
|
400
|
+
str << "#{indent * 3}\"#{inner_h.first}\": "
|
298
401
|
str << node_to_str(inner_h.last, 4)
|
299
|
-
str << ",\n" unless inner_h_idx ==
|
402
|
+
str << ",\n" unless inner_h_idx == h.last.to_a.length.pred
|
300
403
|
end
|
301
404
|
|
302
|
-
str << "\n
|
405
|
+
str << "\n#{indent * 2}" unless h.last.empty?
|
406
|
+
str << '}'
|
407
|
+
|
408
|
+
# format scalar values
|
303
409
|
else
|
304
|
-
str << "\
|
305
|
-
str << node_to_str(h.last)
|
410
|
+
str << "#{indent * 2}\"#{h.first}\": " << node_to_str(h.last)
|
306
411
|
end
|
307
412
|
|
308
|
-
str << ",\n" unless idx ==
|
413
|
+
str << ",\n" unless idx == node.to_a.length.pred
|
309
414
|
end
|
310
415
|
|
311
|
-
str << "\n
|
312
|
-
str << '
|
313
|
-
|
314
|
-
(obj.instance_of?(Hash) && \
|
315
|
-
(obj.key(obj.values.last) === obj.key(node))) || \
|
316
|
-
(obj.instance_of?(Array) && (obj.last == node)))
|
416
|
+
str << "\n#{indent}" unless node.empty?
|
417
|
+
str << '}'
|
418
|
+
str << ', ' unless is_last
|
317
419
|
str << "\n"
|
318
420
|
|
421
|
+
# scalars
|
319
422
|
else
|
320
423
|
str << node_to_str(node)
|
321
|
-
str << ', ' unless
|
322
|
-
((obj.length > 1) && \
|
323
|
-
(obj.instance_of?(Hash) && \
|
324
|
-
(obj.key(obj.values.last) === obj.key(node))) || \
|
325
|
-
(obj.instance_of?(Array) && (obj.last === node)))
|
424
|
+
str << ', ' unless is_last
|
326
425
|
str << "\n"
|
327
426
|
end
|
328
427
|
|
329
|
-
str.gsub(
|
428
|
+
trim str.gsub(/(#{indent})+[\n\r]+/, '')
|
429
|
+
.gsub(/\}\,+/, '},')
|
430
|
+
.gsub(/\]\,+/, '],')
|
330
431
|
end
|
331
|
-
# ~
|
432
|
+
# ~Formatter#format_node
|
332
433
|
|
333
434
|
##
|
334
435
|
# Returns a JSON-appropriate string representation of +node+.
|
335
436
|
#
|
336
437
|
# @param node [#to_s] A visible attribute of a Ruby object.
|
337
|
-
# @param tabs [
|
438
|
+
# @param tabs [Integer] Tab width at which to start printing this node.
|
338
439
|
# @return [String] A formatted string representation of +node+.
|
339
|
-
def
|
440
|
+
def node_to_str(node, tabs = 0)
|
340
441
|
graft = ''
|
341
|
-
|
342
442
|
tabs += 2 if tabs.zero?
|
343
|
-
|
443
|
+
|
444
|
+
if @need_offset
|
344
445
|
tabs -= 1
|
345
|
-
@
|
446
|
+
@left_bracket_offset -= 1
|
346
447
|
end
|
347
448
|
|
449
|
+
indent = @indent * (tabs / 2)
|
450
|
+
|
348
451
|
if node.nil? then graft << 'null'
|
452
|
+
|
349
453
|
elsif node.instance_of?(Hash)
|
350
454
|
|
351
455
|
format_node(node, node).scan(/.*$/) do |n|
|
352
|
-
graft << "\n" <<
|
456
|
+
graft << "\n" << indent << n
|
353
457
|
end
|
354
458
|
|
355
459
|
elsif node.instance_of?(Array)
|
356
|
-
@
|
460
|
+
@need_offset = @left_bracket_offset.positive?
|
357
461
|
|
358
462
|
format_node(node, {}).scan(/.*$/) do |n|
|
359
|
-
graft << "\n" <<
|
463
|
+
graft << "\n" << indent << n
|
360
464
|
end
|
361
465
|
|
362
466
|
elsif !node.instance_of?(String) then graft << node.to_s
|
467
|
+
|
363
468
|
else graft << "\"#{node.gsub(/\"/, '\\"')}\""
|
364
469
|
end
|
365
470
|
|
366
|
-
graft.
|
471
|
+
graft.strip
|
367
472
|
end
|
368
|
-
# ~
|
473
|
+
# ~Formatter#node_to_str
|
474
|
+
|
475
|
+
##
|
476
|
+
# Removes any trailing comma from serialized object members.
|
477
|
+
#
|
478
|
+
# @param node [String] A serialized object member.
|
479
|
+
# @return [String] A copy of +node+ without a trailing comma.
|
480
|
+
def trim(node)
|
481
|
+
if (extra_comma = /(?<trail>,\s*[\]\}])$/.match(node))
|
482
|
+
node.sub(extra_comma[:trail],
|
483
|
+
extra_comma[:trail]
|
484
|
+
.slice(1, node.length.pred)
|
485
|
+
.sub(/^\s/, ''))
|
486
|
+
else node
|
487
|
+
end
|
488
|
+
end
|
489
|
+
# ~Formatter#trim
|
369
490
|
end
|
370
|
-
# ~
|
491
|
+
# ~Formatter
|
371
492
|
|
372
493
|
private_constant :Serializer
|
494
|
+
private_constant :Formatter
|
373
495
|
end
|
374
496
|
# ~TidyJson
|
375
497
|
|
376
498
|
##
|
377
|
-
#
|
499
|
+
# Includes +TidyJson+ in every Ruby class.
|
378
500
|
# ====
|
379
501
|
# class Object
|
380
502
|
# include TidyJson
|