Welcome to mirror list, hosted at ThFree Co, Russian Federation.

result_spec.rb « lib « spec - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 170a2f5e777219950d8903acc8a3cf0679c026e6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# frozen_string_literal: true

require 'spec_helper'

# NOTE:
#       This spec is intended to serve as documentation examples of idiomatic usage for the `Result` type.
#       These examples can be executed as-is in a Rails console to see the results.
#
#       To support this, we have intentionally used some `rubocop:disable` comments to allow for more
#       explicit and readable examples.
# rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
RSpec.describe Result, feature_category: :remote_development do
  describe 'usage of Result.ok and Result.err' do
    context 'when checked with .ok? and .err?' do
      it 'works with ok result' do
        result = Result.ok(:success)
        expect(result.ok?).to eq(true)
        expect(result.err?).to eq(false)
        expect(result.unwrap).to eq(:success)
      end

      it 'works with error result' do
        result = Result.err(:failure)
        expect(result.err?).to eq(true)
        expect(result.ok?).to eq(false)
        expect(result.unwrap_err).to eq(:failure)
      end
    end

    context 'when checked with destructuring' do
      it 'works with ok result' do
        Result.ok(:success) => { ok: } # example of rightward assignment
        expect(ok).to eq(:success)

        Result.ok(:success) => { ok: success_value } # rightward assignment destructuring to different var
        expect(success_value).to eq(:success)
      end

      it 'works with error result' do
        Result.err(:failure) => { err: }
        expect(err).to eq(:failure)

        Result.err(:failure) => { err: error_value }
        expect(error_value).to eq(:failure)
      end
    end

    context 'when checked with pattern matching' do
      def check_result_with_pattern_matching(result)
        case result
        in { ok: Symbol => ok_value }
          { success: ok_value }
        in { err: String => error_value }
          { failure: error_value }
        else
          raise "Unmatched result type: #{result.unwrap.class.name}"
        end
      end

      it 'works with ok result' do
        ok_result = Result.ok(:success_symbol)
        expect(check_result_with_pattern_matching(ok_result)).to eq({ success: :success_symbol })
      end

      it 'works with error result' do
        error_result = Result.err('failure string')
        expect(check_result_with_pattern_matching(error_result)).to eq({ failure: 'failure string' })
      end

      it 'raises error with unmatched type in pattern match' do
        unmatched_type_result = Result.ok([])
        expect do
          check_result_with_pattern_matching(unmatched_type_result)
        end.to raise_error(RuntimeError, 'Unmatched result type: Array')
      end

      it 'raises error with invalid pattern matching key' do
        result = Result.ok(:success)
        expect do
          case result
          in { invalid_pattern_match_because_it_is_not_ok_or_err: :value }
            :unreachable_from_case
          else
            :unreachable_from_else
          end
        end.to raise_error(ArgumentError, 'Use either :ok or :err for pattern matching')
      end
    end
  end

  describe 'usage of #and_then' do
    context 'when passed a proc' do
      it 'returns last ok value in successful chain' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .and_then(->(value) { Result.ok(value + 1) })
            .and_then(->(value) { Result.ok(value + 1) })

        expect(final_result.ok?).to eq(true)
        expect(final_result.unwrap).to eq(3)
      end

      it 'short-circuits the rest of the chain on the first err value encountered' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .and_then(->(value) { Result.err("invalid: #{value}") })
            .and_then(->(value) { Result.ok(value + 1) })

        expect(final_result.err?).to eq(true)
        expect(final_result.unwrap_err).to eq('invalid: 1')
      end
    end

    context 'when passed a module or class (singleton) method object' do
      module MyModuleUsingResult
        def self.double(value)
          Result.ok(value * 2)
        end

        def self.return_err(value)
          Result.err("invalid: #{value}")
        end

        class MyClassUsingResult
          def self.triple(value)
            Result.ok(value * 3)
          end
        end
      end

      it 'returns last ok value in successful chain' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .and_then(::MyModuleUsingResult.method(:double))
            .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple))

        expect(final_result.ok?).to eq(true)
        expect(final_result.unwrap).to eq(6)
      end

      it 'returns first err value in failed chain' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .and_then(::MyModuleUsingResult.method(:double))
            .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple))
            .and_then(::MyModuleUsingResult.method(:return_err))
            .and_then(::MyModuleUsingResult.method(:double))

        expect(final_result.err?).to eq(true)
        expect(final_result.unwrap_err).to eq('invalid: 6')
      end
    end

    describe 'type checking validation' do
      describe 'enforcement of argument type' do
        it 'raises TypeError if passed anything other than a lambda or singleton method object' do
          ex = TypeError
          msg = /expects a lambda or singleton method object/
          # noinspection RubyMismatchedArgumentType
          expect { Result.ok(1).and_then('string') }.to raise_error(ex, msg)
          expect { Result.ok(1).and_then(proc { Result.ok(1) }) }.to raise_error(ex, msg)
          expect { Result.ok(1).and_then(1.method(:to_s)) }.to raise_error(ex, msg)
          expect { Result.ok(1).and_then(Integer.method(:to_s)) }.to raise_error(ex, msg)
        end
      end

      describe 'enforcement of argument arity' do
        it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
          expect do
            Result.ok(1).and_then(->(a, b) { Result.ok(a + b) })
          end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
        end
      end

      describe 'enforcement that passed lambda or method returns a Result type' do
        it 'raises ArgumentError if passed lambda or singleton method object which returns non-Result type' do
          expect do
            Result.ok(1).and_then(->(a) { a + 1 })
          end.to raise_error(TypeError, /expects .* which returns a 'Result' type/)
        end
      end
    end
  end

  describe 'usage of #map' do
    context 'when passed a proc' do
      it 'returns last ok value in successful chain' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .map(->(value) { value + 1 })
            .map(->(value) { value + 1 })

        expect(final_result.ok?).to eq(true)
        expect(final_result.unwrap).to eq(3)
      end

      it 'returns first err value in failed chain' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .and_then(->(value) { Result.err("invalid: #{value}") })
            .map(->(value) { value + 1 })

        expect(final_result.err?).to eq(true)
        expect(final_result.unwrap_err).to eq('invalid: 1')
      end
    end

    context 'when passed a module or class (singleton) method object' do
      module MyModuleNotUsingResult
        def self.double(value)
          value * 2
        end

        class MyClassNotUsingResult
          def self.triple(value)
            value * 3
          end
        end
      end

      it 'returns last ok value in successful chain' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .map(::MyModuleNotUsingResult.method(:double))
            .map(::MyModuleNotUsingResult::MyClassNotUsingResult.method(:triple))

        expect(final_result.ok?).to eq(true)
        expect(final_result.unwrap).to eq(6)
      end

      it 'returns first err value in failed chain' do
        initial_result = Result.ok(1)
        final_result =
          initial_result
            .map(::MyModuleNotUsingResult.method(:double))
            .and_then(->(value) { Result.err("invalid: #{value}") })
            .map(::MyModuleUsingResult.method(:double))

        expect(final_result.err?).to eq(true)
        expect(final_result.unwrap_err).to eq('invalid: 2')
      end
    end

    describe 'type checking validation' do
      describe 'enforcement of argument type' do
        it 'raises TypeError if passed anything other than a lambda or singleton method object' do
          ex = TypeError
          msg = /expects a lambda or singleton method object/
          # noinspection RubyMismatchedArgumentType
          expect { Result.ok(1).map('string') }.to raise_error(ex, msg)
          expect { Result.ok(1).map(proc { 1 }) }.to raise_error(ex, msg)
          expect { Result.ok(1).map(1.method(:to_s)) }.to raise_error(ex, msg)
          expect { Result.ok(1).map(Integer.method(:to_s)) }.to raise_error(ex, msg)
        end
      end

      describe 'enforcement of argument arity' do
        it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
          expect do
            Result.ok(1).map(->(a, b) { a + b })
          end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
        end
      end

      describe 'enforcement that passed lambda or method does not return a Result type' do
        it 'raises TypeError if passed lambda or singleton method object which returns non-Result type' do
          expect do
            Result.ok(1).map(->(a) { Result.ok(a + 1) })
          end.to raise_error(TypeError, /expects .* which returns an unwrapped value, not a 'Result'/)
        end
      end
    end
  end

  describe '#unwrap' do
    it 'returns wrapped value if ok' do
      expect(Result.ok(1).unwrap).to eq(1)
    end

    it 'raises error if err' do
      expect { Result.err('error').unwrap }.to raise_error(RuntimeError, /called.*unwrap.*on an 'err' Result/i)
    end
  end

  describe '#unwrap_err' do
    it 'returns wrapped value if err' do
      expect(Result.err('error').unwrap_err).to eq('error')
    end

    it 'raises error if ok' do
      expect { Result.ok(1).unwrap_err }.to raise_error(RuntimeError, /called.*unwrap_err.*on an 'ok' Result/i)
    end
  end

  describe '#==' do
    it 'implements equality' do
      expect(Result.ok(1)).to eq(Result.ok(1))
      expect(Result.err('error')).to eq(Result.err('error'))
      expect(Result.ok(1)).not_to eq(Result.ok(2))
      expect(Result.err('error')).not_to eq(Result.err('other error'))
      expect(Result.ok(1)).not_to eq(Result.err(1))
    end
  end

  describe 'validation' do
    context 'for enforcing usage of only public interface' do
      context 'when private constructor is called with invalid params' do
        it 'raises ArgumentError if both ok_value and err_value are passed' do
          expect { Result.new(ok_value: :ignored, err_value: :ignored) }
            .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
        end

        it 'raises ArgumentError if neither ok_value nor err_value are passed' do
          expect { Result.new }
            .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
        end
      end
    end
  end
end
# rubocop:enable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration