tidy_json 0.2.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfdad155c72b5ef6d411fbdd2dae876a4d7951e97f2f59f0817d09be0d1b1f8a
4
- data.tar.gz: 40fbd785790e3dc649c1ab8ac16696b359f832cc43da577e71fe9b55a5467212
3
+ metadata.gz: bf122c3cc2a34f2252b766f1a47f52bada4c4ff5b5334555ea62a572848d8531
4
+ data.tar.gz: bcd9134752408044711fc2b4ff3732afba7b65995b9c511c9df7e8fe666df4b5
5
5
  SHA512:
6
- metadata.gz: a62c5325d5ec264c61321518e26997170f5a1b34210d6c93052dfa2ab7aedce355ef0f9f9688dc4a18cd4113fafb121b314528374ba509b7e47f7a1f0f4afb7f
7
- data.tar.gz: 2351e73fd6c2c6e8da966b02cec5da2eeaff8ca77a090c5ac5e7aff96122e27f12ac8c770b1b89ce943e6300e21325b099442750137ca54183fee568b8db048b
6
+ metadata.gz: 0d9d164a90b41f3c5793ebaa04b361a5e1c15bf8ed7580a367421e08c06bcc7cd0f5f8555adc1e9adcff57585c59554472b129797343f78714f964c45c8a7ce6
7
+ data.tar.gz: 950f0eab8248273ce6b80a37918226d2637f436b99552844450bec62d6b3ab40dbe887c81c6fadc4baabd53dc1a2faaac834e6e68082b17d6fd1ac0d709d16ba
data/.yardopts CHANGED
@@ -1 +1,3 @@
1
- --private lib/*.rb lib/tidy_json/version.rb - README.md LICENSE
1
+ --exclude lib/tidy_json/dedication.rb
2
+ --private lib/**/*.rb
3
+ --files README.md,LICENSE
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  gemspec
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019-2020 Robert Di Pardo
3
+ Copyright (c) 2019-2021 Robert Di Pardo
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,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
+ ![Gem Version][gem_version_badge] ![Downloads][gem_downloads] [![Travis CI][travis_build_status_badge]][travis_build_status] [![Circle CI][cci_build_status_badge]][cci_build_status] [![codecov][codecov_badge]][codecov_status]
4
4
 
5
5
  A mixin providing (recursive) JSON serialization and pretty printing.
6
6
 
@@ -19,74 +19,132 @@ gem 'tidy_json'
19
19
  # ...
20
20
  ```
21
21
 
22
+ ### Formatting Options
23
+
24
+ As of version [0.3.0][], most of the same options accepted by [`JSON.generate`][]
25
+ can be passed to `#write_json`, `#to_tidy_json`, or `TidyJson.tidy`.
26
+
27
+ See [the docs][] for a current list of options and their default values.
28
+
22
29
  ### Example
23
30
 
24
31
  ```ruby
25
32
  require 'tidy_json'
26
33
 
27
- class JsonableObject
34
+ class Jsonable
28
35
  attr_reader :a, :b
29
36
  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>'] }
37
+ @a = { a: 'uno', f: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', { 'ichi': "\u{4e00}", 'ni': "\u{4e8c}", 'san': "\u{4e09}", 'yon': "\u{56db}" }]], c: {}, b: 'dos', e: [[]] }
38
+ @b = { z: { iv: 4, ii: 'dos', iii: 3, i: 'uno' }, b: ['deux', 3, '<abbr title="four">IV</abbr>'], a: 1, g: [{ none: [] }], f: %w[x y z] }
32
39
  end
33
40
  end
34
41
 
35
- my_jsonable = JsonableObject.new
42
+ my_jsonable = Jsonable.new
43
+ # => #<Jsonable:0x000055790c93e768 @a={:a=>"uno", :f=>["I", "II", "III", ["i.", "ii.", "iii.", {:ichi=>"一", :ni=>"二", :san=>"三", :yon=>"四"}]], :c=>{}, :b=>"dos", :e=>[[]]}, @b={:z=>{:iv=>4, :ii=>"dos", :iii=>3, :i=>"uno"}, :b=>["deux", 3, "<abbr title=\"four\">IV</abbr>"], :a=>1, :g=>[{:none=>[]}], :f=>["x", "y", "z"]}>
36
44
 
37
45
  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>"]}}
46
+ # => "{\"class\":\"Jsonable\",\"a\":{\"a\":\"uno\",\"f\":[\"I\",\"II\",\"III\",[\"i.\",\"ii.\",\"iii.\",{\"ichi\":\"一\",\"ni\":\"二\",\"san\":\"三\",\"yon\":\"四\"}]],\"c\":{},\"b\":\"dos\",\"e\":[[]]},\"b\":{\"z\":{\"iv\":4,\"ii\":\"dos\",\"iii\":3,\"i\":\"uno\"},\"b\":[\"deux\",3,\"<abbr title=\\\"four\\\">IV</abbr>\"],\"a\":1,\"g\":[{\"none\":[]}],\"f\":[\"x\",\"y\",\"z\"]}}"
39
47
 
40
- puts my_jsonable.to_tidy_json(indent: 8)
48
+ puts my_jsonable.to_tidy_json(indent: 4, sort: true, space_before: 2, ascii_only: true)
41
49
  # {
42
- # "class": "JsonableObject",
43
- # "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
- # }
50
+ # "a" : {
51
+ # "a" : "uno",
52
+ # "b" : "dos",
53
+ # "c" : {},
54
+ # "e" : [
55
+ # []
56
+ # ],
57
+ # "f" : [
58
+ # "I",
59
+ # "II",
60
+ # "III",
61
+ # [
62
+ # "i.",
63
+ # "ii.",
64
+ # "iii.",
65
+ # {
66
+ # "ichi" : "\u4e00",
67
+ # "ni" : "\u4e8c",
68
+ # "san" : "\u4e09",
69
+ # "yon" : "\u56db"
70
+ # }
71
+ # ]
72
+ # ]
73
+ # },
74
+ # "b" : {
75
+ # "a" : 1,
76
+ # "b" : [
77
+ # "deux",
78
+ # 3,
79
+ # "<abbr title=\"four\">IV</abbr>"
80
+ # ],
81
+ # "f" : [
82
+ # "x",
83
+ # "y",
84
+ # "z"
85
+ # ],
86
+ # "g" : [
87
+ # {
88
+ # "none" : []
89
+ # }
90
+ # ],
91
+ # "z" : {
92
+ # "i" : "uno",
93
+ # "ii" : "dos",
94
+ # "iii" : 3,
95
+ # "iv" : 4
96
+ # }
97
+ # },
98
+ # "class" : "Jsonable"
71
99
  # }
72
100
  # => nil
73
101
  ```
74
102
 
75
- ### Dependencies
103
+ ### Command Line Usage
104
+
105
+ After [installing the gem][], pass the name of a file containing JSON to `jtidy`
106
+ (with or without a file extension). Run `jtidy -h` for a complete list of
107
+ formatting options:
108
+
109
+ ```
110
+ jtidy FILE[.json] [-i [2,4,6,8,10,12]] [-p [1..8]] [-v [1..8]] [-o D] [-a D] [-m N] [-e] [-A] [-N] [-s] [-P]
111
+ -i, --indent [2,4,6,8,10,12] The number of spaces to indent each object member [2]
112
+ -p, --prop-name-space [1..8] The number of spaces to put after property names [0]
113
+ -v, --value-space [1..8] The number of spaces to put before property values [1]
114
+ -o, --object-delim D A string of whitespace to delimit object members [\n]
115
+ -a, --array-delim D A string of whitespace to delimit array elements [\n]
116
+ -m, --max-nesting N The maximum level of data structure nesting in the generated JSON; 0 == "no depth checking" [100]
117
+ -e, --escape Escape /'s [false]
118
+ -A, --ascii Generate ASCII characters only [false]
119
+ -N, --nan Allow NaN, Infinity and -Infinity [false]
120
+ -s, --sort Sort property names [false]
121
+ -P, --preview Show preview of output [false]
122
+ -V, --version Show version
123
+ -h, --help Show this help message
124
+ ```
76
125
 
77
- #### Runtime
78
- - [json](https://rubygems.org/gems/json) ~> 2.2
126
+ ### Notice
127
+ The `jtidy` executable bundled with this gem is in no way affiliated with, nor based on,
128
+ the HTML parser and pretty printer [of the same name](https://github.com/jtidy/jtidy).
79
129
 
80
- #### Building
81
- - [minitest](https://rubygems.org/gems/minitest) ~> 5.0
82
- - [yard](https://rubygems.org/gems/yard) ~> 0.9
130
+ The JTidy source code and binaries are licensed under the terms of the Zlib-Libpng License.
131
+ More information is available [here](https://github.com/jtidy/jtidy/blob/master/LICENSE.txt).
83
132
 
84
133
  ### License
85
- [MIT](https://opensource.org/licenses/MIT)
134
+ Distributed under the terms of the [MIT License][].
86
135
 
87
136
 
88
137
  [travis_build_status]: https://travis-ci.com/rdipardo/tidy_json
89
- [cci_build_status]: https://circleci.com/gh/rdipardo/tidy_json
138
+ [cci_build_status]: https://circleci.com/gh/rdipardo/tidy_json/tree/testing
90
139
  [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
140
+ [travis_build_status_badge]: https://travis-ci.com/rdipardo/tidy_json.svg?branch=master
141
+ [codecov_status]: https://codecov.io/gh/rdipardo/tidy_json/branch/testing
142
+ [codecov_badge]: https://codecov.io/gh/rdipardo/tidy_json/branch/testing/graph/badge.svg
143
+ [gem_version_badge]: https://img.shields.io/gem/v/tidy_json?color=%234ec820&label=gem%20version&logo=ruby&logoColor=%23e9573f
144
+ [gem_downloads]: https://img.shields.io/gem/dt/tidy_json?logo=ruby&logoColor=%23e9573f
145
+ [MIT License]: https://github.com/rdipardo/tidy_json/blob/master/LICENSE
146
+ [installing the gem]: https://github.com/rdipardo/tidy_json#installation
147
+ <!-- API spec -->
148
+ [`JSON.generate`]: https://github.com/flori/json/blob/d49c5de49e54a5ad3f6fcf587f98d63266ef9439/lib/json/pure/generator.rb#L111
149
+ [the docs]: https://rubydoc.org/github/rdipardo/tidy_json/TidyJson/Formatter#initialize-instance_method
150
+ [0.3.0]: https://github.com/rdipardo/tidy_json/releases/tag/v0.3.0
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'
data/bin/jtidy ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'tidy_json'
6
+ require_relative 'jtidy_info'
7
+
8
+ class Jtidy # :nodoc:
9
+ OPTIONS = {
10
+ '-i [2,4,6,8,10,12]': [:indent, Integer, '--indent [2,4,6,8,10,12]',
11
+ 'The number of spaces to indent each object member [2]'],
12
+ '-p [1..8]': [:space_before, Integer, '--prop-name-space [1..8]',
13
+ 'The number of spaces to put after property names [0]'],
14
+ '-v [1..8]': [:space, Integer, '--value-space [1..8]',
15
+ 'The number of spaces to put before property values [1]'],
16
+ '-o D': [:object_nl, String, '--object-delim D',
17
+ 'A string of whitespace to delimit object members [\n]'],
18
+ '-a D': [:array_nl, String, '--array-delim D',
19
+ 'A string of whitespace to delimit array elements [\n]'],
20
+ '-m N': [:max_nesting, Integer, '--max-nesting N',
21
+ 'The maximum level of data structure nesting in the generated ' \
22
+ 'JSON; 0 == "no depth checking" [100]'],
23
+ '-e': [:escape_slash, nil, '--escape', 'Escape /\'s [false]'],
24
+ '-A': [:ascii_only, nil, '--ascii', 'Generate ASCII characters only [false]'],
25
+ '-N': [:allow_nan, nil, '--nan', 'Allow NaN, Infinity and -Infinity [false]'],
26
+ '-s': [:sort, nil, '--sort', 'Sort property names [false]'],
27
+ # script-only options
28
+ '-P': [:preview, nil, '--preview', 'Show preview of output [false]']
29
+ }.freeze
30
+
31
+ def self.unescape(str)
32
+ str.gsub(/\\b|\\h|\\n|\\r|\\s|\\t|\\v/,
33
+ {
34
+ '\\b': "\b",
35
+ '\\h': "\h",
36
+ '\\n': "\n",
37
+ '\\r': "\r",
38
+ '\\s': "\s",
39
+ '\\t': "\t",
40
+ '\\v': "\v"
41
+ })
42
+ end
43
+
44
+ def self.show_unused(opts)
45
+ return if opts.empty?
46
+
47
+ ignored = opts.keys.map do |key|
48
+ (OPTIONS.keys.select do |k|
49
+ OPTIONS[k][0].eql? key
50
+ end.first || '')[0..1]
51
+ end
52
+ warn "Ignoring options: #{(ignored.join ', ')}"
53
+ end
54
+
55
+ def self.parse(options)
56
+ format_options = {}
57
+ OptionParser.new do |opts|
58
+ opts.banner = \
59
+ "#{File.basename __FILE__} FILE[.json] " \
60
+ "#{(OPTIONS.keys.map { |k| "[#{k}]" }).join ' '}"
61
+ OPTIONS.each_key do |k|
62
+ opt, type, long_name, desc = OPTIONS[k]
63
+ opts.on(k, long_name, type, desc) do |v|
64
+ format_options[opt] = (type == String ? unescape(v) : v)
65
+ end
66
+ end
67
+
68
+ opts.on_tail('-V', '--version', 'Show version') do
69
+ show_unused format_options
70
+ puts ::JtidyInfo.new.to_s
71
+ exit 0
72
+ end
73
+
74
+ opts.on_tail('-h', '--help', 'Show this help message') do
75
+ show_unused format_options
76
+ puts opts
77
+ exit 0
78
+ end
79
+ end.parse! options
80
+
81
+ format_options
82
+ end
83
+
84
+ private_class_method :unescape, :show_unused
85
+ end
86
+
87
+ begin
88
+ begin
89
+ OPTS = Jtidy.parse(ARGV).freeze
90
+ INPUT_FILE = ARGV[0].freeze
91
+ rescue OptionParser::InvalidOption => e
92
+ warn e.message.capitalize
93
+ raise OptionParser::InvalidArgument
94
+ end
95
+
96
+ if !INPUT_FILE.nil? && !INPUT_FILE.empty?
97
+ tidy = ''
98
+ fname = INPUT_FILE.gsub('\\', '/')
99
+ ext = File.extname(fname)
100
+ input = File.join(
101
+ File.expand_path(File.dirname(fname)), File.basename(fname, ext)
102
+ )
103
+
104
+ begin
105
+ File.open((input + '.json').to_s, 'r') do |json|
106
+ begin
107
+ tidy = TidyJson.tidy(JSON.parse(json.read.strip), OPTS)
108
+ rescue JSON::JSONError => e
109
+ warn "#{__FILE__}.#{__LINE__}: #{e.message}"
110
+ end
111
+ end
112
+
113
+ if tidy.length.positive?
114
+ output = (input + '-tidy.json').to_s
115
+ File.open(output, 'w') { |fd| fd.write(tidy) }
116
+ puts "\nWrote: #{output}"
117
+ puts "#{tidy[0..1024]}\n . . ." if OPTS[:preview]
118
+ end
119
+ rescue Errno::ENOENT, Errno::EACCES, IOError => e
120
+ warn "#{__FILE__}.#{__LINE__}: #{e.message}"
121
+ end
122
+
123
+ else
124
+ Jtidy.parse %w[--help]
125
+ end
126
+ rescue OptionParser::InvalidArgument, OptionParser::MissingArgument
127
+ Jtidy.parse %w[--help]
128
+ end
data/bin/jtidy_info.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TidyJson # :nodoc:
4
+ class JtidyInfo
5
+ NOTICE = [
6
+ '#',
7
+ '# jtidy is in no way affiliated with, nor based on, ',
8
+ '# the HTML parser and pretty printer of the same name.',
9
+ '#',
10
+ '# The JTidy source code and binaries are licensed under',
11
+ '# the terms of the Zlib-Libpng License.',
12
+ '#',
13
+ '# More information is available here:',
14
+ '# https://github.com/jtidy/jtidy/blob/master/LICENSE.txt',
15
+ '#'
16
+ ].join("\n").freeze
17
+
18
+ attr_reader :meta
19
+
20
+ def initialize
21
+ gem = Gem::Specification.find_by_name('tidy_json')
22
+ @meta = {
23
+ name: "jtidy #{gem.version}",
24
+ license: "License: #{gem.license}",
25
+ bugs: "Bugs: #{gem.metadata['bug_tracker_uri']}",
26
+ notice: NOTICE
27
+ }
28
+ end
29
+
30
+ def to_s
31
+ (@meta.values.join "\n").freeze
32
+ end
33
+ end
34
+ end
data/lib/tidy_json.rb CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: false
2
+
1
3
  require 'json'
4
+ require_relative 'tidy_json/serializer'
5
+ require_relative 'tidy_json/formatter'
2
6
  require_relative 'tidy_json/version'
3
7
 
4
8
  ##
@@ -9,49 +13,92 @@ module TidyJson
9
13
  # Emits a pretty-printed JSON representation of the given +obj+.
10
14
  #
11
15
  # @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
16
+ # @param opts [Hash] Output format options.
17
+ # @option (see Formatter#initialize)
14
18
  # @return [String] A pretty-printed JSON string.
15
19
  def self.tidy(obj = {}, opts = {})
16
20
  formatter = Formatter.new(opts)
17
- str = ''
21
+ json = ''
22
+
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]
18
31
 
19
- if obj.instance_of?(Hash)
20
- str << "{\n"
32
+ obj.each do |k, v|
33
+ str << formatter.format[:indent] << "\"#{k}\": "
34
+ str << formatter.format_node(v, obj)
35
+ end
21
36
 
22
- obj.each do |k, v|
23
- str << formatter.indent << "\"#{k}\": "
24
- str << formatter.format_node(v, obj)
37
+ str << "}\n"
38
+ json = JSON.generate(JSON.parse(formatter.trim(str)), formatter.format)
25
39
  end
26
40
 
27
- str << "}\n"
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
+ end
47
+ end
28
48
 
29
- elsif obj.instance_of?(Array)
30
- str << "[\n"
49
+ ##
50
+ # Returns the given +obj+ with keys in ascending order to a maximum depth of
51
+ # 2.
52
+ #
53
+ # @param obj [Hash, Array<Hash>] A dictionary-like object or collection
54
+ # thereof.
55
+ # @return [Hash, Array<Hash>, Object] A copy of the given +obj+ with top- and
56
+ # second-level keys in ascending order, or else an identical copy of +obj+.
57
+ # @note +obj+ is returned unchanged if: 1) it's not iterable; 2) it's an
58
+ # empty collection; 3) any one of its elements is not hashable (and +obj+
59
+ # is an array).
60
+ def self.sort_keys(obj = {})
61
+ return obj if !obj.respond_to?(:each) || obj.empty? ||
62
+ (obj.instance_of?(Array) &&
63
+ !obj.all? { |e| e.respond_to? :keys })
64
+
65
+ sorted = {}
66
+ sorter = lambda { |data, ret_val|
67
+ data.keys.sort.each do |k|
68
+ ret_val[k.to_sym] = if data[k].instance_of? Hash
69
+ sorter.call(data[k], {})
70
+ else
71
+ data[k]
72
+ end
73
+ end
31
74
 
32
- obj.each do |v|
33
- str << formatter.indent
34
- str << formatter.format_node(v, obj)
75
+ return ret_val
76
+ }
77
+
78
+ if obj.instance_of? Array
79
+ temp = {}
80
+ sorted = []
81
+
82
+ (obj.sort_by { |h| h.keys.first }).each_with_index do |h, idx|
83
+ temp[idx] = sorter.call(h, {})
35
84
  end
36
85
 
37
- str << "]\n"
86
+ temp.each_key { |k| sorted << temp[k] }
87
+ else
88
+ sorted = sorter.call(obj, {})
38
89
  end
39
90
 
40
- str
91
+ sorted
41
92
  end
42
93
 
43
94
  ##
44
95
  # Like +TidyJson::tidy+, but callable by the sender object.
45
96
  #
46
- # @param opts [Hash] Formatting options.
47
- # [:indent] the number of white spaces to indent
97
+ # @param opts [Hash] Output format options.
98
+ # @option (see Formatter#initialize)
48
99
  # @return [String] A pretty-printed JSON string.
49
100
  def to_tidy_json(opts = {})
50
- if !instance_variables.empty?
51
- TidyJson.tidy(JSON.parse(stringify), opts)
52
- else
53
- TidyJson.tidy(self, opts)
54
- end
101
+ TidyJson.tidy(self, opts)
55
102
  end
56
103
 
57
104
  ##
@@ -71,337 +118,32 @@ module TidyJson
71
118
  end
72
119
 
73
120
  ##
74
- # Writes a JSON representation of the sender object to the file specified by +out+.
121
+ # Writes a JSON representation of the sender object to the file specified by
122
+ # +out+.
75
123
  #
76
124
  # @param out [String] The destination filename.
77
- # @param opts [Hash] Formatting options for this object's +#to_tidy_json+ method.
78
- # [:tidy] whether or not the output should be pretty-printed
79
- # [:indent] the number of white spaces to indent
125
+ # @param opts [Hash] Output format options.
126
+ # @option (see Formatter#initialize)
127
+ # @option opts [Boolean] :tidy (false) Whether or not the output should be
128
+ # pretty-printed.
80
129
  # @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 })
130
+ def write_json(out = "#{self.class.name}_#{Time.now.to_i}",
131
+ opts = { tidy: false })
82
132
  path = nil
83
133
 
84
134
  File.open("#{out}.json", 'w') do |f|
85
- path = f << to_tidy_json(opts)
135
+ path =
136
+ f << if opts[:tidy] then to_tidy_json(opts)
137
+ elsif instance_variables.empty? then to_json
138
+ else stringify
139
+ end
86
140
  end
87
141
 
88
- path.path
89
- rescue IOError, RuntimeError, NoMethodError => e
142
+ path&.path
143
+ rescue Errno::ENOENT, Errno::EACCES, IOError, RuntimeError, NoMethodError => e
90
144
  warn "#{__FILE__}.#{__LINE__}: #{e.message}"
91
145
  end
92
-
93
- ##
94
- # A purpose-built JSON generator.
95
- #
96
- # @api private
97
- class Serializer
98
- ##
99
- # Searches +obj+ to a *maximum* depth of 2 for readable attributes,
100
- # storing them as key-value pairs in +json_hash+.
101
- #
102
- # @param obj [Object] A Ruby object that can be parsed as JSON.
103
- # @param json_hash [{String,Symbol => #to_s}] Accumulator.
104
- # @return [{String => #to_s}] A hash mapping of +obj+'s visible attributes.
105
- def self.serialize(obj, json_hash)
106
- obj.instance_variables.each do |m|
107
- key = m.to_s[/[^\@]\w*/].to_sym
108
-
109
- next unless key && !key.eql?('')
110
-
111
- begin
112
- val = obj.send(key) # assuming readable attributes . . .
113
- rescue NoMethodError # . . . which may not be always be the case !
114
- json_hash[key] = nil
115
- end
116
-
117
- begin
118
- # process class members of Hash type
119
- if val.instance_of?(Hash)
120
- nested_key = ''
121
- nested = nil
122
-
123
- val.each.any? do |k, v|
124
- if v.instance_variables.first
125
- nested_key = k
126
- nested = v
127
- end
128
- end
129
-
130
- json_hash[key] = val
131
-
132
- if nested
133
- pos = val.keys.select { |k| k === nested_key }.first.to_sym
134
- nested.instance_variables.each do
135
- json_hash[key][pos] = serialize(nested, class: nested.class.name)
136
- end
137
- end
138
-
139
- # process class members of Array type
140
- elsif val.instance_of?(Array)
141
- json_hash[key] = []
142
-
143
- val.each do |elem|
144
- i = val.index(elem)
145
-
146
- # multi-dimensional array
147
- if elem.instance_of?(Array)
148
- nested = []
149
- elem.each do |e|
150
- j = elem.index(e)
151
-
152
- # nested array element is a class object
153
- if e.instance_variables.first
154
- json_hash[key][j] = { class: e.class.name }
155
-
156
- # recur over the contained object
157
- serialize(e, json_hash[key][j])
158
- else
159
- # some kind of collection?
160
- if e.respond_to? :each
161
- temp = []
162
- e.each { |el| temp << el }
163
- nested << temp
164
- else nested << e
165
- end
166
- end
167
- end
168
- # ~iteration of nested array elements
169
-
170
- json_hash[key] << nested
171
-
172
- else
173
- # 1-D array of class objects
174
- if elem.instance_variables.first
175
- json_hash[key] << { class: elem.class.name }
176
- serialize(elem, json_hash[key][i])
177
- else
178
- # element of primitive type (or Array, or Hash):
179
- # leverage 1:1 mapping of Hash:object
180
- if elem.instance_of?(Hash) then json_hash[key] = val
181
- else
182
- # some kind of collection
183
- if elem.respond_to? :each
184
- temp = []
185
- elem.each { |e| temp << e }
186
- json_hash[key] << temp
187
- else json_hash[key] << elem
188
- end
189
- end
190
- end
191
- end
192
- end
193
- # ~iteration of top-level array elements
194
-
195
- # process any nested class members, i.e., handle a recursive call
196
- # to Serializer.serialize
197
- elsif obj.index(val) || json_hash.key?(key)
198
- if val.instance_variables.first
199
- class_elem = { class: val.class.name }
200
- json_hash[key] << class_elem
201
- k = json_hash[key].index(class_elem)
202
- serialize(val, json_hash[key][k])
203
- else
204
- json_hash[key] << val
205
- end
206
-
207
- # process uncollected class members
208
- else
209
- # member a class object
210
- if val.instance_variables.first
211
- json_hash[key] = { class: val.class.name }
212
- serialize(val, json_hash[key])
213
- else
214
- # member a hash element
215
- if json_hash.key?(key) && \
216
- !json_hash[key].has_val?(val) && \
217
- json_hash[key].instance_of?(Hash)
218
-
219
- json_hash[key][key] = val
220
- else
221
- json_hash[key] = val
222
- end
223
- end
224
- end
225
- rescue NoMethodError
226
- # we expected an array to behave like a hash, or vice-versa
227
- json_hash.store(key, val) # a shallow copy is better than nothing
228
- end
229
- end
230
- # ~iteration of instance variables
231
-
232
- json_hash
233
- end
234
- # ~Serializer.serialize
235
- end
236
- # ~Serializer
237
-
238
- ##
239
- # A purpose-built JSON formatter.
240
- #
241
- # @api private
242
- class Formatter
243
- attr_reader :indent
244
-
245
- # @!attribute indent
246
- # @return [String] the string of white space used by this +Formatter+ to indent object members.
247
-
248
- def initialize(format_options = {})
249
- ##
250
- # The number of times to reduce the left indent of a nested array's opening
251
- # bracket
252
- @left_bracket_offset = 0
253
-
254
- ##
255
- # True if printing a nested array
256
- @need_offset = false
257
-
258
- indent_width = format_options[:indent]
259
-
260
- # don't use the more explicit #integer? method because it's defined for
261
- # floating point numbers also
262
- good_width = indent_width.positive? if indent_width.respond_to? :times
263
-
264
- @indent = "\s" * (good_width ? indent_width : 2)
265
- end
266
-
267
- ##
268
- # Returns the given +node+ as pretty-printed JSON.
269
- #
270
- # @param node [#to_s] A visible attribute of +obj+.
271
- # @param obj [{Object => Object}, <Object>] The enumerable object containing +node+.
272
- # @return [String] A formatted string representation of +node+.
273
- def format_node(node, obj)
274
- str = ''
275
- indent = @indent
276
-
277
- if node.instance_of?(Array)
278
- str << "[\n"
279
-
280
- node.each do |elem|
281
- if elem.instance_of?(Hash)
282
- str << "#{(indent * 2)}{\n"
283
-
284
- elem.each_with_index do |inner_h, h_idx|
285
- str << "#{(indent * 3)}\"#{inner_h.first}\": "
286
- str << node_to_str(inner_h.last, 4)
287
- str << ', ' unless h_idx == (elem.to_a.length - 1)
288
- str << "\n"
289
- end
290
-
291
- str << "#{(indent * 2)}}"
292
- str << ',' unless node.index(elem) == (node.length - 1)
293
- str << "\n" unless node.index(elem) == (node.length - 1)
294
-
295
- else
296
-
297
- if elem.instance_of?(Array) && elem.any? { |e| e.instance_of?(Array) }
298
- @left_bracket_offset = elem.take_while { |e| e.instance_of?(Array) }.size
299
- end
300
-
301
- str << (indent * 2)
302
- str << node_to_str(elem)
303
- str << ",\n" unless node.index(elem) == (node.length - 1)
304
- end
305
- end
306
-
307
- str << "\n#{indent}]\n"
308
-
309
- elsif node.instance_of?(Hash)
310
- str << "{\n"
311
-
312
- node.each_with_index do |h, idx|
313
- if h.last.instance_of?(Hash)
314
- key = if h.first.eql? ''
315
- "#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
316
- else
317
- "#{indent * 2}\"#{h.first}\": "
318
- end
319
- str << key
320
- str << "{\n"
321
-
322
- h.last.each_with_index do |inner_h, inner_h_idx|
323
- str << "#{indent * 3}\"#{inner_h.first}\": "
324
- str << node_to_str(inner_h.last, 4)
325
- str << ",\n" unless inner_h_idx == (h.last.to_a.length - 1)
326
- end
327
-
328
- str << "\n#{indent * 2}}"
329
- else
330
- str << "#{indent * 2}\"#{h.first}\": "
331
- str << node_to_str(h.last)
332
- end
333
-
334
- str << ",\n" unless idx == (node.to_a.length - 1)
335
- end
336
-
337
- str << "\n#{indent}}"
338
- str << ', ' unless (obj.length <= 1) || \
339
- ((obj.length > 1) && \
340
- (obj.instance_of?(Hash) && \
341
- (obj.key(obj.values.last) === obj.key(node))) || \
342
- (obj.instance_of?(Array) && (obj.last == node)))
343
- str << "\n"
344
-
345
- else
346
- str << node_to_str(node)
347
- str << ', ' unless (obj.length <= 1) || \
348
- ((obj.length > 1) && \
349
- (obj.instance_of?(Hash) && \
350
- (obj.key(obj.values.last) === obj.key(node))) || \
351
- (obj.instance_of?(Array) && (obj.last === node)))
352
- str << "\n"
353
- end
354
-
355
- str.gsub(/(#{indent})+[\n\r]+/, '').gsub(/\}\,+/, '},').gsub(/\]\,+/, '],')
356
- end
357
- # ~Formatter#format_node
358
-
359
- ##
360
- # Returns a JSON-appropriate string representation of +node+.
361
- #
362
- # @param node [#to_s] A visible attribute of a Ruby object.
363
- # @param tabs [Integer] Tab width at which to start printing this node.
364
- # @return [String] A formatted string representation of +node+.
365
- def node_to_str(node, tabs = 0)
366
- graft = ''
367
- tabs += 2 if tabs.zero?
368
-
369
- if @need_offset
370
- tabs -= 1
371
- @left_bracket_offset -= 1
372
- end
373
-
374
- indent = @indent * (tabs / 2)
375
-
376
- if node.nil? then graft << 'null'
377
-
378
- elsif node.instance_of?(Hash)
379
- format_node(node, node).scan(/.*$/) do |n|
380
- graft << "\n" << indent << n
381
- end
382
-
383
- elsif node.instance_of?(Array)
384
- @need_offset = @left_bracket_offset.positive?
385
-
386
- format_node(node, {}).scan(/.*$/) do |n|
387
- graft << "\n" << indent << n
388
- end
389
-
390
- elsif !node.instance_of?(String) then graft << node.to_s
391
-
392
- else graft << "\"#{node.gsub(/\"/, '\\"')}\""
393
- end
394
-
395
- graft.strip
396
- end
397
- # ~Formatter.node_to_str
398
- end
399
- # ~Formatter
400
-
401
- private_constant :Serializer
402
- private_constant :Formatter
403
146
  end
404
- # ~TidyJson
405
147
 
406
148
  ##
407
149
  # Includes +TidyJson+ in every Ruby class.