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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4eb8fb4c04fad76f6947a6c888e741ebc9154abb224edb87d5a8f3461aaabfc1
4
- data.tar.gz: 3de9d634bec586f32a6d7151d826e78f125dabc6ae93423137bcc1c3985e8ef3
3
+ metadata.gz: 4721bbbd3c051fd826ebc2721567a032a6f64ffb7545c990cef2d5b82fd78305
4
+ data.tar.gz: bd1cf4a0d186f3ef0be4f8e8aba11908fe4e4546198a647e10140901f4c16069
5
5
  SHA512:
6
- metadata.gz: ab3a1deeae77642c99de930d1d9ad7d772b0401bbdcedfc8e2b49c36839b1d6868a3a4b5643cc024304403a6b967855be0c0754c8079e84adc079d0792d9a350
7
- data.tar.gz: 556bdd555bcb02e7976c09097fde14c06a9cbc7fa7bbf20aefa95610ea5d3d3b897ae1e8cb5700f6234ddd0a89f6293f874a3767c762e6321789e3909ecb5ee7
6
+ metadata.gz: d767cc69f61e9de10862ff3ceca4a9fbf880eb748cb7fb12c58f992b6a573a566ea15b6d5867bd47b42eac6a890195d29bd79731de56ef7b1bee90c51d31691d
7
+ data.tar.gz: 52577942ef7db2a668b3d505263507758c6a720271ed9e883a346e962c48e53ba3b4850f3fffd590f4580ff2bfb352c08dc2f2d1e503c4c790b58f896ff84a3a
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,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.2.1)
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.6.4)
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.3)
33
+ reline (0.3.5)
34
34
  io-console (~> 0.5)
35
35
  rexml (3.2.5)
36
- rubocop (1.51.0)
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.28.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.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)
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.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
 
@@ -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
- result.error # => nil on success, nil or error type on failure
92
+ ### Chaining
69
93
 
70
- result.payload # => nil on failure, nil or payload type on failure
94
+ `Typed::Result` supports chaining, so you can chain together methods that return `Typed::Result`s using.
71
95
 
72
- result.payload! # => payload type or raises NilPayloadError on success, raises NoPayloadOnFailureError on failure
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
- 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.
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
- ### Why use Results?
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(error: "I couldn't do it!")
144
+ return Typed::Failure.new("I couldn't do it!")
106
145
 
107
146
  # something other bad thing happened
108
- return Typed::Failure.new(error: "I couldn't do it for another reason!")
147
+ return Typed::Failure.new("I couldn't do it for another reason!")
109
148
 
110
149
  # Success!
111
- Typed::Success.new(payload: 1.12)
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(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,19 @@ 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
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 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,18 @@ 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
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(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,19 @@ 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)
45
59
  end
46
60
  end
47
61
  end
data/sorbet/config CHANGED
@@ -2,3 +2,4 @@
2
2
  .
3
3
  --ignore=tmp/
4
4
  --ignore=vendor/
5
+ --ignore=test/test_data/