Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# => #<Point:0x894aacc @x=1, @y=3>
```

#### Explicit definition

```ruby
require 'value_object'
Expand Down
8 changes: 7 additions & 1 deletion lib/exceptions.rb
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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

Expand Down
28 changes: 20 additions & 8 deletions lib/value_object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def names
end

def predicates
self.class.predicate_symbols
self.class.predicates
end

def check_fields_are_initialized
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
109 changes: 82 additions & 27 deletions spec/value_object_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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