Runtime assertions for Ruby literal.fun
ruby
3
fork

Configure Feed

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

Add _TaggedUnion type with subtype compatibility and tests

+177 -9
+12
lib/literal/types.rb
··· 439 439 ) 440 440 end 441 441 442 + # Matches if the value matches any of the given tagged types. Tags are used to identify which type was matched. 443 + def _TaggedUnion(**members) 444 + TaggedUnionType.new(**members) 445 + end 446 + 447 + # Nilable version of `_TaggedUnion` 448 + def _TaggedUnion?(...) 449 + _Nilable( 450 + _TaggedUnion(...) 451 + ) 452 + end 453 + 442 454 # Matches if *any* given type is matched. 443 455 def _Union(*types) 444 456 UnionType.new(types)
+61
lib/literal/types/tagged_union_type.rb
··· 1 + # frozen_string_literal: true 2 + 3 + class Literal::Types::TaggedUnionType 4 + include Literal::Type 5 + 6 + def initialize(**members) 7 + raise Literal::ArgumentError.new("_TaggedUnion type must have at least one member.") if members.empty? 8 + 9 + flattened = {} 10 + members.each do |tag, type| 11 + if Literal::Types::TaggedUnionType === type 12 + type.members.each do |inner_tag, inner_type| 13 + raise Literal::ArgumentError.new("_TaggedUnion has duplicate tag: #{inner_tag.inspect}") if flattened.key?(inner_tag) 14 + flattened[inner_tag] = inner_type 15 + end 16 + else 17 + raise Literal::ArgumentError.new("_TaggedUnion has duplicate tag: #{tag.inspect}") if flattened.key?(tag) 18 + flattened[tag] = type 19 + end 20 + end 21 + 22 + @members = flattened.freeze 23 + freeze 24 + end 25 + 26 + attr_reader :members 27 + 28 + def inspect 29 + pairs = @members.map { |tag, type| "#{tag}: #{type.inspect}" } 30 + "_TaggedUnion(#{pairs.join(', ')})" 31 + end 32 + 33 + def ===(value) 34 + @members.any? { |_, type| type === value } 35 + end 36 + 37 + def [](tag) 38 + @members[tag] 39 + end 40 + 41 + def tag_for(value) 42 + @members.each { |tag, type| return tag if type === value } 43 + nil 44 + end 45 + 46 + def >=(other) 47 + types = @members.values 48 + 49 + case other 50 + when Literal::Types::TaggedUnionType 51 + other.members.values.all? { |t| types.any? { |t2| Literal.subtype?(t, t2) } } 52 + when Literal::Types::UnionType 53 + other.types.all? { |t| types.any? { |t2| Literal.subtype?(t, t2) } } && 54 + other.primitives.all? { |p| types.any? { |t| Literal.subtype?(p, t) } } 55 + else 56 + types.any? { |t| Literal.subtype?(other, t) } 57 + end 58 + end 59 + 60 + freeze 61 + end
+4 -9
lib/literal/types/union_type.rb
··· 80 80 81 81 case other 82 82 when Literal::Types::UnionType 83 - types_have_at_least_one_subtype = other.types.all? do |other_type| 84 - primitives.any? { |p| Literal.subtype?(other_type, p) } || types.any? { |t| Literal.subtype?(other_type, t) } 85 - end 86 - 87 - primitives_have_at_least_one_subtype = other.primitives.all? do |other_primitive| 88 - primitives.any? { |p| Literal.subtype?(other_primitive, p) } || types.any? { |t| Literal.subtype?(other_primitive, t) } 89 - end 90 - 91 - types_have_at_least_one_subtype && primitives_have_at_least_one_subtype 83 + other.types.all? { |t| primitives.any? { |p| Literal.subtype?(t, p) } || types.any? { |t2| Literal.subtype?(t, t2) } } && 84 + other.primitives.all? { |p| primitives.any? { |p2| Literal.subtype?(p, p2) } || types.any? { |t| Literal.subtype?(p, t) } } 85 + when Literal::Types::TaggedUnionType 86 + other.members.values.all? { |t| primitives.any? { |p| Literal.subtype?(t, p) } || types.any? { |t2| Literal.subtype?(t, t2) } } 92 87 else 93 88 types.any? { |t| Literal.subtype?(other, t) } || primitives.any? { |p| Literal.subtype?(other, p) } 94 89 end
+100
test/types/_tagged_union.test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + include Literal::Types 4 + 5 + test "#members" do 6 + type = _TaggedUnion(name: String, age: Integer) 7 + 8 + assert_equal type.members, { name: String, age: Integer } 9 + end 10 + 11 + test "#[]" do 12 + type = _TaggedUnion(name: String, age: Integer) 13 + 14 + assert_equal type[:name], String 15 + assert_equal type[:age], Integer 16 + assert_equal type[:missing], nil 17 + end 18 + 19 + test "#tag_for" do 20 + type = _TaggedUnion(name: String, age: Integer) 21 + 22 + assert_equal type.tag_for("Alice"), :name 23 + assert_equal type.tag_for(42), :age 24 + assert_equal type.tag_for(:other), nil 25 + end 26 + 27 + test "#inspect" do 28 + type = _TaggedUnion(name: String, age: Integer) 29 + 30 + assert_equal type.inspect, "_TaggedUnion(name: String, age: Integer)" 31 + end 32 + 33 + test "===" do 34 + type = _TaggedUnion(name: String, age: Integer, flag: _Boolean) 35 + 36 + assert type === "Alice" 37 + assert type === 42 38 + assert type === true 39 + assert type === false 40 + 41 + refute type === :symbol 42 + refute type === nil 43 + end 44 + 45 + test "nested tagged unions are flattened" do 46 + type = _TaggedUnion( 47 + name: String, 48 + **_TaggedUnion(age: Integer, flag: _Boolean).members, 49 + ) 50 + 51 + assert_equal type.inspect, "_TaggedUnion(name: String, age: Integer, flag: _Boolean)" 52 + end 53 + 54 + test "raises on empty members" do 55 + assert_raises(Literal::ArgumentError) { _TaggedUnion } 56 + end 57 + 58 + test "hierarchy: tagged union vs tagged union" do 59 + assert_subtype _TaggedUnion(name: String), _TaggedUnion(name: String) 60 + assert_subtype _TaggedUnion(name: String), _TaggedUnion(name: String, age: Integer) 61 + assert_subtype _TaggedUnion(name: String), _TaggedUnion(name: Comparable) 62 + 63 + refute_subtype _TaggedUnion(name: String, age: Integer), _TaggedUnion(name: String) 64 + refute_subtype _TaggedUnion(name: String), _TaggedUnion(age: Integer) 65 + end 66 + 67 + test "hierarchy: plain type vs tagged union" do 68 + assert_subtype String, _TaggedUnion(name: String, age: Integer) 69 + assert_subtype String, _TaggedUnion(name: Comparable) 70 + assert_subtype Integer, _TaggedUnion(name: String, age: Integer) 71 + 72 + refute_subtype Symbol, _TaggedUnion(name: String, age: Integer) 73 + end 74 + 75 + test "hierarchy: tagged union vs union" do 76 + assert_subtype _TaggedUnion(name: String, age: Integer), _Union(String, Integer) 77 + assert_subtype _TaggedUnion(name: String), _Union(String, Integer) 78 + 79 + refute_subtype _TaggedUnion(name: String, age: Integer), _Union(String) 80 + end 81 + 82 + test "hierarchy: union vs tagged union" do 83 + assert_subtype _Union(String, Integer), _TaggedUnion(name: String, age: Integer) 84 + assert_subtype _Union(String), _TaggedUnion(name: String, age: Integer) 85 + 86 + refute_subtype _Union(String, Symbol), _TaggedUnion(name: String, age: Integer) 87 + end 88 + 89 + test "error message" do 90 + error = assert_raises Literal::TypeError do 91 + Literal.check(:symbol, _TaggedUnion(name: String, age: Integer)) 92 + end 93 + 94 + assert_equal error.message, <<~ERROR 95 + Type mismatch 96 + 97 + Expected: _TaggedUnion(name: String, age: Integer) 98 + Actual (Symbol): :symbol 99 + ERROR 100 + end