From 1015f73bcf1348ce66814277ece18c7a341f22c3 Mon Sep 17 00:00:00 2001 From: Eduardo Sebastian Date: Mon, 12 Oct 2015 20:06:17 +0200 Subject: [PATCH] include support for the definition of implicit invariants --- README.md | 28 +++++++++- lib/exceptions.rb | 8 ++- lib/value_object.rb | 28 +++++++--- spec/value_object_spec.rb | 109 ++++++++++++++++++++++++++++---------- 4 files changed, 136 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index ff69185..8589103 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,33 @@ a_point.hash == a_different_point.hash ### Invariants -You can declare invariants to restrict field values on initialization +You can declare invariants to restrict field values on initialization, +both in an implicit or explicit way: + +#### Implicit definition + +```ruby +require 'value_object' + +class Point + include ValueObject + fields :x, :y + invariants do + x < y + end +end + +Point.new(-5, 3) +# ValueObject::ViolatedInvariant: Fields values [-5, 3] violate invariant: implicit + +Point.new(6, 3) +# ValueObject::ViolatedInvariant: Fields values [6, 3] violate invariant: implicit + +Point.new(1, 3) +# => # +``` + +#### Explicit definition ```ruby require 'value_object' diff --git a/lib/exceptions.rb b/lib/exceptions.rb index b4c8dfe..97fd09e 100644 --- a/lib/exceptions.rb +++ b/lib/exceptions.rb @@ -1,4 +1,10 @@ module ValueObject + class BadInvariantDefinition < Exception + def initialize() + super "Invariant must be either declared or specified" + end + end + class NotImplementedInvariant < Exception def initialize(name) super "Invariant #{name} needs to be implemented" @@ -7,7 +13,7 @@ def initialize(name) class ViolatedInvariant < Exception def initialize(name, wrong_values) - super "Fields values " + wrong_values.to_s + " violate invariant: #{name}" + super "Field values " + wrong_values.to_s + " violate invariant: #{name}" end end diff --git a/lib/value_object.rb b/lib/value_object.rb index 56026c6..314f923 100644 --- a/lib/value_object.rb +++ b/lib/value_object.rb @@ -38,7 +38,7 @@ def names end def predicates - self.class.predicate_symbols + self.class.predicates end def check_fields_are_initialized @@ -71,10 +71,18 @@ def uninitialized_field_names def check_invariants return if predicates.nil? - predicates.each do |predicate| - valid = invariant_holds?(predicate) - raise ViolatedInvariant.new(predicate, values) unless valid + if predicates[:implicit_invariants] + valid = instance_eval(&predicates[:implicit_invariants]) + raise ViolatedInvariant.new("implicit", values) unless valid end + + if predicates[:explicit_invariants] + predicates[:explicit_invariants].each do |predicate| + valid = invariant_holds?(predicate) + raise ViolatedInvariant.new(predicate, values) unless valid + end + end + end def invariant_holds?(predicate_symbol) @@ -90,8 +98,8 @@ def field_names @field_names end - def predicate_symbols - @predicate_symbols + def predicates + { implicit_invariants: @predicate_block, explicit_invariants: @predicate_symbols } end def fields(*names) @@ -101,8 +109,12 @@ def fields(*names) @field_names = names end - def invariants(*predicate_symbols) - @predicate_symbols = predicate_symbols + def invariants(*predicate_symbols, &predicate_block) + raise BadInvariantDefinition.new if (predicate_symbols.empty? && !block_given?) || (!predicate_symbols.empty? && block_given?) + + @predicate_symbols = predicate_symbols unless block_given? + @predicate_block = predicate_block end end + end diff --git a/spec/value_object_spec.rb b/spec/value_object_spec.rb index 9b1fd9c..65fe384 100644 --- a/spec/value_object_spec.rb +++ b/spec/value_object_spec.rb @@ -37,7 +37,7 @@ class Point describe "restrictions" do describe "on declaration" do - it "must at least have one field" do + it "must have at least one field" do expect { class Point include ValueObject @@ -78,41 +78,96 @@ class Point end describe "forcing invariants" do - it "forces declared invariants" do - class Point - include ValueObject - fields :x, :y - invariants :x_less_than_y, :inside_first_quadrant - - private - def x_less_than_y - x < y + describe "on declaration" do + it "some invariant must be defined" do + expect { + class Point + include ValueObject + fields :x, :y + invariants + end + }.to raise_error(ValueObject::BadInvariantDefinition) + end + + it "cannot define both implicit and explicit invariants" do + expect { + class Point + include ValueObject + fields :x, :y + invariants :x_less_than_y, :inside_first_quadrant do + y % 2 == 0 + end + end + }.to raise_error(ValueObject::BadInvariantDefinition) + end + end + + describe "on initialization" do + + it "forces implicit invariants with several conditions" do + class Point + include ValueObject + fields :x, :y + invariants do + x < y + end end - def inside_first_quadrant - x > 0 && y > 0 + expect { + Point.new(6, 3) + }.to raise_error(ValueObject::ViolatedInvariant, "Field values [6, 3] violate invariant: implicit") + end + + it "forces implicit invariants" do + class Point + include ValueObject + fields :x, :y + invariants do + x < y + end end + + expect { + Point.new(6, 3) + }.to raise_error(ValueObject::ViolatedInvariant, "Field values [6, 3] violate invariant: implicit") end - expect { - Point.new(6, 3) - }.to raise_error(ValueObject::ViolatedInvariant, "Fields values [6, 3] violate invariant: x_less_than_y") + it "forces explicit invariants" do + class Point + include ValueObject + fields :x, :y + invariants :x_less_than_y, :inside_first_quadrant - expect { - Point.new(-5, 3) - }.to raise_error(ValueObject::ViolatedInvariant, "Fields values [-5, 3] violate invariant: inside_first_quadrant") - end + private + def x_less_than_y + x < y + end - it "raises an exception when a declared invariant has not been implemented" do - class Point - include ValueObject - fields :x, :y - invariants :integers + def inside_first_quadrant + x > 0 && y > 0 + end + end + + expect { + Point.new(6, 3) + }.to raise_error(ValueObject::ViolatedInvariant, "Field values [6, 3] violate invariant: x_less_than_y") + + expect { + Point.new(-5, 3) + }.to raise_error(ValueObject::ViolatedInvariant, "Field values [-5, 3] violate invariant: inside_first_quadrant") end - expect { - Point.new(5, 2) - }.to raise_error(ValueObject::NotImplementedInvariant, "Invariant integers needs to be implemented") + it "raises an exception when a declared invariant has not been implemented" do + class Point + include ValueObject + fields :x, :y + invariants :integers + end + + expect { + Point.new(5, 2) + }.to raise_error(ValueObject::NotImplementedInvariant, "Invariant integers needs to be implemented") + end end end end