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,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module Sorbet
|
|
7
|
+
module Toon
|
|
8
|
+
module Reconstructor
|
|
9
|
+
class << self
|
|
10
|
+
def reconstruct(value, signature: nil, struct_class: nil, role: :output)
|
|
11
|
+
target_class = struct_class || resolve_struct_class(signature, role)
|
|
12
|
+
return value unless target_class
|
|
13
|
+
|
|
14
|
+
convert_hash_to_struct(value, target_class)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def resolve_struct_class(signature, role)
|
|
20
|
+
return nil unless signature
|
|
21
|
+
|
|
22
|
+
case role
|
|
23
|
+
when :input
|
|
24
|
+
signature.input_struct_class
|
|
25
|
+
else
|
|
26
|
+
signature.output_struct_class
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def convert_hash_to_struct(hash, struct_class)
|
|
31
|
+
return hash unless hash.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
attributes = {}
|
|
34
|
+
|
|
35
|
+
struct_class.props.each do |prop_name, prop_info|
|
|
36
|
+
raw_value = fetch_value(hash, prop_name)
|
|
37
|
+
next if raw_value.nil?
|
|
38
|
+
|
|
39
|
+
type_object = prop_info[:type_object] || T::Utils.coerce(prop_info[:type])
|
|
40
|
+
attributes[prop_name] = convert_value(raw_value, type_object)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
struct_class.new(**attributes)
|
|
44
|
+
rescue StandardError
|
|
45
|
+
hash
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def fetch_value(hash, prop_name)
|
|
49
|
+
key = prop_name.to_s
|
|
50
|
+
return hash[key] if hash.key?(key)
|
|
51
|
+
return hash[prop_name] if hash.key?(prop_name)
|
|
52
|
+
|
|
53
|
+
sym_key = key.to_sym
|
|
54
|
+
hash[sym_key] if hash.key?(sym_key)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def convert_value(value, type_object)
|
|
58
|
+
return nil if value.nil?
|
|
59
|
+
return value unless type_object
|
|
60
|
+
|
|
61
|
+
case type_object
|
|
62
|
+
when T::Types::TypedArray
|
|
63
|
+
convert_typed_array(value, type_object)
|
|
64
|
+
when T::Types::TypedSet
|
|
65
|
+
convert_typed_set(value, type_object)
|
|
66
|
+
when T::Types::TypedHash
|
|
67
|
+
convert_typed_hash(value, type_object)
|
|
68
|
+
when T::Types::Simple
|
|
69
|
+
convert_simple(value, type_object)
|
|
70
|
+
when T::Types::Union
|
|
71
|
+
convert_union(value, type_object)
|
|
72
|
+
else
|
|
73
|
+
value
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def convert_simple(value, simple_type)
|
|
78
|
+
raw = simple_type.raw_type
|
|
79
|
+
return convert_hash_to_struct(value, raw) if struct_class?(raw) && value.is_a?(Hash)
|
|
80
|
+
return deserialize_enum(raw, value) if enum_class?(raw) && !value.is_a?(raw)
|
|
81
|
+
|
|
82
|
+
value
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def convert_typed_array(value, typed_array)
|
|
86
|
+
return value unless value.is_a?(Array)
|
|
87
|
+
|
|
88
|
+
value.map { |element| convert_value(element, typed_array.type) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def convert_typed_set(value, typed_set)
|
|
92
|
+
return value unless value.is_a?(Array)
|
|
93
|
+
|
|
94
|
+
Set.new(value.map { |element| convert_value(element, typed_set.type) })
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def convert_typed_hash(value, typed_hash)
|
|
98
|
+
return value unless value.is_a?(Hash)
|
|
99
|
+
|
|
100
|
+
value.each_with_object({}) do |(key, val), memo|
|
|
101
|
+
converted_key = coerce_hash_key(key, typed_hash.keys)
|
|
102
|
+
memo[converted_key] = convert_value(val, typed_hash.values)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def convert_union(value, union_type)
|
|
107
|
+
return nil if value.nil? && union_type.types.any? { |member| nil_type?(member) }
|
|
108
|
+
|
|
109
|
+
if value.is_a?(Hash)
|
|
110
|
+
explicit_type = union_struct_from_type_field(value, union_type)
|
|
111
|
+
return convert_value(value, explicit_type) if explicit_type
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
union_type.types.each do |member|
|
|
115
|
+
next if nil_type?(member)
|
|
116
|
+
|
|
117
|
+
converted = convert_value(value, member)
|
|
118
|
+
return converted unless converted.equal?(value)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
value
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def union_struct_from_type_field(hash, union_type)
|
|
125
|
+
type_name = hash['_type'] || hash[:_type]
|
|
126
|
+
return nil unless type_name
|
|
127
|
+
|
|
128
|
+
union_type.types.find do |member|
|
|
129
|
+
struct_type?(member) && struct_name(member.raw_type) == type_name
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def struct_type?(type)
|
|
134
|
+
type.is_a?(T::Types::Simple) && struct_class?(type.raw_type)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def nil_type?(type)
|
|
138
|
+
type.is_a?(T::Types::Simple) && type.raw_type == NilClass
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def struct_class?(klass)
|
|
142
|
+
klass.is_a?(Class) && klass < T::Struct
|
|
143
|
+
rescue StandardError
|
|
144
|
+
false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def enum_class?(klass)
|
|
148
|
+
klass.is_a?(Class) && klass < T::Enum
|
|
149
|
+
rescue StandardError
|
|
150
|
+
false
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def deserialize_enum(enum_class, value)
|
|
154
|
+
return value if value.is_a?(enum_class)
|
|
155
|
+
return enum_class.deserialize(value) if enum_class.respond_to?(:deserialize)
|
|
156
|
+
|
|
157
|
+
enum_class.values.find { |member| member.serialize == value } || value
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def coerce_hash_key(key, key_type)
|
|
161
|
+
return key unless key_type.is_a?(T::Types::Simple)
|
|
162
|
+
|
|
163
|
+
case key_type.raw_type
|
|
164
|
+
when Symbol
|
|
165
|
+
key.to_sym
|
|
166
|
+
when Integer
|
|
167
|
+
key.to_i
|
|
168
|
+
when Float
|
|
169
|
+
key.to_f
|
|
170
|
+
else
|
|
171
|
+
key.to_s
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def struct_name(klass)
|
|
176
|
+
klass.name&.split('::')&.last
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../constants'
|
|
4
|
+
|
|
5
|
+
module Sorbet
|
|
6
|
+
module Toon
|
|
7
|
+
module Shared
|
|
8
|
+
module LiteralUtils
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def boolean_or_null_literal?(token)
|
|
12
|
+
return false if token.nil?
|
|
13
|
+
|
|
14
|
+
token == Constants::TRUE_LITERAL ||
|
|
15
|
+
token == Constants::FALSE_LITERAL ||
|
|
16
|
+
token == Constants::NULL_LITERAL
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def numeric_literal?(token)
|
|
20
|
+
return false if token.nil? || token.empty?
|
|
21
|
+
|
|
22
|
+
# Reject numbers with leading zeros (except 0.x cases)
|
|
23
|
+
if token.length > 1 && token.start_with?('0') && token[1] != '.'
|
|
24
|
+
return false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
numeric_value = Float(token)
|
|
28
|
+
numeric_value.finite?
|
|
29
|
+
rescue ArgumentError
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../constants'
|
|
4
|
+
require_relative '../errors'
|
|
5
|
+
|
|
6
|
+
module Sorbet
|
|
7
|
+
module Toon
|
|
8
|
+
module Shared
|
|
9
|
+
module StringUtils
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def escape_string(value)
|
|
13
|
+
value
|
|
14
|
+
.gsub('\\') { Constants::BACKSLASH * 2 }
|
|
15
|
+
.gsub('"', "#{Constants::BACKSLASH}#{Constants::DOUBLE_QUOTE}")
|
|
16
|
+
.gsub("\n", "#{Constants::BACKSLASH}n")
|
|
17
|
+
.gsub("\r", "#{Constants::BACKSLASH}r")
|
|
18
|
+
.gsub("\t", "#{Constants::BACKSLASH}t")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def unescape_string(value)
|
|
22
|
+
result = +''
|
|
23
|
+
i = 0
|
|
24
|
+
while i < value.length
|
|
25
|
+
char = value[i]
|
|
26
|
+
if char == Constants::BACKSLASH
|
|
27
|
+
raise Sorbet::Toon::DecodeError, 'Invalid escape sequence: backslash at end of string' if i + 1 >= value.length
|
|
28
|
+
|
|
29
|
+
next_char = value[i + 1]
|
|
30
|
+
case next_char
|
|
31
|
+
when 'n'
|
|
32
|
+
result << Constants::NEWLINE
|
|
33
|
+
when 't'
|
|
34
|
+
result << Constants::TAB
|
|
35
|
+
when 'r'
|
|
36
|
+
result << Constants::CARRIAGE_RETURN
|
|
37
|
+
when Constants::BACKSLASH
|
|
38
|
+
result << Constants::BACKSLASH
|
|
39
|
+
when Constants::DOUBLE_QUOTE
|
|
40
|
+
result << Constants::DOUBLE_QUOTE
|
|
41
|
+
else
|
|
42
|
+
raise Sorbet::Toon::DecodeError, "Invalid escape sequence: \\#{next_char}"
|
|
43
|
+
end
|
|
44
|
+
i += 2
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result << char
|
|
49
|
+
i += 1
|
|
50
|
+
end
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def find_closing_quote(content, start_index)
|
|
55
|
+
i = start_index + 1
|
|
56
|
+
while i < content.length
|
|
57
|
+
if content[i] == Constants::BACKSLASH && i + 1 < content.length
|
|
58
|
+
i += 2
|
|
59
|
+
next
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return i if content[i] == Constants::DOUBLE_QUOTE
|
|
63
|
+
|
|
64
|
+
i += 1
|
|
65
|
+
end
|
|
66
|
+
-1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find_unquoted_char(content, char, start_index = 0)
|
|
70
|
+
in_quotes = false
|
|
71
|
+
i = start_index
|
|
72
|
+
|
|
73
|
+
while i < content.length
|
|
74
|
+
if content[i] == Constants::BACKSLASH && i + 1 < content.length && in_quotes
|
|
75
|
+
i += 2
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if content[i] == Constants::DOUBLE_QUOTE
|
|
80
|
+
in_quotes = !in_quotes
|
|
81
|
+
i += 1
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
return i if content[i] == char && !in_quotes
|
|
86
|
+
|
|
87
|
+
i += 1
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
-1
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../constants'
|
|
4
|
+
require_relative 'literal_utils'
|
|
5
|
+
|
|
6
|
+
module Sorbet
|
|
7
|
+
module Toon
|
|
8
|
+
module Shared
|
|
9
|
+
module Validation
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
UNQUOTED_KEY_REGEX = /\A[A-Z_][\w.]*\z/i.freeze
|
|
13
|
+
NUMERIC_LIKE_REGEX = /\A-?\d+(?:\.\d+)?(?:e[+-]?\d+)?\z/i.freeze
|
|
14
|
+
LEADING_ZERO_REGEX = /\A0\d+\z/.freeze
|
|
15
|
+
|
|
16
|
+
def valid_unquoted_key?(key)
|
|
17
|
+
UNQUOTED_KEY_REGEX.match?(key)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def safe_unquoted?(value, delimiter = Constants::COMMA)
|
|
21
|
+
return false if value.nil? || value.empty?
|
|
22
|
+
return false if value != value.strip
|
|
23
|
+
return false if LiteralUtils.boolean_or_null_literal?(value) || numeric_like?(value)
|
|
24
|
+
return false if value.include?(Constants::COLON)
|
|
25
|
+
return false if value.include?(Constants::DOUBLE_QUOTE) || value.include?(Constants::BACKSLASH)
|
|
26
|
+
return false if value.match?(/[{}\[\]]/)
|
|
27
|
+
return false if value.match?(/[\n\r\t]/)
|
|
28
|
+
return false if value.include?(delimiter)
|
|
29
|
+
return false if value.start_with?(Constants::LIST_ITEM_MARKER)
|
|
30
|
+
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def numeric_like?(value)
|
|
35
|
+
NUMERIC_LIKE_REGEX.match?(value) || LEADING_ZERO_REGEX.match?(value)
|
|
36
|
+
end
|
|
37
|
+
private_class_method :numeric_like?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sorbet-runtime'
|
|
4
|
+
|
|
5
|
+
module Sorbet
|
|
6
|
+
module Toon
|
|
7
|
+
module SignatureFormatter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def describe_signature(signature_class, role)
|
|
11
|
+
struct_class = struct_for(signature_class, role)
|
|
12
|
+
return "No #{role} fields defined." unless struct_class
|
|
13
|
+
|
|
14
|
+
descriptors = field_descriptors(signature_class, role)
|
|
15
|
+
describe_struct(struct_class, descriptors)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def struct_for(signature_class, role)
|
|
19
|
+
return nil unless signature_class
|
|
20
|
+
|
|
21
|
+
case role
|
|
22
|
+
when :input
|
|
23
|
+
signature_class.input_struct_class if signature_class.respond_to?(:input_struct_class)
|
|
24
|
+
else
|
|
25
|
+
signature_class.output_struct_class if signature_class.respond_to?(:output_struct_class)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
private_class_method :struct_for
|
|
29
|
+
|
|
30
|
+
def field_descriptors(signature_class, role)
|
|
31
|
+
return {} unless signature_class
|
|
32
|
+
|
|
33
|
+
case role
|
|
34
|
+
when :input
|
|
35
|
+
signature_class.respond_to?(:input_field_descriptors) ? signature_class.input_field_descriptors || {} : {}
|
|
36
|
+
else
|
|
37
|
+
signature_class.respond_to?(:output_field_descriptors) ? signature_class.output_field_descriptors || {} : {}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
private_class_method :field_descriptors
|
|
41
|
+
|
|
42
|
+
def describe_struct(struct_class, descriptors)
|
|
43
|
+
lines = []
|
|
44
|
+
|
|
45
|
+
struct_class.props.each do |prop_name, prop_info|
|
|
46
|
+
descriptor = descriptors[prop_name]
|
|
47
|
+
type_object = descriptor&.type || prop_info[:type_object] || prop_info[:type]
|
|
48
|
+
type_info = describe_type(type_object)
|
|
49
|
+
optional = descriptor&.has_default || prop_info[:fully_optional]
|
|
50
|
+
|
|
51
|
+
line = "- #{prop_name}"
|
|
52
|
+
type_label = type_info[:label]
|
|
53
|
+
line << " (#{type_label}"
|
|
54
|
+
line << ', optional' if optional
|
|
55
|
+
line << ')'
|
|
56
|
+
if descriptor&.description
|
|
57
|
+
line << " — #{descriptor.description}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
lines << line
|
|
61
|
+
|
|
62
|
+
if type_info[:tabular_columns]
|
|
63
|
+
lines << " • Tabular columns: #{type_info[:tabular_columns].join(', ')}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
return "No fields defined." if lines.empty?
|
|
68
|
+
|
|
69
|
+
lines.join("\n")
|
|
70
|
+
end
|
|
71
|
+
private_class_method :describe_struct
|
|
72
|
+
|
|
73
|
+
def describe_type(type)
|
|
74
|
+
case type
|
|
75
|
+
when T::Types::TypedArray
|
|
76
|
+
inner = describe_type(type.type)
|
|
77
|
+
{
|
|
78
|
+
label: "Array<#{inner[:label]}>",
|
|
79
|
+
tabular_columns: inner[:tabular_columns]
|
|
80
|
+
}
|
|
81
|
+
when T::Types::TypedSet
|
|
82
|
+
inner = describe_type(type.type)
|
|
83
|
+
{
|
|
84
|
+
label: "Set<#{inner[:label]}>"
|
|
85
|
+
}
|
|
86
|
+
when T::Types::TypedHash
|
|
87
|
+
key = describe_type(type.keys)
|
|
88
|
+
value = describe_type(type.values)
|
|
89
|
+
{
|
|
90
|
+
label: "Hash<#{key[:label]} => #{value[:label]}>"
|
|
91
|
+
}
|
|
92
|
+
when T::Types::Union
|
|
93
|
+
members = type.types.reject { |member| nil_type?(member) }
|
|
94
|
+
labels = members.map { |member| describe_type(member)[:label] }.uniq
|
|
95
|
+
{ label: labels.join(' | ') }
|
|
96
|
+
when T::Private::Types::TypeAlias
|
|
97
|
+
describe_type(type.aliased_type)
|
|
98
|
+
when T::Types::Simple
|
|
99
|
+
describe_class(type.raw_type)
|
|
100
|
+
when Class
|
|
101
|
+
describe_class(type)
|
|
102
|
+
else
|
|
103
|
+
{ label: type_label_from_object(type) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
private_class_method :describe_type
|
|
107
|
+
|
|
108
|
+
def describe_class(klass)
|
|
109
|
+
return { label: 'nil' } if klass.nil?
|
|
110
|
+
|
|
111
|
+
if struct_class?(klass)
|
|
112
|
+
{
|
|
113
|
+
label: klass.name ? klass.name.split('::').last : 'Struct',
|
|
114
|
+
tabular_columns: klass.props.keys.map(&:to_s)
|
|
115
|
+
}
|
|
116
|
+
elsif enum_class?(klass)
|
|
117
|
+
values = klass.respond_to?(:values) ? klass.values.map(&:serialize).join(', ') : ''
|
|
118
|
+
{ label: values.empty? ? klass.name || 'Enum' : "Enum<#{values}>" }
|
|
119
|
+
else
|
|
120
|
+
{ label: primitive_label(klass) }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
private_class_method :describe_class
|
|
124
|
+
|
|
125
|
+
def primitive_label(klass)
|
|
126
|
+
case klass.name
|
|
127
|
+
when 'String' then 'String'
|
|
128
|
+
when 'Integer' then 'Integer'
|
|
129
|
+
when 'Float' then 'Float'
|
|
130
|
+
when 'TrueClass', 'FalseClass' then 'Boolean'
|
|
131
|
+
when 'Numeric' then 'Number'
|
|
132
|
+
when 'Date' then 'Date'
|
|
133
|
+
when 'DateTime', 'Time' then 'DateTime'
|
|
134
|
+
else
|
|
135
|
+
klass.name || klass.to_s
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
private_class_method :primitive_label
|
|
139
|
+
|
|
140
|
+
def type_label_from_object(type)
|
|
141
|
+
if type.respond_to?(:name)
|
|
142
|
+
type.name
|
|
143
|
+
else
|
|
144
|
+
type.to_s
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
private_class_method :type_label_from_object
|
|
148
|
+
|
|
149
|
+
def struct_class?(klass)
|
|
150
|
+
klass.is_a?(Class) && klass < T::Struct
|
|
151
|
+
rescue StandardError
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
private_class_method :struct_class?
|
|
155
|
+
|
|
156
|
+
def enum_class?(klass)
|
|
157
|
+
klass.is_a?(Class) && klass < T::Enum
|
|
158
|
+
rescue StandardError
|
|
159
|
+
false
|
|
160
|
+
end
|
|
161
|
+
private_class_method :enum_class?
|
|
162
|
+
|
|
163
|
+
def nil_type?(type)
|
|
164
|
+
(type.is_a?(T::Types::Simple) && type.raw_type == NilClass) ||
|
|
165
|
+
type == T::Utils.coerce(NilClass)
|
|
166
|
+
rescue StandardError
|
|
167
|
+
false
|
|
168
|
+
end
|
|
169
|
+
private_class_method :nil_type?
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sorbet
|
|
4
|
+
module Toon
|
|
5
|
+
module StructExtensions
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def from_toon(payload, **options)
|
|
12
|
+
Sorbet::Toon.decode(payload, struct_class: self, **options)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_toon(**options)
|
|
17
|
+
Sorbet::Toon.encode(self, **options)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/sorbet/toon.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sorbet-runtime'
|
|
4
|
+
|
|
5
|
+
require_relative 'toon/version'
|
|
6
|
+
require_relative 'toon/errors'
|
|
7
|
+
require_relative 'toon/constants'
|
|
8
|
+
require_relative 'toon/codec'
|
|
9
|
+
require_relative 'toon/normalizer'
|
|
10
|
+
require_relative 'toon/config'
|
|
11
|
+
require_relative 'toon/encoder'
|
|
12
|
+
require_relative 'toon/decoder'
|
|
13
|
+
require_relative 'toon/reconstructor'
|
|
14
|
+
require_relative 'toon/signature_formatter'
|
|
15
|
+
require_relative 'toon/struct_extensions'
|
|
16
|
+
require_relative 'toon/enum_extensions'
|
|
17
|
+
|
|
18
|
+
module Sorbet
|
|
19
|
+
module Toon
|
|
20
|
+
class << self
|
|
21
|
+
def encode(value, **options)
|
|
22
|
+
Encoder.encode(value, config: config, **options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def decode(payload, struct_class: nil, **options)
|
|
26
|
+
Decoder.decode(payload, config: config, struct_class: struct_class, **options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def configure
|
|
30
|
+
yield(config)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def config
|
|
34
|
+
@config ||= Config.new
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reset_config!(new_config = nil)
|
|
38
|
+
@config = new_config&.copy || Config.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def enable_extensions!
|
|
42
|
+
return if extensions_enabled?
|
|
43
|
+
|
|
44
|
+
T::Struct.include(Sorbet::Toon::StructExtensions)
|
|
45
|
+
T::Enum.include(Sorbet::Toon::EnumExtensions)
|
|
46
|
+
@extensions_enabled = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def extensions_enabled?
|
|
50
|
+
!!@extensions_enabled
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Sorbet::Toon.enable_extensions!
|
metadata
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: sorbet-toon
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vicente Reig Rincón de Arellano
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: sorbet-runtime
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.5'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.5'
|
|
26
|
+
description: Ruby port of the TOON encoder/decoder used inside DSPy.rb. Provides Sorbet-aware
|
|
27
|
+
normalization, reconstruction, and prompt-ready helpers so signatures can round-trip
|
|
28
|
+
through TOON without hand-written serializers.
|
|
29
|
+
email:
|
|
30
|
+
- hey@vicente.services
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE
|
|
36
|
+
- lib/sorbet/toon.rb
|
|
37
|
+
- lib/sorbet/toon/README.md
|
|
38
|
+
- lib/sorbet/toon/codec.rb
|
|
39
|
+
- lib/sorbet/toon/config.rb
|
|
40
|
+
- lib/sorbet/toon/constants.rb
|
|
41
|
+
- lib/sorbet/toon/decode/decoders.rb
|
|
42
|
+
- lib/sorbet/toon/decode/parser.rb
|
|
43
|
+
- lib/sorbet/toon/decode/scanner.rb
|
|
44
|
+
- lib/sorbet/toon/decode/validation.rb
|
|
45
|
+
- lib/sorbet/toon/decoder.rb
|
|
46
|
+
- lib/sorbet/toon/encode/encoders.rb
|
|
47
|
+
- lib/sorbet/toon/encode/normalize.rb
|
|
48
|
+
- lib/sorbet/toon/encode/primitives.rb
|
|
49
|
+
- lib/sorbet/toon/encode/writer.rb
|
|
50
|
+
- lib/sorbet/toon/encoder.rb
|
|
51
|
+
- lib/sorbet/toon/enum_extensions.rb
|
|
52
|
+
- lib/sorbet/toon/errors.rb
|
|
53
|
+
- lib/sorbet/toon/normalizer.rb
|
|
54
|
+
- lib/sorbet/toon/reconstructor.rb
|
|
55
|
+
- lib/sorbet/toon/shared/literal_utils.rb
|
|
56
|
+
- lib/sorbet/toon/shared/string_utils.rb
|
|
57
|
+
- lib/sorbet/toon/shared/validation.rb
|
|
58
|
+
- lib/sorbet/toon/signature_formatter.rb
|
|
59
|
+
- lib/sorbet/toon/struct_extensions.rb
|
|
60
|
+
- lib/sorbet/toon/version.rb
|
|
61
|
+
homepage: https://github.com/vicentereig/dspy.rb/blob/main/lib/sorbet/toon/README.md
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://github.com/vicentereig/dspy.rb/blob/main/lib/sorbet/toon/README.md
|
|
66
|
+
documentation_uri: https://github.com/vicentereig/dspy.rb/blob/main/lib/sorbet/toon/README.md
|
|
67
|
+
rdoc_options: []
|
|
68
|
+
require_paths:
|
|
69
|
+
- lib
|
|
70
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.1'
|
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '0'
|
|
80
|
+
requirements: []
|
|
81
|
+
rubygems_version: 3.6.9
|
|
82
|
+
specification_version: 4
|
|
83
|
+
summary: TOON encode/decode pipeline for Sorbet signatures.
|
|
84
|
+
test_files: []
|