tidy_json 0.2.2 → 0.5.0

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.
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TidyJson
4
+ ##
5
+ # A purpose-built JSON generator.
6
+ #
7
+ # @api private
8
+ class Serializer
9
+ ##
10
+ # Searches +obj+ to a maximum depth of 2 for readable attributes, storing
11
+ # them as key-value pairs in +json_hash+.
12
+ #
13
+ # @param obj [Object] A Ruby object that can be parsed as JSON.
14
+ # @param json_hash [{String,Symbol => #to_s}] Accumulator.
15
+ # @return [{String => #to_s}] A hash mapping of +obj+'s visible attributes.
16
+ def self.serialize(obj, json_hash)
17
+ obj.instance_variables.each do |m|
18
+ key = m.to_s[/[^\@]\w*/].to_sym
19
+
20
+ next unless key && !key.eql?('')
21
+
22
+ begin
23
+ val = obj.send(key) # assuming readable attributes . . .
24
+ rescue NoMethodError # . . . which may not be always be the case !
25
+ json_hash[key] = nil
26
+ end
27
+
28
+ begin
29
+ # process class members of Hash type
30
+ if val.instance_of?(Hash)
31
+ nested_key = ''
32
+ nested = nil
33
+
34
+ val.each.any? do |k, v|
35
+ unless v.instance_variables.empty?
36
+ nested_key = k
37
+ nested = v
38
+ end
39
+ end
40
+
41
+ json_hash[key] = val
42
+
43
+ if nested
44
+ pos = val.keys.select { |k| k === nested_key }.first.to_sym
45
+ nested.instance_variables.each do
46
+ json_hash[key][pos] = serialize(nested,
47
+ class: nested.class.name)
48
+ end
49
+ end
50
+
51
+ # process class members of Array type
52
+ elsif val.instance_of?(Array)
53
+ json_hash[key] = []
54
+
55
+ val.each do |elem|
56
+ i = val.index(elem)
57
+
58
+ # member is a multi-dimensional collection
59
+ if elem.respond_to?(:each)
60
+ nested = []
61
+ elem.each do |e|
62
+ j = if elem.respond_to?(:key)
63
+ elem.key(e)
64
+ else elem.index(e)
65
+ end
66
+
67
+ # nested element is a class object
68
+ if !e.instance_variables.empty?
69
+ json_hash[key][j] = { class: e.class.name }
70
+
71
+ # recur over the contained object
72
+ serialize(e, json_hash[key][j])
73
+
74
+ # some kind of collection?
75
+ elsif e.respond_to?(:each)
76
+ temp = []
77
+ e.each do |el|
78
+ temp << if el.instance_variables.empty? then el
79
+ else JSON.parse(el.stringify)
80
+ end
81
+ end
82
+
83
+ nested << temp
84
+
85
+ # scalar type
86
+ else nested << e
87
+ end
88
+ end
89
+ # ~iteration of nested array elements
90
+
91
+ json_hash[key] << nested
92
+
93
+ # member is a flat array
94
+ elsif !elem.instance_variables.empty? # class object?
95
+ json_hash[key] << { class: elem.class.name }
96
+ serialize(elem, json_hash[key][i])
97
+
98
+ # scalar type
99
+ else json_hash[key] << elem
100
+ end
101
+ end
102
+ # ~iteration of top-level array elements
103
+
104
+ end
105
+ rescue NoMethodError
106
+ # we expected an array to behave like a hash, or vice-versa
107
+ json_hash.store(key, val) # a shallow copy is better than nothing
108
+ end
109
+ end
110
+ # ~iteration of instance variables
111
+
112
+ json_hash
113
+ end
114
+ # ~Serializer.serialize
115
+ end
116
+
117
+ private_constant :Serializer
118
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TidyJson
4
- VERSION = '0.2.2'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/tidy_json.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  require 'json'
4
+ require_relative 'tidy_json/serializer'
5
+ require_relative 'tidy_json/formatter'
4
6
  require_relative 'tidy_json/version'
5
7
 
6
8
  ##
@@ -16,36 +18,32 @@ module TidyJson
16
18
  # @return [String] A pretty-printed JSON string.
17
19
  def self.tidy(obj = {}, opts = {})
18
20
  formatter = Formatter.new(opts)
19
- obj = sort_keys(obj) if formatter.sorted
20
- str = ''
21
+ json = ''
21
22
 
22
- if obj.instance_of?(Hash)
23
- str << "{\n"
24
-
25
- obj.each do |k, v|
26
- str << formatter.indent << "\"#{k}\": "
27
- str << formatter.format_node(v, obj)
28
- end
29
-
30
- str << "}\n"
23
+ begin
24
+ if obj.instance_variables.empty?
25
+ obj = sort_keys(obj) if formatter.format[:sorted]
26
+ json = JSON.generate(obj, formatter.format)
27
+ else
28
+ str = "{\n"
29
+ obj = JSON.parse(obj.stringify)
30
+ obj = sort_keys(obj) if formatter.format[:sorted]
31
31
 
32
- elsif obj.instance_of?(Array)
33
- str << "[\n"
32
+ obj.each do |k, v|
33
+ str << formatter.format[:indent] << "\"#{k}\": "
34
+ str << formatter.format_node(v, obj)
35
+ end
34
36
 
35
- obj.each do |v|
36
- str << formatter.indent
37
- str << formatter.format_node(v, obj)
37
+ str << "}\n"
38
+ json = JSON.generate(JSON.parse(formatter.trim(str)), formatter.format)
38
39
  end
39
40
 
40
- str << "]\n"
41
- end
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))
41
+ json.gsub(/[\n\r]{2,}/, "\n")
42
+ .gsub(/\[\s+\]/, '[]')
43
+ .gsub(/{\s+}/, '{}') << "\n"
44
+ rescue JSON::JSONError => e
45
+ warn "#{__FILE__}.#{__LINE__}: #{e.message}"
46
46
  end
47
-
48
- str
49
47
  end
50
48
 
51
49
  ##
@@ -85,7 +83,7 @@ module TidyJson
85
83
  temp[idx] = sorter.call(h, {})
86
84
  end
87
85
 
88
- temp.keys.each { |k| sorted << temp[k] }
86
+ temp.each_key { |k| sorted << temp[k] }
89
87
  else
90
88
  sorted = sorter.call(obj, {})
91
89
  end
@@ -100,11 +98,7 @@ module TidyJson
100
98
  # @option (see Formatter#initialize)
101
99
  # @return [String] A pretty-printed JSON string.
102
100
  def to_tidy_json(opts = {})
103
- if !instance_variables.empty?
104
- TidyJson.tidy(JSON.parse(stringify), opts)
105
- else
106
- TidyJson.tidy(self, opts)
107
- end
101
+ TidyJson.tidy(self, opts)
108
102
  end
109
103
 
110
104
  ##
@@ -139,359 +133,17 @@ module TidyJson
139
133
 
140
134
  File.open("#{out}.json", 'w') do |f|
141
135
  path =
142
- f << if !instance_variables.empty?
143
- if opts[:tidy] then to_tidy_json(opts)
144
- else stringify
145
- end
146
- else
147
- if opts[:tidy] then to_tidy_json(opts)
148
- else to_json
149
- end
136
+ f << if opts[:tidy] then to_tidy_json(opts)
137
+ elsif instance_variables.empty? then to_json
138
+ else stringify
150
139
  end
151
140
  end
152
141
 
153
142
  path&.path
154
- rescue IOError, RuntimeError, NoMethodError => e
143
+ rescue Errno::ENOENT, Errno::EACCES, IOError, RuntimeError, NoMethodError => e
155
144
  warn "#{__FILE__}.#{__LINE__}: #{e.message}"
156
145
  end
157
-
158
- ##
159
- # A purpose-built JSON generator.
160
- #
161
- # @api private
162
- class Serializer
163
- ##
164
- # Searches +obj+ to a maximum depth of 2 for readable attributes, storing
165
- # them as key-value pairs in +json_hash+.
166
- #
167
- # @param obj [Object] A Ruby object that can be parsed as JSON.
168
- # @param json_hash [{String,Symbol => #to_s}] Accumulator.
169
- # @return [{String => #to_s}] A hash mapping of +obj+'s visible attributes.
170
- def self.serialize(obj, json_hash)
171
- obj.instance_variables.each do |m|
172
- key = m.to_s[/[^\@]\w*/].to_sym
173
-
174
- next unless key && !key.eql?('')
175
-
176
- begin
177
- val = obj.send(key) # assuming readable attributes . . .
178
- rescue NoMethodError # . . . which may not be always be the case !
179
- json_hash[key] = nil
180
- end
181
-
182
- begin
183
- # process class members of Hash type
184
- if val.instance_of?(Hash)
185
- nested_key = ''
186
- nested = nil
187
-
188
- val.each.any? do |k, v|
189
- unless v.instance_variables.empty?
190
- nested_key = k
191
- nested = v
192
- end
193
- end
194
-
195
- json_hash[key] = val
196
-
197
- if nested
198
- pos = val.keys.select { |k| k === nested_key }.first.to_sym
199
- nested.instance_variables.each do
200
- json_hash[key][pos] = serialize(nested,
201
- class: nested.class.name)
202
- end
203
- end
204
-
205
- # process class members of Array type
206
- elsif val.instance_of?(Array)
207
- json_hash[key] = []
208
-
209
- val.each do |elem|
210
- i = val.index(elem)
211
-
212
- # member is a multi-dimensional array
213
- if elem.instance_of?(Array)
214
- nested = []
215
- elem.each do |e|
216
- j = elem.index(e)
217
-
218
- # nested array element is a class object
219
- if !e.instance_variables.empty?
220
- json_hash[key][j] = { class: e.class.name }
221
-
222
- # recur over the contained object
223
- serialize(e, json_hash[key][j])
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
233
- end
234
- end
235
- # ~iteration of nested array elements
236
-
237
- json_hash[key] << nested
238
-
239
- # member is a flat array
240
- else
241
- # class object?
242
- if !elem.instance_variables.empty?
243
- json_hash[key] << { class: elem.class.name }
244
- serialize(elem, json_hash[key][i])
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
258
- end
259
- end
260
- end
261
- # ~iteration of top-level array elements
262
-
263
- # process any nested class members, i.e., handle a recursive call
264
- # to Serializer.serialize
265
- elsif obj.index(val) || json_hash.key?(key)
266
- if !val.instance_variables.empty?
267
- class_elem = { class: val.class.name }
268
- json_hash[key] << class_elem
269
- k = json_hash[key].index(class_elem)
270
- serialize(val, json_hash[key][k])
271
- else
272
- json_hash[key] << val
273
- end
274
-
275
- # process uncollected class members
276
- else
277
- # member is a class object
278
- if !val.instance_variables.empty?
279
- json_hash[key] = { class: val.class.name }
280
- serialize(val, json_hash[key])
281
-
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
291
- end
292
- end
293
- rescue NoMethodError
294
- # we expected an array to behave like a hash, or vice-versa
295
- json_hash.store(key, val) # a shallow copy is better than nothing
296
- end
297
- end
298
- # ~iteration of instance variables
299
-
300
- json_hash
301
- end
302
- # ~Serializer.serialize
303
- end
304
- # ~Serializer
305
-
306
- ##
307
- # A purpose-built JSON formatter.
308
- #
309
- # @api private
310
- class Formatter
311
- attr_reader :indent, :sorted
312
- # @!attribute indent
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.
319
-
320
- ##
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
329
- @left_bracket_offset = 0
330
-
331
- # True if printing a nested array
332
- @need_offset = false
333
-
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
341
- end
342
- # ~Formatter#initialize
343
-
344
- ##
345
- # Returns the given +node+ as pretty-printed JSON.
346
- #
347
- # @param node [#to_s] A visible attribute of +obj+.
348
- # @param obj [{Object => #to_s}, <#to_s>] The enumerable object
349
- # containing +node+.
350
- # @return [String] A formatted string representation of +node+.
351
- def format_node(node, obj)
352
- str = ''
353
- indent = @indent
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
-
365
- if node.instance_of?(Array)
366
- str << "[\n"
367
-
368
- # format array elements
369
- node.each do |elem|
370
- if elem.instance_of?(Hash)
371
- str << "#{(indent * 2)}{\n"
372
-
373
- elem.each_with_index do |inner_h, h_idx|
374
- str << "#{(indent * 3)}\"#{inner_h.first}\": "
375
- str << node_to_str(inner_h.last, 4)
376
- str << ', ' unless h_idx == elem.to_a.length.pred
377
- str << "\n"
378
- end
379
-
380
- str << "#{(indent * 2)}}"
381
- str << ',' unless node.index(elem) == node.length.pred
382
- str << "\n" unless node.index(elem) == node.length.pred
383
-
384
- # element a primitive, or a nested array
385
- else
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
391
- end
392
-
393
- str << (indent * 2) << node_to_str(elem)
394
- str << ",\n" unless node.index(elem) == node.length.pred
395
- end
396
- end
397
-
398
- str << "\n#{indent}]\n"
399
-
400
- elsif node.instance_of?(Hash)
401
- str << "{\n"
402
-
403
- # format elements as key-value pairs
404
- node.each_with_index do |h, idx|
405
- # format values which are hashes themselves
406
- if h.last.instance_of?(Hash)
407
- key = if h.first.eql? ''
408
- "#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
409
- else
410
- "#{indent * 2}\"#{h.first}\": "
411
- end
412
-
413
- str << key << "{\n"
414
-
415
- h.last.each_with_index do |inner_h, inner_h_idx|
416
- str << "#{indent * 3}\"#{inner_h.first}\": "
417
- str << node_to_str(inner_h.last, 4)
418
- str << ",\n" unless inner_h_idx == h.last.to_a.length.pred
419
- end
420
-
421
- str << "\n#{indent * 2}}"
422
-
423
- # format plain values
424
- else
425
- str << "#{indent * 2}\"#{h.first}\": " << node_to_str(h.last)
426
- end
427
-
428
- str << ",\n" unless idx == node.to_a.length.pred
429
- end
430
-
431
- str << "\n#{indent}}"
432
- str << ', ' unless is_last
433
- str << "\n"
434
-
435
- # format primitive types
436
- else
437
- str << node_to_str(node)
438
- str << ', ' unless is_last
439
- str << "\n"
440
- end
441
-
442
- str.gsub(/(#{indent})+[\n\r]+/, '')
443
- .gsub(/\}\,+/, '},')
444
- .gsub(/\]\,+/, '],')
445
- end
446
- # ~Formatter#format_node
447
-
448
- ##
449
- # Returns a JSON-appropriate string representation of +node+.
450
- #
451
- # @param node [#to_s] A visible attribute of a Ruby object.
452
- # @param tabs [Integer] Tab width at which to start printing this node.
453
- # @return [String] A formatted string representation of +node+.
454
- def node_to_str(node, tabs = 0)
455
- graft = ''
456
- tabs += 2 if tabs.zero?
457
-
458
- if @need_offset
459
- tabs -= 1
460
- @left_bracket_offset -= 1
461
- end
462
-
463
- indent = @indent * (tabs / 2)
464
-
465
- if node.nil? then graft << 'null'
466
-
467
- elsif node.instance_of?(Hash)
468
-
469
- format_node(node, node).scan(/.*$/) do |n|
470
- graft << "\n" << indent << n
471
- end
472
-
473
- elsif node.instance_of?(Array)
474
- @need_offset = @left_bracket_offset.positive?
475
-
476
- format_node(node, {}).scan(/.*$/) do |n|
477
- graft << "\n" << indent << n
478
- end
479
-
480
- elsif !node.instance_of?(String) then graft << node.to_s
481
-
482
- else graft << "\"#{node.gsub(/\"/, '\\"')}\""
483
- end
484
-
485
- graft.strip
486
- end
487
- # ~Formatter#node_to_str
488
- end
489
- # ~Formatter
490
-
491
- private_constant :Serializer
492
- private_constant :Formatter
493
146
  end
494
- # ~TidyJson
495
147
 
496
148
  ##
497
149
  # Includes +TidyJson+ in every Ruby class.