dry-types/spec/dry/types/intersection_spec.rb

234 lines
6.2 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Dry::Types::Intersection do
let(:t) { Dry.Types }
let(:callable_type) { t.Interface(:call) }
let(:procable_type) { t.Interface(:to_proc) }
let(:function_type) { callable_type & procable_type }
describe "common nominal behavior" do
subject(:type) { function_type }
it_behaves_like "Dry::Types::Nominal#meta"
it_behaves_like "Dry::Types::Nominal without primitive"
it_behaves_like "a composable constructor"
it "is frozen" do
expect(type).to be_frozen
end
end
it_behaves_like "a constrained type" do
let(:type) { function_type }
it_behaves_like "a composable constructor"
end
describe "#[]" do
it "works with two pass-through types" do
type = t::Nominal::Hash & t.Hash(foo: t::Nominal::Integer)
expect(type[{foo: ""}]).to eq({foo: ""})
expect(type[{foo: 312}]).to eq({foo: 312})
end
it "works with two strict types" do
type = t::Strict::Hash & t.Hash(foo: t::Strict::Integer)
expect(type[{foo: 312}]).to eq({foo: 312})
expect { type[312] }.to raise_error(Dry::Types::CoercionError)
end
it "is aliased as #call" do
type = t::Nominal::Hash & t.Hash(foo: t::Nominal::Integer)
expect(type.call({foo: ""})).to eq({foo: ""})
expect(type.call({foo: 312})).to eq({foo: 312})
end
it "works with two constructor & constrained types" do
left = t.Array(t::Strict::Hash)
right = t.Array(t.Hash(foo: t::Nominal::Integer))
type = left & right
expect(type[[{foo: 312}]]).to eql([{foo: 312}])
end
it "works with two complex types with constraints" do
type =
t
.Array(t.Array(t::Coercible::String.constrained(min_size: 5)).constrained(size: 2))
.constrained(min_size: 1) &
t
.Array(t.Array(t::Coercible::String.constrained(format: /foo/)).constrained(size: 2))
.constrained(min_size: 2)
expect(type.([%w[foofoo barfoo], %w[bazfoo fooqux]])).to eql(
[%w[foofoo barfoo], %w[bazfoo fooqux]]
)
expect { type[:oops] }.to raise_error(Dry::Types::ConstraintError, /:oops/)
expect { type[[]] }.to raise_error(Dry::Types::ConstraintError, /\[\]/)
expect { type.([%i[foo]]) }.to raise_error(Dry::Types::ConstraintError, /\[:foo\]/)
expect { type.([[1], [2]]) }.to raise_error(Dry::Types::ConstraintError, /2, \[1\]/)
expect { type.([%w[foofoo barfoo], %w[bazfoo foo]]) }.to raise_error(
Dry::Types::ConstraintError,
/min_size\?\(5, "foo"\)/
)
end
end
describe "#try" do
subject(:type) { function_type }
it "returns success when value passed" do
expect(type.try(-> {})).to be_success
end
it "returns failure when value did not pass" do
expect(type.try(:foo)).to be_failure
end
end
describe "#success" do
subject(:type) { function_type }
it "returns success when value passed" do
expect(type.success(-> {})).to be_success
end
it "raises ArgumentError when non of the types have a valid input" do
expect { type.success("foo") }.to raise_error(ArgumentError, /Invalid success value 'foo' /)
end
end
describe "#failure" do
subject(:type) { Dry::Types["integer"] & Dry::Types["string"] }
it "returns failure when invalid value is passed" do
expect(type.failure(true)).to be_failure
end
end
describe "#===" do
subject(:type) { function_type }
it "returns boolean" do
expect(type.===(-> {})).to eql(true)
expect(type.===(nil)).to eql(false) # rubocop:disable Style/NilComparison
end
context "in case statement" do
let(:value) do
case -> {}
when type
"accepted"
else
"invalid"
end
end
it "returns correct value" do
expect(value).to eql("accepted")
end
end
end
describe "#default" do
it "returns a default value intersection type" do
type = (t::Nominal::Nil & t::Nominal::Nil).default("foo")
expect(type.call).to eql("foo")
end
end
describe "#constructor" do
let(:type) do
(t::Nominal::String & t::Nominal::Nil).constructor do |input|
input ? "#{input} world" : input
end
end
it "returns the correct value" do
expect(type.call("hello")).to eql("hello world")
expect(type.call(nil)).to eql(nil)
expect(type.call(10)).to eql("10 world")
end
it "returns if value is valid" do
expect(type.valid?("hello")).to eql(true)
expect(type.valid?(nil)).to eql(true)
expect(type.valid?(10)).to eql(true)
end
end
describe "#rule" do
let(:type) { function_type }
it "returns a rule" do
rule = type.rule
expect(rule.(-> {})).to be_success
expect(rule.(nil)).to be_failure
end
end
describe "#to_s" do
context "shallow intersection" do
let(:type) { t::Nominal::String & t::Nominal::Integer }
it "returns string representation of the type" do
expect(type.to_s).to eql("#<Dry::Types[Intersection<Nominal<String> & Nominal<Integer>>]>")
end
end
context "intersection tree" do
let(:type) { t::Nominal::String & t::Nominal::Integer & t::Nominal::Date & t::Nominal::Time }
it "returns string representation of the type" do
expect(type.to_s).to eql(
"#<Dry::Types[Intersection<" \
"Nominal<String> & " \
"Nominal<Integer> & " \
"Nominal<Date> & " \
"Nominal<Time>" \
">]>"
)
end
end
end
context "with map type" do
let(:map_type) { t::Nominal::Hash.map(t::Nominal::Symbol, t::Nominal::String) }
let(:schema_type) { t.Hash(foo: t::Strict::String) }
subject(:type) { map_type & schema_type }
it "rejects invalid input" do
expect(type.valid?({foo: 1, bar: 1})).to be false
expect { type[{foo: 1, bar: 1}] }.to raise_error(Dry::Types::SchemaError)
end
end
describe "#meta" do
context "optional types" do
let(:meta) { {foo: :bar} }
subject(:type) { Dry::Types["string"].optional }
it "uses meta from the right branch" do
expect(type.meta(meta).meta).to eql(meta)
expect(type.meta(meta).right.meta).to eql(meta)
end
end
end
end