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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/result.rb')
-rw-r--r--lib/result.rb209
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