typespec_from_serializers 0.1.1 → 0.2.1
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/CHANGELOG.md +46 -1
- data/README.md +3 -8
- data/lib/typespec_from_serializers/dsl/controller.rb +43 -0
- data/lib/typespec_from_serializers/dsl/serializer.rb +74 -0
- data/lib/typespec_from_serializers/dsl.rb +2 -44
- data/lib/typespec_from_serializers/generator.rb +838 -120
- data/lib/typespec_from_serializers/io.rb +30 -0
- data/lib/typespec_from_serializers/openapi_compiler.rb +62 -0
- data/lib/typespec_from_serializers/railtie.rb +91 -3
- data/lib/typespec_from_serializers/rbi.rb +186 -0
- data/lib/typespec_from_serializers/runner.rb +54 -0
- data/lib/typespec_from_serializers/sorbet.rb +461 -0
- data/lib/typespec_from_serializers/version.rb +1 -1
- data/lib/typespec_from_serializers.rb +2 -0
- metadata +51 -2
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TypeSpecFromSerializers
|
|
4
|
+
# Public: Sorbet integration for type inference.
|
|
5
|
+
#
|
|
6
|
+
# This module provides optional integration with Sorbet's runtime type system
|
|
7
|
+
# and RBI files. It can extract type information from:
|
|
8
|
+
# 1. Runtime signatures (T::Utils.signature_for_method)
|
|
9
|
+
# 2. RBI files generated by Tapioca (in sorbet/rbi/ directory)
|
|
10
|
+
#
|
|
11
|
+
# The module gracefully degrades when sorbet-runtime is not present, making
|
|
12
|
+
# Sorbet integration completely optional with zero configuration required.
|
|
13
|
+
module Sorbet
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Public: Check if Sorbet runtime is available.
|
|
17
|
+
#
|
|
18
|
+
# Returns true if sorbet-runtime gem is loaded and the necessary APIs
|
|
19
|
+
# are available for type extraction.
|
|
20
|
+
#
|
|
21
|
+
# Returns Boolean
|
|
22
|
+
def available?
|
|
23
|
+
!!(defined?(T::Utils) && T::Utils.respond_to?(:signature_for_method))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Public: Get the RBI directory path.
|
|
27
|
+
#
|
|
28
|
+
# Returns Pathname or nil if directory doesn't exist or can't be accessed
|
|
29
|
+
def rbi_path
|
|
30
|
+
return @rbi_path if defined?(@rbi_path)
|
|
31
|
+
|
|
32
|
+
@rbi_path = begin
|
|
33
|
+
TypeSpecFromSerializers.rbi_dir.then { |path| path if path.exist? && path.directory? }
|
|
34
|
+
rescue
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Public: Check if RBI files are available for parsing.
|
|
40
|
+
#
|
|
41
|
+
# Returns true if sorbet/rbi/ directory exists (typical Tapioca setup).
|
|
42
|
+
#
|
|
43
|
+
# Returns Boolean
|
|
44
|
+
def rbi_available?
|
|
45
|
+
!rbi_path.nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Public: Extract type information for a method from its Sorbet signature.
|
|
49
|
+
#
|
|
50
|
+
# Tries multiple sources in order:
|
|
51
|
+
# 1. Runtime reflection (T::Utils.signature_for_method)
|
|
52
|
+
# 2. RBI files (if available)
|
|
53
|
+
#
|
|
54
|
+
# klass - The Class containing the method
|
|
55
|
+
# method_name - The Symbol or String name of the method
|
|
56
|
+
#
|
|
57
|
+
# Returns a Hash with keys:
|
|
58
|
+
# :typespec_type - Symbol or String representing the TypeSpec type
|
|
59
|
+
# :nilable - Boolean indicating if type is optional
|
|
60
|
+
# :array - Boolean indicating if type is an array
|
|
61
|
+
# Returns nil if no signature found
|
|
62
|
+
#
|
|
63
|
+
# Examples
|
|
64
|
+
#
|
|
65
|
+
# class MySerializer
|
|
66
|
+
# extend T::Sig
|
|
67
|
+
#
|
|
68
|
+
# sig { returns(String) }
|
|
69
|
+
# def name
|
|
70
|
+
# "example"
|
|
71
|
+
# end
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# extract_type_for(MySerializer, :name)
|
|
75
|
+
# # => { typespec_type: :string, nilable: false, array: false }
|
|
76
|
+
#
|
|
77
|
+
def extract_type_for(klass, method_name)
|
|
78
|
+
# Try TypeDSL first (if available), then runtime reflection, then RBI files
|
|
79
|
+
extract_from_type_dsl(klass, method_name) ||
|
|
80
|
+
(available? ? extract_from_runtime(klass, method_name) : nil) ||
|
|
81
|
+
(rbi_available? ? extract_from_rbi(klass, method_name) : nil)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Internal: Extract type from TypeDSL declaration.
|
|
88
|
+
def extract_from_type_dsl(klass, method_name)
|
|
89
|
+
return nil unless klass.respond_to?(:type_for_method)
|
|
90
|
+
|
|
91
|
+
type_annotation = klass.type_for_method(method_name)
|
|
92
|
+
return nil unless type_annotation
|
|
93
|
+
|
|
94
|
+
# Handle plain Ruby classes
|
|
95
|
+
if type_annotation.is_a?(Class)
|
|
96
|
+
type_name = type_annotation.name
|
|
97
|
+
mapped_type = config_type_mapping[type_name]
|
|
98
|
+
result = {
|
|
99
|
+
typespec_type: mapped_type || type_name,
|
|
100
|
+
nilable: false,
|
|
101
|
+
array: false,
|
|
102
|
+
type_class: type_annotation,
|
|
103
|
+
}
|
|
104
|
+
return result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Handle Sorbet type annotations
|
|
108
|
+
sorbet_type_to_typespec(type_annotation)
|
|
109
|
+
rescue => e
|
|
110
|
+
warn "TypeSpec: Failed to extract type for #{klass}##{method_name}: #{e.class} - #{e.message}"
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Internal: Get type mapping from config.
|
|
115
|
+
def config_type_mapping
|
|
116
|
+
TypeSpecFromSerializers.config.sorbet_to_typespec_type_mapping
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Internal: Extract type from runtime reflection.
|
|
120
|
+
def extract_from_runtime(klass, method_name)
|
|
121
|
+
method = klass.instance_method(method_name.to_sym)
|
|
122
|
+
extract_from_method(method)
|
|
123
|
+
rescue NameError
|
|
124
|
+
# Method doesn't exist on this class
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Internal: Extract type from RBI files.
|
|
129
|
+
def extract_from_rbi(klass, method_name)
|
|
130
|
+
class_name = klass.name
|
|
131
|
+
return nil unless class_name
|
|
132
|
+
|
|
133
|
+
# Look up in unified RBI index
|
|
134
|
+
signature = rbi_unified_index["#{class_name}##{method_name}"]
|
|
135
|
+
return nil unless signature
|
|
136
|
+
|
|
137
|
+
# Parse the signature return type
|
|
138
|
+
parse_rbi_return_type(signature)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Internal: Build a unified index from all RBI files.
|
|
142
|
+
#
|
|
143
|
+
# Returns Hash mapping "ClassName#method_name" => return_type_string
|
|
144
|
+
def rbi_unified_index
|
|
145
|
+
return @rbi_unified_index if defined?(@rbi_unified_index)
|
|
146
|
+
|
|
147
|
+
@rbi_unified_index = {}
|
|
148
|
+
return @rbi_unified_index unless rbi_path&.exist?
|
|
149
|
+
|
|
150
|
+
# Find RBI files, excluding gems/ (third-party gems won't have serializer methods)
|
|
151
|
+
# Only check annotations/ and dsl/ directories for performance
|
|
152
|
+
rbi_files = [
|
|
153
|
+
*Pathname.glob(rbi_path.join("annotations", "**", "*.rbi")),
|
|
154
|
+
*Pathname.glob(rbi_path.join("dsl", "**", "*.rbi")),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Parse each file and merge into unified index
|
|
158
|
+
rbi_files.each do |rbi_file|
|
|
159
|
+
tree = parse_rbi_file_cached(rbi_file)
|
|
160
|
+
next unless tree
|
|
161
|
+
|
|
162
|
+
# Extract all method signatures from this file
|
|
163
|
+
tree.index.each do |fqn, nodes|
|
|
164
|
+
next unless fqn.include?("#") # Only instance methods
|
|
165
|
+
|
|
166
|
+
nodes.each do |node|
|
|
167
|
+
return_type = node&.sigs&.first&.return_type
|
|
168
|
+
@rbi_unified_index[fqn] ||= return_type if return_type
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
rescue
|
|
172
|
+
# Ignore errors in individual files
|
|
173
|
+
next
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
@rbi_unified_index
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Internal: Parse an RBI file and cache the result.
|
|
180
|
+
#
|
|
181
|
+
# Returns parsed RBI tree or nil if parsing fails
|
|
182
|
+
def parse_rbi_file_cached(rbi_file)
|
|
183
|
+
@rbi_tree_cache ||= {}
|
|
184
|
+
@rbi_tree_cache[rbi_file.to_s] ||= ::RBI::Parser.parse_file(rbi_file)
|
|
185
|
+
rescue
|
|
186
|
+
# Cache the failure to avoid re-parsing
|
|
187
|
+
@rbi_tree_cache[rbi_file.to_s] = nil
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Internal: Parse a return type from RBI signature using RBI type system.
|
|
191
|
+
#
|
|
192
|
+
# type_string - String like "String", "T.nilable(Integer)", "T::Array[String]"
|
|
193
|
+
#
|
|
194
|
+
# Returns Hash with :typespec_type, :nilable, :array or nil
|
|
195
|
+
def parse_rbi_return_type(type_string)
|
|
196
|
+
type_string&.strip&.then { |s| ::RBI::Type.parse_string(s) }&.then { |t| rbi_type_to_typespec(t) }
|
|
197
|
+
rescue
|
|
198
|
+
# Parsing failed - return nil
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Internal: Convert an RBI::Type to TypeSpec type information.
|
|
203
|
+
#
|
|
204
|
+
# rbi_type - RBI::Type object (Nilable, Generic, Simple, etc.)
|
|
205
|
+
#
|
|
206
|
+
# Returns Hash with :typespec_type, :nilable, :array or nil
|
|
207
|
+
def rbi_type_to_typespec(rbi_type)
|
|
208
|
+
result = {
|
|
209
|
+
typespec_type: nil,
|
|
210
|
+
nilable: false,
|
|
211
|
+
array: false,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Unwrap T.nilable
|
|
215
|
+
if rbi_type.is_a?(::RBI::Type::Nilable)
|
|
216
|
+
result[:nilable] = true
|
|
217
|
+
rbi_type = rbi_type.type
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Unwrap T::Array
|
|
221
|
+
if rbi_type.is_a?(::RBI::Type::Generic) &&
|
|
222
|
+
rbi_type.name.in?(["::T::Array", "T::Array"])
|
|
223
|
+
result[:array] = true
|
|
224
|
+
rbi_type = rbi_type.params.first
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Extract the base type name
|
|
228
|
+
type_name = case rbi_type
|
|
229
|
+
when ::RBI::Type::Simple, ::RBI::Type::Generic
|
|
230
|
+
rbi_type.name
|
|
231
|
+
else
|
|
232
|
+
rbi_type.to_s
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Map to TypeSpec type (remove leading :: if present)
|
|
236
|
+
result[:typespec_type] = map_rbi_type(type_name.delete_prefix("::"))
|
|
237
|
+
|
|
238
|
+
result[:typespec_type] ? result : nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Internal: Map an RBI type string to TypeSpec type.
|
|
242
|
+
def map_rbi_type(type_string)
|
|
243
|
+
TypeSpecFromSerializers.config.sorbet_to_typespec_type_mapping[type_string] ||
|
|
244
|
+
((type_string == "T::Boolean") ? :boolean : type_string)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Internal: Extract type information from a Method object.
|
|
248
|
+
def extract_from_method(method)
|
|
249
|
+
sig = T::Utils.signature_for_method(method)
|
|
250
|
+
return nil unless sig&.return_type
|
|
251
|
+
|
|
252
|
+
sorbet_type_to_typespec(sig.return_type)
|
|
253
|
+
rescue
|
|
254
|
+
# Sorbet introspection failed, fall back to other inference methods
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Internal: Convert a Sorbet type object to TypeSpec type information.
|
|
259
|
+
#
|
|
260
|
+
# sorbet_type - The Sorbet type object from the signature
|
|
261
|
+
#
|
|
262
|
+
# Returns Hash with :typespec_type, :nilable, :array, :type_class keys
|
|
263
|
+
# Returns nil if type cannot be mapped
|
|
264
|
+
def sorbet_type_to_typespec(sorbet_type)
|
|
265
|
+
base_result = {typespec_type: nil, nilable: false, array: false, type_class: nil}
|
|
266
|
+
|
|
267
|
+
# Unwrap T.nilable if present
|
|
268
|
+
if nilable?(sorbet_type)
|
|
269
|
+
base_result[:nilable] = true
|
|
270
|
+
sorbet_type = unwrap_nilable(sorbet_type)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Dispatch to specific type handlers
|
|
274
|
+
handle_array_type(sorbet_type, base_result) ||
|
|
275
|
+
handle_hash_type(sorbet_type, base_result) ||
|
|
276
|
+
handle_set_type(sorbet_type, base_result) ||
|
|
277
|
+
handle_union_type(sorbet_type, base_result) ||
|
|
278
|
+
handle_untyped(sorbet_type, base_result) ||
|
|
279
|
+
handle_simple_type(sorbet_type, base_result)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Internal: Handle T::Array types
|
|
283
|
+
def handle_array_type(sorbet_type, result)
|
|
284
|
+
return nil unless sorbet_type.is_a?(T::Types::TypedArray)
|
|
285
|
+
|
|
286
|
+
result[:array] = true
|
|
287
|
+
element_type = sorbet_type.type
|
|
288
|
+
|
|
289
|
+
# Shape types inside arrays: T::Array[{lon: Float, lat: Float}]
|
|
290
|
+
if shape_type = extract_shape_typespec(element_type)
|
|
291
|
+
return result.merge(typespec_type: shape_type)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Nested complex types: T::Array[T::Array[X]], T::Array[T::Hash[K,V]]
|
|
295
|
+
if complex_element?(element_type)
|
|
296
|
+
nested = sorbet_type_to_typespec(element_type)
|
|
297
|
+
return nested&.then { |n|
|
|
298
|
+
typespec = n[:array] ? "#{n[:typespec_type]}[]" : n[:typespec_type]
|
|
299
|
+
result.merge(typespec_type: typespec)
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Simple array elements
|
|
304
|
+
handle_simple_type(element_type, result)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Internal: Handle T::Hash types
|
|
308
|
+
def handle_hash_type(sorbet_type, result)
|
|
309
|
+
return nil unless sorbet_type.is_a?(T::Types::TypedHash)
|
|
310
|
+
|
|
311
|
+
value_type = map_sorbet_type(sorbet_type.values)
|
|
312
|
+
result.merge(typespec_type: "Record<#{value_type}>")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Internal: Handle T::Set types (converted to arrays in TypeSpec)
|
|
316
|
+
def handle_set_type(sorbet_type, result)
|
|
317
|
+
return nil unless defined?(T::Types::TypedSet) && sorbet_type.is_a?(T::Types::TypedSet)
|
|
318
|
+
|
|
319
|
+
element_type = map_sorbet_type(sorbet_type.type)
|
|
320
|
+
result.merge(typespec_type: element_type, array: true)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Internal: Handle T.any union types
|
|
324
|
+
def handle_union_type(sorbet_type, result)
|
|
325
|
+
return nil unless sorbet_type.is_a?(T::Types::Union) && !nilable?(sorbet_type)
|
|
326
|
+
|
|
327
|
+
non_nil_types = sorbet_type.types.reject { |t| t == T::Utils.coerce(NilClass) }
|
|
328
|
+
|
|
329
|
+
case non_nil_types.size
|
|
330
|
+
when 1
|
|
331
|
+
sorbet_type_to_typespec(non_nil_types.first)
|
|
332
|
+
else
|
|
333
|
+
mapped_types = non_nil_types.map { |t| map_sorbet_type(t) }.compact.uniq
|
|
334
|
+
typespec = mapped_types.size == 1 ? mapped_types.first : mapped_types.join(" | ")
|
|
335
|
+
result.merge(typespec_type: typespec)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Internal: Handle T.untyped
|
|
340
|
+
def handle_untyped(sorbet_type, result)
|
|
341
|
+
return nil unless sorbet_type.is_a?(T::Types::Untyped)
|
|
342
|
+
|
|
343
|
+
result.merge(typespec_type: "unknown")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Internal: Handle simple types (String, Integer, custom classes, etc.)
|
|
347
|
+
def handle_simple_type(sorbet_type, result)
|
|
348
|
+
result[:type_class] = extract_type_class(sorbet_type)
|
|
349
|
+
result[:typespec_type] = map_sorbet_type(sorbet_type)
|
|
350
|
+
result[:typespec_type] ? result : nil
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Internal: Extract shape type from FixedHash (e.g., {lon: Float, lat: Float})
|
|
354
|
+
def extract_shape_typespec(type)
|
|
355
|
+
return nil unless defined?(T::Types::FixedHash) && type.is_a?(T::Types::FixedHash)
|
|
356
|
+
|
|
357
|
+
# Access shape fields through multiple fallback paths
|
|
358
|
+
types_hash = [:@types, :@inner_types].find do |ivar|
|
|
359
|
+
type.instance_variable_get(ivar) rescue nil
|
|
360
|
+
end&.then { |ivar| type.instance_variable_get(ivar) } ||
|
|
361
|
+
(type.types if type.respond_to?(:types))
|
|
362
|
+
|
|
363
|
+
return nil unless types_hash
|
|
364
|
+
|
|
365
|
+
fields = types_hash.transform_values { |v| map_sorbet_type(v) }
|
|
366
|
+
"{#{fields.map { |k, v| "#{k}: #{v}" }.join(", ")}}"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Internal: Check if type is a complex nested type
|
|
370
|
+
def complex_element?(type)
|
|
371
|
+
type.is_a?(T::Types::TypedArray) ||
|
|
372
|
+
type.is_a?(T::Types::TypedHash) ||
|
|
373
|
+
(defined?(T::Types::TypedSet) && type.is_a?(T::Types::TypedSet))
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Internal: Extract the Ruby class from a Sorbet type object.
|
|
377
|
+
#
|
|
378
|
+
# sorbet_type - The Sorbet type object
|
|
379
|
+
#
|
|
380
|
+
# Returns Class or nil
|
|
381
|
+
def extract_type_class(sorbet_type)
|
|
382
|
+
# Try multiple extraction strategies in order of preference
|
|
383
|
+
extract_from_pair_union(sorbet_type) ||
|
|
384
|
+
extract_from_typed_wrapper(sorbet_type) ||
|
|
385
|
+
extract_direct_class(sorbet_type)
|
|
386
|
+
rescue
|
|
387
|
+
# Type introspection failed - return nil
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Internal: Extract class from SimplePairUnion (e.g., T::Boolean)
|
|
391
|
+
def extract_from_pair_union(sorbet_type)
|
|
392
|
+
return nil unless sorbet_type.instance_of?(::T::Private::Types::SimplePairUnion)
|
|
393
|
+
|
|
394
|
+
[:@raw_a, :@raw_b]
|
|
395
|
+
.map { |ivar| sorbet_type.instance_variable_get(ivar) }
|
|
396
|
+
.reject { |type| type == NilClass }
|
|
397
|
+
.find { |type| type.is_a?(Class) }
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Internal: Extract class from typed wrappers (TypedArray, Simple, etc.)
|
|
401
|
+
def extract_from_typed_wrapper(sorbet_type)
|
|
402
|
+
[:type, :raw_type]
|
|
403
|
+
.lazy
|
|
404
|
+
.select { |method| sorbet_type.respond_to?(method) }
|
|
405
|
+
.map { |method| sorbet_type.public_send(method) }
|
|
406
|
+
.find { |value| value.is_a?(Class) }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Internal: Extract class if sorbet_type itself is a Class
|
|
410
|
+
def extract_direct_class(sorbet_type)
|
|
411
|
+
sorbet_type if sorbet_type.is_a?(Class)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Internal: Check if a Sorbet type is nilable (T.nilable wrapper).
|
|
415
|
+
def nilable?(sorbet_type)
|
|
416
|
+
# Handle SimplePairUnion (T.nilable in runtime)
|
|
417
|
+
if sorbet_type.instance_of?(::T::Private::Types::SimplePairUnion)
|
|
418
|
+
return sorbet_type.instance_variable_get(:@raw_b) == NilClass ||
|
|
419
|
+
sorbet_type.instance_variable_get(:@raw_a) == NilClass
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# T.nilable wraps types in a T::Types::Union with NilClass
|
|
423
|
+
sorbet_type.is_a?(T::Types::Union) &&
|
|
424
|
+
sorbet_type.types.any? { |t| t == T::Utils.coerce(NilClass) }
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Internal: Unwrap T.nilable to get the underlying type.
|
|
428
|
+
def unwrap_nilable(sorbet_type)
|
|
429
|
+
return sorbet_type unless nilable?(sorbet_type)
|
|
430
|
+
|
|
431
|
+
# Handle SimplePairUnion
|
|
432
|
+
if sorbet_type.instance_of?(::T::Private::Types::SimplePairUnion)
|
|
433
|
+
raw_a, raw_b = sorbet_type.instance_variable_get(:@raw_a), sorbet_type.instance_variable_get(:@raw_b)
|
|
434
|
+
return (raw_a == NilClass) ? raw_b : raw_a
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Find the non-nil type in the union
|
|
438
|
+
sorbet_type.types.find { |t| t != T::Utils.coerce(NilClass) }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Internal: Map a Sorbet type to a TypeSpec type.
|
|
442
|
+
#
|
|
443
|
+
# Returns Symbol for built-in types, String for custom types, or nil
|
|
444
|
+
def map_sorbet_type(sorbet_type)
|
|
445
|
+
type_class = extract_type_class(sorbet_type)
|
|
446
|
+
return nil unless type_class
|
|
447
|
+
|
|
448
|
+
type_name = type_class.name
|
|
449
|
+
return "unknown" unless type_name
|
|
450
|
+
|
|
451
|
+
config.sorbet_to_typespec_type_mapping[type_name] ||
|
|
452
|
+
(type_name == "T::Boolean" ? :boolean : type_name)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Internal: Shorthand for config access
|
|
456
|
+
def config
|
|
457
|
+
TypeSpecFromSerializers.config
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: typespec_from_serializers
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Danila Poyarkov
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2025-
|
|
12
|
+
date: 2025-12-19 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: railties
|
|
@@ -59,6 +59,34 @@ dependencies:
|
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '3.2'
|
|
62
|
+
- !ruby/object:Gem::Dependency
|
|
63
|
+
name: rbi
|
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0.3'
|
|
69
|
+
type: :runtime
|
|
70
|
+
prerelease: false
|
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.3'
|
|
76
|
+
- !ruby/object:Gem::Dependency
|
|
77
|
+
name: prism
|
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 1.0.0
|
|
83
|
+
type: :runtime
|
|
84
|
+
prerelease: false
|
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 1.0.0
|
|
62
90
|
- !ruby/object:Gem::Dependency
|
|
63
91
|
name: bundler
|
|
64
92
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -241,6 +269,20 @@ dependencies:
|
|
|
241
269
|
- - ">="
|
|
242
270
|
- !ruby/object:Gem::Version
|
|
243
271
|
version: '0'
|
|
272
|
+
- !ruby/object:Gem::Dependency
|
|
273
|
+
name: sorbet-runtime
|
|
274
|
+
requirement: !ruby/object:Gem::Requirement
|
|
275
|
+
requirements:
|
|
276
|
+
- - ">="
|
|
277
|
+
- !ruby/object:Gem::Version
|
|
278
|
+
version: '0'
|
|
279
|
+
type: :development
|
|
280
|
+
prerelease: false
|
|
281
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
282
|
+
requirements:
|
|
283
|
+
- - ">="
|
|
284
|
+
- !ruby/object:Gem::Version
|
|
285
|
+
version: '0'
|
|
244
286
|
description: typespec_from_serializers helps you by automatically generating TypeSpec
|
|
245
287
|
descriptions for your JSON serializers, allowing you typecheck your frontend code
|
|
246
288
|
to ship fast and with confidence.
|
|
@@ -256,8 +298,15 @@ files:
|
|
|
256
298
|
- README.md
|
|
257
299
|
- lib/typespec_from_serializers.rb
|
|
258
300
|
- lib/typespec_from_serializers/dsl.rb
|
|
301
|
+
- lib/typespec_from_serializers/dsl/controller.rb
|
|
302
|
+
- lib/typespec_from_serializers/dsl/serializer.rb
|
|
259
303
|
- lib/typespec_from_serializers/generator.rb
|
|
304
|
+
- lib/typespec_from_serializers/io.rb
|
|
305
|
+
- lib/typespec_from_serializers/openapi_compiler.rb
|
|
260
306
|
- lib/typespec_from_serializers/railtie.rb
|
|
307
|
+
- lib/typespec_from_serializers/rbi.rb
|
|
308
|
+
- lib/typespec_from_serializers/runner.rb
|
|
309
|
+
- lib/typespec_from_serializers/sorbet.rb
|
|
261
310
|
- lib/typespec_from_serializers/version.rb
|
|
262
311
|
homepage: https://github.com/dannote/typespec_from_serializers
|
|
263
312
|
licenses:
|