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 +4 -4
- data/.yardopts +3 -1
- data/Gemfile +2 -0
- data/LICENSE +1 -1
- data/README.md +105 -47
- data/Rakefile +2 -0
- data/bin/jtidy +128 -0
- data/bin/jtidy_info.rb +34 -0
- data/lib/tidy_json.rb +85 -343
- data/lib/tidy_json/dedication.rb +18 -8
- data/lib/tidy_json/formatter.rb +238 -0
- data/lib/tidy_json/serializer.rb +118 -0
- data/lib/tidy_json/version.rb +3 -1
- data/test/JsonableObject.json +1 -0
- data/test/codecov_runner.rb +16 -0
- data/test/test_tidy_json.rb +121 -33
- data/tidy_json.gemspec +13 -5
- metadata +65 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf122c3cc2a34f2252b766f1a47f52bada4c4ff5b5334555ea62a572848d8531
|
4
|
+
data.tar.gz: bcd9134752408044711fc2b4ff3732afba7b65995b9c511c9df7e8fe666df4b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d9d164a90b41f3c5793ebaa04b361a5e1c15bf8ed7580a367421e08c06bcc7cd0f5f8555adc1e9adcff57585c59554472b129797343f78714f964c45c8a7ce6
|
7
|
+
data.tar.gz: 950f0eab8248273ce6b80a37918226d2637f436b99552844450bec62d6b3ab40dbe887c81c6fadc4baabd53dc1a2faaac834e6e68082b17d6fd1ac0d709d16ba
|
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-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
|
-
#
|
1
|
+
# tidy_json
|
2
2
|
|
3
|
-
[![
|
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
|
34
|
+
class Jsonable
|
28
35
|
attr_reader :a, :b
|
29
36
|
def initialize
|
30
|
-
@a = { a: 'uno',
|
31
|
-
@b = {
|
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 =
|
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"
|
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:
|
48
|
+
puts my_jsonable.to_tidy_json(indent: 4, sort: true, space_before: 2, ascii_only: true)
|
41
49
|
# {
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
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
|
-
###
|
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
|
-
|
78
|
-
|
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
|
-
|
81
|
-
|
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]
|
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
|
-
[
|
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
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]
|
13
|
-
#
|
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
|
-
|
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
|
-
|
20
|
-
|
32
|
+
obj.each do |k, v|
|
33
|
+
str << formatter.format[:indent] << "\"#{k}\": "
|
34
|
+
str << formatter.format_node(v, obj)
|
35
|
+
end
|
21
36
|
|
22
|
-
|
23
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
86
|
+
temp.each_key { |k| sorted << temp[k] }
|
87
|
+
else
|
88
|
+
sorted = sorter.call(obj, {})
|
38
89
|
end
|
39
90
|
|
40
|
-
|
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]
|
47
|
-
#
|
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
|
-
|
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
|
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]
|
78
|
-
#
|
79
|
-
#
|
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}",
|
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 =
|
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
|
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.
|