sorbet-toon 0.1.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 +7 -0
- data/LICENSE +45 -0
- data/lib/sorbet/toon/README.md +168 -0
- data/lib/sorbet/toon/codec.rb +55 -0
- data/lib/sorbet/toon/config.rb +50 -0
- data/lib/sorbet/toon/constants.rb +39 -0
- data/lib/sorbet/toon/decode/decoders.rb +226 -0
- data/lib/sorbet/toon/decode/parser.rb +267 -0
- data/lib/sorbet/toon/decode/scanner.rb +118 -0
- data/lib/sorbet/toon/decode/validation.rb +66 -0
- data/lib/sorbet/toon/decoder.rb +42 -0
- data/lib/sorbet/toon/encode/encoders.rb +246 -0
- data/lib/sorbet/toon/encode/normalize.rb +108 -0
- data/lib/sorbet/toon/encode/primitives.rb +86 -0
- data/lib/sorbet/toon/encode/writer.rb +28 -0
- data/lib/sorbet/toon/encoder.rb +43 -0
- data/lib/sorbet/toon/enum_extensions.rb +28 -0
- data/lib/sorbet/toon/errors.rb +10 -0
- data/lib/sorbet/toon/normalizer.rb +93 -0
- data/lib/sorbet/toon/reconstructor.rb +181 -0
- data/lib/sorbet/toon/shared/literal_utils.rb +35 -0
- data/lib/sorbet/toon/shared/string_utils.rb +95 -0
- data/lib/sorbet/toon/shared/validation.rb +41 -0
- data/lib/sorbet/toon/signature_formatter.rb +172 -0
- data/lib/sorbet/toon/struct_extensions.rb +21 -0
- data/lib/sorbet/toon/version.rb +7 -0
- data/lib/sorbet/toon.rb +56 -0
- metadata +84 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../constants'
|
|
4
|
+
require_relative 'normalize'
|
|
5
|
+
require_relative 'primitives'
|
|
6
|
+
require_relative 'writer'
|
|
7
|
+
|
|
8
|
+
module Sorbet
|
|
9
|
+
module Toon
|
|
10
|
+
module Encode
|
|
11
|
+
module Encoders
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
ResolvedOptions = Struct.new(:indent, :delimiter, :length_marker, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
def encode_value(value, options)
|
|
17
|
+
options = resolve_options(options)
|
|
18
|
+
|
|
19
|
+
if Normalize.json_primitive?(value)
|
|
20
|
+
return Primitives.encode_primitive(value, options.delimiter)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
writer = Writer.new(options.indent)
|
|
24
|
+
|
|
25
|
+
if Normalize.json_array?(value)
|
|
26
|
+
encode_array(nil, value, writer, 0, options)
|
|
27
|
+
elsif Normalize.json_object?(value)
|
|
28
|
+
encode_object(value, writer, 0, options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
writer.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def encode_object(object, writer, depth, options)
|
|
35
|
+
object.each do |key, val|
|
|
36
|
+
encode_key_value_pair(key, val, writer, depth, options)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def encode_key_value_pair(key, value, writer, depth, options)
|
|
41
|
+
encoded_key = Primitives.encode_key(key)
|
|
42
|
+
|
|
43
|
+
if Normalize.json_primitive?(value)
|
|
44
|
+
writer.push(depth, "#{encoded_key}: #{Primitives.encode_primitive(value, options.delimiter)}")
|
|
45
|
+
elsif Normalize.json_array?(value)
|
|
46
|
+
encode_array(key, value, writer, depth, options)
|
|
47
|
+
elsif Normalize.json_object?(value)
|
|
48
|
+
nested_keys = value.keys
|
|
49
|
+
if nested_keys.empty?
|
|
50
|
+
writer.push(depth, "#{encoded_key}:")
|
|
51
|
+
else
|
|
52
|
+
writer.push(depth, "#{encoded_key}:")
|
|
53
|
+
encode_object(value, writer, depth + 1, options)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def encode_array(key, array, writer, depth, options)
|
|
59
|
+
if array.empty?
|
|
60
|
+
header = Primitives.format_header(0, key: key, delimiter: options.delimiter, length_marker: options.length_marker)
|
|
61
|
+
writer.push(depth, header)
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if Normalize.array_of_primitives?(array)
|
|
66
|
+
formatted = encode_inline_array_line(array, options.delimiter, key, options.length_marker)
|
|
67
|
+
writer.push(depth, formatted)
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if Normalize.array_of_arrays?(array)
|
|
72
|
+
all_primitive = array.all? { |arr| Normalize.array_of_primitives?(arr) }
|
|
73
|
+
if all_primitive
|
|
74
|
+
encode_array_of_arrays_as_list_items(key, array, writer, depth, options)
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if Normalize.array_of_objects?(array)
|
|
80
|
+
header = extract_tabular_header(array)
|
|
81
|
+
if header
|
|
82
|
+
encode_array_of_objects_as_tabular(key, array, header, writer, depth, options)
|
|
83
|
+
else
|
|
84
|
+
encode_mixed_array_as_list_items(key, array, writer, depth, options)
|
|
85
|
+
end
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
encode_mixed_array_as_list_items(key, array, writer, depth, options)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def encode_array_of_arrays_as_list_items(prefix, values, writer, depth, options)
|
|
93
|
+
header = Primitives.format_header(values.length, key: prefix, delimiter: options.delimiter, length_marker: options.length_marker)
|
|
94
|
+
writer.push(depth, header)
|
|
95
|
+
|
|
96
|
+
values.each do |arr|
|
|
97
|
+
next unless Normalize.array_of_primitives?(arr)
|
|
98
|
+
|
|
99
|
+
inline = encode_inline_array_line(arr, options.delimiter, nil, options.length_marker)
|
|
100
|
+
writer.push_list_item(depth + 1, inline)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def encode_inline_array_line(values, delimiter, prefix = nil, length_marker = false)
|
|
105
|
+
header = Primitives.format_header(values.length, key: prefix, delimiter: delimiter, length_marker: length_marker)
|
|
106
|
+
return header if values.empty?
|
|
107
|
+
|
|
108
|
+
joined = Primitives.encode_and_join_primitives(values, delimiter)
|
|
109
|
+
"#{header} #{joined}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def encode_array_of_objects_as_tabular(prefix, rows, header_keys, writer, depth, options)
|
|
113
|
+
formatted_header = Primitives.format_header(
|
|
114
|
+
rows.length,
|
|
115
|
+
key: prefix,
|
|
116
|
+
fields: header_keys,
|
|
117
|
+
delimiter: options.delimiter,
|
|
118
|
+
length_marker: options.length_marker
|
|
119
|
+
)
|
|
120
|
+
writer.push(depth, formatted_header)
|
|
121
|
+
write_tabular_rows(rows, header_keys, writer, depth + 1, options)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def extract_tabular_header(rows)
|
|
125
|
+
return nil if rows.empty?
|
|
126
|
+
|
|
127
|
+
first_row = rows.first
|
|
128
|
+
header = first_row.keys
|
|
129
|
+
return nil if header.empty?
|
|
130
|
+
return header if is_tabular_array(rows, header)
|
|
131
|
+
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def is_tabular_array(rows, header)
|
|
136
|
+
rows.all? do |row|
|
|
137
|
+
keys = row.keys
|
|
138
|
+
next false unless keys.length == header.length
|
|
139
|
+
|
|
140
|
+
header.all? do |key|
|
|
141
|
+
row.key?(key) && Normalize.json_primitive?(row[key])
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def write_tabular_rows(rows, header, writer, depth, options)
|
|
147
|
+
rows.each do |row|
|
|
148
|
+
values = header.map { |key| row[key] }
|
|
149
|
+
joined = Primitives.encode_and_join_primitives(values, options.delimiter)
|
|
150
|
+
writer.push(depth, joined)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def encode_mixed_array_as_list_items(prefix, items, writer, depth, options)
|
|
155
|
+
header = Primitives.format_header(items.length, key: prefix, delimiter: options.delimiter, length_marker: options.length_marker)
|
|
156
|
+
writer.push(depth, header)
|
|
157
|
+
|
|
158
|
+
items.each do |item|
|
|
159
|
+
encode_list_item_value(item, writer, depth + 1, options)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def encode_object_as_list_item(object, writer, depth, options)
|
|
164
|
+
keys = object.keys
|
|
165
|
+
if keys.empty?
|
|
166
|
+
writer.push(depth, Constants::LIST_ITEM_MARKER)
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
first_key = keys.first
|
|
171
|
+
first_value = object[first_key]
|
|
172
|
+
encoded_first_key = Primitives.encode_key(first_key)
|
|
173
|
+
|
|
174
|
+
if Normalize.json_primitive?(first_value)
|
|
175
|
+
writer.push_list_item(depth, "#{encoded_first_key}: #{Primitives.encode_primitive(first_value, options.delimiter)}")
|
|
176
|
+
elsif Normalize.json_array?(first_value)
|
|
177
|
+
handle_first_array_item(encoded_first_key, first_key, first_value, writer, depth, options)
|
|
178
|
+
elsif Normalize.json_object?(first_value)
|
|
179
|
+
nested_keys = first_value.keys
|
|
180
|
+
if nested_keys.empty?
|
|
181
|
+
writer.push_list_item(depth, "#{encoded_first_key}:")
|
|
182
|
+
else
|
|
183
|
+
writer.push_list_item(depth, "#{encoded_first_key}:")
|
|
184
|
+
encode_object(first_value, writer, depth + 2, options)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
keys.drop(1).each do |key|
|
|
189
|
+
encode_key_value_pair(key, object[key], writer, depth + 1, options)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def encode_list_item_value(value, writer, depth, options)
|
|
194
|
+
if Normalize.json_primitive?(value)
|
|
195
|
+
writer.push_list_item(depth, Primitives.encode_primitive(value, options.delimiter))
|
|
196
|
+
elsif Normalize.json_array?(value) && Normalize.array_of_primitives?(value)
|
|
197
|
+
inline = encode_inline_array_line(value, options.delimiter, nil, options.length_marker)
|
|
198
|
+
writer.push_list_item(depth, inline)
|
|
199
|
+
elsif Normalize.json_object?(value)
|
|
200
|
+
encode_object_as_list_item(value, writer, depth, options)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def handle_first_array_item(encoded_key, raw_key, array, writer, depth, options)
|
|
205
|
+
if Normalize.array_of_primitives?(array)
|
|
206
|
+
formatted = encode_inline_array_line(array, options.delimiter, raw_key, options.length_marker)
|
|
207
|
+
writer.push_list_item(depth, formatted)
|
|
208
|
+
elsif Normalize.array_of_objects?(array)
|
|
209
|
+
header = extract_tabular_header(array)
|
|
210
|
+
if header
|
|
211
|
+
formatted_header = Primitives.format_header(
|
|
212
|
+
array.length,
|
|
213
|
+
key: raw_key,
|
|
214
|
+
fields: header,
|
|
215
|
+
delimiter: options.delimiter,
|
|
216
|
+
length_marker: options.length_marker
|
|
217
|
+
)
|
|
218
|
+
writer.push_list_item(depth, formatted_header)
|
|
219
|
+
write_tabular_rows(array, header, writer, depth + 1, options)
|
|
220
|
+
else
|
|
221
|
+
writer.push_list_item(depth, "#{encoded_key}[#{array.length}]:")
|
|
222
|
+
array.each do |item|
|
|
223
|
+
encode_object_as_list_item(item, writer, depth + 1, options)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
else
|
|
227
|
+
writer.push_list_item(depth, "#{encoded_key}[#{array.length}]:")
|
|
228
|
+
array.each do |item|
|
|
229
|
+
encode_list_item_value(item, writer, depth + 1, options)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
private_class_method :handle_first_array_item
|
|
234
|
+
|
|
235
|
+
def resolve_options(opts)
|
|
236
|
+
ResolvedOptions.new(
|
|
237
|
+
indent: opts[:indent] || 2,
|
|
238
|
+
delimiter: opts[:delimiter] || Constants::DEFAULT_DELIMITER,
|
|
239
|
+
length_marker: opts[:length_marker] || false
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
private_class_method :resolve_options
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'set'
|
|
5
|
+
require 'bigdecimal'
|
|
6
|
+
|
|
7
|
+
module Sorbet
|
|
8
|
+
module Toon
|
|
9
|
+
module Encode
|
|
10
|
+
module Normalize
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def normalize(value)
|
|
14
|
+
case value
|
|
15
|
+
when nil
|
|
16
|
+
nil
|
|
17
|
+
when String, TrueClass, FalseClass
|
|
18
|
+
value
|
|
19
|
+
when Integer
|
|
20
|
+
normalize_integer(value)
|
|
21
|
+
when Float
|
|
22
|
+
normalize_float(value)
|
|
23
|
+
when Rational
|
|
24
|
+
normalize_float(value.to_f)
|
|
25
|
+
when BigDecimal
|
|
26
|
+
normalize_float(value.to_f)
|
|
27
|
+
when Time, DateTime
|
|
28
|
+
value.iso8601
|
|
29
|
+
when Date
|
|
30
|
+
value.iso8601
|
|
31
|
+
when Array
|
|
32
|
+
value.map { |item| normalize(item) }
|
|
33
|
+
when Set
|
|
34
|
+
value.map { |item| normalize(item) }
|
|
35
|
+
when Hash
|
|
36
|
+
normalize_hash(value)
|
|
37
|
+
else
|
|
38
|
+
try_custom_normalize(value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def json_primitive?(value)
|
|
43
|
+
value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def json_array?(value)
|
|
47
|
+
value.is_a?(Array)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def json_object?(value)
|
|
51
|
+
value.is_a?(Hash)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def array_of_primitives?(array)
|
|
55
|
+
array.all? { |item| json_primitive?(item) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def array_of_arrays?(array)
|
|
59
|
+
array.all? { |item| json_array?(item) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def array_of_objects?(array)
|
|
63
|
+
array.all? { |item| json_object?(item) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def is_plain_object?(value)
|
|
67
|
+
value.is_a?(Hash)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def normalize_integer(value)
|
|
71
|
+
value
|
|
72
|
+
end
|
|
73
|
+
private_class_method :normalize_integer
|
|
74
|
+
|
|
75
|
+
def normalize_float(value)
|
|
76
|
+
return 0 if value.zero?
|
|
77
|
+
return nil unless value.finite?
|
|
78
|
+
value
|
|
79
|
+
end
|
|
80
|
+
private_class_method :normalize_float
|
|
81
|
+
|
|
82
|
+
def normalize_hash(hash)
|
|
83
|
+
result = {}
|
|
84
|
+
hash.each do |key, val|
|
|
85
|
+
result[key.to_s] = normalize(val)
|
|
86
|
+
end
|
|
87
|
+
result
|
|
88
|
+
end
|
|
89
|
+
private_class_method :normalize_hash
|
|
90
|
+
|
|
91
|
+
def try_custom_normalize(value)
|
|
92
|
+
if value.respond_to?(:to_ary)
|
|
93
|
+
normalize(value.to_ary)
|
|
94
|
+
elsif value.respond_to?(:to_hash)
|
|
95
|
+
normalize_hash(value.to_hash)
|
|
96
|
+
elsif value.respond_to?(:to_h)
|
|
97
|
+
normalize_hash(value.to_h)
|
|
98
|
+
elsif value.respond_to?(:to_s)
|
|
99
|
+
value.to_s
|
|
100
|
+
else
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
private_class_method :try_custom_normalize
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
|
|
5
|
+
require_relative '../constants'
|
|
6
|
+
require_relative '../shared/validation'
|
|
7
|
+
require_relative '../shared/literal_utils'
|
|
8
|
+
require_relative '../shared/string_utils'
|
|
9
|
+
|
|
10
|
+
module Sorbet
|
|
11
|
+
module Toon
|
|
12
|
+
module Encode
|
|
13
|
+
module Primitives
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def encode_primitive(value, delimiter = Constants::DEFAULT_DELIMITER)
|
|
17
|
+
case value
|
|
18
|
+
when nil
|
|
19
|
+
Constants::NULL_LITERAL
|
|
20
|
+
when TrueClass, FalseClass
|
|
21
|
+
value.to_s
|
|
22
|
+
when Integer
|
|
23
|
+
value.to_s
|
|
24
|
+
when Float
|
|
25
|
+
format_float(value)
|
|
26
|
+
when Numeric
|
|
27
|
+
value.to_s
|
|
28
|
+
else
|
|
29
|
+
encode_string_literal(value.to_s, delimiter)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def encode_string_literal(value, delimiter = Constants::DEFAULT_DELIMITER)
|
|
34
|
+
if Shared::Validation.safe_unquoted?(value, delimiter)
|
|
35
|
+
value
|
|
36
|
+
else
|
|
37
|
+
"\"#{Shared::StringUtils.escape_string(value)}\""
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def encode_key(key)
|
|
42
|
+
if Shared::Validation.valid_unquoted_key?(key)
|
|
43
|
+
key
|
|
44
|
+
else
|
|
45
|
+
"\"#{Shared::StringUtils.escape_string(key)}\""
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def encode_and_join_primitives(values, delimiter = Constants::DEFAULT_DELIMITER)
|
|
50
|
+
values.map { |v| encode_primitive(v, delimiter) }.join(delimiter)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def format_header(length, key: nil, fields: nil, delimiter: Constants::DEFAULT_DELIMITER, length_marker: false)
|
|
54
|
+
header = +''
|
|
55
|
+
header << encode_key(key) if key
|
|
56
|
+
|
|
57
|
+
header << '['
|
|
58
|
+
header << Constants::HASH if length_marker == Constants::HASH
|
|
59
|
+
header << length.to_i.to_s
|
|
60
|
+
header << delimiter if delimiter != Constants::DEFAULT_DELIMITER
|
|
61
|
+
header << ']'
|
|
62
|
+
|
|
63
|
+
if fields && !fields.empty?
|
|
64
|
+
encoded_fields = fields.map { |field| encode_key(field) }
|
|
65
|
+
header << '{'
|
|
66
|
+
header << encoded_fields.join(delimiter)
|
|
67
|
+
header << '}'
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
header << Constants::COLON
|
|
71
|
+
header
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def format_float(value)
|
|
75
|
+
return '0' if value.zero?
|
|
76
|
+
|
|
77
|
+
decimal = BigDecimal(value.to_s)
|
|
78
|
+
str = decimal.to_s('F')
|
|
79
|
+
str = str.sub(/\.0+\z/, '')
|
|
80
|
+
str.sub(/(\.\d*?)0+\z/, '\1')
|
|
81
|
+
end
|
|
82
|
+
private_class_method :format_float
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../constants'
|
|
4
|
+
|
|
5
|
+
module Sorbet
|
|
6
|
+
module Toon
|
|
7
|
+
module Encode
|
|
8
|
+
class Writer
|
|
9
|
+
def initialize(indent_size)
|
|
10
|
+
@indentation_string = Constants::SPACE * indent_size
|
|
11
|
+
@lines = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def push(depth, content)
|
|
15
|
+
@lines << "#{@indentation_string * depth}#{content}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def push_list_item(depth, content)
|
|
19
|
+
push(depth, "#{Constants::LIST_ITEM_PREFIX}#{content}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
@lines.join("\n")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'normalizer'
|
|
4
|
+
require_relative 'codec'
|
|
5
|
+
|
|
6
|
+
module Sorbet
|
|
7
|
+
module Toon
|
|
8
|
+
module Encoder
|
|
9
|
+
CONFIG_KEYS = %i[indent delimiter length_marker include_type_metadata].freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def encode(value, config:, signature: nil, role: :output, **overrides)
|
|
13
|
+
config_overrides = extract_overrides(overrides, CONFIG_KEYS)
|
|
14
|
+
resolved = config.resolve(config_overrides)
|
|
15
|
+
|
|
16
|
+
normalized = Sorbet::Toon::Normalizer.normalize(
|
|
17
|
+
value,
|
|
18
|
+
signature: signature,
|
|
19
|
+
role: role,
|
|
20
|
+
include_type_metadata: resolved.include_type_metadata
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
Sorbet::Toon::Codec.encode(
|
|
24
|
+
normalized,
|
|
25
|
+
indent: resolved.indent,
|
|
26
|
+
delimiter: resolved.delimiter,
|
|
27
|
+
length_marker: resolved.length_marker
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def extract_overrides(options, keys)
|
|
34
|
+
keys.each_with_object({}) do |key, memo|
|
|
35
|
+
next unless options.key?(key)
|
|
36
|
+
|
|
37
|
+
memo[key] = options.delete(key)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sorbet
|
|
4
|
+
module Toon
|
|
5
|
+
module EnumExtensions
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def from_toon(payload, **options)
|
|
12
|
+
value = Sorbet::Toon.decode(payload, **options)
|
|
13
|
+
return value if value.is_a?(self)
|
|
14
|
+
|
|
15
|
+
if respond_to?(:deserialize)
|
|
16
|
+
deserialize(value)
|
|
17
|
+
else
|
|
18
|
+
values.find { |member| member.serialize == value }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_toon(**options)
|
|
24
|
+
Sorbet::Toon.encode(serialize, **options)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
require_relative 'errors'
|
|
7
|
+
|
|
8
|
+
module Sorbet
|
|
9
|
+
module Toon
|
|
10
|
+
module Normalizer
|
|
11
|
+
class << self
|
|
12
|
+
def normalize(value, signature: nil, role: :output, include_type_metadata: false)
|
|
13
|
+
context = {
|
|
14
|
+
signature: signature,
|
|
15
|
+
role: role,
|
|
16
|
+
include_type_metadata: include_type_metadata
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
normalize_value(value, context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def normalize_value(value, context)
|
|
25
|
+
return nil if value.nil?
|
|
26
|
+
|
|
27
|
+
case value
|
|
28
|
+
when T::Struct
|
|
29
|
+
normalize_struct(value, context)
|
|
30
|
+
when T::Enum
|
|
31
|
+
value.serialize
|
|
32
|
+
when Array
|
|
33
|
+
value.map { |item| normalize_value(item, context) }
|
|
34
|
+
when Set
|
|
35
|
+
value.map { |item| normalize_value(item, context) }
|
|
36
|
+
when Hash
|
|
37
|
+
normalize_hash(value, context)
|
|
38
|
+
else
|
|
39
|
+
normalize_primitive(value)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_struct(struct, context)
|
|
44
|
+
result = {}
|
|
45
|
+
if context[:include_type_metadata]
|
|
46
|
+
result['_type'] = type_label_for(struct.class)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
struct.class.props.each do |prop_name, prop_info|
|
|
50
|
+
prop_value = struct.send(prop_name)
|
|
51
|
+
next if prop_value.nil? && prop_info[:fully_optional]
|
|
52
|
+
|
|
53
|
+
result[prop_name.to_s] = normalize_value(prop_value, context)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def normalize_hash(hash, context)
|
|
60
|
+
hash.each_with_object({}) do |(key, value), memo|
|
|
61
|
+
memo[key_to_string(key)] = normalize_value(value, context)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def normalize_primitive(value)
|
|
66
|
+
case value
|
|
67
|
+
when Float
|
|
68
|
+
return nil unless value.finite?
|
|
69
|
+
return 0.0 if value.zero?
|
|
70
|
+
|
|
71
|
+
value
|
|
72
|
+
else
|
|
73
|
+
if value.respond_to?(:serialize)
|
|
74
|
+
value.serialize
|
|
75
|
+
else
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def key_to_string(key)
|
|
82
|
+
key.to_s
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def type_label_for(klass)
|
|
86
|
+
return 'AnonymousStruct' if klass.name.nil? || klass.name.empty?
|
|
87
|
+
|
|
88
|
+
klass.name.split('::').last
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|