Skip to content

Commit

Permalink
Association extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
davout committed Oct 27, 2016
1 parent 739e0a9 commit c9d1a49
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 23 deletions.
7 changes: 7 additions & 0 deletions .yardopts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
--readme README.md
--title 'Temporality Documentation'
--charset utf-8
--markup markdown
--private
'lib/**/*.rb'

21 changes: 12 additions & 9 deletions lib/temporality.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
require 'date'

require 'temporality/version'

require 'temporality/slice_collection'
require 'temporality/errors'
require 'temporality/validation'
Expand All @@ -14,15 +13,15 @@
require 'temporality/transaction'
require 'temporality/association_extensions'

# = Temporality
# # Temporality
#
# Root module for temporal functionality, include it in ActiveRecord classes
# to benefit from the temporality features.
#
# This functionality requires a +starts_on+ and +ends_on+ attribute pair defined
# on the models in which the module is included.
#
# == Example
# ## Example
#
# This will define three classes with temporality constraints. An employment
# contract will be required to be temporally within the bounds of the legal entity
Expand Down Expand Up @@ -66,22 +65,26 @@ module Temporality
FUTURE_INFINITY = Date.new(5000, 1, 1)

# Prepended modules
PREPENDS = [ AttributeOverrides, Validation ]
PREPENDS = [ AttributeOverrides, Validation ]

# Extensions to the included class
EXTENDS = [ Associations, Scopes ]
EXTENDS = [ Associations, Scopes ]

# Inclusions for the included class
INCLUDES = [ DefaultBoundaryValues ]
INCLUDES = [ DefaultBoundaryValues ]

#
# Sets-up all the required behaviour for temporal functionality on `base`
#
# @param base [ActiveRecord::Base]
#
def self.included(base)
PREPENDS.each { |mod| base.prepend(mod) }
EXTENDS.each { |mod| base.extend(mod) }
INCLUDES.each { |mod| base.include(mod) }

# TODO : On va peut-être pas l'inclure 50 fois ce truc...
ActiveRecord::Base.send(:include, DayCount)
end

end

ActiveRecord::Base.send(:include, Temporality::DayCount)

80 changes: 80 additions & 0 deletions lib/temporality/association_extensions.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,85 @@
module Temporality

#
# This module provides an override for the `build` method on an association
# proxy in order to automatically set the temporal bounds on built children
# unless these were explicitly provided.
#
module AssociationExtensions

#
# Override for `ActiveRecord::Relation#build` that provides automatic
# values for the temporal bounds.
#
# @return [ActiveRecord::Base] The built model
#
def build(**kwargs, &block)
with_default_bounds_scope(**kwargs) { super(**kwargs, &block) }
end

#
# Override for `ActiveRecord::Relation#create` that provides automatic
# values for the temporal bounds.
#
# @return [ActiveRecord::Base] The built model
#
def create(**kwargs, &block)
with_default_bounds_scope(**kwargs) { super(**kwargs, &block) }
end

#
# Override for `ActiveRecord::Relation#create!` that provides automatic
# values for the temporal bounds.
#
# @return [ActiveRecord::Base] The built model
#
def create!(**kwargs, &block)
with_default_bounds_scope(**kwargs) { super(**kwargs, &block) }
end

#
# Yields to the given block with a scope defining default values for the
# temporal bounds.
#
def with_default_bounds_scope(**kwargs)
return unless block_given?

owner = proxy_association.owner
klass = proxy_association.klass

starts_on = kwargs[:starts_on]
ends_on = kwargs[:ends_on]

if temporal_compatible_association?(owner, klass.new)
last_child = scope.order('ends_on ASC').last

ends_on ||= owner.ends_on

if scope.count.zero?
starts_on ||= owner.starts_on
elsif last_child.ends_on < owner.ends_on
starts_on ||= last_child.ends_on + 1
end
end

starts_on ||= Temporality::PAST_INFINITY
ends_on ||= Temporality::FUTURE_INFINITY

where(starts_on: starts_on).where(ends_on: ends_on).scoping { yield }
end


private

#
# Returns `true` if the association parent and children have temporal bounds
#
# @return [Boolean]
#
def temporal_compatible_association?(*models)
models.all? { |m| m.respond_to?(:starts_on) && m.respond_to?(:ends_on) }
end

end
end

2 changes: 1 addition & 1 deletion lib/temporality/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Temporality

# Temporality gem version
VERSION = '0.0.5'
VERSION = '0.0.6'

end

2 changes: 1 addition & 1 deletion spec/support/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class Dummy < ActiveRecord::Base

class Person < ActiveRecord::Base
include Temporality
has_many :dogs
has_many :dogs, extend: Temporality::AssociationExtensions
end

class Dog < ActiveRecord::Base
Expand Down
87 changes: 75 additions & 12 deletions spec/temporality/association_extensions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,96 @@

RSpec.describe Temporality::AssociationExtensions do

# TODO Try with keyword args, when passing a starts_on and ends on it shouldn't override

let(:person) { Person.create({ starts_on: Date.new(2016, 1, 1), ends_on: Date.new(2016, 12, 31) }) }

describe '#build' do
let(:dog) { person.dogs.build }

context 'when creating a child with no sibling records' do
it 'should inherit its parents starts_on bound' do
expect(dog.starts_on).to eql(person.starts_on)
context 'when building a child' do
context 'with no siblings' do
it 'should inherit its parents starts_on bound' do
expect(dog.starts_on).to eql(person.starts_on)
end

it 'should inherit its parents ends_on bound' do
expect(dog.ends_on).to eql(person.ends_on)
end
end

it 'should inherit its parents ends_on bound' do
expect(dog.ends_on).to eql(person.ends_on)
context 'with existing siblings' do
before { person.dogs.create({ starts_on: Date.new(2016, 3, 15), ends_on: Date.new(2016, 6, 30) }) }

it 'should set starts_on on the child as the date following ends_on on the previous sibling' do
expect(dog.starts_on).to eql(Date.new(2016, 7, 1))
end

it 'should inherit its parents ends_on bound' do
expect(dog.ends_on).to eql(person.ends_on)
end
end

context 'with an incompatible sibling' do
before { person.dogs.create({ starts_on: Date.new(2016, 3, 15), ends_on: Date.new(2016, 12, 31) }) }

it 'should not inherit its parents starts_on bound' do
expect(dog.starts_on).to eql(Temporality::PAST_INFINITY)
end

it 'should inherit its parents ends_on bound' do
expect(dog.ends_on).to eql(person.ends_on)
end
end
end
end

context 'when creating a child with existing siblings' do
before { person.dogs.create({ starts_on: Date.new(2016, 3, 15), ends_on: Date.new(2016, 6, 30) }) }
describe '#create' do
context 'when creating a child with defined attributes' do
let(:dog) { person.dogs.create(starts_on: Date.new(2016, 12, 13), ends_on: Date.new(2016, 12, 15)) }

it 'should not inherit its parents starts_on bound' do
expect(dog.starts_on).to eql(Temporality::PAST_INFINITY)
it 'should not override the start date' do
expect(dog.starts_on).to eql(Date.new(2016, 12, 13))
end

it 'should not inherit its parents ends_on bound' do
expect(dog.ends_on).to eql(Temporality::FUTURE_INFINITY)
it 'should not override the end date' do
expect(dog.ends_on).to eql(Date.new(2016, 12, 15))
end
end
end

context 'when creating a child without defined attributes' do
let(:dog) { person.dogs.create }

context 'with no siblings' do
it 'should inherit its parents starts_on bound' do
expect(dog.starts_on).to eql(person.starts_on)
end

it 'should inherit its parents ends_on bound' do
expect(dog.ends_on).to eql(person.ends_on)
end
end

context 'with existing siblings' do
before { person.dogs.create({ starts_on: Date.new(2016, 3, 15), ends_on: Date.new(2016, 6, 30) }) }

it 'should set starts_on on the child as the date following ends_on on the previous sibling' do
expect(dog.starts_on).to eql(Date.new(2016, 7, 1))
end

it 'should inherit its parents ends_on bound' do
expect(dog.ends_on).to eql(person.ends_on)
end
end

context 'with an incompatible sibling' do
before { person.dogs.create({ starts_on: Date.new(2016, 3, 15), ends_on: Date.new(2016, 12, 31) }) }

it 'should fail to create the child' do
expect { dog }.to raise_error(Temporality::Violation)
end
end
end
end
end

0 comments on commit c9d1a49

Please sign in to comment.