tidy_json 0.2.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|