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 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