Runtime assertions for Ruby literal.fun
ruby
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Bring back Literal Results (#352)

authored by

Joel Drapper and committed by
GitHub
d920283b 68c5d58e

+529 -2
+20
lib/literal.rb
··· 81 81 Literal::Brand.new(...) 82 82 end 83 83 84 + def self.Result(success_type, failure_type) 85 + result_type = Result::Generic.new(success_type, failure_type) 86 + 87 + if block_given? 88 + result = yield(result_type) 89 + Literal.check(result, result_type) 90 + result 91 + else 92 + result_type 93 + end 94 + end 95 + 96 + def self.Success(type) 97 + Success::Generic.new(type) 98 + end 99 + 100 + def self.Failure(type) 101 + Failure::Generic.new(type) 102 + end 103 + 84 104 def self.check(value, type) 85 105 if type === value 86 106 true
+79 -1
lib/literal/failure.rb
··· 1 1 # frozen_string_literal: true 2 2 3 3 class Literal::Failure < Literal::Result 4 - def initialize(error) 4 + class Generic 5 + include Literal::Type 6 + 7 + def initialize(type) 8 + @type = type 9 + freeze 10 + end 11 + 12 + attr_reader :type 13 + 14 + def ===(object) 15 + Literal::Failure === object && @type === object.error! 16 + end 17 + 18 + def inspect 19 + "Literal::Failure(#{@type.inspect})" 20 + end 21 + 22 + freeze 23 + end 24 + 25 + def initialize(error, success_type:, failure_type:) 5 26 @error = error 27 + 28 + @success_type = success_type 29 + @failure_type = failure_type 30 + 31 + Literal.check(error, failure_type) 32 + 33 + freeze 34 + end 35 + 36 + attr_reader :success_type, :failure_type 37 + 38 + def success? 39 + false 40 + end 41 + 42 + def failure? 43 + true 44 + end 45 + 46 + def value! 47 + raise Literal::ArgumentError.new("Failure has no value") 48 + end 49 + 50 + def deconstruct 51 + [@error] 52 + end 53 + 54 + def deconstruct_keys(keys) 55 + if @error.respond_to?(:deconstruct_keys) 56 + @error.deconstruct_keys(keys) 57 + else 58 + {} 59 + end 60 + end 61 + 62 + def error! 63 + @error 64 + end 65 + 66 + def map(type) 67 + raise ArgumentError unless block_given? 68 + 69 + Literal::Failure.new( 70 + @error, 71 + success_type: type, 72 + failure_type: @failure_type 73 + ) 74 + end 75 + 76 + def then 77 + raise ArgumentError unless block_given? 78 + self 79 + end 80 + 81 + def value_or 82 + raise ArgumentError unless block_given? 83 + yield(@error) 6 84 end 7 85 end
+43
lib/literal/result.rb
··· 1 1 # frozen_string_literal: true 2 2 3 3 class Literal::Result 4 + class Generic 5 + include Literal::Type 6 + 7 + def initialize(success_type, failure_type) 8 + @success_type = success_type 9 + @failure_type = failure_type 10 + 11 + freeze 12 + end 13 + 14 + attr_reader :success_type, :failure_type 15 + 16 + def ===(object) 17 + case object 18 + when Literal::Success 19 + @success_type === object.value! 20 + when Literal::Failure 21 + @failure_type === object.error! 22 + end 23 + end 24 + 25 + def success(value) 26 + Literal::Success.new( 27 + value, 28 + success_type: @success_type, 29 + failure_type: @failure_type 30 + ) 31 + end 32 + 33 + def failure(error) 34 + Literal::Failure.new( 35 + error, 36 + success_type: @success_type, 37 + failure_type: @failure_type 38 + ) 39 + end 40 + 41 + freeze 42 + end 43 + 44 + def handle(&) 45 + Literal::ResultHandler.new(self).handle(&) 46 + end 4 47 end
+54
lib/literal/result_handler.rb
··· 1 + # frozen_string_literal: true 2 + 3 + class Literal::ResultHandler 4 + def initialize(result) 5 + @result = result 6 + @success_cases = [] 7 + @failure_cases = [] 8 + end 9 + 10 + def handle 11 + return @result unless block_given? 12 + 13 + yield(self) 14 + 15 + unless Literal.subtype?(@result.success_type, covered_type(@success_cases)) 16 + raise Literal::ArgumentError.new("No success handler covers #{@result.success_type.inspect}") 17 + end 18 + 19 + unless Literal.subtype?(@result.failure_type, covered_type(@failure_cases)) 20 + raise Literal::ArgumentError.new("No failure handler covers #{@result.failure_type.inspect}") 21 + end 22 + 23 + case @result 24 + when Literal::Success 25 + @success_cases.each do |type, block| 26 + if type === @result.value! 27 + return block&.call(@result.value!) 28 + end 29 + end 30 + when Literal::Failure 31 + @failure_cases.each do |type, block| 32 + if type === @result.error! 33 + return block&.call(@result.error!) 34 + end 35 + end 36 + end 37 + 38 + raise Literal::ArgumentError.new("Unhandled result type: #{@result.class}") 39 + end 40 + 41 + def success(type = Literal::Types::_Any?, &block) 42 + @success_cases << [type, block] 43 + end 44 + 45 + def failure(type = Literal::Types::_Any?, &block) 46 + @failure_cases << [type, block] 47 + end 48 + 49 + private def covered_type(cases) 50 + return Literal::Types::NeverType::Instance if cases.empty? 51 + 52 + Literal::Types::_Union(*cases.map(&:first)) 53 + end 54 + end
+97 -1
lib/literal/success.rb
··· 1 1 # frozen_string_literal: true 2 2 3 3 class Literal::Success < Literal::Result 4 - def initialize(value) 4 + class Generic 5 + include Literal::Type 6 + 7 + def initialize(type) 8 + @type = type 9 + freeze 10 + end 11 + 12 + attr_reader :type 13 + 14 + def ===(object) 15 + Literal::Success === object && @type === object.value! 16 + end 17 + 18 + def inspect 19 + "Literal::Success(#{@type.inspect})" 20 + end 21 + 22 + freeze 23 + end 24 + 25 + def initialize(value, success_type:, failure_type:) 5 26 @value = value 27 + 28 + @success_type = success_type 29 + @failure_type = failure_type 30 + 31 + Literal.check(@value, success_type) 32 + 33 + freeze 34 + end 35 + 36 + attr_reader :success_type, :failure_type 37 + 38 + def success? 39 + true 40 + end 41 + 42 + def failure? 43 + false 44 + end 45 + 46 + def value! 47 + @value 48 + end 49 + 50 + def deconstruct 51 + [@value] 52 + end 53 + 54 + def deconstruct_keys(keys) 55 + if @value.respond_to?(:deconstruct_keys) 56 + @value.deconstruct_keys(keys) 57 + else 58 + {} 59 + end 60 + end 61 + 62 + def error! 63 + raise Literal::ArgumentError.new("Success has no error") 64 + end 65 + 66 + def map(type) 67 + raise ArgumentError unless block_given? 68 + result = yield(@value) 69 + 70 + Literal::Success.new( 71 + result, 72 + success_type: type, 73 + failure_type: @failure_type 74 + ) 75 + end 76 + 77 + def then 78 + raise ArgumentError unless block_given? 79 + result = yield(@value) 80 + 81 + case result 82 + when Literal::Failure 83 + Literal::Failure.new( 84 + result.error!, 85 + success_type: result.success_type, 86 + failure_type: Literal::Types::_Union(@failure_type, result.failure_type) 87 + ) 88 + when Literal::Success 89 + Literal::Success.new( 90 + result.value!, 91 + success_type: result.success_type, 92 + failure_type: Literal::Types::_Union(@failure_type, result.failure_type) 93 + ) 94 + else 95 + raise Literal::ArgumentError.new("Expected block to return a Literal::Result, got #{result.class.inspect}") 96 + end 97 + end 98 + 99 + def value_or 100 + raise ArgumentError unless block_given? 101 + @value 6 102 end 7 103 end
+152
test/result.test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + include Literal::Types 4 + 5 + test "result block returns a checked result" do 6 + result = Literal::Result(Integer, Symbol) do |type| 7 + type.success(42) 8 + end 9 + 10 + assert Literal::Success === result 11 + assert_equal 42, result.value! 12 + end 13 + 14 + test "result block must return a matching result" do 15 + assert_raises(Literal::TypeError) do 16 + Literal::Result(Integer, Symbol) do 17 + Literal::Result(String, Symbol) { |type| type.success("42") } 18 + end 19 + end 20 + end 21 + 22 + test "then adopts returned success type" do 23 + result = Literal::Result(Integer, Symbol) { |type| type.success(42) } 24 + .then { |value| Literal::Result(String, RuntimeError) { |type| type.success(value.to_s) } } 25 + 26 + assert result.success? 27 + assert_equal "42", result.value! 28 + assert Literal::Result(String, _Union(Symbol, RuntimeError)) === result 29 + end 30 + 31 + test "then unions failure types" do 32 + result = Literal::Result(Integer, Symbol) { |type| type.success(42) } 33 + .then { Literal::Result(String, RuntimeError) { |type| type.failure(RuntimeError.new("boom")) } } 34 + 35 + assert result.failure? 36 + assert RuntimeError === result.error! 37 + assert Literal::Result(String, _Union(Symbol, RuntimeError)) === result 38 + end 39 + 40 + test "failure then does not yield" do 41 + original = Literal::Result(Integer, Symbol) { |type| type.failure(:nope) } 42 + yielded = false 43 + 44 + result = original.then do 45 + yielded = true 46 + Literal::Result(String, RuntimeError) { |type| type.success("ok") } 47 + end 48 + 49 + refute yielded 50 + assert result.equal?(original) 51 + end 52 + 53 + test "then block must return result" do 54 + result = Literal::Result(Integer, Symbol) { |type| type.success(42) } 55 + 56 + error = assert_raises(Literal::ArgumentError) do 57 + result.then(&:to_s) 58 + end 59 + 60 + assert_equal "Expected block to return a Literal::Result, got String", error.message 61 + end 62 + 63 + test "failure map updates success type metadata" do 64 + result = Literal::Result(Integer, Symbol) { |type| type.failure(:nope) } 65 + mapped = result.map(String, &:to_s) 66 + 67 + assert mapped.failure? 68 + assert_equal :nope, mapped.error! 69 + assert_equal String, mapped.success_type 70 + assert_equal Symbol, mapped.failure_type 71 + end 72 + 73 + test "success deconstruct_keys delegates to wrapped value" do 74 + person_class = Class.new do 75 + def initialize(name) 76 + @name = name 77 + end 78 + 79 + def deconstruct_keys(keys) 80 + h = { name: @name } 81 + keys ? h.slice(*keys) : h 82 + end 83 + end 84 + 85 + result = Literal::Result(person_class, Symbol) { |type| type.success(person_class.new("Joel")) } 86 + 87 + assert_equal({ name: "Joel" }, result.deconstruct_keys([:name])) 88 + assert_equal({}, result.deconstruct_keys([:value])) 89 + end 90 + 91 + test "failure deconstruct_keys delegates to wrapped error" do 92 + error_class = Class.new do 93 + def initialize(message) 94 + @message = message 95 + end 96 + 97 + def deconstruct_keys(keys) 98 + h = { message: @message } 99 + keys ? h.slice(*keys) : h 100 + end 101 + end 102 + 103 + result = Literal::Result(String, error_class) { |type| type.failure(error_class.new("oops")) } 104 + 105 + assert_equal({ message: "oops" }, result.deconstruct_keys([:message])) 106 + assert_equal({}, result.deconstruct_keys([:error])) 107 + end 108 + 109 + test "deconstruct_keys returns empty hash when wrapped object does not support it" do 110 + success = Literal::Result(Integer, Symbol) { |type| type.success(1) } 111 + failure = Literal::Result(String, Symbol) { |type| type.failure(:oops) } 112 + 113 + assert_equal({}, success.deconstruct_keys([:anything])) 114 + assert_equal({}, failure.deconstruct_keys([:anything])) 115 + end 116 + 117 + test "pattern matches success with positional pattern" do 118 + result = Literal::Result(Integer, Symbol) { |type| type.success(1) } 119 + 120 + matched = case result 121 + in Literal::Success[Integer] 122 + true 123 + else 124 + false 125 + end 126 + 127 + assert matched 128 + end 129 + 130 + test "pattern matches success with delegated keyword pattern" do 131 + message_class = Class.new do 132 + def initialize(message) 133 + @message = message 134 + end 135 + 136 + def deconstruct_keys(keys) 137 + h = { message: @message } 138 + keys ? h.slice(*keys) : h 139 + end 140 + end 141 + 142 + result = Literal::Result(message_class, Symbol) { |type| type.success(message_class.new("Hello")) } 143 + 144 + matched = case result 145 + in Literal::Success[message: "Hello"] 146 + true 147 + else 148 + false 149 + end 150 + 151 + assert matched 152 + end
+84
test/result_handler.test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + include Literal::Types 4 + 5 + test "handles success cases" do 6 + result = Literal::Result(String, Symbol) { |type| type.success("hello") } 7 + 8 + handled = result.handle do |on| 9 + on.success(String, &:upcase) 10 + on.failure(Symbol) 11 + end 12 + 13 + assert_equal "HELLO", handled 14 + end 15 + 16 + test "handles failure cases" do 17 + result = Literal::Result(String, Symbol) { |type| type.failure(:missing) } 18 + 19 + handled = result.handle do |on| 20 + on.success(String) 21 + on.failure(Symbol) { |error| "error: #{error}" } 22 + end 23 + 24 + assert_equal "error: missing", handled 25 + end 26 + 27 + test "raises for unhandled result" do 28 + result = Literal::Result(String, Symbol) { |type| type.success("hello") } 29 + 30 + error = assert_raises(Literal::ArgumentError) do 31 + result.handle do |on| 32 + on.failure(Symbol) { |it| it } 33 + end 34 + end 35 + 36 + assert_equal "No success handler covers String", error.message 37 + end 38 + 39 + test "can ignore a branch without a block" do 40 + result = Literal::Result(String, Symbol) { |type| type.success("hello") } 41 + 42 + handled = result.handle do |on| 43 + on.success(String) 44 + on.failure(Symbol) 45 + end 46 + 47 + assert_equal nil, handled 48 + end 49 + 50 + test "treats split handlers as exhaustive for union success types" do 51 + result = Literal::Result(_Union(String, Integer), Symbol) { |type| type.success("hello") } 52 + 53 + handled = result.handle do |on| 54 + on.success(String, &:upcase) 55 + on.success(Integer) { |value| value + 1 } 56 + on.failure(Symbol) 57 + end 58 + 59 + assert_equal "HELLO", handled 60 + end 61 + 62 + test "treats split handlers as exhaustive for union failure types" do 63 + result = Literal::Result(String, _Union(Symbol, RuntimeError)) { |type| type.failure(RuntimeError.new("boom")) } 64 + 65 + handled = result.handle do |on| 66 + on.success(String) 67 + on.failure(Symbol) { |error| "symbol: #{error}" } 68 + on.failure(RuntimeError, &:message) 69 + end 70 + 71 + assert_equal "boom", handled 72 + end 73 + 74 + test "uses mapped success type metadata for failure coverage" do 75 + result = Literal::Result(Integer, Symbol) { |type| type.failure(:missing) } 76 + .map(String, &:to_s) 77 + 78 + handled = result.handle do |on| 79 + on.success(String) 80 + on.failure(Symbol) { |error| "error: #{error}" } 81 + end 82 + 83 + assert_equal "error: missing", handled 84 + end