From c50f545a13957268bba2bc0399ec64291c5be0c9 Mon Sep 17 00:00:00 2001 From: Genar Trias Ortiz Date: Thu, 22 Aug 2024 10:58:43 +0200 Subject: [PATCH] recover maybe --- lib/schema_registry/maybe.rb | 80 +++++++++++++++++++++++- lib/schema_registry/maybe/absent.rb | 81 ++++++++++++++++++++++++ lib/schema_registry/maybe/present.rb | 93 ++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 lib/schema_registry/maybe/absent.rb create mode 100644 lib/schema_registry/maybe/present.rb diff --git a/lib/schema_registry/maybe.rb b/lib/schema_registry/maybe.rb index b100bf1..b581adc 100644 --- a/lib/schema_registry/maybe.rb +++ b/lib/schema_registry/maybe.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require_relative('../runtime_generic') +require_relative('maybe/absent') # Represents an instance of an object that may or may not be present. This can be useful in certain # cases where `nil` represents a valid value instead of an absent value, i.e. update DTOs. @@ -10,16 +11,91 @@ # hold a maximum of 1 elements at a time. module Maybe extend T::Sig - extend T::Generic extend RuntimeGeneric include Kernel interface! - # NOTE: Beware of implementing a `Maybe#value` method in the interface so you can call it without # type safety. >:( Value = type_member(:out) { { upper: BasicObject } } + sig do + type_parameters(:Key) + .params(input: T::Hash[T.type_parameter(:Key), T.untyped]) + .returns(T::Hash[T.type_parameter(:Key), T.untyped]) + end + # You can use this method to easily transform a Hash with `Maybe` values into one without them, + # filtering out `Maybe` instances that are empty and unwrapping the present ones. + # + # Given a hash containing `Maybe` instances, returns a hash with only the values that are present. + # It also unwraps the present values. + # + # For convenience, it also recursively serializes nested T::Structs and strips nested hashes, + # arrays and sets. + # + # ```ruby + # Maybe.strip({ a: Maybe.from(1), b: Maybe.empty, c: Maybe.from(3) }) + # # => { a: 1, c: 3 } + # ``` + def self.strip(input) # rubocop:disable Metrics/PerceivedComplexity + input + .reject { |_key, value| value == Maybe.empty } + .to_h do |key, value| + unwrapped = value.is_a?(Maybe::Present) ? value.value : value + enumerated = + if unwrapped.is_a?(Array) || unwrapped.is_a?(Set) + unwrapped.map { |v| v.is_a?(T::Struct) ? Maybe.strip(v.serialize) : Maybe.strip(v) } + else + unwrapped + end + serialized = enumerated.is_a?(T::Struct) ? enumerated.serialize : enumerated + stripped = serialized.is_a?(Hash) ? Maybe.strip(serialized) : serialized + + [key, stripped] + end + end + + sig { returns(Absent) } + # Creates an empty instance. + def self.empty + Absent.new + end + + sig { returns(Absent) } + # Creates an empty instance. + # Alias for self.empty + def self.none + empty + end + + sig { returns(Absent) } + # Creates an empty instance. + # Alias for self.empty + def self.absent + empty + end + + sig do + type_parameters(:Value) + .params(value: T.all(BasicObject, T.type_parameter(:Value))) + .returns(Maybe[T.all(BasicObject, T.type_parameter(:Value))]) + end + # Creates an instance containing the specified value. + # Necessary to make this work with sorbet-coerce + def self.new(value) + from(value) + end + + sig do + type_parameters(:Value) + .params(value: T.all(BasicObject, T.type_parameter(:Value))) + .returns(Maybe[T.all(BasicObject, T.type_parameter(:Value))]) + end + # Creates an instance containing the specified value. + def self.from(value) + Present[T.all(BasicObject, T.type_parameter(:Value))].new(value) + end + sig { abstract.returns(T::Boolean) } # `true` if this `Maybe` contains a value, `false` otherwise. def present?; end diff --git a/lib/schema_registry/maybe/absent.rb b/lib/schema_registry/maybe/absent.rb new file mode 100644 index 0000000..3bb3bd4 --- /dev/null +++ b/lib/schema_registry/maybe/absent.rb @@ -0,0 +1,81 @@ +# typed: strict +# frozen_string_literal: true + +module Maybe + # Class used to represent the empty case + class Absent + extend T::Sig + extend T::Generic + include Maybe + final! + + Value = type_member { { fixed: T.noreturn } } + + sig(:final) { override.returns(FalseClass) } + def present? + false + end + + sig(:final) { override.returns(TrueClass) } + def absent? + true + end + + sig(:final) { override.returns(TrueClass) } + def empty? + absent? + end + + sig(:final) do + override + .type_parameters(:Default) + .params(default: T.type_parameter(:Default)) + .returns(T.type_parameter(:Default)) + end + def or_default(default) + default + end + + sig(:final) do + override + .type_parameters(:Return) + .params(_block: T.proc.params(v: Value).returns(T.type_parameter(:Return))) + .returns(T.nilable(T.type_parameter(:Return))) + end + def when_present(&_block) + nil + end + + sig(:final) do + override + .type_parameters(:Return) + .params(_block: T.proc.returns(T.type_parameter(:Return))) + .returns(T.nilable(T.type_parameter(:Return))) + end + def when_absent(&_block) + yield + end + + sig(:final) do + override.params(_block: T.proc.params(value: Value).returns(T::Boolean)).returns(Maybe[Value]) + end + def filter(&_block) + self + end + + sig(:final) do + override + .type_parameters(:Default) + .params(_block: T.proc.params(value: Value).returns(T.type_parameter(:Default))) + .returns(Maybe[T.all(BasicObject, T.type_parameter(:Default))]) + end + def map(&_block) + self + end + + sig(:final) { override.params(other: BasicObject).returns(T::Boolean) } + def ==(other) + self.class === other # rubocop:disable Style/CaseEquality + end + end +end diff --git a/lib/schema_registry/maybe/present.rb b/lib/schema_registry/maybe/present.rb new file mode 100644 index 0000000..11825cb --- /dev/null +++ b/lib/schema_registry/maybe/present.rb @@ -0,0 +1,93 @@ +# typed: strict +# frozen_string_literal: true + +module Maybe + # Class used to represent the case when a value is available + class Present + extend T::Sig + extend T::Generic + include Maybe + final! + + Value = type_member(:out) { { upper: BasicObject } } + + sig(:final) { params(value: Value).void } + def initialize(value) + @value = value + end + + sig(:final) { override.returns(TrueClass) } + def present? + true + end + + sig(:final) { override.returns(FalseClass) } + def absent? + false + end + + sig(:final) { override.returns(FalseClass) } + def empty? + absent? + end + + sig(:final) do + override.type_parameters(:Default).params(_default: T.type_parameter(:Default)).returns(Value) + end + def or_default(_default) + value + end + + sig(:final) do + override + .type_parameters(:Return) + .params(_block: T.proc.params(v: Value).returns(T.type_parameter(:Return))) + .returns(T.nilable(T.type_parameter(:Return))) + end + def when_present(&_block) + yield value + end + + sig(:final) do + override + .type_parameters(:Return) + .params(_block: T.proc.returns(T.type_parameter(:Return))) + .returns(T.nilable(T.type_parameter(:Return))) + end + def when_absent(&_block) + nil + end + + sig(:final) do + override.params(_block: T.proc.params(value: Value).returns(T::Boolean)).returns(Maybe[Value]) + end + def filter(&_block) + return self if yield value + + Absent.new + end + + sig(:final) do + override + .type_parameters(:Default) + .params( + _block: + T.proc.params(value: Value).returns(T.all(BasicObject, T.type_parameter(:Default))) + ) + .returns(Maybe[T.all(BasicObject, T.type_parameter(:Default))]) + end + def map(&_block) + mapped = yield value + Present[T.all(BasicObject, T.type_parameter(:Default))].new(mapped) + end + + sig(:final) { override.params(other: BasicObject).returns(T::Boolean) } + def ==(other) + return false unless self.class === other # rubocop:disable Style/CaseEquality + value == other.value + end + + sig(:final) { returns(Value) } + attr_reader :value + end +end