tidy_json 0.1.1 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c8377fc4c17ae11f718b6cadd5a0bbf5a806c1706f8184370e036dbeb714d73
4
- data.tar.gz: dd9dcac5f8b3294f1edbd7a05762ad89277e6b593b0610d79d501e3d68e62fea
3
+ metadata.gz: 595bfb60293652c3aec843420229bfe1c55c84a8387c7f60e5b4d5a62f47a9df
4
+ data.tar.gz: 5ac9762a852f6753ff1bb8c52a2eac9224a55455bd14c9cf817377138fe21233
5
5
  SHA512:
6
- metadata.gz: 62b421c64ac5ecbd6fcc54a9e43f57d1e04ae52b6b6ab21c5eb65d65b4a267e44a28dde2ee67cf80167c5d0857c242c246e3881d510f00493e44b835fb17b52a
7
- data.tar.gz: 0b7534efee7f35486f1bc8f60dab7031f762aa6fdc69a7eb83d603b2cfae25583aa702e4009ff8dba284a0e1b3136e0c4bafe39f43e235951d7bc93a767eca1a
6
+ metadata.gz: 4e1bbd8d6caf301d83b83793188a0d4d8620876ed7930a4d38103aea5e5e9b79106762bb1b4caf0b03828d8e4713a0e1703bebe17dd12551f6d094d389865df3
7
+ data.tar.gz: dfac6e1f21f516cd8311d3777497c0f2ab1563d4303b719c5faf2b3b7250d16ac4e4ffd429fe6abde5b67b78908dc1c3f6651854269253b747cb3b1ef10399b7
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019 Robert Di Pardo
3
+ Copyright (c) 2019-2020 Robert Di Pardo
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,24 +1,16 @@
1
- # TidyJson
1
+ # tidy_json
2
2
 
3
- [![Build Status](https://travis-ci.com/rdipardo/tidy_json.svg)](https://travis-ci.com/rdipardo/tidy_json)
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
- $ gem install -​-development tidy_json
10
+ $ gem install tidy_json
19
11
  ```
20
12
 
21
- Or, with `bundler`:
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
- JSON.parse [].stringify
36
- # => {"class"=>"Array"}
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
- complex_object = { :a => 1, :b => ['two', 3, '<abbr title="four">IV</abbr>'] }
39
- # => {:a=>1, :b=>["two", 3, "<abbr title=\"four\">IV</abbr>"]}
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 complex_object.to_tidy_json
41
+ puts my_jsonable.to_tidy_json(indent: 4, sort: true)
42
42
  # {
43
- # "a": 1,
44
- # "b":
45
- # [
46
- # "two",
47
- # 3,
48
- # "<abbr title=\"four\">IV</abbr>"
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
- - [bundler](https://rubygems.org/gems/bundler) ~> 2.1
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://opensource.org/licenses/MIT)
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
3
5
  require 'yard'
@@ -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 << Serializer.format_node(v, obj)
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 << Serializer.format_node(v, obj)
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
- # Like +TidyJson::tidy+, but callable by the sender object with the option *not* to pretty-print.
47
+ # Returns the given +obj+ with keys in ascending order to a maximum depth of
48
+ # 2.
41
49
  #
42
- # @param pretty [Boolean] Whether or not the returned string should be pretty-printed.
43
- # @return [String] A pretty-printed JSON string.
44
- def to_tidy_json(pretty = true)
45
- if !instance_variables.empty?
46
- if pretty then TidyJson.tidy(JSON.parse(stringify))
47
- else stringify
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
- else
50
- if pretty then TidyJson.tidy(self)
51
- else to_json
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 pretty-printed JSON representation of the sender object to the file specified by +out+.
122
+ # Writes a JSON representation of the sender object to the file specified by
123
+ # +out+.
74
124
  #
75
- # @param pretty [Boolean] Whether or not the output should be pretty-printed.
76
- # @param out [String] The destination filename. Defaults to <tt><obj_class_name>_<current_UNIX_time></tt>.json
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(pretty = true, out = "#{self.class.name}_#{Time.now.to_i}")
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 = f << to_tidy_json(pretty)
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.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
- # The number of times to reduce the left margin of a nested array's opening
97
- # bracket
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
- if v.instance_variables.first
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, class: nested.class.name)
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 array
153
- if elem.instance_of?(Array)
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.index(e)
206
+ j = if elem.respond_to?(:key)
207
+ elem.key(e)
208
+ else elem.index(e)
209
+ end
157
210
 
158
- # nested array element is a class object
159
- if e.instance_variables.first
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
- else
165
- # some kind of collection?
166
- if e.respond_to? :each
167
- temp = []
168
- e.each { |el| temp << el }
169
- nested << temp
170
- else nested << e
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
- else
179
- # 1-D array of class objects
180
- if elem.instance_variables.first
181
- json_hash[key] << { class: elem.class.name }
182
- serialize(elem, json_hash[key][i])
183
- else
184
- # element of primitive type (or Array, or Hash):
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.first
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 data members
214
- else
215
- # member a class object
216
- if val.instance_variables.first
217
- json_hash[key] = { class: val.class.name }
218
- serialize(val, json_hash[key])
219
- else
220
- # member a hash element
221
- if json_hash.key?(key) && \
222
- !json_hash[key].has_val?(val) && \
223
- json_hash[key].instance_of?(Hash)
224
-
225
- json_hash[key][key] = val
226
- else
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 => Object}, <Object>] The enumerable object containing +node+.
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 self.format_node(node, obj)
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 << "\n\t[\n"
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 << "\t\t{\n"
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 << "\t\t\t\"#{inner_h.first}\":"
260
- str << node_to_str(inner_h.last)
261
- str << ', ' unless h_idx == (elem.to_a.length - 1)
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 << "\t\t}"
266
- str << ',' unless node.index(elem) == (node.length - 1)
267
- str << "\n" unless node.index(elem) == (node.length - 1)
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
- if elem.instance_of?(Array) && elem.any? { |e| e.instance_of?(Array) }
272
- @margins_to_backspace = elem.take_while { |e| e.instance_of?(Array) }.size
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 << "\t\t"
276
- str << node_to_str(elem)
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\t]\n"
379
+ str << "\n#{indent}" unless node.empty?
380
+ str << "]\n"
282
381
 
283
382
  elsif node.instance_of?(Hash)
284
- str << "\n\t{\n"
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
- "\t\t\"<##{h.last.class.name.downcase}>\":"
391
+ "#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
290
392
  else
291
- "\t\t\"#{h.first}\":"
393
+ "#{indent * 2}\"#{h.first}\": "
292
394
  end
293
- str << key
294
- str << "\n\t\t\t{\n"
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 << "\t\t\t\t\"#{inner_h.first}\":"
400
+ str << "#{indent * 3}\"#{inner_h.first}\": "
298
401
  str << node_to_str(inner_h.last, 4)
299
- str << ",\n" unless inner_h_idx == (h.last.to_a.length - 1)
402
+ str << ",\n" unless inner_h_idx == h.last.to_a.length.pred
300
403
  end
301
404
 
302
- str << "\n\t\t\t}"
405
+ str << "\n#{indent * 2}" unless h.last.empty?
406
+ str << '}'
407
+
408
+ # format scalar values
303
409
  else
304
- str << "\t\t\"#{h.first}\": "
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 == (node.to_a.length - 1)
413
+ str << ",\n" unless idx == node.to_a.length.pred
309
414
  end
310
415
 
311
- str << "\n\t}"
312
- str << ', ' unless (obj.length <= 1) || \
313
- ((obj.length > 1) && \
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 (obj.length <= 1) || \
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(/\t+[\n\r]+/, '').gsub(/\}\,+/, '},').gsub(/\]\,+/, '],')
428
+ trim str.gsub(/(#{indent})+[\n\r]+/, '')
429
+ .gsub(/\}\,+/, '},')
430
+ .gsub(/\]\,+/, '],')
330
431
  end
331
- # ~Serializer.format_node
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 [Fixnum] Tab width at which to start printing this node.
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 self.node_to_str(node, tabs = 0)
440
+ def node_to_str(node, tabs = 0)
340
441
  graft = ''
341
-
342
442
  tabs += 2 if tabs.zero?
343
- if @should_backspace
443
+
444
+ if @need_offset
344
445
  tabs -= 1
345
- @margins_to_backspace -= 1
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" << ("\t" * tabs).to_s << n
456
+ graft << "\n" << indent << n
353
457
  end
354
458
 
355
459
  elsif node.instance_of?(Array)
356
- @should_backspace = @margins_to_backspace.positive?
460
+ @need_offset = @left_bracket_offset.positive?
357
461
 
358
462
  format_node(node, {}).scan(/.*$/) do |n|
359
- graft << "\n" << ("\t" * tabs).to_s << 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.rstrip
471
+ graft.strip
367
472
  end
368
- # ~Serializer.node_to_str
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
- # ~Serializer
491
+ # ~Formatter
371
492
 
372
493
  private_constant :Serializer
494
+ private_constant :Formatter
373
495
  end
374
496
  # ~TidyJson
375
497
 
376
498
  ##
377
- # Exposes the +TidyJson+ mixin to all Ruby objects.
499
+ # Includes +TidyJson+ in every Ruby class.
378
500
  # ====
379
501
  # class Object
380
502
  # include TidyJson