require "spec" private record NamedTupleSpecObj, x : Int32 do def_equals @x end describe "NamedTuple" do it "does NamedTuple.new, without type vars" do NamedTuple.new(x: 1, y: 2).should eq({x: 1, y: 2}) NamedTuple.new(z: NamedTupleSpecObj.new(10)).should eq({z: NamedTupleSpecObj.new(10)}) end it "does NamedTuple.new, with type vars" do NamedTuple(foo: Int32, bar: String).new(foo: 1, bar: "a").should eq({foo: 1, bar: "a"}) NamedTuple(z: NamedTupleSpecObj).new(z: NamedTupleSpecObj.new(10)).should eq({z: NamedTupleSpecObj.new(10)}) typeof(NamedTuple.new).new.should eq(NamedTuple.new) t = NamedTuple(foo: Int32 | String, bar: Int32 | String).new(foo: 1, bar: "a") t.should eq({foo: 1, bar: "a"}) t.class.should_not eq(NamedTuple(foo: Int32, bar: String)) end it "does NamedTuple.new, with hyphen in key" do NamedTuple("a-b": String).new("a-b": "foo").should eq({"a-b": "foo"}) end it "does NamedTuple.from" do t = NamedTuple(foo: Int32, bar: Int32).from({:foo => 1, :bar => 2}) t.should eq({foo: 1, bar: 2}) t.class.should eq(NamedTuple(foo: Int32, bar: Int32)) t = NamedTuple(foo: Int32, bar: Int32).from({"foo" => 1, "bar" => 2}) t.should eq({foo: 1, bar: 2}) t.class.should eq(NamedTuple(foo: Int32, bar: Int32)) t = NamedTuple("foo bar": Int32, "baz qux": Int32).from({"foo bar" => 1, "baz qux" => 2}) t.should eq({"foo bar": 1, "baz qux": 2}) t.class.should eq(NamedTuple("foo bar": Int32, "baz qux": Int32)) t = NamedTuple("\"": Int32, "\#{exit}": Int32).from({"\"" => 2, "\#{exit}" => 3}) t.should eq({"\"": 2, "\#{exit}": 3}) t.class.should eq(NamedTuple("\"": Int32, "\#{exit}": Int32)) expect_raises ArgumentError do NamedTuple(foo: Int32, bar: Int32).from({:foo => 1}) end expect_raises KeyError do NamedTuple(foo: Int32, bar: Int32).from({:foo => 1, :baz => 2}) end expect_raises(TypeCastError, /[Cc]ast from String to Int32 failed/) do NamedTuple(foo: Int32, bar: Int32).from({:foo => 1, :bar => "foo"}) end end it "does NamedTuple#from" do t = {foo: Int32, bar: Int32}.from({:foo => 1, :bar => 2}) t.should eq({foo: 1, bar: 2}) t.class.should eq(NamedTuple(foo: Int32, bar: Int32)) t = {foo: Int32, bar: Int32}.from({"foo" => 1, "bar" => 2}) t.should eq({foo: 1, bar: 2}) t.class.should eq(NamedTuple(foo: Int32, bar: Int32)) expect_raises ArgumentError do {foo: Int32, bar: Int32}.from({:foo => 1}) end expect_raises KeyError do {foo: Int32, bar: Int32}.from({:foo => 1, :baz => 2}) end expect_raises(TypeCastError, /[Cc]ast from String to Int32 failed/) do {foo: Int32, bar: Int32}.from({:foo => 1, :bar => "foo"}) end t = {foo: Int32, bar: Int32}.from({"foo" => 1, :bar => 2}) t.should eq({foo: 1, bar: 2}) t.class.should eq(NamedTuple(foo: Int32, bar: Int32)) t = {foo: Int32, bar: Int32}.from({"foo" => 1, :bar => 2} of String | Int32 | Symbol => Int32) t.should eq({foo: 1, bar: 2}) t.class.should eq(NamedTuple(foo: Int32, bar: Int32)) t = {"\"": Int32, "\#{exit}": Int32}.from({"\"" => 2, "\#{exit}" => 3}) t.should eq({"\"": 2, "\#{exit}": 3}) t.class.should eq(NamedTuple("\"": Int32, "\#{exit}": Int32)) end it "gets size" do {a: 1, b: 3}.size.should eq(2) end describe "#[] with non-literal index" do it "gets named tuple value with Symbol key" do tup = {a: 1, b: 'a'} key = :a val = tup[key] val.should eq(1) typeof(val).should eq(Int32 | Char) key = :b val = tup[key] val.should eq('a') typeof(val).should eq(Int32 | Char) end it "gets named tuple value with String key" do tup = {a: 1, b: 'a'} key = "a" val = tup[key] val.should eq(1) typeof(val).should eq(Int32 | Char) key = "b" val = tup[key] val.should eq('a') typeof(val).should eq(Int32 | Char) end it "raises missing key" do tup = {a: 1, b: 'a'} key = :c expect_raises(KeyError) { tup[key] } key = "d" expect_raises(KeyError) { tup[key] } end end describe "#[]? with non-literal index" do it "gets named tuple value or nil with Symbol key" do tup = {a: 1, b: 'a'} key = :a val = tup[key]? val.should eq(1) typeof(val).should eq(Int32 | Char | Nil) key = :b val = tup[key]? val.should eq('a') typeof(val).should eq(Int32 | Char | Nil) key = :c val = tup[key]? val.should be_nil typeof(val).should eq(Int32 | Char | Nil) end it "gets named tuple value or nil with String key" do tup = {a: 1, b: 'a'} key = "a" val = tup[key]? val.should eq(1) typeof(val).should eq(Int32 | Char | Nil) key = "b" val = tup[key]? val.should eq('a') typeof(val).should eq(Int32 | Char | Nil) key = "c" val = tup[key]? val.should be_nil typeof(val).should eq(Int32 | Char | Nil) end end describe ".[] with non-literal index" do it "gets named tuple metaclass value with Symbol key" do tup = NamedTuple(a: Int32, b: Char) key = :a val = tup[key] val.should eq(Int32) typeof(val).should eq(Union(Int32.class, Char.class)) key = :b val = tup[key] val.should eq(Char) typeof(val).should eq(Union(Int32.class, Char.class)) end it "gets named tuple metaclass value with String key" do tup = NamedTuple(a: Int32, b: Char) key = "a" val = tup[key] val.should eq(Int32) typeof(val).should eq(Union(Int32.class, Char.class)) key = "b" val = tup[key] val.should eq(Char) typeof(val).should eq(Union(Int32.class, Char.class)) end it "raises missing key" do tup = NamedTuple(a: Int32, b: Char) key = :c expect_raises(KeyError) { tup[key] } key = "d" expect_raises(KeyError) { tup[key] } end end describe ".[]? with non-literal index" do it "gets named tuple metaclass value or nil with Symbol key" do tup = NamedTuple(a: Int32, b: Char) key = :a val = tup[key]? val.should eq(Int32) typeof(val).should eq(Union(Int32.class, Char.class, Nil)) key = :b val = tup[key]? val.should eq(Char) typeof(val).should eq(Union(Int32.class, Char.class, Nil)) key = :c val = tup[key]? val.should be_nil typeof(val).should eq(Union(Int32.class, Char.class, Nil)) end it "gets named tuple metaclass value or nil with String key" do tup = NamedTuple(a: Int32, b: Char) key = "a" val = tup[key]? val.should eq(Int32) typeof(val).should eq(Union(Int32.class, Char.class, Nil)) key = "b" val = tup[key]? val.should eq(Char) typeof(val).should eq(Union(Int32.class, Char.class, Nil)) key = "c" val = tup[key]? val.should be_nil typeof(val).should eq(Union(Int32.class, Char.class, Nil)) end end describe "#dig?" do it "gets the value at given path given splat" do h = {a: {b: {c: [10, 20]}}, x: {a: "b"}} h.dig?(:a, :b, :c).should eq([10, 20]) h.dig?("x", "a").should eq("b") end it "returns nil if not found" do h = {a: {b: {c: 300}}, x: {a: "b"}} h.dig?("a", "b", "c", "d", "e").should be_nil h.dig?("z").should be_nil h.dig?("").should be_nil end end describe "#dig" do it "gets the value at given path given splat" do h = {a: {b: {c: [10, 20]}}, x: {a: "b", c: nil}} h.dig(:a, :b, :c).should eq([10, 20]) h.dig("x", "a").should eq("b") h.dig("x", "c").should be_nil end it "raises KeyError if not found" do h = {a: {b: {c: 300}}, x: {a: "b"}} expect_raises KeyError, %(NamedTuple value not diggable for key: "c") do h.dig("a", "b", "c", "d", "e") end expect_raises KeyError, %(Missing named tuple key: "z") do h.dig("z") end expect_raises KeyError, %(Missing named tuple key: "") do h.dig("") end end end it "computes a hash value" do tup1 = {a: 1, b: 'a'} tup1.hash.should eq(tup1.dup.hash) tup2 = {b: 'a', a: 1} tup2.hash.should eq(tup1.hash) end it "does each" do tup = {a: 1, b: "hello"} i = 0 tup.each do |key, value| case i when 0 key.should eq(:a) value.should eq(1) when 1 key.should eq(:b) value.should eq("hello") else fail "shouldn't happen" end i += 1 end.should be_nil i.should eq(2) end it "does each_key" do tup = {a: 1, b: "hello"} i = 0 tup.each_key do |key| case i when 0 key.should eq(:a) when 1 key.should eq(:b) else fail "shouldn't happen" end i += 1 end.should be_nil i.should eq(2) end it "does each_value" do tup = {a: 1, b: "hello"} i = 0 tup.each_value do |value| case i when 0 value.should eq(1) when 1 value.should eq("hello") else fail "shouldn't happen" end i += 1 end.should be_nil i.should eq(2) end it "does each_with_index" do tup = {a: 1, b: "hello"} i = 0 tup.each_with_index do |key, value, index| case i when 0 key.should eq(:a) value.should eq(1) index.should eq(0) when 1 key.should eq(:b) value.should eq("hello") index.should eq(1) else fail "shouldn't happen" end i += 1 end.should be_nil i.should eq(2) end it "does has_key? with symbol" do tup = {a: 1, b: 'a'} tup.has_key?(:a).should be_true tup.has_key?(:b).should be_true tup.has_key?(:c).should be_false end it "does has_key? with string" do tup = {a: 1, b: 'a'} tup.has_key?("a").should be_true tup.has_key?("b").should be_true tup.has_key?("c").should be_false end it "does empty" do {a: 1}.empty?.should be_false NamedTuple.new.empty?.should be_true end describe "#to_a" do it "creates an array of key-value pairs" do tup = {a: 1, b: 'a'} tup.to_a.should eq([{:a, 1}, {:b, 'a'}]) end it "preserves key type for empty named tuples" do tup = NamedTuple.new arr = tup.to_a arr.should be_empty arr.should be_a(Array({Symbol, NoReturn})) end end it "does map" do tup = {a: 1, b: 'a'} strings = tup.map { |k, v| "#{k.inspect}-#{v.inspect}" } strings.should eq([":a-1", ":b-'a'"]) end it "compares with same named tuple type" do tup1 = {a: 1, b: 'a'} tup2 = {b: 'a', a: 1} tup3 = {a: 1, b: 'b'} tup1.should eq(tup2) tup1.should_not eq(tup3) end it "compares with other named tuple type" do tup1 = {a: 1, b: 'a'} tup2 = {b: 'a', a: 1.0} tup3 = {b: 'a', a: 1.1} tup1.should eq(tup2) tup1.should_not eq(tup3) end it "compares with named tuple union (#5131)" do tup1 = {a: 1, b: 'a'} tup2 = {a: 1, c: 'b'} u = tup1 || tup2 u.should eq(u) v = tup2 || tup1 u.should_not eq(v) end describe "#to_h" do it "creates a hash" do tup1 = {a: 1, b: "hello"} hash = tup1.to_h hash.should eq({:a => 1, :b => "hello"}) end it "creates an empty hash from an empty named tuple" do tup = NamedTuple.new hash = tup.to_h hash.should be_empty hash.should be_a(Hash(Symbol, NoReturn)) end end it "does to_s" do tup = {a: 1, b: "hello"} tup.to_s.should eq(%({a: 1, b: "hello"})) end it "dups" do tup1 = {a: 1, b: [1, 2, 3]} tup2 = tup1.dup tup1[:b] << 4 tup2[:b].should be(tup1[:b]) end it "clones" do tup1 = {a: 1, b: [1, 2, 3]} tup2 = tup1.clone tup1[:b] << 4 tup2[:b].should eq([1, 2, 3]) tup2 = {"foo bar": 1} tup2.clone.should eq(tup2) tup3 = NamedTuple.new tup3.clone.should eq(tup3) end it "does keys" do tup = {a: 1, b: 2} tup.keys.should eq({:a, :b}) end it "does sorted_keys" do tup = {foo: 1, bar: 2, baz: 3} tup.sorted_keys.should eq({:bar, :baz, :foo}) end it "does values" do tup = {a: 1, b: 'a'} tup.values.should eq({1, 'a'}) end it "merges with other named tuple" do a = {one: 1, two: 2, three: 3, four: 4, five: 5, "im \"string": "works"} b = {two: "Two", three: true, "new one": "ok"} a.merge(b).merge(four: "Four").merge(NamedTuple.new).should eq({one: 1, two: "Two", three: true, four: "Four", five: 5, "new one": "ok", "im \"string": "works"}) end it "merges two empty named tuples" do NamedTuple.new.merge(NamedTuple.new).should eq(NamedTuple.new) end it "does reverse merge with other named tuple" do a = {one: 1, two: 2, three: 3, four: 4, five: 5, "im \"string": "works"} b = {two: "Two", three: true, "new one": "ok"} a.reverse_merge(b).reverse_merge(four: "Four").reverse_merge(NamedTuple.new).should eq({one: 1, two: 2, three: 3, four: 4, five: 5, "new one": "ok", "im \"string": "works"}) end it "does reverse merge two empty named tuples" do NamedTuple.new.reverse_merge(NamedTuple.new).should eq(NamedTuple.new) end it "does types" do tuple = {a: 1, b: 'a', c: "hello"} tuple.class.types.to_s.should eq("{a: Int32, b: Char, c: String}") end end