diff --git a/CHANGELOG.md b/CHANGELOG.md index 326b476..5613ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## Unreleased ### BREAKING CHANGES - Rename the `Or` shape to `Any` +- Add a `Method` shape (where the shape description is the name of a method which, when called on a + test object, must return a truthy value). This is a breaking change because the `Shaped::Shape` + constructor will now return an instance of `Shaped::Shapes::Method` rather than + `Shaped::Shapes::Equality` when called with a Symbol argument. ### Added - Add an `All` shape (w/ multiple sub-shapes, all of which must be matched) diff --git a/README.md b/README.md index ed0a409..a86fd7e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Validate the "shape" of Ruby objects! * [Shaped::Shapes::Array](#shapedshapesarray) * [Shaped::Shapes::Class](#shapedshapesclass) * [ActiveModel validations](#activemodel-validations) + * [Shaped::Shapes::Method](#shapedshapesmethod) * [Shaped::Shapes::Callable](#shapedshapescallable) * [Shaped::Shapes::Equality](#shapedshapesequality) * [Shaped::Shapes::Any](#shapedshapesany) @@ -30,7 +31,7 @@ Validate the "shape" of Ruby objects! * [For maintainers](#for-maintainers) * [License](#license) - + @@ -205,6 +206,21 @@ shape.matched_by?('a@b.c') # too short # => false ``` +## Shaped::Shapes::Method + +This shape allows specifying a method name that, when called upon a test object, must return a +truthy value in order for `matched_by?` to be true. + +```rb +shape = Shaped::Shape(:odd?) + +shape.matched_by?(55) +# => true + +shape.matched_by?(60) +# => false +``` + ## Shaped::Shapes::Callable This shape is very powerful if you need a very customized shape definition; you can define any diff --git a/lib/shaped.rb b/lib/shaped.rb index e8ca172..f673458 100644 --- a/lib/shaped.rb +++ b/lib/shaped.rb @@ -30,6 +30,7 @@ def self.Shape(*shape_descriptions) when Shaped::Shape then shape_description when Hash then Shaped::Shapes::Hash.new(shape_description) when Array then Shaped::Shapes::Array.new(shape_description) + when Symbol then Shaped::Shapes::Method.new(shape_description) when Class then Shaped::Shapes::Class.new(shape_description, validation_options) else if shape_description.respond_to?(:call) diff --git a/lib/shaped/shapes/method.rb b/lib/shaped/shapes/method.rb new file mode 100644 index 0000000..fde9c00 --- /dev/null +++ b/lib/shaped/shapes/method.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Shaped::Shapes::Method < Shaped::Shape + def initialize(method_name) + @method_name = method_name + end + + def matched_by?(object) + !!object.public_send(@method_name) + end + + def to_s + "object returning truthy for ##{@method_name}" + end +end diff --git a/spec/shaped_spec.rb b/spec/shaped_spec.rb index a7573fc..5b45c68 100644 --- a/spec/shaped_spec.rb +++ b/spec/shaped_spec.rb @@ -33,6 +33,14 @@ end end + context 'when called with a Symbol' do + let(:shape_description) { :valid? } + + it 'returns an instance of Shaped::Shapes::Method' do + expect(shape).to be_a(Shaped::Shapes::Method) + end + end + context 'when called with a Class' do let(:shape_description) { Numeric } diff --git a/spec/shapes/all_spec.rb b/spec/shapes/all_spec.rb index 90e6eb9..6bb0482 100644 --- a/spec/shapes/all_spec.rb +++ b/spec/shapes/all_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Shaped::Shapes::All do subject(:all_shape) { Shaped::Shapes::All.new(*all_shape_descriptions) } - let(:all_shape_descriptions) { [Numeric, ->(number) { number.even? }] } + let(:all_shape_descriptions) { [Numeric, :even?] } let(:test_object) { 88 } describe '#initialize' do @@ -32,7 +32,7 @@ context 'when the test object satisfies all of the sub-shape descriptions' do before do expect(test_object).to be_a(all_shape_descriptions.first) - expect(all_shape_descriptions.second.call(test_object)).to eq(true) + expect(test_object.public_send(all_shape_descriptions.second)).to eq(true) end it 'returns true' do @@ -45,7 +45,7 @@ before do expect(test_object).to be_a(all_shape_descriptions.first) # matched - expect(all_shape_descriptions.second.call(test_object)).to eq(false) # not matched + expect(test_object.public_send(all_shape_descriptions.second)).to eq(false) # not matched end it 'returns false' do @@ -57,8 +57,8 @@ describe '#to_s' do subject(:to_s) { all_shape.to_s } - it 'returns a readably formatted description of the list of allowed shapes' do - expect(to_s).to match(/Numeric AND Proc test defined at .*all_spec\.rb:\d+/) + it 'returns a readably formatted description of the list of required shapes' do + expect(to_s).to eq('Numeric AND object returning truthy for #even?') end end end diff --git a/spec/shapes/method_spec.rb b/spec/shapes/method_spec.rb new file mode 100644 index 0000000..38d834a --- /dev/null +++ b/spec/shapes/method_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.describe Shaped::Shapes::Method do + subject(:method_shape) { Shaped::Shapes::Method.new(method_shape_description) } + + let(:method_shape_description) { :even? } + let(:test_object) { 64 } + + describe '#initialize' do + it 'does not raise an error' do + expect { method_shape }.not_to raise_error + end + end + + describe '#matched_by?' do + subject(:matched_by?) { method_shape.matched_by?(test_object) } + + context 'when the test object returns a truthy value when called with the specified method' do + before { expect(test_object).to be_even } + + it 'returns true' do + expect(matched_by?).to eq(true) + end + end + + context 'when the test object returns a falsy value when called with the specified method' do + before { expect(test_object).not_to be_even } + + let(:test_object) { 71 } + + it 'returns false' do + expect(matched_by?).to eq(false) + end + end + end + + describe '#to_s' do + subject(:to_s) { method_shape.to_s } + + it 'returns a string naming the method that must be matched' do + expect(to_s).to eq('object returning truthy for #even?') + end + end +end