tidy_json 0.3.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +0 -1
- data/Gemfile +7 -0
- data/LICENSE +1 -1
- data/README.md +38 -8
- data/bin/jtidy +143 -0
- data/bin/jtidy_info.rb +31 -0
- data/lib/tidy_json/formatter.rb +26 -32
- data/lib/tidy_json/serializer.rb +88 -19
- data/lib/tidy_json/version.rb +1 -1
- data/test/codecov_runner.rb +2 -2
- data/test/test_tidy_json.rb +19 -15
- data/tidy_json.gemspec +7 -8
- metadata +11 -56
- data/lib/tidy_json/dedication.rb +0 -20
- data/test/JsonableObject.json +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3074ee12d1a54599a8c9160fc353c5b7969655296769ddf8c43a6ed0a731af77
|
4
|
+
data.tar.gz: 8c5b373ec10867f9b2b3c66c8ee1e14e7973cdd7e21fc0c1de6e832affebf600
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe1e81b90fb55da164698cb56b45232b64d1fcea274beb64f0ea1762e5eea0728904a3c487172d6bf266755b9f962126277d54b185ea6baf81b7029769bcc894
|
7
|
+
data.tar.gz: 0a84a04ba5440f12df61b45cbdc86cef16ce5c3dd2b866eacf5249999873e0c204344c20488c3857fd0aa288fedb388e5c195d2aed8b4c1aa53e52f105df362a
|
data/.yardopts
CHANGED
data/Gemfile
CHANGED
data/LICENSE
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2019-
|
3
|
+
Copyright (c) 2019-2022 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
1
|
# tidy_json
|
2
2
|
|
3
|
-
![Gem Version][gem_version_badge] ![Downloads][gem_downloads] [![Travis CI][travis_build_status_badge]][travis_build_status] [![
|
3
|
+
![Gem Version][gem_version_badge] ![Downloads][gem_downloads] [![Travis CI][travis_build_status_badge]][travis_build_status] [![codecov][codecov_badge]][codecov_status]
|
4
4
|
|
5
5
|
A mixin providing (recursive) JSON serialization and pretty printing.
|
6
6
|
|
@@ -100,21 +100,51 @@ puts my_jsonable.to_tidy_json(indent: 4, sort: true, space_before: 2, ascii_only
|
|
100
100
|
# => nil
|
101
101
|
```
|
102
102
|
|
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] [-d out[.json]] [-i [2,4,6,8,10,12]] [-p [1..8]] [-v [1..8]] [-o D] [-a D] [-m N] [-e] [-A] [-N] [-s] [-f] [-P]
|
111
|
+
-d, --dest out[.json] Name of output file
|
112
|
+
-i, --indent [2,4,6,8,10,12] The number of spaces to indent each object member [2]
|
113
|
+
-p, --prop-name-space [1..8] The number of spaces to put after property names [0]
|
114
|
+
-v, --value-space [1..8] The number of spaces to put before property values [1]
|
115
|
+
-o, --object-delim D A string of whitespace to delimit object members [\n]
|
116
|
+
-a, --array-delim D A string of whitespace to delimit array elements [\n]
|
117
|
+
-m, --max-nesting N The maximum level of data structure nesting in the generated JSON; 0 == "no depth checking" [100]
|
118
|
+
-e, --escape Escape /'s [false]
|
119
|
+
-A, --ascii Generate ASCII characters only [false]
|
120
|
+
-N, --nan Allow NaN, Infinity and -Infinity [false]
|
121
|
+
-s, --sort Sort property names [false]
|
122
|
+
-f, --force Overwrite source file [false]
|
123
|
+
-P, --preview Show preview of output [false]
|
124
|
+
-V, --version Show version
|
125
|
+
-h, --help Show this help message
|
126
|
+
```
|
127
|
+
|
128
|
+
### Notice
|
129
|
+
The `jtidy` executable bundled with this gem is in no way affiliated with, nor based on,
|
130
|
+
the HTML parser and pretty printer [of the same name](https://github.com/jtidy/jtidy).
|
131
|
+
|
132
|
+
The JTidy source code and binaries are licensed under the terms of the Zlib-Libpng License.
|
133
|
+
More information is available [here](https://raw.githubusercontent.com/jtidy/jtidy/master/LICENSE.txt).
|
134
|
+
|
103
135
|
### License
|
104
136
|
Distributed under the terms of the [MIT License][].
|
105
137
|
|
106
138
|
|
107
|
-
[travis_build_status]: https://travis-ci.com/rdipardo/tidy_json
|
108
|
-
[
|
109
|
-
[cci_build_status_badge]: https://circleci.com/gh/rdipardo/tidy_json.svg?style=svg
|
110
|
-
[travis_build_status_badge]: https://travis-ci.com/rdipardo/tidy_json.svg?branch=master
|
139
|
+
[travis_build_status]: https://app.travis-ci.com/github/rdipardo/tidy_json
|
140
|
+
[travis_build_status_badge]: https://app.travis-ci.com/rdipardo/tidy_json.svg?branch=master
|
111
141
|
[codecov_status]: https://codecov.io/gh/rdipardo/tidy_json/branch/master
|
112
142
|
[codecov_badge]: https://codecov.io/gh/rdipardo/tidy_json/branch/master/graph/badge.svg
|
113
143
|
[gem_version_badge]: https://img.shields.io/gem/v/tidy_json?color=%234ec820&label=gem%20version&logo=ruby&logoColor=%23e9573f
|
114
144
|
[gem_downloads]: https://img.shields.io/gem/dt/tidy_json?logo=ruby&logoColor=%23e9573f
|
115
|
-
[MIT License]: https://
|
116
|
-
|
145
|
+
[MIT License]: https://raw.githubusercontent.com/rdipardo/tidy_json/master/LICENSE
|
146
|
+
[installing the gem]: https://github.com/rdipardo/tidy_json#installation
|
117
147
|
<!-- API spec -->
|
118
148
|
[`JSON.generate`]: https://github.com/flori/json/blob/d49c5de49e54a5ad3f6fcf587f98d63266ef9439/lib/json/pure/generator.rb#L111
|
119
149
|
[the docs]: https://rubydoc.org/github/rdipardo/tidy_json/TidyJson/Formatter#initialize-instance_method
|
120
|
-
[0.3.0]: https://github.com/rdipardo/tidy_json/releases/tag/v0.3.0
|
150
|
+
[0.3.0]: https://github.com/rdipardo/tidy_json/releases/tag/v0.3.0
|
data/bin/jtidy
ADDED
@@ -0,0 +1,143 @@
|
|
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
|
+
'-d out[.json]': [:dest, String, '--dest', 'Name of output file'],
|
11
|
+
'-i [2,4,6,8,10,12]': [:indent, Integer, '--indent [2,4,6,8,10,12]',
|
12
|
+
'The number of spaces to indent each object member [2]'],
|
13
|
+
'-p [1..8]': [:space_before, Integer, '--prop-name-space [1..8]',
|
14
|
+
'The number of spaces to put after property names [0]'],
|
15
|
+
'-v [1..8]': [:space, Integer, '--value-space [1..8]',
|
16
|
+
'The number of spaces to put before property values [1]'],
|
17
|
+
'-o D': [:object_nl, String, '--object-delim D',
|
18
|
+
'A string of whitespace to delimit object members [\n]'],
|
19
|
+
'-a D': [:array_nl, String, '--array-delim D',
|
20
|
+
'A string of whitespace to delimit array elements [\n]'],
|
21
|
+
'-m N': [:max_nesting, Integer, '--max-nesting N',
|
22
|
+
'The maximum level of data structure nesting in the generated ' \
|
23
|
+
'JSON; 0 == "no depth checking" [100]'],
|
24
|
+
'-e': [:escape_slash, nil, '--escape', 'Escape /\'s [false]'],
|
25
|
+
'-A': [:ascii_only, nil, '--ascii', 'Generate ASCII characters only [false]'],
|
26
|
+
'-N': [:allow_nan, nil, '--nan', 'Allow NaN, Infinity and -Infinity [false]'],
|
27
|
+
'-s': [:sort, nil, '--sort', 'Sort property names [false]'],
|
28
|
+
'-f': [:force, nil, '--force', 'Overwrite source file [false]'],
|
29
|
+
# script-only options
|
30
|
+
'-P': [:preview, nil, '--preview', 'Show preview of output [false]']
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
def self.unescape(str)
|
34
|
+
str.gsub(/\\b|\\h|\\n|\\r|\\s|\\t|\\v/,
|
35
|
+
{
|
36
|
+
'\\b': "\b",
|
37
|
+
'\\h': "\h",
|
38
|
+
'\\n': "\n",
|
39
|
+
'\\r': "\r",
|
40
|
+
'\\s': "\s",
|
41
|
+
'\\t': "\t",
|
42
|
+
'\\v': "\v"
|
43
|
+
})
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.show_unused(opts)
|
47
|
+
return if opts.empty?
|
48
|
+
|
49
|
+
ignored = opts.keys.map do |key|
|
50
|
+
(OPTIONS.keys.select do |k|
|
51
|
+
OPTIONS[k][0].eql? key
|
52
|
+
end.first || '')[0..1]
|
53
|
+
end
|
54
|
+
warn "Ignoring options: #{ignored.join ', '}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.parse(options)
|
58
|
+
format_options = {}
|
59
|
+
OptionParser.new do |opts|
|
60
|
+
opts.banner = \
|
61
|
+
"#{File.basename __FILE__} FILE[.json] " \
|
62
|
+
"#{(OPTIONS.keys.map { |k| "[#{k}]" }).join ' '}"
|
63
|
+
OPTIONS.each_key do |k|
|
64
|
+
opt, type, long_name, desc = OPTIONS[k]
|
65
|
+
opts.on(k, long_name, type, desc) do |v|
|
66
|
+
format_options[opt] = (type == String ? unescape(v) : v)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on_tail('-V', '--version', 'Show version') do
|
71
|
+
show_unused format_options
|
72
|
+
puts ::JtidyInfo.new.to_s
|
73
|
+
exit 0
|
74
|
+
end
|
75
|
+
|
76
|
+
opts.on_tail('-h', '--help', 'Show this help message') do
|
77
|
+
show_unused format_options
|
78
|
+
puts opts
|
79
|
+
exit 0
|
80
|
+
end
|
81
|
+
end.parse! options
|
82
|
+
|
83
|
+
format_options
|
84
|
+
end
|
85
|
+
|
86
|
+
private_class_method :unescape, :show_unused
|
87
|
+
end
|
88
|
+
|
89
|
+
begin
|
90
|
+
begin
|
91
|
+
OPTS = Jtidy.parse(ARGV).freeze
|
92
|
+
INPUT_FILE = ARGV[0].freeze
|
93
|
+
rescue OptionParser::InvalidOption => e
|
94
|
+
warn e.message.capitalize
|
95
|
+
raise OptionParser::InvalidArgument
|
96
|
+
end
|
97
|
+
|
98
|
+
if INPUT_FILE.nil? || INPUT_FILE.strip.empty?
|
99
|
+
Jtidy.parse %w[--help]
|
100
|
+
|
101
|
+
else
|
102
|
+
tidy = ''
|
103
|
+
fname = INPUT_FILE.strip.gsub('\\', '/')
|
104
|
+
ext = File.extname(fname)
|
105
|
+
input = File.join(
|
106
|
+
File.expand_path(File.dirname(fname)), File.basename(fname, ext)
|
107
|
+
).to_s
|
108
|
+
outfile = unless OPTS[:dest].nil? || OPTS[:dest].strip.empty?
|
109
|
+
fname = OPTS[:dest].strip.gsub('\\', '/')
|
110
|
+
ext = File.extname(fname)
|
111
|
+
File.join(
|
112
|
+
File.expand_path(File.dirname(fname)), File.basename(fname, ext)
|
113
|
+
).to_s
|
114
|
+
end
|
115
|
+
|
116
|
+
begin
|
117
|
+
File.open("#{input}.json", 'r') do |json|
|
118
|
+
begin
|
119
|
+
tidy = TidyJson.tidy(JSON.parse(json.read.strip), OPTS)
|
120
|
+
rescue JSON::JSONError => e
|
121
|
+
warn "#{__FILE__}.#{__LINE__}: #{e.message}"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if tidy.length.positive?
|
126
|
+
output = (if OPTS[:force]
|
127
|
+
outfile.nil? ? input : outfile
|
128
|
+
elsif Regexp.new("(#{outfile})", Regexp::IGNORECASE) =~ input
|
129
|
+
warn "Can't overwrite #{input}.json without '--force' option"
|
130
|
+
"#{input}-tidy"
|
131
|
+
else outfile
|
132
|
+
end) + '.json'
|
133
|
+
File.write(output, tidy)
|
134
|
+
puts "\nWrote: #{output}"
|
135
|
+
puts "#{tidy[0..1024]}\n . . ." if OPTS[:preview]
|
136
|
+
end
|
137
|
+
rescue Errno::ENOENT, Errno::EACCES, IOError => e
|
138
|
+
warn "#{__FILE__}.#{__LINE__}: #{e.message}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
rescue OptionParser::InvalidArgument, OptionParser::MissingArgument
|
142
|
+
Jtidy.parse %w[--help]
|
143
|
+
end
|
data/bin/jtidy_info.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TidyJson
|
4
|
+
class JtidyInfo # :nodoc:
|
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
|
+
'# More information is available here:',
|
11
|
+
'# https://github.com/rdipardo/tidy_json#command-line-usage',
|
12
|
+
'#'
|
13
|
+
].join("\n").freeze
|
14
|
+
|
15
|
+
attr_reader :meta
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
gem = Gem::Specification.find_by_name('tidy_json')
|
19
|
+
@meta = {
|
20
|
+
name: "# jtidy #{gem.version}",
|
21
|
+
license: "# License: #{gem.license}",
|
22
|
+
bugs: "# Bugs: #{gem.metadata['bug_tracker_uri']}",
|
23
|
+
notice: NOTICE
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
(@meta.values.join "\n").freeze
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/tidy_json/formatter.rb
CHANGED
@@ -14,16 +14,16 @@ module TidyJson
|
|
14
14
|
# Returns a new instance of +Formatter+.
|
15
15
|
#
|
16
16
|
# @param opts [Hash] Formatting options.
|
17
|
-
# @option opts [[2,4,6,8,10,12]] :indent (2)
|
18
|
-
# indent each object member.
|
19
|
-
# @option opts [[2..8]] :space_before (0) The number of spaces to put
|
20
|
-
# before each +:+ delimiter.
|
21
|
-
# @option opts [[2..8]] :space (1) The number of spaces to put after
|
22
|
-
# each +:+ delimiter.
|
23
|
-
# @option opts [String] :object_nl ("\n") A string to put at the end of
|
17
|
+
# @option opts [[2,4,6,8,10,12]] :indent (2) The number of spaces to indent
|
24
18
|
# each object member.
|
25
|
-
# @option opts [
|
26
|
-
#
|
19
|
+
# @option opts [[1..8]] :space_before (0) The number of spaces to put after
|
20
|
+
# property names.
|
21
|
+
# @option opts [[1..8]] :space (1) The number of spaces to put before
|
22
|
+
# property values.
|
23
|
+
# @option opts [String] :object_nl ("\n") A string of whitespace to delimit
|
24
|
+
# object members.
|
25
|
+
# @option opts [String] :array_nl ("\n") A string of whitespace to delimit
|
26
|
+
# array elements.
|
27
27
|
# @option opts [Numeric] :max_nesting (100) The maximum level of data
|
28
28
|
# structure nesting in the generated JSON. Disable depth checking by
|
29
29
|
# passing +max_nesting: 0+.
|
@@ -31,12 +31,12 @@ module TidyJson
|
|
31
31
|
# slash (/) should be escaped.
|
32
32
|
# @option opts [Boolean] :ascii_only (false) Whether or not only ASCII
|
33
33
|
# characters should be generated.
|
34
|
-
# @option opts [Boolean] :allow_nan (false) Whether or not +NaN+,
|
35
|
-
# +Infinity+ and +-Infinity
|
36
|
-
#
|
34
|
+
# @option opts [Boolean] :allow_nan (false) Whether or not to allow +NaN+,
|
35
|
+
# +Infinity+ and +-Infinity+. If +false+, an exception is thrown if one
|
36
|
+
# of these values is encountered.
|
37
37
|
# @option opts [Boolean] :sort (false) Whether or not object members should
|
38
|
-
# be sorted by
|
39
|
-
# @see https://github.com/flori/json/blob/
|
38
|
+
# be sorted by property name.
|
39
|
+
# @see https://github.com/flori/json/blob/b8c1c640cd375f2e2ccca1b18bf943f80ad04816/lib/json/pure/generator.rb#L111 JSON::Pure::Generator
|
40
40
|
def initialize(opts = {})
|
41
41
|
# The number of times to reduce the left indent of a nested array's
|
42
42
|
# opening bracket
|
@@ -46,18 +46,19 @@ module TidyJson
|
|
46
46
|
@need_offset = false
|
47
47
|
|
48
48
|
valid_indent = (2..12).step(2).include?(opts[:indent])
|
49
|
-
valid_space_before = (
|
50
|
-
valid_space_after = (
|
49
|
+
valid_space_before = (1..8).include?(opts[:space_before])
|
50
|
+
valid_space_after = (1..8).include?(opts[:space])
|
51
51
|
# don't test for the more explicit :integer? method because it's defined
|
52
52
|
# for floating point numbers also
|
53
53
|
valid_depth = opts[:max_nesting] >= 0 \
|
54
54
|
if opts[:max_nesting].respond_to?(:times)
|
55
|
+
valid_newline = ->(str) { str.respond_to?(:strip) && str.strip.empty? }
|
55
56
|
@format = {
|
56
57
|
indent: "\s" * (valid_indent ? opts[:indent] : 2),
|
57
58
|
space_before: "\s" * (valid_space_before ? opts[:space_before] : 0),
|
58
59
|
space: "\s" * (valid_space_after ? opts[:space] : 1),
|
59
|
-
object_nl: opts[:object_nl]
|
60
|
-
array_nl: opts[:array_nl]
|
60
|
+
object_nl: (valid_newline.call(opts[:object_nl]) ? opts[:object_nl] : "\n"),
|
61
|
+
array_nl: (valid_newline.call(opts[:array_nl]) ? opts[:array_nl] : "\n"),
|
61
62
|
max_nesting: valid_depth ? opts[:max_nesting] : 100,
|
62
63
|
escape_slash: opts[:escape_slash] || false,
|
63
64
|
ascii_only: opts[:ascii_only] || false,
|
@@ -79,10 +80,8 @@ module TidyJson
|
|
79
80
|
indent = @format[:indent]
|
80
81
|
|
81
82
|
is_last = (obj.length <= 1) ||
|
82
|
-
(obj.
|
83
|
-
(obj.
|
84
|
-
!(node === obj.first) &&
|
85
|
-
(obj.size.pred == obj.rindex(node))))
|
83
|
+
(obj.instance_of?(Array) &&
|
84
|
+
(obj.size.pred == obj.rindex(node)))
|
86
85
|
|
87
86
|
if node.instance_of?(Array)
|
88
87
|
str << '['
|
@@ -131,12 +130,7 @@ module TidyJson
|
|
131
130
|
node.each_with_index do |h, idx|
|
132
131
|
# format values which are hashes themselves
|
133
132
|
if h.last.instance_of?(Hash)
|
134
|
-
key =
|
135
|
-
"#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
|
136
|
-
else
|
137
|
-
"#{indent * 2}\"#{h.first}\": "
|
138
|
-
end
|
139
|
-
|
133
|
+
key = "#{indent * 2}\"#{h.first || "<##{h.last.class.name.downcase}>"}\": "
|
140
134
|
str << key << '{'
|
141
135
|
str << "\n" unless h.last.empty?
|
142
136
|
|
@@ -170,8 +164,8 @@ module TidyJson
|
|
170
164
|
end
|
171
165
|
|
172
166
|
trim str.gsub(/(#{indent})+[\n\r]+/, '')
|
173
|
-
.gsub(/\}
|
174
|
-
.gsub(/\]
|
167
|
+
.gsub(/\},+/, '},')
|
168
|
+
.gsub(/\],+/, '],')
|
175
169
|
end
|
176
170
|
# ~Formatter#format_node
|
177
171
|
|
@@ -209,7 +203,7 @@ module TidyJson
|
|
209
203
|
|
210
204
|
elsif !node.instance_of?(String) then graft << node.to_s
|
211
205
|
|
212
|
-
else graft << "\"#{node.gsub(
|
206
|
+
else graft << "\"#{node.gsub(/"/, '\\"')}\""
|
213
207
|
end
|
214
208
|
|
215
209
|
graft.strip
|
@@ -222,7 +216,7 @@ module TidyJson
|
|
222
216
|
# @param node [String] A serialized object member.
|
223
217
|
# @return [String] A copy of +node+ without a trailing comma.
|
224
218
|
def trim(node)
|
225
|
-
if (extra_comma = /(?<trail>,\s*[\]
|
219
|
+
if (extra_comma = /(?<trail>,\s*[\]}]\s*)$/.match(node))
|
226
220
|
node.sub(extra_comma[:trail],
|
227
221
|
extra_comma[:trail]
|
228
222
|
.slice(1, node.length.pred)
|
data/lib/tidy_json/serializer.rb
CHANGED
@@ -7,15 +7,93 @@ module TidyJson
|
|
7
7
|
# @api private
|
8
8
|
class Serializer
|
9
9
|
##
|
10
|
-
# Searches +obj+
|
11
|
-
#
|
10
|
+
# Searches +obj+ for readable attributes, storing them as key-value pairs in
|
11
|
+
# +json_hash+.
|
12
12
|
#
|
13
13
|
# @param obj [Object] A Ruby object that can be parsed as JSON.
|
14
14
|
# @param json_hash [{String,Symbol => #to_s}] Accumulator.
|
15
15
|
# @return [{String => #to_s}] A hash mapping of +obj+'s visible attributes.
|
16
|
+
# @note Hashes will be searched for nested objects to a maximum depth of 2;
|
17
|
+
# arrays to a maximum depth of 3.
|
18
|
+
# @example
|
19
|
+
# class Obj
|
20
|
+
# class Child
|
21
|
+
# def initialize; @a = { a: 1 } end
|
22
|
+
# attr_reader :a
|
23
|
+
# end
|
24
|
+
# def initialize; @a = { b: Child.new } end
|
25
|
+
# attr_accessor :a
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# o = Obj.new
|
29
|
+
# puts o.to_tidy_json
|
30
|
+
# <<JSON
|
31
|
+
# {
|
32
|
+
# "class": "Obj",
|
33
|
+
# "a": {
|
34
|
+
# "b": {
|
35
|
+
# "class": "Obj::Child",
|
36
|
+
# "a": {
|
37
|
+
# "a": 1
|
38
|
+
# }
|
39
|
+
# }
|
40
|
+
# }
|
41
|
+
# }
|
42
|
+
# JSON
|
43
|
+
#
|
44
|
+
# # depth > 2: unreachable objects are not serialized
|
45
|
+
# o.a = { b: { c: { d: Obj::Child.new } } }
|
46
|
+
# puts o.to_tidy_json
|
47
|
+
# <<JSON
|
48
|
+
# {
|
49
|
+
# "class": "Obj",
|
50
|
+
# "a": {
|
51
|
+
# "b": {
|
52
|
+
# "c": {
|
53
|
+
# "d": "#<Obj::Child:0x0000559d4da865c0>"
|
54
|
+
# }
|
55
|
+
# }
|
56
|
+
# }
|
57
|
+
# }
|
58
|
+
# JSON
|
59
|
+
#
|
60
|
+
# # object arrays can be nested up to 3 levels deep
|
61
|
+
# o.a = [ [ Obj::Child.new, [ Obj::Child.new, [ Obj::Child.new ] ] ] ]
|
62
|
+
# puts o.to_tidy_json
|
63
|
+
# <<JSON
|
64
|
+
# {
|
65
|
+
# "class": "Obj",
|
66
|
+
# "a": [
|
67
|
+
# {
|
68
|
+
# "class": "Obj::Child",
|
69
|
+
# "a": {
|
70
|
+
# "a": 1
|
71
|
+
# }
|
72
|
+
# },
|
73
|
+
# [
|
74
|
+
# [
|
75
|
+
# {
|
76
|
+
# "class": "Obj::Child",
|
77
|
+
# "a": {
|
78
|
+
# "a": 1
|
79
|
+
# }
|
80
|
+
# },
|
81
|
+
# [
|
82
|
+
# {
|
83
|
+
# "class": "Obj::Child",
|
84
|
+
# "a": {
|
85
|
+
# "a": 1
|
86
|
+
# }
|
87
|
+
# }
|
88
|
+
# ]
|
89
|
+
# ]
|
90
|
+
# ]
|
91
|
+
# ]
|
92
|
+
# }
|
93
|
+
# JSON
|
16
94
|
def self.serialize(obj, json_hash)
|
17
95
|
obj.instance_variables.each do |m|
|
18
|
-
key = m.to_s[/[
|
96
|
+
key = m.to_s[/[^@]\w*/].to_sym
|
19
97
|
|
20
98
|
next unless key && !key.eql?('')
|
21
99
|
|
@@ -28,24 +106,10 @@ module TidyJson
|
|
28
106
|
begin
|
29
107
|
# process class members of Hash type
|
30
108
|
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
109
|
json_hash[key] = val
|
42
110
|
|
43
|
-
|
44
|
-
|
45
|
-
nested.instance_variables.each do
|
46
|
-
json_hash[key][pos] = serialize(nested,
|
47
|
-
class: nested.class.name)
|
48
|
-
end
|
111
|
+
val.each.any? do |k, v|
|
112
|
+
json_hash[key][k.to_sym] = serialize(v, class: v.class.name) unless v.instance_variables.empty?
|
49
113
|
end
|
50
114
|
|
51
115
|
# process class members of Array type
|
@@ -75,6 +139,11 @@ module TidyJson
|
|
75
139
|
elsif e.respond_to?(:each)
|
76
140
|
temp = []
|
77
141
|
e.each do |el|
|
142
|
+
if el.respond_to?(:each)
|
143
|
+
el.each_with_index do |ell, idx|
|
144
|
+
el[idx] = serialize(ell, class: ell.class.name) unless ell.instance_variables.empty?
|
145
|
+
end
|
146
|
+
end
|
78
147
|
temp << if el.instance_variables.empty? then el
|
79
148
|
else JSON.parse(el.stringify)
|
80
149
|
end
|
data/lib/tidy_json/version.rb
CHANGED
data/test/codecov_runner.rb
CHANGED
@@ -7,8 +7,8 @@ if ENV['COVERAGE']
|
|
7
7
|
SimpleCov.command_name 'Unit Tests'
|
8
8
|
|
9
9
|
if ENV['CI']
|
10
|
-
require '
|
11
|
-
SimpleCov.formatter = SimpleCov::Formatter::
|
10
|
+
require 'simplecov-cobertura'
|
11
|
+
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
|
12
12
|
end
|
13
13
|
rescue LoadError
|
14
14
|
warn "Can't locate coverage drivers! Try running: `bundle install` first."
|
data/test/test_tidy_json.rb
CHANGED
@@ -11,11 +11,11 @@ class Nested
|
|
11
11
|
attr_accessor :a, :b, :c, :d, :e
|
12
12
|
|
13
13
|
def initialize
|
14
|
-
@a = [[], [[5, 6, 7]], [1, 2, {
|
15
|
-
@b = {
|
16
|
-
@c = [[1, 2, 3, [4]], {
|
17
|
-
@d = [{}, [], [1, 2, 3, [4]], {
|
18
|
-
@e = [[1, 2, 3, [4]], {
|
14
|
+
@a = [[], [[5, 6, 7]], [1, 2, { three: 3, four: [5, 6] }, [4]], { inner: 1 }]
|
15
|
+
@b = { a: [5], b: [1, 2, 3, [4]], c: [5], d: { inner: 1 }, e: [6] }
|
16
|
+
@c = [[1, 2, 3, [4]], { inner: 1 }, {}]
|
17
|
+
@d = [{}, [], [1, 2, 3, [4]], { inner: 1 }, []]
|
18
|
+
@e = [[1, 2, 3, [4]], { inner: 1 }, {}]
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
@@ -25,8 +25,8 @@ class JsonableObject
|
|
25
25
|
def initialize
|
26
26
|
@h = { one: 'uno', two: 'dos', three: %w[eine zwei drei], cuatro: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', 'iv.']] }
|
27
27
|
@a = ['k', 'l', %w[M N O P], 'q', 'r', 's', [10, 456, ['<abbr title="Reel 2, Dialog Track 2">R2D2</abbr>', 'R', 2, 'D', ['two']]], 'u', 'v', 'x', 'y', %w[Z AB]]
|
28
|
-
@b = [{
|
29
|
-
@c = [[Nested.new, [Nested.new], [Nested.new]]]
|
28
|
+
@b = [{ uno: Nested.new }, [Nested.new, [[Nested.new], Nested.new, Nested.new], [Nested.new]]]
|
29
|
+
@c = [[Nested.new, [Nested.new, [Nested.new]], [Nested.new]]]
|
30
30
|
@d = []
|
31
31
|
@f = {}
|
32
32
|
end
|
@@ -64,13 +64,13 @@ class TidyJsonTest < Test::Unit::TestCase
|
|
64
64
|
assert_equal([{ a: 1 }, { b: 2 }, { c: 3, d: { a: 9, f: 56, i: '34', ii: '35' } }],
|
65
65
|
TidyJson.sort_keys(hash_array))
|
66
66
|
assert_equal({ a: 'one', b: 'two', c: 3 },
|
67
|
-
TidyJson.sort_keys(
|
67
|
+
TidyJson.sort_keys(b: 'two', c: 3, a: 'one'))
|
68
68
|
assert_equal([], TidyJson.sort_keys([]), 'return empty arrays unchanged')
|
69
69
|
assert_equal({}, TidyJson.sort_keys({}), 'return empty hashes unchanged')
|
70
70
|
assert_equal([3, 2, 1], TidyJson.sort_keys([3, 2, 1]),
|
71
71
|
'return arrays of keyless objects unchanged')
|
72
72
|
assert_equal([{ b: 'two' }, 'one'],
|
73
|
-
TidyJson.sort_keys([{
|
73
|
+
TidyJson.sort_keys([{ b: 'two' }, 'one']),
|
74
74
|
'arrays with any keyless objects should be returned unchanged')
|
75
75
|
end
|
76
76
|
|
@@ -82,25 +82,29 @@ class TidyJsonTest < Test::Unit::TestCase
|
|
82
82
|
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",
|
83
83
|
nested_hash_array.to_tidy_json(indent: 8, sort: true))
|
84
84
|
assert_equal("{\n \"a\": \"one\",\n \"b\": \"two\",\n \"c\": 3\n}\n",
|
85
|
-
{
|
85
|
+
{ b: 'two', c: 3, a: 'one' }.to_tidy_json(indent: 6, sort: true))
|
86
86
|
assert_equal("[]\n", [].to_tidy_json(sort: true))
|
87
87
|
assert_equal("{}\n", {}.to_tidy_json(sort: true))
|
88
88
|
assert_equal("[\n 3,\n 2,\n 1\n]\n",
|
89
89
|
[3, 2, 1].to_tidy_json(indent: 8, sort: true))
|
90
90
|
assert_equal("[\n {\n \"b\": \"two\"\n },\n \"one\"\n]\n",
|
91
|
-
[{
|
91
|
+
[{ b: 'two' }, 'one'].to_tidy_json(indent: 4, sort: true))
|
92
92
|
end
|
93
93
|
|
94
94
|
def test_tidy_instance
|
95
95
|
assert_equal({}.to_tidy_json, "{}\n")
|
96
96
|
assert_equal([].to_tidy_json, "[]\n")
|
97
97
|
assert_equal(String.new.to_tidy_json, "\"\"\n")
|
98
|
-
assert_equal(JsonableObject.new.to_tidy_json.length,
|
98
|
+
assert_equal(JsonableObject.new.to_tidy_json.length, 17_774)
|
99
99
|
end
|
100
100
|
|
101
101
|
def test_stringify_instance
|
102
|
-
File.
|
103
|
-
|
102
|
+
output = @t.write_json(File.join(__dir__, @t.class.name))
|
103
|
+
assert(File.exist?(output))
|
104
|
+
File.open(output, 'r:UTF-8') do |json|
|
105
|
+
text = json.read.strip
|
106
|
+
assert_equal(@t.stringify, text)
|
107
|
+
assert_no_match(/(#<Nested:0x)[a-z0-9]+>/, text, 'Some objects were not serialized!')
|
104
108
|
end
|
105
109
|
rescue Errno::ENOENT, Errno::EACCES, IOError => e
|
106
110
|
flunk "#{__FILE__}.#{__LINE__}: #{e.message}"
|
@@ -132,7 +136,7 @@ class TidyJsonTest < Test::Unit::TestCase
|
|
132
136
|
|
133
137
|
def test_indent_bounds_checking
|
134
138
|
assert_equal("{\n \"a\": \"one\",\n \"b\": \"two\",\n \"c\": 3\n}\n",
|
135
|
-
{
|
139
|
+
{ b: 'two', c: 3, a: 'one' }.to_tidy_json(indent: 5, sort: true),
|
136
140
|
'odd values should fall back to default of 2')
|
137
141
|
assert_equal([].to_tidy_json(indent: '16'), "[]\n",
|
138
142
|
'values > 12 should fall back to default of 2')
|