sorbet_typed-props 0.1.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 +7 -0
- data/COPYING +674 -0
- data/README.md +131 -0
- data/lib/sorbet_typed/props/version.rb +8 -0
- data/lib/sorbet_typed/props.rb +18 -0
- data/lib/sorbet_typed/railway.rb.tmp +9 -0
- data/lib/sorbet_typed/railway.tmp/fail_fast.rb.tmp +16 -0
- data/lib/sorbet_typed/railway.tmp/railway.rb.tmp +259 -0
- data/lib/sorbet_typed/railway.tmp/version.rb.tmp +8 -0
- data/lib/sorbet_typed/railway.tmp/version_spec.rb.tmp +10 -0
- data/lib/sorbet_typed/railway_spec.rb.tmp +5 -0
- data/lib/tapioca/dsl/compilers/sorbet_typed_props_constructor/prop_to_param_converter.rb +86 -0
- data/lib/tapioca/dsl/compilers/sorbet_typed_props_constructor.rb +75 -0
- metadata +100 -0
data/README.md
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
# SorbetTyped::Props
|
2
|
+
|
3
|
+
An extension of sorbets native props syntax, to make it usable and fully typed in any class. Mainly provides a tapioca
|
4
|
+
dsl compiler to generate the initializer signature when using props outside of `T::Struct`.
|
5
|
+
|
6
|
+
You can use the [`T::Struct` `props` and `const` syntax](https://sorbet.org/docs/tstruct) to define attributes on any
|
7
|
+
class.
|
8
|
+
|
9
|
+
This should make it easier to create classes with a set of attributes they should be initialized with. Inspiration was
|
10
|
+
the
|
11
|
+
[integration of literal properties into phlex](https://www.phlex.fun/miscellaneous/literal-properties.html#literal-properties),
|
12
|
+
which doesn't really work with sorbet, if you want to have everything fully typed (and not use two runtime typesystems
|
13
|
+
in parallel).
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Install the gem and add to the application's Gemfile by executing:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
bundle add sorbet_typed-props
|
21
|
+
```
|
22
|
+
|
23
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
24
|
+
|
25
|
+
```bash
|
26
|
+
gem install sorbet_typed-props
|
27
|
+
```
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
Include the `SorbetTyped::Props` module in any class you want to have `T::Struct`-like attributes:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class MyClass
|
35
|
+
include SorbetTyped::Props
|
36
|
+
|
37
|
+
prop :my_prop, String
|
38
|
+
end
|
39
|
+
|
40
|
+
my_object = MyClass.new(my_prop: 'foo')
|
41
|
+
|
42
|
+
my_object.my_prop # => "foo"
|
43
|
+
my_object.my_prop = 'bar'
|
44
|
+
```
|
45
|
+
|
46
|
+
### Method Visibility
|
47
|
+
|
48
|
+
If you want attributes in your initializer but not be part of your public class interface, you can use ruby's visibility
|
49
|
+
modifiers. Unfortunately I found no better way and did not want to modify sorbet's prop syntax.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class MyClass
|
53
|
+
extend T::Sig
|
54
|
+
include SorbetTyped::Props
|
55
|
+
|
56
|
+
prop :my_prop, Integer # reader and writer are public
|
57
|
+
const :my_const, String # reader ist public, has no writer
|
58
|
+
|
59
|
+
prop :prop_with_private_writer, String # reader is public, writer should be private
|
60
|
+
private :prop_with_private_writer= # makes the writer private
|
61
|
+
|
62
|
+
const :my_private_prop, String # reader and writer are private
|
63
|
+
private :my_private_prop, :my_private_prop=
|
64
|
+
|
65
|
+
sig { void }
|
66
|
+
def foo
|
67
|
+
self.prop_with_private_writer = 'foo'
|
68
|
+
end
|
69
|
+
|
70
|
+
sig { returns(String) }
|
71
|
+
def bar
|
72
|
+
self.my_private_prop
|
73
|
+
end
|
74
|
+
|
75
|
+
sig { void }
|
76
|
+
def baz
|
77
|
+
self.my_private_prop = 'baz'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
my_object = MyClass.new(my_prop: 1, my_const: 'my_const', prop_with_private_writer: 'abc', my_private_prop: 'xyz')
|
82
|
+
|
83
|
+
my_object.my_prop # => 1
|
84
|
+
my_object.my_prop = 2
|
85
|
+
|
86
|
+
my_object.my_const # => "my_const"
|
87
|
+
my_object.my_const = 'abc' # => Setter method `my_const=` does not exist on `MyClass`
|
88
|
+
|
89
|
+
my_object.prop_with_private_writer # => "abc"
|
90
|
+
my_object.prop_with_private_writer = 'foo' # => Non-private call to private method `prop_with_private_writer=` on `MyClass`
|
91
|
+
my_object.foo
|
92
|
+
my_object.prop_with_private_writer # => "foo"
|
93
|
+
|
94
|
+
my_object.my_private_prop # => Non-private call to private method `my_private_prop` on `MyClass`
|
95
|
+
my_object.bar # => "xyz"
|
96
|
+
my_object.my_private_prop = 'baz' # => Non-private call to private method `my_private_prop=` on `MyClass`
|
97
|
+
my_object.baz
|
98
|
+
my_object.bar # => "baz"
|
99
|
+
```
|
100
|
+
|
101
|
+
Putting prop-definition after an access modifier without arguments does not work:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class MyClass
|
105
|
+
include SorbetTyped::Props
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
prop :my_prop, String # <= still public
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
## Development
|
114
|
+
|
115
|
+
The project uses [mise-en-place](https://mise.jdx.dev/) as development tool.
|
116
|
+
|
117
|
+
After checking out the repo, run `mise run setup` to install dependencies. Then, run `mise test` to run the tests. You
|
118
|
+
can also run `mise task ls` for a list of available tasks.
|
119
|
+
|
120
|
+
RSpec is used as test suite. Spec files can and should be placed right beside their associated class files.
|
121
|
+
|
122
|
+
<!-- WIP: develop and document release workflow -->
|
123
|
+
|
124
|
+
<!-- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
|
125
|
+
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
|
126
|
+
push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). -->
|
127
|
+
|
128
|
+
## Contributing
|
129
|
+
|
130
|
+
Bug reports and pull requests are welcome on GitLab at
|
131
|
+
[gitlab.com/sorbet_typed/props](https://gitlab.com/sorbet_typed/props).
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
require_relative 'props/version'
|
6
|
+
|
7
|
+
# an abstraction module to be included in any class to make the sorbet props
|
8
|
+
# interface usable. It's just a wrapper around T::Props and
|
9
|
+
# T::Props::Constructor. It's also the ancestor the custom tapioca compiler
|
10
|
+
# looks for when generating initializer type annotation rbis.
|
11
|
+
module SorbetTyped::Props
|
12
|
+
extend T::Helpers
|
13
|
+
|
14
|
+
include T::Props
|
15
|
+
include T::Props::Constructor
|
16
|
+
|
17
|
+
mixes_in_class_methods(T::Props::ClassMethods)
|
18
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
require_relative 'railway/version'
|
6
|
+
|
7
|
+
# This class provides a monad-style railway pattern, inspired by dry-monads do-syntax
|
8
|
+
class SorbetTyped::Railway # rubocop:todo Lint/EmptyClass
|
9
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Typed::Monads::FailFast
|
5
|
+
extend T::Generic
|
6
|
+
|
7
|
+
Elem = type_member(:out)
|
8
|
+
|
9
|
+
sig { returns(Elem) }
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
sig { params(value: Elem).void }
|
13
|
+
def initialize(value)
|
14
|
+
@value = value
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# WIP: do we need https://sorbet.org/docs/exhaustiveness
|
5
|
+
|
6
|
+
# WIP: move subclasses into own files
|
7
|
+
|
8
|
+
# implementation of a sorbet-typed and -compatible monad-style short-circuit
|
9
|
+
# pattern where you can run multiple steps, use their value OR let them return
|
10
|
+
# early.
|
11
|
+
#
|
12
|
+
# Developed by @kramer and pending to be made open source under
|
13
|
+
# https://gitlab.com/richardkramer/sorbet_typed. But for now we want to already
|
14
|
+
# use the code, so everything already working is copied here.
|
15
|
+
#
|
16
|
+
# How to use:
|
17
|
+
#
|
18
|
+
|
19
|
+
# ```ruby
|
20
|
+
# sig { returns(SuccessType, SorbetTyped::ShortCircuit::Shorted[ShortedType]))}
|
21
|
+
# def do_something
|
22
|
+
# return SorbetTyped::ShortCircuit.short(info: shorted_value) if something_wrong?
|
23
|
+
|
24
|
+
# return SuccessType.new
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# # this will return either the success type or a shorted result with the
|
28
|
+
# # shorted type as `info`.
|
29
|
+
# result = SorbetTyped::ShortCircuit[SuccessType, ShortedType].new.run do |circuit_breaker|
|
30
|
+
# foo = circuit_breaker.(do_something) # => will break early if do_something returns a circuit breaking result
|
31
|
+
# bar = circuit_breaker.(do_something_else(foo)) # => will break early if do_something_else(foo) returns a circuit breaking result
|
32
|
+
|
33
|
+
# do_another_thing(bar)
|
34
|
+
# end
|
35
|
+
# ```
|
36
|
+
#
|
37
|
+
# SuccessType and ShortedType might be anything you want. Just specify the types
|
38
|
+
# when initializing the SorbetTyped::ShortCircuit instance.
|
39
|
+
module SorbetTyped
|
40
|
+
class ShortCircuit
|
41
|
+
extend T::Generic
|
42
|
+
|
43
|
+
# The error thrown and handled within the run block to implement failing fast
|
44
|
+
# from any nesting depth
|
45
|
+
class CircuitShortedError < StandardError
|
46
|
+
extend T::Generic
|
47
|
+
|
48
|
+
FailureData = type_member(:out)
|
49
|
+
|
50
|
+
sig { returns(FailureData) }
|
51
|
+
attr_reader :failure
|
52
|
+
|
53
|
+
sig { params(failure: FailureData).void }
|
54
|
+
def initialize(failure:)
|
55
|
+
@failure = failure
|
56
|
+
super()
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Signal to fail fast. Info can be anything, e.g. an error object.
|
61
|
+
class Shorted
|
62
|
+
extend T::Generic
|
63
|
+
|
64
|
+
Elem = type_member(:out)
|
65
|
+
|
66
|
+
sig { returns(Elem) }
|
67
|
+
attr_reader :info
|
68
|
+
|
69
|
+
sig { params(info: Elem).void }
|
70
|
+
def initialize(info)
|
71
|
+
@info = info
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Callable implementing the logic to fail fast if the input is a Shorted
|
76
|
+
# signal. To be used like Dry::Monads `yield foo` syntax.
|
77
|
+
#
|
78
|
+
# Injected as parameter into a short circuit block, so you can use it directly.
|
79
|
+
class CircuitBreaker
|
80
|
+
extend T::Generic
|
81
|
+
|
82
|
+
Result = type_member
|
83
|
+
|
84
|
+
sig do
|
85
|
+
type_parameters(:U, :E).
|
86
|
+
params(result: T.all(T.type_parameter(:U), T.any(T.type_parameter(:E), Shorted[Result]))).
|
87
|
+
returns(T.type_parameter(:E))
|
88
|
+
end
|
89
|
+
def call(result)
|
90
|
+
case result
|
91
|
+
when Shorted
|
92
|
+
raise CircuitShortedError[T.type_parameter(:U)].new(failure: result)
|
93
|
+
else
|
94
|
+
result
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
SuccessResult = type_member
|
100
|
+
ShortedResult = type_member
|
101
|
+
|
102
|
+
sig do
|
103
|
+
type_parameters(:U).
|
104
|
+
params(info: T.type_parameter(:U)).
|
105
|
+
returns(Shorted[T.type_parameter(:U)])
|
106
|
+
end
|
107
|
+
def self.short(info:)
|
108
|
+
Shorted[T.type_parameter(:U)].new(info)
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Store if the last run failed fast
|
114
|
+
sig { returns(T::Boolean) }
|
115
|
+
attr_accessor :short_circuited
|
116
|
+
|
117
|
+
public
|
118
|
+
|
119
|
+
sig { void }
|
120
|
+
def initialize
|
121
|
+
@short_circuited = T.let(false, T::Boolean)
|
122
|
+
end
|
123
|
+
|
124
|
+
sig do
|
125
|
+
params(
|
126
|
+
_block:
|
127
|
+
T.proc.
|
128
|
+
params(fail_fast: CircuitBreaker[ShortedResult]).
|
129
|
+
returns(T.any(SuccessResult, Shorted[ShortedResult]))
|
130
|
+
).
|
131
|
+
returns(T.any(SuccessResult, Shorted[ShortedResult]))
|
132
|
+
end
|
133
|
+
def run(&_block)
|
134
|
+
self.short_circuited = false
|
135
|
+
yield(CircuitBreaker[ShortedResult].new)
|
136
|
+
rescue CircuitShortedError => exception
|
137
|
+
self.short_circuited = true
|
138
|
+
|
139
|
+
# NOTE: sorbet is only able to detect
|
140
|
+
# `SorbetTyped::ShortCircuit::CircuitShortedError[T.anything]` in this rescue. But
|
141
|
+
# because it must come from `CircuitBreaker[ShortedResult]`, it will also
|
142
|
+
# always be `CircuitShortedError[SorbetTyped::ShortCircuit::Shorted[ShortedResult]]`. So we
|
143
|
+
# tell that to sorbet.
|
144
|
+
#
|
145
|
+
# NOTE: if one would manually throw this exception WITH A DIFFERENT failure
|
146
|
+
# data type, this would not raise, because generic types get erased at
|
147
|
+
# runtime. So just NEVER raise the CircuitShortedError error manually.
|
148
|
+
T.cast(exception, CircuitShortedError[SorbetTyped::ShortCircuit::Shorted[ShortedResult]]).failure
|
149
|
+
end
|
150
|
+
|
151
|
+
sig do
|
152
|
+
params(
|
153
|
+
block:
|
154
|
+
T.proc.
|
155
|
+
params(fail_fast: CircuitBreaker[ShortedResult]).
|
156
|
+
returns(T.any(SuccessResult, SorbetTyped::ShortCircuit::Shorted[ShortedResult]))
|
157
|
+
).
|
158
|
+
returns(T.any(SuccessResult, ShortedResult))
|
159
|
+
end
|
160
|
+
def run_without_signal(&block)
|
161
|
+
result = run(&block)
|
162
|
+
|
163
|
+
case result
|
164
|
+
when SorbetTyped::ShortCircuit::Shorted
|
165
|
+
# NOTE: sorbet is only able to detect
|
166
|
+
# ```
|
167
|
+
# T.all(
|
168
|
+
# Typed::Monads::Shorted[T.anything],
|
169
|
+
# T.any(
|
170
|
+
# Typed::Monads::Shorted[Typed::Monads::ShortCircuit::ShortedResult],
|
171
|
+
# Typed::Monads::ShortCircuit::SuccessResult
|
172
|
+
# )
|
173
|
+
# )
|
174
|
+
# ```
|
175
|
+
# , which leaves no other option
|
176
|
+
# than `Typed::Monads::Shorted[ShortedResult]`. So we tell that to the
|
177
|
+
# typechecker.
|
178
|
+
T.cast(result, SorbetTyped::ShortCircuit::Shorted[ShortedResult]).info
|
179
|
+
else
|
180
|
+
result
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
sig { returns(T::Boolean) }
|
185
|
+
def last_run_short_circuited?
|
186
|
+
short_circuited
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
#####################################################################################################################
|
192
|
+
#####################################################################################################################
|
193
|
+
#####################################################################################################################
|
194
|
+
|
195
|
+
# An example class using above short circuit pattern. It's fully checked against
|
196
|
+
# sorbet. You might comment it in and run `srb tc` to see the `T.reveal_type`
|
197
|
+
# results in action.
|
198
|
+
#
|
199
|
+
# WIP: move to specs
|
200
|
+
class SorbetTyped::ShortCircuitExample
|
201
|
+
extend T::Sig
|
202
|
+
|
203
|
+
sig { void }
|
204
|
+
def call
|
205
|
+
# Success Value might be Integer or String, Failure might be Symbol or Float
|
206
|
+
short_circuit = SorbetTyped::ShortCircuit[T.any(Integer, String), T.any(Symbol, Float)].new
|
207
|
+
|
208
|
+
result = short_circuit.run do |circuit_breaker|
|
209
|
+
T.reveal_type(circuit_breaker.(3)) # => Integer; sorbet knows the Input-Type would be the output type if it's not a Shorted signal
|
210
|
+
|
211
|
+
foo_var = circuit_breaker.(foo)
|
212
|
+
|
213
|
+
'bar'
|
214
|
+
end
|
215
|
+
|
216
|
+
short_circuit.last_run_short_circuited?
|
217
|
+
# => depends on the `[true, false].sample` below
|
218
|
+
#
|
219
|
+
# the `foo` call contains a shorted circuit, propagating the Shorted signal outwards and failing this circuit
|
220
|
+
|
221
|
+
T.reveal_type(result) # => T.any(Integer, String, SorbetTyped::Railway::Shorted[T.any(Symbol, Float)])
|
222
|
+
|
223
|
+
# the result might be any of the success type or the Shorted signal object
|
224
|
+
# with its shorted types. Use case...when logic to operate on the different
|
225
|
+
# return types.
|
226
|
+
|
227
|
+
if result.is_a? SorbetTyped::ShortCircuit::Shorted
|
228
|
+
T.reveal_type result # => SorbetTyped::Railway::Shorted[T.any(Symbol, Float)]
|
229
|
+
T.reveal_type result.info # => T.any(Symbol, Float); the for the railway defined shorted result types
|
230
|
+
puts result.info
|
231
|
+
|
232
|
+
# handle error
|
233
|
+
return
|
234
|
+
end
|
235
|
+
|
236
|
+
T.reveal_type(result)
|
237
|
+
# => T.any(Integer, String); sorbet statically knows, this can never be a shorted result, as we handled it before and return early
|
238
|
+
end
|
239
|
+
|
240
|
+
sig do
|
241
|
+
returns(T.any(Integer, SorbetTyped::ShortCircuit::Shorted[Symbol]))
|
242
|
+
end
|
243
|
+
def foo
|
244
|
+
SorbetTyped::ShortCircuit[Integer, Symbol].new.run do |circuit_breaker|
|
245
|
+
# this sometimes fast fails the short circuit and propagates the signal outwards
|
246
|
+
circuit_breaker.call validate # WIP: edge case where wrong typing does not raise errors in typechecker
|
247
|
+
|
248
|
+
42
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
sig { returns(T.any(NilClass, SorbetTyped::ShortCircuit::Shorted[Symbol])) }
|
253
|
+
def validate
|
254
|
+
return SorbetTyped::ShortCircuit.short(info: :foo) if [true, false].sample
|
255
|
+
return SorbetTyped::ShortCircuit.short(info: :bar) if [true, false].sample
|
256
|
+
|
257
|
+
SorbetTyped::ShortCircuit.short(info: :baz) if [true, false].sample
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
RSpec.describe SorbetTyped::Railway do # rubocop:disable RSpec/SpecFilePathFormat
|
5
|
+
let(:version) { SorbetTyped::Railway::VERSION }
|
6
|
+
|
7
|
+
it 'has a version number' do
|
8
|
+
expect(version).not_to be_nil
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Tapioca # rubocop:disable Style/ClassAndModuleChildren
|
5
|
+
module Dsl
|
6
|
+
module Compilers
|
7
|
+
class SorbetTypedPropsConstructor < Tapioca::Dsl::Compiler
|
8
|
+
# convert sorbet prop to rbi parameter
|
9
|
+
class PropToParamConverter
|
10
|
+
# representation of sorbet prop details
|
11
|
+
class Details < T::ImmutableStruct
|
12
|
+
# identifiable default value when the respective key was not passed to the struct deserializer
|
13
|
+
class ParamValueEnum < T::Enum # rubocop:disable Sorbet/MultipleTEnumValues -- an enum is an easy way to have an identifiable object without having to rely on it's value. Maybe there is another way, but I could not get it to work with a simple class and a constant would be value-bound.
|
14
|
+
enums do
|
15
|
+
ParamNotPresent = new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
extend T::Sig
|
20
|
+
|
21
|
+
const :type_object, T::Types::Simple
|
22
|
+
const :_nilable, T::Boolean, default: false
|
23
|
+
const :default, Object, default: ParamValueEnum::ParamNotPresent
|
24
|
+
const :factory, Object, default: ParamValueEnum::ParamNotPresent
|
25
|
+
|
26
|
+
sig { returns(T::Boolean) }
|
27
|
+
def optional?
|
28
|
+
_nilable || default != ParamValueEnum::ParamNotPresent || factory != ParamValueEnum::ParamNotPresent
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
extend T::Sig
|
33
|
+
include Tapioca::RBIHelper
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
sig { returns(Symbol) }
|
38
|
+
attr_reader :name
|
39
|
+
|
40
|
+
sig { returns(Details) }
|
41
|
+
attr_reader :details
|
42
|
+
|
43
|
+
public
|
44
|
+
|
45
|
+
sig { params(name: Symbol, details: Details).void }
|
46
|
+
def initialize(name:, details:)
|
47
|
+
@name = name
|
48
|
+
@details = details
|
49
|
+
end
|
50
|
+
|
51
|
+
sig { returns(RBI::TypedParam) }
|
52
|
+
def create_rbi_parameter
|
53
|
+
if optional?
|
54
|
+
create_kw_opt_param(parameter_name, type:, default: 'nil')
|
55
|
+
else
|
56
|
+
create_kw_param(parameter_name, type:)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
sig { returns(T::Boolean) }
|
63
|
+
def optional?
|
64
|
+
details.optional?
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { returns(String) }
|
68
|
+
def type
|
69
|
+
details_type = details.type_object.name
|
70
|
+
|
71
|
+
if optional?
|
72
|
+
"T.nilable(#{details_type})"
|
73
|
+
else
|
74
|
+
details_type.to_s
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
sig { returns(String) }
|
79
|
+
def parameter_name
|
80
|
+
name.to_s
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'sorbet_typed_props_constructor/prop_to_param_converter'
|
5
|
+
|
6
|
+
module Tapioca # rubocop:disable Style/ClassAndModuleChildren
|
7
|
+
module Dsl
|
8
|
+
module Compilers
|
9
|
+
# tapioca compiler generating rbi containing the initializer interface for a class using sorbet SorbetTyped::Props
|
10
|
+
class SorbetTypedPropsConstructor < Tapioca::Dsl::Compiler
|
11
|
+
extend T::Sig
|
12
|
+
extend T::Generic
|
13
|
+
|
14
|
+
ConstantType = type_member { { fixed: T.class_of(SorbetTyped::Props) } }
|
15
|
+
|
16
|
+
sig { override.returns(T::Enumerable[Module]) }
|
17
|
+
def self.gather_constants
|
18
|
+
# Collect all the classes that include SorbetTyped::Props
|
19
|
+
all_classes.select { |klass| klass < SorbetTyped::Props }
|
20
|
+
end
|
21
|
+
|
22
|
+
sig { override.void }
|
23
|
+
def decorate
|
24
|
+
# NOTE: do not generate a new initializer signature, if the class sets
|
25
|
+
# its own and doesn't use the prop generated one.
|
26
|
+
return if constant.private_instance_methods(false).include?(:initialize)
|
27
|
+
|
28
|
+
props = constant.props
|
29
|
+
return if props.empty?
|
30
|
+
|
31
|
+
params = params_from_props(props:)
|
32
|
+
define_initializer_sig(constant:, params:)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
sig { params(props: T.untyped).returns(T::Array[RBI::TypedParam]) }
|
38
|
+
def params_from_props(props:)
|
39
|
+
props.map do |name, details|
|
40
|
+
create_param_from_prop(name:, details:)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { params(name: Symbol, details: T.untyped).returns(RBI::TypedParam) }
|
45
|
+
def create_param_from_prop(name:, details:)
|
46
|
+
details_struct =
|
47
|
+
Tapioca::Dsl::Compilers::SorbetTypedPropsConstructor::PropToParamConverter::Details.
|
48
|
+
from_hash(details.transform_keys(&:to_s))
|
49
|
+
|
50
|
+
Tapioca::Dsl::Compilers::SorbetTypedPropsConstructor::PropToParamConverter.
|
51
|
+
new(
|
52
|
+
name:,
|
53
|
+
details: details_struct
|
54
|
+
).
|
55
|
+
create_rbi_parameter
|
56
|
+
end
|
57
|
+
|
58
|
+
# HACK: `T.class_of(SorbetTyped::Props)` fails when passed one of the
|
59
|
+
# anonymous test classes. But active typechecking isn't that relevant
|
60
|
+
# here.
|
61
|
+
sig { params(constant: T.class_of(SorbetTyped::Props), params: T::Array[RBI::TypedParam]).void.checked(:never) }
|
62
|
+
def define_initializer_sig(constant:, params:)
|
63
|
+
root.create_path(constant) do |klass|
|
64
|
+
klass.create_method(
|
65
|
+
'initialize',
|
66
|
+
parameters: params,
|
67
|
+
return_type: 'void',
|
68
|
+
comments: [RBI::Comment.new('Generated from sorbet SorbetTyped::Props')]
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|