tidy_json 0.2.0 → 0.4.0

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