diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-19 17:16:28 +0300 |
commit | e4384360a16dd9a19d4d2d25d0ef1f2b862ed2a6 (patch) | |
tree | 2fcdfa7dcdb9db8f5208b2562f4b4e803d671243 /lib/result.rb | |
parent | ffda4e7bcac36987f936b4ba515995a6698698f0 (diff) |
Add latest changes from gitlab-org/gitlab@16-2-stable-eev16.2.0-rc42
Diffstat (limited to 'lib/result.rb')
-rw-r--r-- | lib/result.rb | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/lib/result.rb b/lib/result.rb new file mode 100644 index 00000000000..5e72b3f13cb --- /dev/null +++ b/lib/result.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +# A (partial) implementation of the functional Result type, with naming conventions based on the +# Rust implementation (https://doc.rust-lang.org/std/result/index.html) +# +# Modern Ruby 3+ destructuring and pattern matching are supported. +# +# - See "Railway Oriented Programming and the Result Class" in `ee/lib/remote_development/README.md` for details +# and example usage. +# - See `spec/lib/result_spec.rb` for detailed executable example usage. +# - See https://en.wikipedia.org/wiki/Result_type for a general description of the Result pattern. +# - See https://fsharpforfunandprofit.com/rop/ for how this can be used with Railway Oriented Programming (ROP) +# to improve design and architecture +# - See https://doc.rust-lang.org/std/result/ for the Rust implementation. + +# NOTE: This class is intentionally not namespaced to allow for more concise, readable, and explicit usage. +# It it a generic reusable implementation of the Result type, and is not specific to any domain +# rubocop:disable Gitlab/NamespacedClass +class Result + # The .ok and .err factory class methods are the only way to create a Result + # + # "self.ok" corresponds to Ok(T) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Ok + # + # @param [Object, #new] ok_value + # @return [Result] + def self.ok(ok_value) + new(ok_value: ok_value) + end + + # "self.err" corresponds to Err(E) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Err + # + # @param [Object, #new] ok_value + # @return [Result] + def self.err(err_value) + new(err_value: err_value) + end + + # "#unwrap" corresponds to "unwrap" in Rust. + # + # @return [Object] + # @raise [RuntimeError] if called on an "err" Result + def unwrap + ok? ? value : raise("Called Result#unwrap on an 'err' Result") + end + + # "#unwrap" corresponds to "unwrap" in Rust. + # + # @return [Object] + # @raise [RuntimeError] if called on an "ok" Result + def unwrap_err + err? ? value : raise("Called Result#unwrap_err on an 'ok' Result") + end + + # The `ok?` attribute will be true if the Result was constructed with .ok, and false if it was constructed with .err + # + # "#ok?" corresponds to "is_ok" in Rust. + # @return [Boolean] + def ok? + # We don't make `@ok` an attr_reader, because we don't want to confusingly shadow the class method `.ok` + @ok + end + + # The `err?` attribute will be false if the Result was constructed with .ok, and true if it was constructed with .err + # "#err?" corresponds to "is_err" in Rust. + # + # @return [Boolean] + def err? + !ok? + end + + # `and_then` is a functional way to chain together operations which may succeed or have errors. It is passed + # a lambda or class (singleton) method object, and must return a Result object representing "ok" + # or "err". + # + # If the Result object it is called on is "ok", then the passed lambda or singleton method + # is called with the value contained in the Result. + # + # If the Result object it is called on is "err", then it is returned without calling the passed + # lambda or method. + # + # It only supports being passed a lambda, or a class (singleton) method object + # which responds to `call` with a single argument (arity of 1). If multiple values are needed, + # pass a hash or array. Note that passing `Proc` objects is NOT supported, even though the YARD + # annotation contains `Proc` (because the type of a lambda is also `Proc`). + # + # Passing instance methods to `and_then` is not supported, because the methods in the chain should be + # stateless "pure functions", and should not be persisting or referencing any instance state anyway. + # + # "#and_then" corresponds to "and_then" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then + # + # @param [Proc, Method] lambda_or_singleton_method + # @return [Result] + # @raise [TypeError] + def and_then(lambda_or_singleton_method) + validate_lambda_or_singleton_method(lambda_or_singleton_method) + + # Return/passthough the Result itself if it is an err + return self if err? + + # If the Result is ok, call the lambda or singleton method with the contained value + result = lambda_or_singleton_method.call(value) + + unless result.is_a?(Result) + err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns a 'Result' type " \ + ", but instead received '#{lambda_or_singleton_method.inspect}' which returned '#{result.class}'. " \ + "Check that the previous method calls in the '#and_then' chain are correct." + raise(TypeError, err_msg) + end + + result + end + + # `map` is similar to `and_then`, but it is used for "single track" methods which always succeed, + # and have no possibility of returning an error (but they may still raise exceptions, + # which is unrelated to the Result handling). The passed lambda or singleton method must return + # a value, not a Result. + # + # If the Result object it is called on is "ok", then the passed lambda or singleton method + # is called with the value contained in the Result. + # + # If the Result object it is called on is "err", then it is returned without calling the passed + # lambda or method. + # + # "#map" corresponds to "map" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.map + # + # @param [Proc, Method] lambda_or_singleton_method + # @return [Result] + # @raise [TypeError] + def map(lambda_or_singleton_method) + validate_lambda_or_singleton_method(lambda_or_singleton_method) + + # Return/passthrough the Result itself if it is an err + return self if err? + + # If the Result is ok, call the lambda or singleton method with the contained value + mapped_value = lambda_or_singleton_method.call(value) + + if mapped_value.is_a?(Result) + err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns an unwrapped " \ + "value, not a 'Result', but instead received '#{lambda_or_singleton_method.inspect}' which returned " \ + "a 'Result'." + raise(TypeError, err_msg) + end + + # wrap the returned mapped_value in an "ok" Result. + Result.ok(mapped_value) + end + + # `to_h` supports destructuring of a result object, for example: `result => { ok: }; puts ok` + # + # @return [Hash] + def to_h + ok? ? { ok: value } : { err: value } + end + + # `deconstruct_keys` supports pattern matching on a Result object with a `case` statement. See specs for examples. + # + # @param [Array] keys + # @return [Hash] + # @raise [ArgumentError] + def deconstruct_keys(keys) + raise(ArgumentError, 'Use either :ok or :err for pattern matching') unless [[:ok], [:err]].include?(keys) + + to_h + end + + # @return [Boolean] + def ==(other) + # NOTE: The underlying `@ok` instance variable is a boolean, so we only need to check `ok?`, not `err?` too + self.class == other.class && other.ok? == ok? && other.instance_variable_get(:@value) == value + end + + private + + # The `value` attribute will contain either the ok_value or the err_value + attr_reader :value + + def initialize(ok_value: nil, err_value: nil) + if (!ok_value.nil? && !err_value.nil?) || (ok_value.nil? && err_value.nil?) + raise(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err') + end + + @ok = err_value.nil? + @value = ok? ? ok_value : err_value + end + + # @param [Proc, Method] lambda_or_singleton_method + # @return [void] + # @raise [TypeError] + def validate_lambda_or_singleton_method(lambda_or_singleton_method) + is_lambda = lambda_or_singleton_method.is_a?(Proc) && lambda_or_singleton_method.lambda? + is_singleton_method = lambda_or_singleton_method.is_a?(Method) && lambda_or_singleton_method.owner.singleton_class? + unless is_lambda || is_singleton_method + err_msg = "'Result##{__method__}' expects a lambda or singleton method object, " \ + "but instead received '#{lambda_or_singleton_method.inspect}'." + raise(TypeError, err_msg) + end + + arity = lambda_or_singleton_method.arity + + return if arity == 1 + + err_msg = "'Result##{__method__}' expects a lambda or singleton method object with a single argument " \ + "(arity of 1), but instead received '#{lambda_or_singleton_method.inspect}' with an arity of #{arity}." + raise(ArgumentError, err_msg) + end +end + +# rubocop:enable Gitlab/NamespacedClass |