tidy_json 0.2.2 → 0.5.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 +7 -0
- data/LICENSE +1 -1
- data/README.md +84 -38
- data/bin/jtidy +143 -0
- data/bin/jtidy_info.rb +34 -0
- data/lib/tidy_json/dedication.rb +8 -2
- data/lib/tidy_json/formatter.rb +238 -0
- data/lib/tidy_json/serializer.rb +118 -0
- data/lib/tidy_json/version.rb +1 -1
- data/lib/tidy_json.rb +28 -376
- data/test/JsonableObject.json +1 -0
- data/test/codecov_runner.rb +16 -0
- data/test/test_tidy_json.rb +61 -31
- data/tidy_json.gemspec +8 -4
- metadata +28 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 53537db0fe360e1e7d7fe1ba75cb91418c6efa9ee9f108c2f8a2e3020bffed39
|
4
|
+
data.tar.gz: f665d2faff39441554d62de9cf91a6af9462d146501f1ba979d0b5ea21785e9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 01d9f6899b775b39c1cad8acbdf9bdbf563f5a61bd74ca5e395c47d0acb0cc8f963328d5a617c2366aeccd70d5b2f2d423c59485e828055e8743b945e45afd52
|
7
|
+
data.tar.gz: ae1c4ce4ee8b0b123b3627c694bfb9a2acec49b53387d830ce6acd2215347305552e6fbe2248197991f47ab0be07cd2888c824bb71fee3f56a86678dd05a063c
|
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] [![codecov][codecov_badge]][codecov_status]
|
4
4
|
|
5
5
|
A mixin providing (recursive) JSON serialization and pretty printing.
|
6
6
|
|
@@ -19,6 +19,13 @@ 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
|
@@ -27,22 +34,27 @@ require 'tidy_json'
|
|
27
34
|
class Jsonable
|
28
35
|
attr_reader :a, :b
|
29
36
|
def initialize
|
30
|
-
@a = { a: 'uno', f: ['I', 'II', 'III', ['i.', 'ii.', 'iii.', { 'ichi': "\u{4e00}", 'ni': "\u{4e8c}", 'san': "\u{4e09}", 'yon': "\u{56db}" }]], b: 'dos' }
|
31
|
-
@b = { z: { iv: 4, ii: '
|
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
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: 4, sort: true)
|
48
|
+
puts my_jsonable.to_tidy_json(indent: 4, sort: true, space_before: 2, ascii_only: true)
|
41
49
|
# {
|
42
|
-
# "a": {
|
43
|
-
# "a": "uno",
|
44
|
-
# "b": "dos",
|
45
|
-
# "
|
50
|
+
# "a" : {
|
51
|
+
# "a" : "uno",
|
52
|
+
# "b" : "dos",
|
53
|
+
# "c" : {},
|
54
|
+
# "e" : [
|
55
|
+
# []
|
56
|
+
# ],
|
57
|
+
# "f" : [
|
46
58
|
# "I",
|
47
59
|
# "II",
|
48
60
|
# "III",
|
@@ -51,54 +63,88 @@ puts my_jsonable.to_tidy_json(indent: 4, sort: true)
|
|
51
63
|
# "ii.",
|
52
64
|
# "iii.",
|
53
65
|
# {
|
54
|
-
# "ichi": "
|
55
|
-
# "ni": "
|
56
|
-
# "san": "
|
57
|
-
# "yon": "
|
66
|
+
# "ichi" : "\u4e00",
|
67
|
+
# "ni" : "\u4e8c",
|
68
|
+
# "san" : "\u4e09",
|
69
|
+
# "yon" : "\u56db"
|
58
70
|
# }
|
59
71
|
# ]
|
60
72
|
# ]
|
61
73
|
# },
|
62
|
-
# "b": {
|
63
|
-
# "a": 1,
|
64
|
-
# "b": [
|
65
|
-
# "
|
74
|
+
# "b" : {
|
75
|
+
# "a" : 1,
|
76
|
+
# "b" : [
|
77
|
+
# "deux",
|
66
78
|
# 3,
|
67
79
|
# "<abbr title=\"four\">IV</abbr>"
|
68
80
|
# ],
|
69
|
-
# "f": [
|
81
|
+
# "f" : [
|
70
82
|
# "x",
|
71
83
|
# "y",
|
72
84
|
# "z"
|
73
85
|
# ],
|
74
|
-
# "
|
75
|
-
#
|
76
|
-
#
|
77
|
-
#
|
78
|
-
#
|
86
|
+
# "g" : [
|
87
|
+
# {
|
88
|
+
# "none" : []
|
89
|
+
# }
|
90
|
+
# ],
|
91
|
+
# "z" : {
|
92
|
+
# "i" : "uno",
|
93
|
+
# "ii" : "dos",
|
94
|
+
# "iii" : 3,
|
95
|
+
# "iv" : 4
|
79
96
|
# }
|
80
97
|
# },
|
81
|
-
# "class": "Jsonable"
|
98
|
+
# "class" : "Jsonable"
|
82
99
|
# }
|
83
100
|
# => nil
|
84
101
|
```
|
85
102
|
|
86
|
-
###
|
103
|
+
### Command Line Usage
|
87
104
|
|
88
|
-
|
89
|
-
|
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:
|
90
108
|
|
91
|
-
|
92
|
-
- [
|
93
|
-
- [
|
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
|
+
```
|
94
127
|
|
95
|
-
###
|
96
|
-
|
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).
|
97
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://github.com/jtidy/jtidy/blob/master/LICENSE.txt).
|
98
134
|
|
99
|
-
|
100
|
-
[
|
101
|
-
|
102
|
-
|
103
|
-
[
|
104
|
-
[
|
135
|
+
### License
|
136
|
+
Distributed under the terms of the [MIT License][].
|
137
|
+
|
138
|
+
|
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
|
141
|
+
[codecov_status]: https://codecov.io/gh/rdipardo/tidy_json/branch/master
|
142
|
+
[codecov_badge]: https://codecov.io/gh/rdipardo/tidy_json/branch/master/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/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
|
+
tidy = ''
|
100
|
+
fname = INPUT_FILE.strip.gsub('\\', '/')
|
101
|
+
ext = File.extname(fname)
|
102
|
+
input = File.join(
|
103
|
+
File.expand_path(File.dirname(fname)), File.basename(fname, ext)
|
104
|
+
).to_s
|
105
|
+
outfile = unless OPTS[:dest].nil? || OPTS[:dest].strip.empty?
|
106
|
+
fname = OPTS[:dest].strip.gsub('\\', '/')
|
107
|
+
ext = File.extname(fname)
|
108
|
+
File.join(
|
109
|
+
File.expand_path(File.dirname(fname)), File.basename(fname, ext)
|
110
|
+
).to_s
|
111
|
+
end
|
112
|
+
|
113
|
+
begin
|
114
|
+
File.open((input + '.json'), 'r') do |json|
|
115
|
+
begin
|
116
|
+
tidy = TidyJson.tidy(JSON.parse(json.read.strip), OPTS)
|
117
|
+
rescue JSON::JSONError => e
|
118
|
+
warn "#{__FILE__}.#{__LINE__}: #{e.message}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
if tidy.length.positive?
|
123
|
+
output = (if OPTS[:force]
|
124
|
+
outfile.nil? ? input : outfile
|
125
|
+
elsif Regexp.new("(#{outfile})", Regexp::IGNORECASE) =~ input
|
126
|
+
warn "Can't overwrite #{input}.json without '--force' option"
|
127
|
+
"#{input}-tidy"
|
128
|
+
else outfile
|
129
|
+
end) + '.json'
|
130
|
+
File.open(output, 'w') { |fd| fd.write(tidy) }
|
131
|
+
puts "\nWrote: #{output}"
|
132
|
+
puts "#{tidy[0..1024]}\n . . ." if OPTS[:preview]
|
133
|
+
end
|
134
|
+
rescue Errno::ENOENT, Errno::EACCES, IOError => e
|
135
|
+
warn "#{__FILE__}.#{__LINE__}: #{e.message}"
|
136
|
+
end
|
137
|
+
|
138
|
+
else
|
139
|
+
Jtidy.parse %w[--help]
|
140
|
+
end
|
141
|
+
rescue OptionParser::InvalidArgument, OptionParser::MissingArgument
|
142
|
+
Jtidy.parse %w[--help]
|
143
|
+
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/dedication.rb
CHANGED
@@ -7,8 +7,14 @@ module TidyJson # :nodoc:
|
|
7
7
|
"#{'.' * 17} to the memory of #{'.' * 17}\n#{'.' * 52}\n" \
|
8
8
|
"#{'.' * 17} MICHAEL DI PARDO #{'.' * 17}\n#{'.' * 52}\n" \
|
9
9
|
"#{'.' * 12} Please consider supporting #{'.' * 12}\n" \
|
10
|
-
"#{'.' *
|
10
|
+
"#{'.' * 11} multiple sclerosis research: #{'.' * 11}\n" \
|
11
11
|
"#{'.' * 52}\n" \
|
12
|
+
"#{'.' * 23} (US) #{'.' * 23}\n" \
|
13
|
+
"#{'.' * 5} https://www.nationalmssociety.org/Donate " \
|
14
|
+
"#{'.' * 5}\n" \
|
15
|
+
"#{'.' * 21} (Canada) #{'.' * 21}\n" \
|
12
16
|
"#{'.' * 8} https://mssociety.ca/get-involved #{'.' * 9}\n" \
|
13
|
-
"#{'.' *
|
17
|
+
"#{'.' * 23} (UK) #{'.' * 23}\n" \
|
18
|
+
"#{'.' * 4} https://www.mssociety.org.uk/get-involved "\
|
19
|
+
"#{'.' * 5}\n#{'.' * 52}\n\n"
|
14
20
|
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module TidyJson
|
4
|
+
##
|
5
|
+
# A purpose-built JSON formatter.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Formatter
|
9
|
+
# @return [Hash] the JSON format options specified by this +Formatter+
|
10
|
+
# instance.
|
11
|
+
attr_reader :format
|
12
|
+
|
13
|
+
##
|
14
|
+
# Returns a new instance of +Formatter+.
|
15
|
+
#
|
16
|
+
# @param opts [Hash] Formatting options.
|
17
|
+
# @option opts [[2,4,6,8,10,12]] :indent (2) The number of spaces to indent
|
18
|
+
# each object member.
|
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
|
+
# @option opts [Numeric] :max_nesting (100) The maximum level of data
|
28
|
+
# structure nesting in the generated JSON. Disable depth checking by
|
29
|
+
# passing +max_nesting: 0+.
|
30
|
+
# @option opts [Boolean] :escape_slash (false) Whether or not a forward
|
31
|
+
# slash (/) should be escaped.
|
32
|
+
# @option opts [Boolean] :ascii_only (false) Whether or not only ASCII
|
33
|
+
# characters should be generated.
|
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
|
+
# @option opts [Boolean] :sort (false) Whether or not object members should
|
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
|
+
def initialize(opts = {})
|
41
|
+
# The number of times to reduce the left indent of a nested array's
|
42
|
+
# opening bracket
|
43
|
+
@left_bracket_offset = 0
|
44
|
+
|
45
|
+
# True if printing a nested array
|
46
|
+
@need_offset = false
|
47
|
+
|
48
|
+
valid_indent = (2..12).step(2).include?(opts[:indent])
|
49
|
+
valid_space_before = (1..8).include?(opts[:space_before])
|
50
|
+
valid_space_after = (1..8).include?(opts[:space])
|
51
|
+
# don't test for the more explicit :integer? method because it's defined
|
52
|
+
# for floating point numbers also
|
53
|
+
valid_depth = opts[:max_nesting] >= 0 \
|
54
|
+
if opts[:max_nesting].respond_to?(:times)
|
55
|
+
valid_newline = ->(str) { str.respond_to?(:strip) && str.strip.empty? }
|
56
|
+
@format = {
|
57
|
+
indent: "\s" * (valid_indent ? opts[:indent] : 2),
|
58
|
+
space_before: "\s" * (valid_space_before ? opts[:space_before] : 0),
|
59
|
+
space: "\s" * (valid_space_after ? opts[:space] : 1),
|
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"),
|
62
|
+
max_nesting: valid_depth ? opts[:max_nesting] : 100,
|
63
|
+
escape_slash: opts[:escape_slash] || false,
|
64
|
+
ascii_only: opts[:ascii_only] || false,
|
65
|
+
allow_nan: opts[:allow_nan] || false,
|
66
|
+
sorted: opts[:sort] || false
|
67
|
+
}
|
68
|
+
end
|
69
|
+
# ~Formatter#initialize
|
70
|
+
|
71
|
+
##
|
72
|
+
# Returns the given +node+ as pretty-printed JSON.
|
73
|
+
#
|
74
|
+
# @param node [#to_s] A visible attribute of +obj+.
|
75
|
+
# @param obj [{Object => #to_s}, <#to_s>] The enumerable object
|
76
|
+
# containing +node+.
|
77
|
+
# @return [String] A formatted string representation of +node+.
|
78
|
+
def format_node(node, obj)
|
79
|
+
str = ''
|
80
|
+
indent = @format[:indent]
|
81
|
+
|
82
|
+
is_last = (obj.length <= 1) ||
|
83
|
+
(obj.length > 1 &&
|
84
|
+
(obj.instance_of?(Array) &&
|
85
|
+
!(node === obj.first) &&
|
86
|
+
(obj.size.pred == obj.rindex(node))))
|
87
|
+
|
88
|
+
if node.instance_of?(Array)
|
89
|
+
str << '['
|
90
|
+
str << "\n" unless node.empty?
|
91
|
+
|
92
|
+
# format array elements
|
93
|
+
node.each do |elem|
|
94
|
+
if elem.instance_of?(Hash)
|
95
|
+
str << "#{indent * 2}{"
|
96
|
+
str << "\n" unless elem.empty?
|
97
|
+
|
98
|
+
elem.each_with_index do |inner_h, h_idx|
|
99
|
+
str << "#{indent * 3}\"#{inner_h.first}\": "
|
100
|
+
str << node_to_str(inner_h.last, 4)
|
101
|
+
str << ',' unless h_idx == elem.to_a.length.pred
|
102
|
+
str << "\n"
|
103
|
+
end
|
104
|
+
|
105
|
+
str << (indent * 2).to_s unless elem.empty?
|
106
|
+
str << '}'
|
107
|
+
|
108
|
+
# element a scalar, or a nested array
|
109
|
+
else
|
110
|
+
is_nested_array = elem.instance_of?(Array) &&
|
111
|
+
elem.any? { |e| e.instance_of?(Array) }
|
112
|
+
if is_nested_array
|
113
|
+
@left_bracket_offset = \
|
114
|
+
elem.take_while { |e| e.instance_of?(Array) }.size
|
115
|
+
end
|
116
|
+
|
117
|
+
str << (indent * 2) << node_to_str(elem)
|
118
|
+
end
|
119
|
+
|
120
|
+
str << ",\n" unless node.index(elem) == node.length.pred
|
121
|
+
end
|
122
|
+
|
123
|
+
str << "\n#{indent}" unless node.empty?
|
124
|
+
str << ']'
|
125
|
+
str << ",\n" unless is_last
|
126
|
+
|
127
|
+
elsif node.instance_of?(Hash)
|
128
|
+
str << '{'
|
129
|
+
str << "\n" unless node.empty?
|
130
|
+
|
131
|
+
# format elements as key-value pairs
|
132
|
+
node.each_with_index do |h, idx|
|
133
|
+
# format values which are hashes themselves
|
134
|
+
if h.last.instance_of?(Hash)
|
135
|
+
key = if h.first.eql? ''
|
136
|
+
"#{indent * 2}\"<##{h.last.class.name.downcase}>\": "
|
137
|
+
else
|
138
|
+
"#{indent * 2}\"#{h.first}\": "
|
139
|
+
end
|
140
|
+
|
141
|
+
str << key << '{'
|
142
|
+
str << "\n" unless h.last.empty?
|
143
|
+
|
144
|
+
h.last.each_with_index do |inner_h, inner_h_idx|
|
145
|
+
str << "#{indent * 3}\"#{inner_h.first}\": "
|
146
|
+
str << node_to_str(inner_h.last, 4)
|
147
|
+
str << ",\n" unless inner_h_idx == h.last.to_a.length.pred
|
148
|
+
end
|
149
|
+
|
150
|
+
str << "\n#{indent * 2}" unless h.last.empty?
|
151
|
+
str << '}'
|
152
|
+
|
153
|
+
# format scalar values
|
154
|
+
else
|
155
|
+
str << "#{indent * 2}\"#{h.first}\": " << node_to_str(h.last)
|
156
|
+
end
|
157
|
+
|
158
|
+
str << ",\n" unless idx == node.to_a.length.pred
|
159
|
+
end
|
160
|
+
|
161
|
+
str << "\n#{indent}" unless node.empty?
|
162
|
+
str << '}'
|
163
|
+
str << ',' unless is_last
|
164
|
+
str << "\n"
|
165
|
+
|
166
|
+
# scalars
|
167
|
+
else
|
168
|
+
str << node_to_str(node)
|
169
|
+
str << ',' unless is_last
|
170
|
+
str << "\n"
|
171
|
+
end
|
172
|
+
|
173
|
+
trim str.gsub(/(#{indent})+[\n\r]+/, '')
|
174
|
+
.gsub(/\}\,+/, '},')
|
175
|
+
.gsub(/\]\,+/, '],')
|
176
|
+
end
|
177
|
+
# ~Formatter#format_node
|
178
|
+
|
179
|
+
##
|
180
|
+
# Returns a JSON-appropriate string representation of +node+.
|
181
|
+
#
|
182
|
+
# @param node [#to_s] A visible attribute of a Ruby object.
|
183
|
+
# @param tabs [Integer] Tab width at which to start printing this node.
|
184
|
+
# @return [String] A formatted string representation of +node+.
|
185
|
+
def node_to_str(node, tabs = 0)
|
186
|
+
graft = ''
|
187
|
+
tabs += 2 if tabs.zero?
|
188
|
+
|
189
|
+
if @need_offset
|
190
|
+
tabs -= 1
|
191
|
+
@left_bracket_offset -= 1
|
192
|
+
end
|
193
|
+
|
194
|
+
indent = @format[:indent] * (tabs / 2)
|
195
|
+
|
196
|
+
if node.nil? then graft << 'null'
|
197
|
+
|
198
|
+
elsif node.instance_of?(Hash)
|
199
|
+
|
200
|
+
format_node(node, node).scan(/.*$/) do |n|
|
201
|
+
graft << "\n" << indent << n
|
202
|
+
end
|
203
|
+
|
204
|
+
elsif node.instance_of?(Array)
|
205
|
+
@need_offset = @left_bracket_offset.positive?
|
206
|
+
|
207
|
+
format_node(node, {}).scan(/.*$/) do |n|
|
208
|
+
graft << "\n" << indent << n
|
209
|
+
end
|
210
|
+
|
211
|
+
elsif !node.instance_of?(String) then graft << node.to_s
|
212
|
+
|
213
|
+
else graft << "\"#{node.gsub(/\"/, '\\"')}\""
|
214
|
+
end
|
215
|
+
|
216
|
+
graft.strip
|
217
|
+
end
|
218
|
+
# ~Formatter#node_to_str
|
219
|
+
|
220
|
+
##
|
221
|
+
# Removes any trailing comma from serialized object members.
|
222
|
+
#
|
223
|
+
# @param node [String] A serialized object member.
|
224
|
+
# @return [String] A copy of +node+ without a trailing comma.
|
225
|
+
def trim(node)
|
226
|
+
if (extra_comma = /(?<trail>,\s*[\]\}]\s*)$/.match(node))
|
227
|
+
node.sub(extra_comma[:trail],
|
228
|
+
extra_comma[:trail]
|
229
|
+
.slice(1, node.length.pred)
|
230
|
+
.sub(/^\s/, "\n"))
|
231
|
+
else node
|
232
|
+
end
|
233
|
+
end
|
234
|
+
# ~Formatter#trim
|
235
|
+
end
|
236
|
+
|
237
|
+
private_constant :Formatter
|
238
|
+
end
|