sorbet-result 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +13 -13
- data/README.md +59 -20
- data/Rakefile +6 -1
- data/lib/typed/failure.rb +28 -9
- data/lib/typed/no_error_on_success_error.rb +14 -0
- data/lib/typed/no_payload_on_failure_error.rb +1 -1
- data/lib/typed/result.rb +11 -6
- data/lib/typed/success.rb +28 -14
- data/sorbet/config +1 -0
- data/sorbet/rbi/gems/{rubocop-ast@1.28.1.rbi → rubocop-ast@1.29.0.rbi} +43 -27
- data/sorbet/rbi/gems/{rubocop@1.51.0.rbi → rubocop@1.52.0.rbi} +384 -198
- metadata +7 -7
- data/lib/typed/nil_payload_error.rb +0 -14
- /data/sorbet/rbi/gems/{irb@1.6.4.rbi → irb@1.7.0.rbi} +0 -0
- /data/sorbet/rbi/gems/{reline@0.3.3.rbi → reline@0.3.5.rbi} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4721bbbd3c051fd826ebc2721567a032a6f64ffb7545c990cef2d5b82fd78305
|
4
|
+
data.tar.gz: bd1cf4a0d186f3ef0be4f8e8aba11908fe4e4546198a647e10140901f4c16069
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d767cc69f61e9de10862ff3ceca4a9fbf880eb748cb7fb12c58f992b6a573a566ea15b6d5867bd47b42eac6a890195d29bd79731de56ef7b1bee90c51d31691d
|
7
|
+
data.tar.gz: 52577942ef7db2a668b3d505263507758c6a720271ed9e883a346e962c48e53ba3b4850f3fffd590f4580ff2bfb352c08dc2f2d1e503c4c790b58f896ff84a3a
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
8
|
|
9
|
+
## [0.3.0] - 2023-06-06
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- Add `.blank` to create a `Typed::Success` with a `nil` payload or a `Typed::Failure` with a `nil` error.
|
14
|
+
- Add `#and_then` to `Typed::Result` to allow chaining of results. See #14 for more details.
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
|
18
|
+
- *Breaking* Make `Typed::Success#Error` and `Typed::Failure#Payload` fixed to `T.noreturn`. This allows to specify the other type_member only when using generics. See #8 for more details
|
19
|
+
- *Breaking* Remove `T.nilable` from `Payload` and `Error` parameters in `Typed::Success.new` and `Typed::Failure.new`. Nilability will now need to be specified in the generic type. This also means that you'll need to use the new `.blank` instead of `.new` when you want to create a `Typed::Success` or `Typed::Failure` with a `nil` payload or error.
|
20
|
+
- *Breaking* Change `Typed::Success` and `Typed::Failure` initialize arguments from keyword to positional.
|
21
|
+
- Improve `Typed::Success.new` and `Typed::Failure.new` to make them generic methods and automatically infer the type of the `payload` and `error` arguments. See #8 for more details
|
22
|
+
|
9
23
|
## [0.2.1] - 2023-05-18
|
10
24
|
|
11
25
|
### Added
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
sorbet-result (0.
|
4
|
+
sorbet-result (0.3.0)
|
5
5
|
sorbet-runtime (~> 0.5)
|
6
6
|
zeitwerk (~> 2.6)
|
7
7
|
|
@@ -14,7 +14,7 @@ GEM
|
|
14
14
|
reline (>= 0.3.1)
|
15
15
|
diff-lcs (1.5.0)
|
16
16
|
io-console (0.6.0)
|
17
|
-
irb (1.
|
17
|
+
irb (1.7.0)
|
18
18
|
reline (>= 0.3.0)
|
19
19
|
json (2.6.3)
|
20
20
|
minitest (5.18.0)
|
@@ -30,10 +30,10 @@ GEM
|
|
30
30
|
sorbet-runtime (>= 0.5.9204)
|
31
31
|
unparser
|
32
32
|
regexp_parser (2.8.0)
|
33
|
-
reline (0.3.
|
33
|
+
reline (0.3.5)
|
34
34
|
io-console (~> 0.5)
|
35
35
|
rexml (3.2.5)
|
36
|
-
rubocop (1.
|
36
|
+
rubocop (1.52.0)
|
37
37
|
json (~> 2.3)
|
38
38
|
parallel (~> 1.10)
|
39
39
|
parser (>= 3.2.0.0)
|
@@ -43,7 +43,7 @@ GEM
|
|
43
43
|
rubocop-ast (>= 1.28.0, < 2.0)
|
44
44
|
ruby-progressbar (~> 1.7)
|
45
45
|
unicode-display_width (>= 2.4.0, < 3.0)
|
46
|
-
rubocop-ast (1.
|
46
|
+
rubocop-ast (1.29.0)
|
47
47
|
parser (>= 3.2.1.0)
|
48
48
|
rubocop-minitest (0.31.0)
|
49
49
|
rubocop (>= 1.39, < 2.0)
|
@@ -52,14 +52,14 @@ GEM
|
|
52
52
|
rubocop-sorbet (0.7.0)
|
53
53
|
rubocop (>= 0.90.0)
|
54
54
|
ruby-progressbar (1.13.0)
|
55
|
-
sorbet (0.5.
|
56
|
-
sorbet-static (= 0.5.
|
57
|
-
sorbet-runtime (0.5.
|
58
|
-
sorbet-static (0.5.
|
59
|
-
sorbet-static (0.5.
|
60
|
-
sorbet-static-and-runtime (0.5.
|
61
|
-
sorbet (= 0.5.
|
62
|
-
sorbet-runtime (= 0.5.
|
55
|
+
sorbet (0.5.10864)
|
56
|
+
sorbet-static (= 0.5.10864)
|
57
|
+
sorbet-runtime (0.5.10864)
|
58
|
+
sorbet-static (0.5.10864-universal-darwin-22)
|
59
|
+
sorbet-static (0.5.10864-x86_64-linux)
|
60
|
+
sorbet-static-and-runtime (0.5.10864)
|
61
|
+
sorbet (= 0.5.10864)
|
62
|
+
sorbet-runtime (= 0.5.10864)
|
63
63
|
spoom (1.2.1)
|
64
64
|
sorbet (>= 0.5.10187)
|
65
65
|
sorbet-runtime (>= 0.5.9204)
|
data/README.md
CHANGED
@@ -14,35 +14,49 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
14
14
|
|
15
15
|
## Usage
|
16
16
|
|
17
|
+
### Getting Started
|
18
|
+
|
17
19
|
Using a basic Result in your methods is as simple as indicating something could return a `Typed::Result`.
|
18
20
|
|
21
|
+
In practice though, you won't return instances of `Typed::Result`, but rather `Typed::Success` or `Typed::Failure`.
|
22
|
+
|
23
|
+
`Typed::Success` can hold a payload, and `Typed::Failure` can hold an error, but if you don't have such information to provide you can simply use `Typed::Success.blank` or `Typed::Failure.blank`.
|
24
|
+
|
25
|
+
`Typed::Result` is powered by Sorbet's Generics, so you'll need to specify it as `Typed::Result[Success, Error]`, where `Success` represents the type of `payload` in case of a success, while `Error` represents the type of `error` in case of an error.
|
26
|
+
|
19
27
|
```ruby
|
20
|
-
sig { params(resource_id: Integer).returns(Typed::Result) }
|
28
|
+
sig { params(resource_id: Integer).returns(Typed::Result[NilClass, NilClass]) }
|
21
29
|
def call_api(resource_id)
|
22
30
|
# something bad happened
|
23
|
-
return Typed::Failure.
|
31
|
+
return Typed::Failure.blank
|
24
32
|
|
25
33
|
# something other bad thing happened
|
26
|
-
return Typed::Failure.
|
34
|
+
return Typed::Failure.blank
|
27
35
|
|
28
36
|
# Success!
|
29
|
-
Typed::Success.
|
37
|
+
Typed::Success.blank
|
30
38
|
end
|
31
39
|
```
|
32
40
|
|
33
41
|
Generally, it's nice to have a payload with results, and it's nice to have more information on failures. We can indicate what types these are in our signatures for better static checks. Note that payloads and errors can be _any_ type.
|
34
42
|
|
43
|
+
`Typed::Result`, `Typed::Success` and `Typed::Failure` are all generic types, so you can specify the payload and error types when you use them.
|
44
|
+
|
45
|
+
`Typed::Result` will need both the success and the error types to be specified, while `Typed::Success` and `Typed::Failure` will only need the success or error type respectively.
|
46
|
+
|
47
|
+
`Typed::Success.new` and `Typed::Failure.new` are generic methods, so the payload or error type will be inferred by the parameter type.
|
48
|
+
|
35
49
|
```ruby
|
36
50
|
sig { params(resource_id: Integer).returns(Typed::Result[Float, String]) }
|
37
51
|
def call_api(resource_id)
|
38
|
-
#
|
39
|
-
return Typed::Failure.new(
|
52
|
+
# Something bad happened
|
53
|
+
return Typed::Failure.new("I couldn't do it!") # => Typed::Failure[String]
|
40
54
|
|
41
|
-
#
|
42
|
-
return Typed::Failure.new(
|
55
|
+
# Some other bad thing happened
|
56
|
+
return Typed::Failure.new("I couldn't do it for another reason!") # => Typed::Failure[String]
|
43
57
|
|
44
58
|
# Success!
|
45
|
-
Typed::Success.new(
|
59
|
+
Typed::Success.new(1.12) # => Typed::Success[Float]
|
46
60
|
end
|
47
61
|
```
|
48
62
|
|
@@ -51,9 +65,9 @@ end
|
|
51
65
|
Further, if another part of your program needs the Result, it can depend on _only_ `Typed::Success`es (or `Typed::Failure`s if you're doing something with those results).
|
52
66
|
|
53
67
|
```ruby
|
54
|
-
sig { params(success_result: Typed::Success).void }
|
68
|
+
sig { params(success_result: Typed::Success[String]).void }
|
55
69
|
def do_something_with_resource(success_result)
|
56
|
-
|
70
|
+
success_result.payload # => String
|
57
71
|
end
|
58
72
|
```
|
59
73
|
|
@@ -64,17 +78,42 @@ result = call_api(1)
|
|
64
78
|
|
65
79
|
result.success? # => true if success, false if failure
|
66
80
|
result.failure? # => true if failure, false if success
|
81
|
+
result.payload # => nil on failure, payload type on failure
|
82
|
+
result.error # => nil on success, error type on failure
|
83
|
+
|
84
|
+
# You can combine all the above to write flow-sensitive type-checked code
|
85
|
+
if result.success?
|
86
|
+
T.assert_type!(result.payload, Float)
|
87
|
+
else
|
88
|
+
T.assert_type!(result.error, String)
|
89
|
+
end
|
90
|
+
```
|
67
91
|
|
68
|
-
|
92
|
+
### Chaining
|
69
93
|
|
70
|
-
|
94
|
+
`Typed::Result` supports chaining, so you can chain together methods that return `Typed::Result`s using.
|
71
95
|
|
72
|
-
|
73
|
-
```
|
96
|
+
To do so, use the `#and_then` method to transform the payload of a `Typed::Success` into another `Typed::Result`, or return a `Typed::Failure` as is.
|
74
97
|
|
75
|
-
|
98
|
+
```ruby
|
99
|
+
# In this example, retrieve_user and send_notification both return a Typed::Result
|
100
|
+
# retrieve_user: Typed::Result[User, RetrieveUserError
|
101
|
+
# send_notification: Typed::Result[T::Boolean, SendNotificationError]
|
102
|
+
res = retrieve_user(user_id)
|
103
|
+
.and_then { |user| send_notification(user.email) } # this block will only run if retrieve_user returns a Typed::Success
|
104
|
+
|
105
|
+
# The actual type of `res` is Typed::Result[T::Boolean, T.any(RetrieveUserError, SendNotificationError)]
|
106
|
+
# because only the last operation can return a success, but any operation can return a failure.
|
107
|
+
if res.success?
|
108
|
+
# Notification sent successfully, we can do something with res.payload coming from send_notification.
|
109
|
+
res.payload # => T::Boolean
|
110
|
+
else
|
111
|
+
# Something went wrong, res.error could be either from retrieve_user or send_notification
|
112
|
+
res.error # => T.any(RetrieveUserError, SendNotificationError)
|
113
|
+
end
|
114
|
+
```
|
76
115
|
|
77
|
-
|
116
|
+
## Why use Results?
|
78
117
|
|
79
118
|
Let's say you're working on a method that reaches out to an API and fetches a resource. We hope to get a successful response and continue on in our program, but you can imagine several scenarios where we don't get that response: our authentication could fail, the server could return a 5XX response code, or the resource we were querying could have moved or not exist any more.
|
80
119
|
|
@@ -102,13 +141,13 @@ Railway Oriented Programming, which comes from the functional programming commun
|
|
102
141
|
sig { params(resource_id: Integer).returns(Typed::Result[Float, String]) }
|
103
142
|
def call_api(resource_id)
|
104
143
|
# something bad happened
|
105
|
-
return Typed::Failure.new(
|
144
|
+
return Typed::Failure.new("I couldn't do it!")
|
106
145
|
|
107
146
|
# something other bad thing happened
|
108
|
-
return Typed::Failure.new(
|
147
|
+
return Typed::Failure.new("I couldn't do it for another reason!")
|
109
148
|
|
110
149
|
# Success!
|
111
|
-
Typed::Success.new(
|
150
|
+
Typed::Success.new(1.12)
|
112
151
|
end
|
113
152
|
```
|
114
153
|
|
data/Rakefile
CHANGED
@@ -7,6 +7,11 @@ Minitest::TestTask.create do |t|
|
|
7
7
|
t.test_globs = ["test/**/*_test.rb"]
|
8
8
|
end
|
9
9
|
|
10
|
+
desc "Test Compiler output"
|
11
|
+
task :compiler do
|
12
|
+
sh "./test/test_type_checker.sh"
|
13
|
+
end
|
14
|
+
|
10
15
|
require "rubocop/rake_task"
|
11
16
|
|
12
17
|
RuboCop::RakeTask.new
|
@@ -21,4 +26,4 @@ task :sorbet do
|
|
21
26
|
sh "bundle exec srb tc"
|
22
27
|
end
|
23
28
|
|
24
|
-
task default: %i[rubocop:autocorrect_all sorbet test]
|
29
|
+
task default: %i[rubocop:autocorrect_all sorbet test compiler]
|
data/lib/typed/failure.rb
CHANGED
@@ -7,14 +7,28 @@ module Typed
|
|
7
7
|
extend T::Sig
|
8
8
|
extend T::Generic
|
9
9
|
|
10
|
-
Payload = type_member
|
10
|
+
Payload = type_member { { fixed: T.noreturn } }
|
11
11
|
Error = type_member
|
12
12
|
|
13
|
-
sig { override.returns(
|
13
|
+
sig { override.returns(Error) }
|
14
14
|
attr_reader :error
|
15
15
|
|
16
|
-
sig
|
17
|
-
|
16
|
+
sig do
|
17
|
+
type_parameters(:T)
|
18
|
+
.params(error: T.type_parameter(:T))
|
19
|
+
.returns(Typed::Failure[T.type_parameter(:T)])
|
20
|
+
end
|
21
|
+
def self.new(error)
|
22
|
+
super(error)
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(Typed::Failure[NilClass]) }
|
26
|
+
def self.blank
|
27
|
+
new(nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
sig { params(error: Error).void }
|
31
|
+
def initialize(error)
|
18
32
|
@error = error
|
19
33
|
super()
|
20
34
|
end
|
@@ -29,14 +43,19 @@ module Typed
|
|
29
43
|
true
|
30
44
|
end
|
31
45
|
|
32
|
-
sig { override.returns(T.
|
46
|
+
sig { override.returns(T.noreturn) }
|
33
47
|
def payload
|
34
|
-
|
48
|
+
raise NoPayloadOnFailureError
|
35
49
|
end
|
36
50
|
|
37
|
-
sig
|
38
|
-
|
39
|
-
|
51
|
+
sig do
|
52
|
+
override
|
53
|
+
.type_parameters(:U, :T)
|
54
|
+
.params(_block: T.proc.params(arg0: Payload).returns(Result[T.type_parameter(:U), T.type_parameter(:T)]))
|
55
|
+
.returns(Result[T.type_parameter(:U), Error])
|
56
|
+
end
|
57
|
+
def and_then(&_block)
|
58
|
+
self
|
40
59
|
end
|
41
60
|
end
|
42
61
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Typed
|
5
|
+
# Error when user attempts to access payload from a Failure Result.
|
6
|
+
class NoErrorOnSuccessError < StandardError
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { void }
|
10
|
+
def initialize
|
11
|
+
super("Attempted to access `error` from a Success Result. You were probably expecting a Failure Result. Check the result with `#success?` or `#failure?` before attempting to access `error`.") # rubocop:disable Layout/LineLength
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -8,7 +8,7 @@ module Typed
|
|
8
8
|
|
9
9
|
sig { void }
|
10
10
|
def initialize
|
11
|
-
super("Attempted to access
|
11
|
+
super("Attempted to access `payload` from a Failure Result. You were probably expecting a Success Result. Check the result with `#success?` or `#failure?` before attempting to access `payload`.") # rubocop:disable Layout/LineLength
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
data/lib/typed/result.rb
CHANGED
@@ -10,8 +10,8 @@ module Typed
|
|
10
10
|
|
11
11
|
abstract!
|
12
12
|
|
13
|
-
Payload = type_member
|
14
|
-
Error = type_member
|
13
|
+
Payload = type_member(:out)
|
14
|
+
Error = type_member(:out)
|
15
15
|
|
16
16
|
sig { abstract.returns(T::Boolean) }
|
17
17
|
def success?; end
|
@@ -19,13 +19,18 @@ module Typed
|
|
19
19
|
sig { abstract.returns(T::Boolean) }
|
20
20
|
def failure?; end
|
21
21
|
|
22
|
-
sig { abstract.returns(
|
22
|
+
sig { abstract.returns(Payload) }
|
23
23
|
def payload; end
|
24
24
|
|
25
|
-
sig { abstract.returns(
|
25
|
+
sig { abstract.returns(Error) }
|
26
26
|
def error; end
|
27
27
|
|
28
|
-
sig
|
29
|
-
|
28
|
+
sig do
|
29
|
+
abstract
|
30
|
+
.type_parameters(:U, :T)
|
31
|
+
.params(_block: T.proc.params(arg0: Payload).returns(Result[T.type_parameter(:U), T.type_parameter(:T)]))
|
32
|
+
.returns(T.any(Result[T.type_parameter(:U), T.type_parameter(:T)], Result[T.type_parameter(:U), Error]))
|
33
|
+
end
|
34
|
+
def and_then(&_block); end
|
30
35
|
end
|
31
36
|
end
|
data/lib/typed/success.rb
CHANGED
@@ -8,13 +8,27 @@ module Typed
|
|
8
8
|
extend T::Generic
|
9
9
|
|
10
10
|
Payload = type_member
|
11
|
-
Error = type_member
|
11
|
+
Error = type_member { { fixed: T.noreturn } }
|
12
12
|
|
13
|
-
sig { override.returns(
|
13
|
+
sig { override.returns(Payload) }
|
14
14
|
attr_reader :payload
|
15
15
|
|
16
|
-
sig
|
17
|
-
|
16
|
+
sig do
|
17
|
+
type_parameters(:T)
|
18
|
+
.params(payload: T.type_parameter(:T))
|
19
|
+
.returns(Typed::Success[T.type_parameter(:T)])
|
20
|
+
end
|
21
|
+
def self.new(payload)
|
22
|
+
super(payload)
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(Typed::Success[NilClass]) }
|
26
|
+
def self.blank
|
27
|
+
new(nil)
|
28
|
+
end
|
29
|
+
|
30
|
+
sig { params(payload: Payload).void }
|
31
|
+
def initialize(payload)
|
18
32
|
@payload = payload
|
19
33
|
super()
|
20
34
|
end
|
@@ -29,19 +43,19 @@ module Typed
|
|
29
43
|
false
|
30
44
|
end
|
31
45
|
|
32
|
-
sig { override.returns(
|
46
|
+
sig { override.returns(T.noreturn) }
|
33
47
|
def error
|
34
|
-
|
48
|
+
raise NoErrorOnSuccessError
|
35
49
|
end
|
36
50
|
|
37
|
-
sig
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
51
|
+
sig do
|
52
|
+
override
|
53
|
+
.type_parameters(:U, :T)
|
54
|
+
.params(block: T.proc.params(arg0: Payload).returns(Result[T.type_parameter(:U), T.type_parameter(:T)]))
|
55
|
+
.returns(Result[T.type_parameter(:U), T.type_parameter(:T)])
|
56
|
+
end
|
57
|
+
def and_then(&block)
|
58
|
+
block.call(payload)
|
45
59
|
end
|
46
60
|
end
|
47
61
|
end
|
data/sorbet/config
CHANGED