sorbet-result 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4eb8fb4c04fad76f6947a6c888e741ebc9154abb224edb87d5a8f3461aaabfc1
4
- data.tar.gz: 3de9d634bec586f32a6d7151d826e78f125dabc6ae93423137bcc1c3985e8ef3
3
+ metadata.gz: 55fcf298c304ef8f0101c1a6461c7ded68f9bfb87fead9d12ad23760b4d2268b
4
+ data.tar.gz: 96ee6aaac4f115f97065977d0cd21f510c9dcdab08b73f14855760171c3e5053
5
5
  SHA512:
6
- metadata.gz: ab3a1deeae77642c99de930d1d9ad7d772b0401bbdcedfc8e2b49c36839b1d6868a3a4b5643cc024304403a6b967855be0c0754c8079e84adc079d0792d9a350
7
- data.tar.gz: 556bdd555bcb02e7976c09097fde14c06a9cbc7fa7bbf20aefa95610ea5d3d3b897ae1e8cb5700f6234ddd0a89f6293f874a3767c762e6321789e3909ecb5ee7
6
+ metadata.gz: 9a0e1ed605028d5d98109173d11a530a7f7025c33462a918ced64bf240e06e740f78371d5a249bc0866cfd4ac1ebeb4cfef2fe58e17a0cad2a8cfd6a646a5796
7
+ data.tar.gz: 454f9d376b8a1066aaa2cbbd350f20b43fc71c1ffece65cee72dc8299118260d2dfb0b5b38e73eb2427d9df6c6ce69e222e21243c32292d1a5859c6fcad3cfb9
data/.rubocop.yml CHANGED
@@ -26,3 +26,8 @@ Style/StringLiteralsInInterpolation:
26
26
 
27
27
  Layout/LineLength:
28
28
  Max: 120
29
+
30
+ Lint/UselessMethodDefinition:
31
+ Exclude:
32
+ - lib/typed/failure.rb
33
+ - lib/typed/success.rb
data/CHANGELOG.md CHANGED
@@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.1] - 2023-06-20
10
+
11
+ ### Added
12
+
13
+ - Add `#on_error` to `Typed::Result` to allow behavior when an error is encountered during chaining.
14
+ - Add `#payload_or` to `Typed::Result` to allow callers to specify a default value if `Failure` is returned.
15
+
16
+ ## [0.3.0] - 2023-06-06
17
+
18
+ ### Added
19
+
20
+ - Add `.blank` to create a `Typed::Success` with a `nil` payload or a `Typed::Failure` with a `nil` error.
21
+ - Add `#and_then` to `Typed::Result` to allow chaining of results. See #14 for more details.
22
+
23
+ ### Changed
24
+
25
+ - *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
26
+ - *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.
27
+ - *Breaking* Change `Typed::Success` and `Typed::Failure` initialize arguments from keyword to positional.
28
+ - 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
29
+
9
30
  ## [0.2.1] - 2023-05-18
10
31
 
11
32
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-result (0.2.1)
4
+ sorbet-result (0.3.1)
5
5
  sorbet-runtime (~> 0.5)
6
6
  zeitwerk (~> 2.6)
7
7
 
@@ -14,14 +14,16 @@ GEM
14
14
  reline (>= 0.3.1)
15
15
  diff-lcs (1.5.0)
16
16
  io-console (0.6.0)
17
- irb (1.6.4)
17
+ irb (1.7.0)
18
18
  reline (>= 0.3.0)
19
19
  json (2.6.3)
20
- minitest (5.18.0)
20
+ minitest (5.18.1)
21
21
  netrc (0.11.0)
22
22
  parallel (1.23.0)
23
- parser (3.2.2.1)
23
+ parser (3.2.2.3)
24
24
  ast (~> 2.4.1)
25
+ racc
26
+ racc (1.7.1)
25
27
  rainbow (3.1.1)
26
28
  rake (13.0.6)
27
29
  rbi (0.0.16)
@@ -29,21 +31,21 @@ GEM
29
31
  parser (>= 2.6.4.0)
30
32
  sorbet-runtime (>= 0.5.9204)
31
33
  unparser
32
- regexp_parser (2.8.0)
33
- reline (0.3.3)
34
+ regexp_parser (2.8.1)
35
+ reline (0.3.5)
34
36
  io-console (~> 0.5)
35
37
  rexml (3.2.5)
36
- rubocop (1.51.0)
38
+ rubocop (1.52.1)
37
39
  json (~> 2.3)
38
40
  parallel (~> 1.10)
39
- parser (>= 3.2.0.0)
41
+ parser (>= 3.2.2.3)
40
42
  rainbow (>= 2.2.2, < 4.0)
41
43
  regexp_parser (>= 1.8, < 3.0)
42
44
  rexml (>= 3.2.5, < 4.0)
43
45
  rubocop-ast (>= 1.28.0, < 2.0)
44
46
  ruby-progressbar (~> 1.7)
45
47
  unicode-display_width (>= 2.4.0, < 3.0)
46
- rubocop-ast (1.28.1)
48
+ rubocop-ast (1.29.0)
47
49
  parser (>= 3.2.1.0)
48
50
  rubocop-minitest (0.31.0)
49
51
  rubocop (>= 1.39, < 2.0)
@@ -52,14 +54,14 @@ GEM
52
54
  rubocop-sorbet (0.7.0)
53
55
  rubocop (>= 0.90.0)
54
56
  ruby-progressbar (1.13.0)
55
- sorbet (0.5.10832)
56
- sorbet-static (= 0.5.10832)
57
- sorbet-runtime (0.5.10832)
58
- sorbet-static (0.5.10832-universal-darwin-22)
59
- sorbet-static (0.5.10832-x86_64-linux)
60
- sorbet-static-and-runtime (0.5.10832)
61
- sorbet (= 0.5.10832)
62
- sorbet-runtime (= 0.5.10832)
57
+ sorbet (0.5.10880)
58
+ sorbet-static (= 0.5.10880)
59
+ sorbet-runtime (0.5.10880)
60
+ sorbet-static (0.5.10880-universal-darwin-22)
61
+ sorbet-static (0.5.10880-x86_64-linux)
62
+ sorbet-static-and-runtime (0.5.10880)
63
+ sorbet (= 0.5.10880)
64
+ sorbet-runtime (= 0.5.10880)
63
65
  spoom (1.2.1)
64
66
  sorbet (>= 0.5.10187)
65
67
  sorbet-runtime (>= 0.5.9204)
@@ -75,7 +77,7 @@ GEM
75
77
  yard-sorbet
76
78
  thor (1.2.2)
77
79
  unicode-display_width (2.4.2)
78
- unparser (0.6.7)
80
+ unparser (0.6.8)
79
81
  diff-lcs (~> 1.3)
80
82
  parser (>= 3.2.0)
81
83
  yard (0.9.34)
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.new
31
+ return Typed::Failure.blank
24
32
 
25
33
  # something other bad thing happened
26
- return Typed::Failure.new
34
+ return Typed::Failure.blank
27
35
 
28
36
  # Success!
29
- Typed::Success.new
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
- # something bad happened
39
- return Typed::Failure.new(error: "I couldn't do it!")
52
+ # Something bad happened
53
+ return Typed::Failure.new("I couldn't do it!") # => Typed::Failure[String]
40
54
 
41
- # something other bad thing happened
42
- return Typed::Failure.new(error: "I couldn't do it for another reason!")
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(payload: 1.12)
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
 
@@ -62,19 +76,58 @@ Finally, there are a few methods you can use on both `Typed::Result` types.
62
76
  ```ruby
63
77
  result = call_api(1)
64
78
 
65
- result.success? # => true if success, false if failure
66
- result.failure? # => true if failure, false if success
79
+ result.success? # => true on success, false on failure
80
+ result.failure? # => true on failure, false on success
81
+ result.payload # => nil on failure, payload type on failure
82
+ result.error # => nil on success, error type on failure
83
+ result.payload_or("fallback") # => returns payload on success, given value on failure
84
+
85
+ # You can combine all the above to write flow-sensitive type-checked code
86
+ if result.success?
87
+ T.assert_type!(result.payload, Float)
88
+ else
89
+ T.assert_type!(result.error, String)
90
+ end
91
+ ```
92
+
93
+ ### Chaining
67
94
 
68
- result.error # => nil on success, nil or error type on failure
95
+ `Typed::Result` supports chaining, so you can chain together methods that return `Typed::Result`s using.
69
96
 
70
- result.payload # => nil on failure, nil or payload type on failure
97
+ 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.
71
98
 
72
- result.payload! # => payload type or raises NilPayloadError on success, raises NoPayloadOnFailureError on failure
99
+ ```ruby
100
+ # In this example, retrieve_user and send_notification both return a Typed::Result
101
+ # retrieve_user: Typed::Result[User, RetrieveUserError
102
+ # send_notification: Typed::Result[T::Boolean, SendNotificationError]
103
+ res = retrieve_user(user_id)
104
+ .and_then { |user| send_notification(user.email) } # this block will only run if retrieve_user returns a Typed::Success
105
+
106
+ # The actual type of `res` is Typed::Result[T::Boolean, T.any(RetrieveUserError, SendNotificationError)]
107
+ # because only the last operation can return a success, but any operation can return a failure.
108
+ if res.success?
109
+ # Notification sent successfully, we can do something with res.payload coming from send_notification.
110
+ res.payload # => T::Boolean
111
+ else
112
+ # Something went wrong, res.error could be either from retrieve_user or send_notification
113
+ res.error # => T.any(RetrieveUserError, SendNotificationError)
114
+ end
115
+ ```
116
+
117
+ You can also use the `#on_error` chain to take an action only on failure, such as logging or capturing error information in an error monitoring service.
118
+
119
+ ```ruby
120
+ # In this example, retrieve_user and send_notification both return a Typed::Result
121
+ # retrieve_user: Typed::Result[User, RetrieveUserError
122
+ # send_notification: Typed::Result[T::Boolean, SendNotificationError]
123
+ res = retrieve_user(user_id)
124
+ .and_then { |user| send_notification(user.email) } # this block will only run if retrieve_user returns a Typed::Success
125
+ .on_error { |error| puts "Encountered this error: #{error}"}
73
126
  ```
74
127
 
75
- The `payload!` method is useful if you don't want to do a `T.must` escape hatch, since the payload _could_ always be `nil`. If you're writing Railway inspired code, you should do the `success?` check before calling `payload!` and know that you've returned a payload on the Result.
128
+ If the above chain does not fail, the `puts` statement is never run. If the chain does yield a `Failure`, the `puts` block is executed and the `Failure` is ultimately returned.
76
129
 
77
- ### Why use Results?
130
+ ## Why use Results?
78
131
 
79
132
  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
133
 
@@ -102,13 +155,13 @@ Railway Oriented Programming, which comes from the functional programming commun
102
155
  sig { params(resource_id: Integer).returns(Typed::Result[Float, String]) }
103
156
  def call_api(resource_id)
104
157
  # something bad happened
105
- return Typed::Failure.new(error: "I couldn't do it!")
158
+ return Typed::Failure.new("I couldn't do it!")
106
159
 
107
160
  # something other bad thing happened
108
- return Typed::Failure.new(error: "I couldn't do it for another reason!")
161
+ return Typed::Failure.new("I couldn't do it for another reason!")
109
162
 
110
163
  # Success!
111
- Typed::Success.new(payload: 1.12)
164
+ Typed::Success.new(1.12)
112
165
  end
113
166
  ```
114
167
 
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(T.nilable(Error)) }
13
+ sig { override.returns(Error) }
14
14
  attr_reader :error
15
15
 
16
- sig { params(error: T.nilable(Error)).void }
17
- def initialize(error: nil)
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,39 @@ module Typed
29
43
  true
30
44
  end
31
45
 
32
- sig { override.returns(T.nilable(Payload)) }
46
+ sig { override.returns(T.noreturn) }
33
47
  def payload
34
- nil
48
+ raise NoPayloadOnFailureError
35
49
  end
36
50
 
37
- sig { override.returns(Payload) }
38
- def payload!
39
- raise NoPayloadOnFailureError
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
59
+ end
60
+
61
+ sig do
62
+ override
63
+ .params(block: T.proc.params(arg0: Error).void)
64
+ .returns(T.self_type)
65
+ end
66
+ def on_error(&block)
67
+ block.call(error)
68
+ self
69
+ end
70
+
71
+ sig do
72
+ override
73
+ .type_parameters(:Fallback)
74
+ .params(value: T.type_parameter(:Fallback))
75
+ .returns(T.any(Payload, T.type_parameter(:Fallback)))
76
+ end
77
+ def payload_or(value)
78
+ value
40
79
  end
41
80
  end
42
81
  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 a payload from a Failure Result. You were probably expecting a Success Result. Check the result with #success? or #failure? before attempting to access the payload.") # rubocop:disable Layout/LineLength
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,33 @@ module Typed
19
19
  sig { abstract.returns(T::Boolean) }
20
20
  def failure?; end
21
21
 
22
- sig { abstract.returns(T.nilable(Payload)) }
22
+ sig { abstract.returns(Payload) }
23
23
  def payload; end
24
24
 
25
- sig { abstract.returns(T.nilable(Error)) }
25
+ sig { abstract.returns(Error) }
26
26
  def error; end
27
27
 
28
- sig { abstract.returns(Payload) }
29
- def payload!; end
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
35
+
36
+ sig do
37
+ abstract
38
+ .params(block: T.proc.params(arg0: Error).void)
39
+ .returns(T.self_type)
40
+ end
41
+ def on_error(&block); end
42
+
43
+ sig do
44
+ abstract
45
+ .type_parameters(:Fallback)
46
+ .params(value: T.type_parameter(:Fallback))
47
+ .returns(T.any(Payload, T.type_parameter(:Fallback)))
48
+ end
49
+ def payload_or(value); end
30
50
  end
31
51
  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(T.nilable(Payload)) }
13
+ sig { override.returns(Payload) }
14
14
  attr_reader :payload
15
15
 
16
- sig { params(payload: T.nilable(Payload)).void }
17
- def initialize(payload: nil)
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,38 @@ module Typed
29
43
  false
30
44
  end
31
45
 
32
- sig { override.returns(NilClass) }
46
+ sig { override.returns(T.noreturn) }
33
47
  def error
34
- nil
48
+ raise NoErrorOnSuccessError
35
49
  end
36
50
 
37
- sig { override.returns(Payload) }
38
- def payload!
39
- case @payload
40
- when nil
41
- raise NilPayloadError
42
- else
43
- @payload
44
- end
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)
59
+ end
60
+
61
+ sig do
62
+ override
63
+ .params(_block: T.proc.params(arg0: Error).void)
64
+ .returns(T.self_type)
65
+ end
66
+ def on_error(&_block)
67
+ self
68
+ end
69
+
70
+ sig do
71
+ override
72
+ .type_parameters(:Fallback)
73
+ .params(_value: T.type_parameter(:Fallback))
74
+ .returns(T.any(Payload, T.type_parameter(:Fallback)))
75
+ end
76
+ def payload_or(_value)
77
+ payload
45
78
  end
46
79
  end
47
80
  end
data/sorbet/config CHANGED
@@ -2,3 +2,4 @@
2
2
  .
3
3
  --ignore=tmp/
4
4
  --ignore=vendor/
5
+ --ignore=test/test_data/