tidy_json 0.2.1 → 0.2.2

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: 1ee82014d81eee49d6386266ea0b95ac1774291db69bdc50e32a062b7e39613e
4
- data.tar.gz: fa30e666d9762736b9f61e89ce94b0ab2384df7cc7b011dd21114399d88b56e1
3
+ metadata.gz: e53774339fb56a01ae47f0dd087159e9a1d2fa070dccda937d5a7de399e07ba9
4
+ data.tar.gz: 9f6736f1c5ef84b2d4e89b507b7fa417b3ab713b01226c55c86afc54d07699ab
5
5
  SHA512:
6
- metadata.gz: 0fe08f848f90052cef2c6cff40c56cd9abd7afefc9d8998091d64fa6a8f710df2682f4c7c17eac9bbdb9185b5418e7da7ecfb6e8a1f82a860b4faaa481bfb97c
7
- data.tar.gz: 3b6c7bbb72b216e9759f2c822ad35e206b41faafa21f3f9eb5bb79313008ba64562d99159d1f6e2e1485e9833e8a2f7916a8c8d1a307ab929b1f77f7f32a878d
6
+ metadata.gz: f35fe6a646a7e47c5926e04a10222cd483f45d177d3808d3de51040404b6d9ab4fdee940ceb077dba5e74b55ca171699331896485091e6c0c1a0f7c9f1ecb422
7
+ data.tar.gz: d599497adc2efb538341d2b4ad09b9b7b9f47ffd30ee056f9c47ceeda42953a6a23eb8266aa87c7ed220424a4a68ec323c864f10e7a78618bd1688dd4e4d627e
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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # TidyJson
1
+ # tidy_json
2
2
 
3
- [![Build Status][travis_build_status_badge]][travis_build_status] [![cci_build_status_badge]][cci_build_status] ![Gem Version][gem_version_badge]
3
+ [![Build Status][travis_build_status_badge]][travis_build_status] [![cci_build_status_badge]][cci_build_status] [![Gem Version][gem_version_badge]][gem_version]
4
4
 
5
5
  A mixin providing (recursive) JSON serialization and pretty printing.
6
6
 
@@ -27,47 +27,58 @@ require 'tidy_json'
27
27
  class Jsonable
28
28
  attr_reader :a, :b
29
29
  def initialize
30
- @a = { a: 'uno', b: 'dos', c: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', { 'ichi': "\u{4e00}", 'ni': "\u{4e8c}", 'san': "\u{4e09}", 'yon': "\u{56db}" }]] }
31
- @b = { a: 1, b: ['two', 3, '<abbr title="four">IV</abbr>'] }
30
+ @a = { a: 'uno', f: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', { 'ichi': "\u{4e00}", 'ni': "\u{4e8c}", 'san': "\u{4e09}", 'yon': "\u{56db}" }]], b: 'dos' }
31
+ @b = { z: { iv: 4, ii: 'duos', iii: 3, i: 'one' }, b: ['two', 3, '<abbr title="four">IV</abbr>'], a: 1, f: %w[x y z] }
32
32
  end
33
33
  end
34
34
 
35
35
  my_jsonable = Jsonable.new
36
36
 
37
37
  JSON.parse my_jsonable.stringify
38
- # => {"class"=>"Jsonable", "a"=>{"a"=>"uno", "b"=>"dos", "c"=>["I", "II", "III", ["i.", "ii.", "iii.", {"ichi"=>"一", "ni"=>"二", "san"=>"三", "yon"=>"四"}]]}, "b"=>{"a"=>1, "b"=>["two", 3, "<abbr title=\"four\">IV</abbr>"]}}
38
+ # => {"class"=>"Jsonable", "a"=>{"a"=>"uno", "f"=>["I", "II", "III", ["i.", "ii.", "iii.", {"ichi"=>"一", "ni"=>"二", "san"=>"三", "yon"=>"四"}]], "b"=>"dos"}, "b"=>{"z"=>{"iv"=>4, "ii"=>"duos", "iii"=>3, "i"=>"one"}, "b"=>["two", 3, "<abbr title=\"four\">IV</abbr>"], "a"=>1, "f"=>["x", "y", "z"]}}
39
39
 
40
- puts my_jsonable.to_tidy_json(indent: 4)
40
+ puts my_jsonable.to_tidy_json(indent: 4, sort: true)
41
41
  # {
42
- # "class": "Jsonable",
43
42
  # "a": {
44
- # "a": "uno",
45
- # "b": "dos",
46
- # "c": [
47
- # "I",
48
- # "II",
49
- # "III",
50
- # [
51
- # "i.",
52
- # "ii.",
53
- # "iii.",
54
- # {
55
- # "ichi": "一",
56
- # "ni": "二",
57
- # "san": "三",
58
- # "yon": "四"
59
- # }
60
- # ]
61
- # ]
62
- # },
63
- # "b": {
64
- # "a": 1,
65
- # "b": [
66
- # "two",
67
- # 3,
68
- # "<abbr title=\"four\">IV</abbr>"
69
- # ]
70
- # }
43
+ # "a": "uno",
44
+ # "b": "dos",
45
+ # "f": [
46
+ # "I",
47
+ # "II",
48
+ # "III",
49
+ # [
50
+ # "i.",
51
+ # "ii.",
52
+ # "iii.",
53
+ # {
54
+ # "ichi": "一",
55
+ # "ni": "二",
56
+ # "san": "三",
57
+ # "yon": "四"
58
+ # }
59
+ # ]
60
+ # ]
61
+ # },
62
+ # "b": {
63
+ # "a": 1,
64
+ # "b": [
65
+ # "two",
66
+ # 3,
67
+ # "<abbr title=\"four\">IV</abbr>"
68
+ # ],
69
+ # "f": [
70
+ # "x",
71
+ # "y",
72
+ # "z"
73
+ # ],
74
+ # "z": {
75
+ # "i": "one",
76
+ # "ii": "duos",
77
+ # "iii": 3,
78
+ # "iv": 4
79
+ # }
80
+ # },
81
+ # "class": "Jsonable"
71
82
  # }
72
83
  # => nil
73
84
  ```
@@ -78,15 +89,16 @@ puts my_jsonable.to_tidy_json(indent: 4)
78
89
  - [json](https://rubygems.org/gems/json) ~> 2.2
79
90
 
80
91
  #### Building
81
- - [minitest](https://rubygems.org/gems/minitest) ~> 5.0
92
+ - [test-unit](https://rubygems.org/gems/test-unit) ~> 3.3
82
93
  - [yard](https://rubygems.org/gems/yard) ~> 0.9
83
94
 
84
95
  ### License
85
- [MIT](https://opensource.org/licenses/MIT)
96
+ [MIT](https://github.com/rdipardo/tidy_json/blob/master/LICENSE)
86
97
 
87
98
 
88
99
  [travis_build_status]: https://travis-ci.com/rdipardo/tidy_json
89
- [cci_build_status]: https://circleci.com/gh/rdipardo/tidy_json
100
+ [cci_build_status]: https://circleci.com/gh/rdipardo/tidy_json/tree/master
90
101
  [cci_build_status_badge]: https://circleci.com/gh/rdipardo/tidy_json.svg?style=svg
91
- [travis_build_status_badge]: https://travis-ci.com/rdipardo/tidy_json.svg
92
- [gem_version_badge]: https://img.shields.io/gem/v/tidy_json
102
+ [travis_build_status_badge]: https://travis-ci.com/rdipardo/tidy_json.svg?branch=master
103
+ [gem_version]: https://badge.fury.io/rb/tidy_json
104
+ [gem_version_badge]: https://badge.fury.io/rb/tidy_json.svg
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,11 +11,12 @@ 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.
12
- # @param opts [Hash] Formatting options.
13
- # [:indent] the number of white spaces to indent
14
+ # @param opts [Hash] Output format options.
15
+ # @option (see Formatter#initialize)
14
16
  # @return [String] A pretty-printed JSON string.
15
17
  def self.tidy(obj = {}, opts = {})
16
18
  formatter = Formatter.new(opts)
19
+ obj = sort_keys(obj) if formatter.sorted
17
20
  str = ''
18
21
 
19
22
  if obj.instance_of?(Hash)
@@ -37,14 +40,64 @@ module TidyJson
37
40
  str << "]\n"
38
41
  end
39
42
 
43
+ if (extra_comma = /(?<trail>,\s*[\]\}])$/.match(str))
44
+ str = str.sub(extra_comma[:trail],
45
+ extra_comma[:trail].slice(1, str.length.pred))
46
+ end
47
+
40
48
  str
41
49
  end
42
50
 
51
+ ##
52
+ # Returns the given +obj+ with keys in ascending order to a maximum depth of
53
+ # 2.
54
+ #
55
+ # @param obj [Hash, Array<Hash>] A dictionary-like object or collection
56
+ # thereof.
57
+ # @return [Hash, Array<Hash>, Object] A copy of the given +obj+ with top- and
58
+ # second-level keys in ascending order, or else an identical copy of +obj+.
59
+ # @note +obj+ is returned unchanged if: 1) it's not iterable; 2) it's an
60
+ # empty collection; 3) any one of its elements is not hashable (and +obj+
61
+ # is an array).
62
+ def self.sort_keys(obj = {})
63
+ return obj if !obj.respond_to?(:each) || obj.empty? ||
64
+ (obj.instance_of?(Array) &&
65
+ !obj.all? { |e| e.respond_to? :keys })
66
+
67
+ sorted = {}
68
+ sorter = lambda { |data, ret_val|
69
+ data.keys.sort.each do |k|
70
+ ret_val[k.to_sym] = if data[k].instance_of? Hash
71
+ sorter.call(data[k], {})
72
+ else
73
+ data[k]
74
+ end
75
+ end
76
+
77
+ return ret_val
78
+ }
79
+
80
+ if obj.instance_of? Array
81
+ temp = {}
82
+ sorted = []
83
+
84
+ (obj.sort_by { |h| h.keys.first }).each_with_index do |h, idx|
85
+ temp[idx] = sorter.call(h, {})
86
+ end
87
+
88
+ temp.keys.each { |k| sorted << temp[k] }
89
+ else
90
+ sorted = sorter.call(obj, {})
91
+ end
92
+
93
+ sorted
94
+ end
95
+
43
96
  ##
44
97
  # Like +TidyJson::tidy+, but callable by the sender object.
45
98
  #
46
- # @param opts [Hash] Formatting options.
47
- # [:indent] the number of white spaces to indent
99
+ # @param opts [Hash] Output format options.
100
+ # @option (see Formatter#initialize)
48
101
  # @return [String] A pretty-printed JSON string.
49
102
  def to_tidy_json(opts = {})
50
103
  if !instance_variables.empty?
@@ -71,14 +124,17 @@ module TidyJson
71
124
  end
72
125
 
73
126
  ##
74
- # Writes a JSON representation of the sender object to the file specified by +out+.
127
+ # Writes a JSON representation of the sender object to the file specified by
128
+ # +out+.
75
129
  #
76
130
  # @param out [String] The destination filename.
77
- # @param opts [Hash] Formatting options for this object's +#to_tidy_json+ method, when called.
78
- # [:tidy] whether or not the output should be pretty-printed
79
- # [:indent] the number of white spaces to indent
131
+ # @param opts [Hash] Output format options.
132
+ # @option (see Formatter#initialize)
133
+ # @option opts [Boolean] :tidy (false) Whether or not the output should be
134
+ # pretty-printed.
80
135
  # @return [String, nil] The path to the written output file, if successful.
81
- def write_json(out = "#{self.class.name}_#{Time.now.to_i}", opts = { tidy: false })
136
+ def write_json(out = "#{self.class.name}_#{Time.now.to_i}",
137
+ opts = { tidy: false })
82
138
  path = nil
83
139
 
84
140
  File.open("#{out}.json", 'w') do |f|
@@ -91,10 +147,10 @@ module TidyJson
91
147
  if opts[:tidy] then to_tidy_json(opts)
92
148
  else to_json
93
149
  end
94
- end
150
+ end
95
151
  end
96
152
 
97
- path.path
153
+ path&.path
98
154
  rescue IOError, RuntimeError, NoMethodError => e
99
155
  warn "#{__FILE__}.#{__LINE__}: #{e.message}"
100
156
  end
@@ -105,8 +161,8 @@ module TidyJson
105
161
  # @api private
106
162
  class Serializer
107
163
  ##
108
- # Searches +obj+ to a *maximum* depth of 2 for readable attributes,
109
- # storing them as key-value pairs in +json_hash+.
164
+ # Searches +obj+ to a maximum depth of 2 for readable attributes, storing
165
+ # them as key-value pairs in +json_hash+.
110
166
  #
111
167
  # @param obj [Object] A Ruby object that can be parsed as JSON.
112
168
  # @param json_hash [{String,Symbol => #to_s}] Accumulator.
@@ -130,7 +186,7 @@ module TidyJson
130
186
  nested = nil
131
187
 
132
188
  val.each.any? do |k, v|
133
- if v.instance_variables.first
189
+ unless v.instance_variables.empty?
134
190
  nested_key = k
135
191
  nested = v
136
192
  end
@@ -141,7 +197,8 @@ module TidyJson
141
197
  if nested
142
198
  pos = val.keys.select { |k| k === nested_key }.first.to_sym
143
199
  nested.instance_variables.each do
144
- json_hash[key][pos] = serialize(nested, class: nested.class.name)
200
+ json_hash[key][pos] = serialize(nested,
201
+ class: nested.class.name)
145
202
  end
146
203
  end
147
204
 
@@ -152,50 +209,52 @@ module TidyJson
152
209
  val.each do |elem|
153
210
  i = val.index(elem)
154
211
 
155
- # multi-dimensional array
212
+ # member is a multi-dimensional array
156
213
  if elem.instance_of?(Array)
157
214
  nested = []
158
215
  elem.each do |e|
159
216
  j = elem.index(e)
160
217
 
161
218
  # nested array element is a class object
162
- if e.instance_variables.first
219
+ if !e.instance_variables.empty?
163
220
  json_hash[key][j] = { class: e.class.name }
164
221
 
165
222
  # recur over the contained object
166
223
  serialize(e, json_hash[key][j])
167
- else
168
- # some kind of collection?
169
- if e.respond_to? :each
170
- temp = []
171
- e.each { |el| temp << el }
172
- nested << temp
173
- else nested << e
174
- end
224
+
225
+ # some kind of collection?
226
+ elsif e.respond_to?(:each)
227
+ temp = []
228
+ e.each { |el| temp << el }
229
+ nested << temp
230
+
231
+ # primitive type
232
+ else nested << e
175
233
  end
176
234
  end
177
235
  # ~iteration of nested array elements
178
236
 
179
237
  json_hash[key] << nested
180
238
 
239
+ # member is a flat array
181
240
  else
182
- # 1-D array of class objects
183
- if elem.instance_variables.first
241
+ # class object?
242
+ if !elem.instance_variables.empty?
184
243
  json_hash[key] << { class: elem.class.name }
185
244
  serialize(elem, json_hash[key][i])
186
- else
187
- # element of primitive type (or Array, or Hash):
188
- # leverage 1:1 mapping of Hash:object
189
- if elem.instance_of?(Hash) then json_hash[key] = val
190
- else
191
- # some kind of collection
192
- if elem.respond_to? :each
193
- temp = []
194
- elem.each { |e| temp << e }
195
- json_hash[key] << temp
196
- else json_hash[key] << elem
197
- end
198
- end
245
+
246
+ # leverage 1:1 mapping of Hash:object
247
+ elsif elem.instance_of?(Hash)
248
+ json_hash[key] = val
249
+
250
+ # some kind of collection
251
+ elsif elem.respond_to?(:each)
252
+ temp = []
253
+ elem.each { |e| temp << e }
254
+ json_hash[key] << temp
255
+
256
+ # primitive type
257
+ else json_hash[key] << elem
199
258
  end
200
259
  end
201
260
  end
@@ -204,7 +263,7 @@ module TidyJson
204
263
  # process any nested class members, i.e., handle a recursive call
205
264
  # to Serializer.serialize
206
265
  elsif obj.index(val) || json_hash.key?(key)
207
- if val.instance_variables.first
266
+ if !val.instance_variables.empty?
208
267
  class_elem = { class: val.class.name }
209
268
  json_hash[key] << class_elem
210
269
  k = json_hash[key].index(class_elem)
@@ -215,20 +274,20 @@ module TidyJson
215
274
 
216
275
  # process uncollected class members
217
276
  else
218
- # member a class object
219
- if val.instance_variables.first
277
+ # member is a class object
278
+ if !val.instance_variables.empty?
220
279
  json_hash[key] = { class: val.class.name }
221
280
  serialize(val, json_hash[key])
222
- else
223
- # member a hash element
224
- if json_hash.key?(key) && \
225
- !json_hash[key].has_val?(val) && \
226
- json_hash[key].instance_of?(Hash)
227
281
 
228
- json_hash[key][key] = val
229
- else
230
- json_hash[key] = val
231
- end
282
+ # member belongs to a contained object
283
+ elsif json_hash.key?(key) &&
284
+ !json_hash[key].has_val?(val) &&
285
+ json_hash[key].instance_of?(Hash)
286
+
287
+ json_hash[key][key] = val
288
+
289
+ # primitive member
290
+ else json_hash[key] = val
232
291
  end
233
292
  end
234
293
  rescue NoMethodError
@@ -249,47 +308,64 @@ module TidyJson
249
308
  #
250
309
  # @api private
251
310
  class Formatter
252
- attr_reader :indent
253
-
311
+ attr_reader :indent, :sorted
254
312
  # @!attribute indent
255
- # @return [String] the string of white space used by this +Formatter+ to indent object members.
313
+ # @return [String] the string of white space used by this +Formatter+ to
314
+ # indent object members.
315
+
316
+ # @!attribute sorted
317
+ # @return [Boolean] whether or not this +Formatter+ will sort object
318
+ # members by key name.
256
319
 
257
320
  ##
258
- # Returns a new instance of +Formatter+.
259
- # @param format_options [Hash] Formatting options.
260
- # [:indent] the number of white spaces to indent. The default is 2.
261
- def initialize(format_options = {})
262
- ##
263
- # The number of times to reduce the left indent of a nested array's opening
264
- # bracket
321
+ # @param opts [Hash] Formatting options.
322
+ # @option opts [[2,4,6,8,10,12]] :indent (2) An even number of white spaces
323
+ # to indent each object member.
324
+ # @option opts [Boolean] :sort (false) Whether or not object members should
325
+ # be sorted by key.
326
+ def initialize(opts = {})
327
+ # The number of times to reduce the left indent of a nested array's
328
+ # opening bracket
265
329
  @left_bracket_offset = 0
266
330
 
267
- ##
268
331
  # True if printing a nested array
269
332
  @need_offset = false
270
333
 
271
- indent_width = format_options[:indent]
272
-
273
- # don't use the more explicit #integer? method because it's defined for
274
- # floating point numbers also
275
- good_width = indent_width.positive? if indent_width.respond_to? :times
276
-
277
- @indent = "\s" * (good_width ? indent_width : 2)
334
+ # don't test for the more explicit :integer? method because it's defined
335
+ # for floating point numbers also
336
+ valid_width = opts[:indent].positive? \
337
+ if opts[:indent].respond_to?(:times) &&
338
+ (2..12).step(2).include?(opts[:indent])
339
+ @indent = "\s" * (valid_width ? opts[:indent] : 2)
340
+ @sorted = opts[:sort] || false
278
341
  end
342
+ # ~Formatter#initialize
279
343
 
280
344
  ##
281
345
  # Returns the given +node+ as pretty-printed JSON.
282
346
  #
283
347
  # @param node [#to_s] A visible attribute of +obj+.
284
- # @param obj [{Object => Object}, <Object>] The enumerable object containing +node+.
348
+ # @param obj [{Object => #to_s}, <#to_s>] The enumerable object
349
+ # containing +node+.
285
350
  # @return [String] A formatted string representation of +node+.
286
351
  def format_node(node, obj)
287
352
  str = ''
288
353
  indent = @indent
289
354
 
355
+ # BUG: arrays containing repeated elements may produce a trailing comma
356
+ # since Array#index returns the first occurance; in this case the last
357
+ # element can't be detected by index; a temporary hack in TidyJson::tidy
358
+ # attempts to correct for this
359
+ is_last = (obj.length <= 1) ||
360
+ (obj.length > 1 &&
361
+ (obj.instance_of?(Hash) &&
362
+ (obj.key(obj.values.last) === obj.key(node))) ||
363
+ (obj.instance_of?(Array) && obj.size.pred == obj.index(node)))
364
+
290
365
  if node.instance_of?(Array)
291
366
  str << "[\n"
292
367
 
368
+ # format array elements
293
369
  node.each do |elem|
294
370
  if elem.instance_of?(Hash)
295
371
  str << "#{(indent * 2)}{\n"
@@ -297,23 +373,25 @@ module TidyJson
297
373
  elem.each_with_index do |inner_h, h_idx|
298
374
  str << "#{(indent * 3)}\"#{inner_h.first}\": "
299
375
  str << node_to_str(inner_h.last, 4)
300
- str << ', ' unless h_idx == (elem.to_a.length - 1)
376
+ str << ', ' unless h_idx == elem.to_a.length.pred
301
377
  str << "\n"
302
378
  end
303
379
 
304
380
  str << "#{(indent * 2)}}"
305
- str << ',' unless node.index(elem) == (node.length - 1)
306
- str << "\n" unless node.index(elem) == (node.length - 1)
381
+ str << ',' unless node.index(elem) == node.length.pred
382
+ str << "\n" unless node.index(elem) == node.length.pred
307
383
 
384
+ # element a primitive, or a nested array
308
385
  else
309
-
310
- if elem.instance_of?(Array) && elem.any? { |e| e.instance_of?(Array) }
311
- @left_bracket_offset = elem.take_while { |e| e.instance_of?(Array) }.size
386
+ is_nested_array = elem.instance_of?(Array) &&
387
+ elem.any? { |e| e.instance_of?(Array) }
388
+ if is_nested_array
389
+ @left_bracket_offset = \
390
+ elem.take_while { |e| e.instance_of?(Array) }.size
312
391
  end
313
392
 
314
- str << (indent * 2)
315
- str << node_to_str(elem)
316
- str << ",\n" unless node.index(elem) == (node.length - 1)
393
+ str << (indent * 2) << node_to_str(elem)
394
+ str << ",\n" unless node.index(elem) == node.length.pred
317
395
  end
318
396
  end
319
397
 
@@ -322,50 +400,48 @@ module TidyJson
322
400
  elsif node.instance_of?(Hash)
323
401
  str << "{\n"
324
402
 
403
+ # format elements as key-value pairs
325
404
  node.each_with_index do |h, idx|
405
+ # format values which are hashes themselves
326
406
  if h.last.instance_of?(Hash)
327
407
  key = if h.first.eql? ''
328
408
  "#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
329
409
  else
330
410
  "#{indent * 2}\"#{h.first}\": "
331
411
  end
332
- str << key
333
- str << "{\n"
412
+
413
+ str << key << "{\n"
334
414
 
335
415
  h.last.each_with_index do |inner_h, inner_h_idx|
336
416
  str << "#{indent * 3}\"#{inner_h.first}\": "
337
417
  str << node_to_str(inner_h.last, 4)
338
- str << ",\n" unless inner_h_idx == (h.last.to_a.length - 1)
418
+ str << ",\n" unless inner_h_idx == h.last.to_a.length.pred
339
419
  end
340
420
 
341
421
  str << "\n#{indent * 2}}"
422
+
423
+ # format plain values
342
424
  else
343
- str << "#{indent * 2}\"#{h.first}\": "
344
- str << node_to_str(h.last)
425
+ str << "#{indent * 2}\"#{h.first}\": " << node_to_str(h.last)
345
426
  end
346
427
 
347
- str << ",\n" unless idx == (node.to_a.length - 1)
428
+ str << ",\n" unless idx == node.to_a.length.pred
348
429
  end
349
430
 
350
431
  str << "\n#{indent}}"
351
- str << ', ' unless (obj.length <= 1) || \
352
- ((obj.length > 1) && \
353
- (obj.instance_of?(Hash) && \
354
- (obj.key(obj.values.last) === obj.key(node))) || \
355
- (obj.instance_of?(Array) && (obj.last == node)))
432
+ str << ', ' unless is_last
356
433
  str << "\n"
357
434
 
435
+ # format primitive types
358
436
  else
359
437
  str << node_to_str(node)
360
- str << ', ' unless (obj.length <= 1) || \
361
- ((obj.length > 1) && \
362
- (obj.instance_of?(Hash) && \
363
- (obj.key(obj.values.last) === obj.key(node))) || \
364
- (obj.instance_of?(Array) && (obj.last === node)))
438
+ str << ', ' unless is_last
365
439
  str << "\n"
366
440
  end
367
441
 
368
- str.gsub(/(#{indent})+[\n\r]+/, '').gsub(/\}\,+/, '},').gsub(/\]\,+/, '],')
442
+ str.gsub(/(#{indent})+[\n\r]+/, '')
443
+ .gsub(/\}\,+/, '},')
444
+ .gsub(/\]\,+/, '],')
369
445
  end
370
446
  # ~Formatter#format_node
371
447
 
@@ -389,6 +465,7 @@ module TidyJson
389
465
  if node.nil? then graft << 'null'
390
466
 
391
467
  elsif node.instance_of?(Hash)
468
+
392
469
  format_node(node, node).scan(/.*$/) do |n|
393
470
  graft << "\n" << indent << n
394
471
  end
@@ -407,7 +484,7 @@ module TidyJson
407
484
 
408
485
  graft.strip
409
486
  end
410
- # ~Formatter.node_to_str
487
+ # ~Formatter#node_to_str
411
488
  end
412
489
  # ~Formatter
413
490
 
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TidyJson # :nodoc:
4
- DEDICATION = "\n#{'.' * 50}\n#{'.' * 19} IN MEMORIAM #{'.' * 18}\n" \
5
- "#{'.' * 16} Michael Di Pardo #{'.' * 16}\n" \
6
- "#{'.' * 11} Feb 4, 1950 - Oct 28, 2019 #{'.' * 11}\n" \
7
- "#{'.' * 50}\n" \
8
- "#{'.' * 11} Please consider supporting #{'.' * 11}\n" \
9
- "#{'.' * 13} the MS Society of Canada #{'.' * 11}\n" \
10
- "#{'.' * 8} https://mssociety.ca/get-involved #{'.' * 7}\n" \
11
- "#{'.' * 50}\n\n"
4
+ DEDICATION = "\n#{'.' * 52}\n" \
5
+ "#{'.' * 14} This gem is dedicated " \
6
+ "#{'.' * 15}\n" \
7
+ "#{'.' * 17} to the memory of #{'.' * 17}\n#{'.' * 52}\n" \
8
+ "#{'.' * 17} MICHAEL DI PARDO #{'.' * 17}\n#{'.' * 52}\n" \
9
+ "#{'.' * 12} Please consider supporting #{'.' * 12}\n" \
10
+ "#{'.' * 13} the MS Society of Canada #{'.' * 13}\n" \
11
+ "#{'.' * 52}\n" \
12
+ "#{'.' * 8} https://mssociety.ca/get-involved #{'.' * 9}\n" \
13
+ "#{'.' * 52}\n\n"
12
14
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TidyJson
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
  end
@@ -1,4 +1,6 @@
1
- require 'minitest/autorun'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
2
4
  require 'tidy_json'
3
5
 
4
6
  ##
@@ -13,7 +15,7 @@ class JsonableObject
13
15
  end
14
16
  end
15
17
 
16
- class TidyJsonTest < Minitest::Test
18
+ class TidyJsonTest < Test::Unit::TestCase
17
19
  @@t = JsonableObject.new
18
20
  @@t2 = JsonableObject.new
19
21
  @@t3 = JsonableObject.new
@@ -27,8 +29,44 @@ class TidyJsonTest < Minitest::Test
27
29
  end
28
30
 
29
31
  def test_tidy_static
30
- assert_equal(TidyJson.tidy(a: 'one', A: 'ONE', b: nil), "{\n \"a\": \"one\", \n \"A\": \"ONE\", \n \"b\": null\n}\n")
31
- assert_equal(TidyJson.tidy({}).length, 4)
32
+ assert_equal("{\n \"a\": \"one\", \n \"A\": \"ONE\", \n \"b\": null\n}\n",
33
+ TidyJson.tidy(a: 'one', A: 'ONE', b: nil))
34
+ assert_equal(4, TidyJson.tidy({}).length)
35
+ end
36
+
37
+ def test_sort_keys_static
38
+ hash = { c: 3, d: { i: '34', ii: '35', f: 56, a: 9 }, a: 1, b: 2 }
39
+ hash_array = [{ c: 3, d: { i: '34', ii: '35', f: 56, a: 9 } }, { a: 1 }, { b: 2 }]
40
+ assert_equal({ a: 1, b: 2, c: 3, d: { a: 9, f: 56, i: '34', ii: '35' } },
41
+ TidyJson.sort_keys(hash))
42
+ assert_equal([{ a: 1 }, { b: 2 }, { c: 3, d: { a: 9, f: 56, i: '34', ii: '35' } }],
43
+ TidyJson.sort_keys(hash_array))
44
+ assert_equal({ a: 'one', b: 'two', c: 3 },
45
+ TidyJson.sort_keys('b': 'two', 'c': 3, 'a': 'one'))
46
+ assert_equal([], TidyJson.sort_keys([]), 'return empty arrays unchanged')
47
+ assert_equal({}, TidyJson.sort_keys({}), 'return empty hashes unchanged')
48
+ assert_equal([3, 2, 1], TidyJson.sort_keys([3, 2, 1]),
49
+ 'return arrays of keyless objects unchanged')
50
+ assert_equal([{ b: 'two' }, 'one'],
51
+ TidyJson.sort_keys([{ 'b': 'two' }, 'one']),
52
+ 'arrays with any keyless objects should be returned unchanged')
53
+ end
54
+
55
+ def test_sort_keys_instance
56
+ flat_hash_array = [{ c: 3 }, { a: 1 }, { b: 2 }]
57
+ nested_hash_array = [{ c: 3, d: { i: '34', ii: '35', f: 56, a: 9 } }, { a: 1 }, { b: 2 }]
58
+ assert_equal("[\n {\n \"a\": 1\n }, \n {\n \"b\": 2\n }, \n {\n \"c\": 3\n }\n]\n",
59
+ flat_hash_array.to_tidy_json(sort: true))
60
+ assert_equal("[\n {\n \"a\": 1\n }, \n {\n \"b\": 2\n }, \n {\n \"c\": 3,\n \"d\": {\n \"a\": 9,\n \"f\": 56,\n \"i\": \"34\",\n \"ii\": \"35\"\n }\n }\n]\n",
61
+ nested_hash_array.to_tidy_json(indent: 8, sort: true))
62
+ assert_equal("{\n \"a\": \"one\", \n \"b\": \"two\", \n \"c\": 3\n}\n",
63
+ { 'b': 'two', 'c': 3, 'a': 'one' }.to_tidy_json(indent: 6, sort: true))
64
+ assert_equal("[\n]\n", [].to_tidy_json(sort: true))
65
+ assert_equal("{\n}\n", {}.to_tidy_json(sort: true))
66
+ assert_equal("[\n 3, \n 2, \n 1\n]\n",
67
+ [3, 2, 1].to_tidy_json(indent: 8, sort: true))
68
+ assert_equal("[\n {\n \"b\": \"two\"\n }, \n \"one\"\n]\n",
69
+ [{ 'b': 'two' }, 'one'].to_tidy_json(indent: 4, sort: true))
32
70
  end
33
71
 
34
72
  def test_tidy_instance
@@ -43,26 +81,46 @@ class TidyJsonTest < Minitest::Test
43
81
  end
44
82
 
45
83
  def test_writers
46
- output = @@t.write_json
84
+ json_array = []
85
+ assert_nothing_thrown '#stringify returns valid JSON' do
86
+ 3.times { |_| json_array << JSON.parse(@@t.stringify) }
87
+ end
88
+
89
+ output = json_array.write_json
47
90
  assert(File.exist?(output))
48
- pretty_output = @@t.write_json('prettified', tidy: true, indent: 4)
91
+ assert_nothing_thrown 'Raw JSON should be valid' do
92
+ File.open(output, 'r') { |f| JSON.parse(f.read) }
93
+ end
94
+
95
+ pretty_output = \
96
+ json_array.write_json('prettified', tidy: true, sort: true, indent: 8)
49
97
  assert(File.exist?(pretty_output))
98
+ assert_nothing_thrown 'Formatted JSON should be valid' do
99
+ File.open(pretty_output, 'r') { |f| JSON.parse(f.read) }
100
+ end
50
101
  end
51
102
 
52
103
  def test_indent_bounds_checking
53
- assert_equal(Object.new.to_tidy_json(indent: '8'), '')
104
+ assert_equal("{\n \"a\": \"one\", \n \"b\": \"two\", \n \"c\": 3\n}\n",
105
+ { 'b': 'two', 'c': 3, 'a': 'one' }.to_tidy_json(indent: 5, sort: true),
106
+ 'odd values should fall back to default of 2')
107
+ assert_equal([].to_tidy_json(indent: '16'), "[\n]\n",
108
+ 'values > 12 should fall back to default of 2')
54
109
  assert_equal('Object'.to_tidy_json(indent: []), '')
55
110
  assert_equal(0.to_tidy_json(indent: -89), '')
56
111
  assert_equal(3.1425.to_tidy_json(indent: 3.1425), '')
57
112
  assert_equal(''.to_tidy_json(indent: +0), '')
58
113
  assert_equal([].to_tidy_json(indent: -8.00009), "[\n]\n")
59
- assert_equal(JSON.parse(Object.new.stringify).to_tidy_json(indent: nil),
60
- "{\n \"class\": \"Object\"\n}\n")
61
- assert_equal(JSON.parse(''.stringify).to_tidy_json(indent: -16.009),
62
- "{\n \"class\": \"String\"\n}\n")
63
- assert_equal(JSON.parse({}.stringify).to_tidy_json(indent: '8'),
64
- "{\n \"class\": \"Hash\"\n}\n")
65
- assert_equal(JSON.parse(%w[k l m].stringify).to_tidy_json(indent: '<<'),
66
- "{\n \"class\": \"Array\"\n}\n")
114
+ assert_nothing_thrown '#stringify should return valid JSON even when ' \
115
+ 'format options are invalid' do
116
+ assert_equal(JSON.parse(Object.new.stringify).to_tidy_json(indent: nil),
117
+ "{\n \"class\": \"Object\"\n}\n")
118
+ assert_equal(JSON.parse(''.stringify).to_tidy_json(indent: -16.009),
119
+ "{\n \"class\": \"String\"\n}\n")
120
+ assert_equal(JSON.parse({}.stringify).to_tidy_json(indent: '8'),
121
+ "{\n \"class\": \"Hash\"\n}\n")
122
+ assert_equal(JSON.parse(%w[k l m].stringify).to_tidy_json(indent: '<<'),
123
+ "{\n \"class\": \"Array\"\n}\n")
124
+ end
67
125
  end
68
126
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'lib/tidy_json/version'
2
4
  require_relative 'lib/tidy_json/dedication'
3
5
 
@@ -8,9 +10,9 @@ Gem::Specification.new do |spec|
8
10
  spec.summary = 'Serialize any Ruby object as readable JSON'
9
11
  spec.description = 'A mixin providing (recursive) JSON serialization and pretty printing.'
10
12
  spec.authors = ['Robert Di Pardo']
11
- spec.email = 'rdipardo0520@conestogac.on.ca'
13
+ spec.email = 'dipardo.r@gmail.com'
12
14
  spec.homepage = 'https://github.com/rdipardo/tidy_json'
13
- spec.metadata = { 'documentation_uri' => 'https://rubydoc.org/github/rdipardo/tidy_json' }
15
+ spec.metadata = { 'documentation_uri' => 'https://rubydoc.org/github/rdipardo/tidy_json/master' }
14
16
  spec.license = 'MIT'
15
17
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
16
18
  ['.yardopts'].concat(`git ls-files -z`.split("\x0").reject { |f| f.match(/^(\.[\w+\.]+|test|spec|features)/) })
@@ -19,7 +21,7 @@ Gem::Specification.new do |spec|
19
21
  spec.require_paths = ['lib']
20
22
  spec.required_ruby_version = Gem::Requirement.new('>= 2.3')
21
23
  spec.add_runtime_dependency 'json', '~> 2.2'
22
- spec.add_development_dependency 'minitest', '~> 5.0'
24
+ spec.add_development_dependency 'test-unit', '~> 3.3'
23
25
  spec.add_development_dependency 'yard', '~> 0.9'
24
26
  spec.rdoc_options = ['-x test/*']
25
27
  spec.post_install_message = TidyJson::DEDICATION
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tidy_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Di Pardo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-23 00:00:00.000000000 Z
11
+ date: 2020-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -25,19 +25,19 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: minitest
28
+ name: test-unit
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '5.0'
33
+ version: '3.3'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '5.0'
40
+ version: '3.3'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: yard
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.9'
55
55
  description: A mixin providing (recursive) JSON serialization and pretty printing.
56
- email: rdipardo0520@conestogac.on.ca
56
+ email: dipardo.r@gmail.com
57
57
  executables: []
58
58
  extensions: []
59
59
  extra_rdoc_files: []
@@ -72,18 +72,20 @@ homepage: https://github.com/rdipardo/tidy_json
72
72
  licenses:
73
73
  - MIT
74
74
  metadata:
75
- documentation_uri: https://rubydoc.org/github/rdipardo/tidy_json
75
+ documentation_uri: https://rubydoc.org/github/rdipardo/tidy_json/master
76
76
  post_install_message: |2+
77
77
 
78
- ..................................................
79
- ................... IN MEMORIAM ..................
80
- ................ Michael Di Pardo ................
81
- ........... Feb 4, 1950 - Oct 28, 2019 ...........
82
- ..................................................
83
- ........... Please consider supporting ...........
84
- ............. the MS Society of Canada ...........
85
- ........ https://mssociety.ca/get-involved .......
86
- ..................................................
78
+ ....................................................
79
+ .............. This gem is dedicated ...............
80
+ ................. to the memory of .................
81
+ ....................................................
82
+ ................. MICHAEL DI PARDO .................
83
+ ....................................................
84
+ ............ Please consider supporting ............
85
+ ............. the MS Society of Canada .............
86
+ ....................................................
87
+ ........ https://mssociety.ca/get-involved .........
88
+ ....................................................
87
89
 
88
90
  rdoc_options:
89
91
  - "-x test/*"
@@ -100,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
102
  - !ruby/object:Gem::Version
101
103
  version: '0'
102
104
  requirements: []
103
- rubygems_version: 3.0.6
105
+ rubygems_version: 3.0.8
104
106
  signing_key:
105
107
  specification_version: 4
106
108
  summary: Serialize any Ruby object as readable JSON