tidy_json 0.2.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.