tidy_json 0.2.1 → 0.2.2

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