temporality 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8fd5d7006b68f06a13dd0f46790440a31d179384
4
- data.tar.gz: f4e6b4354ede8fc9c0ad6fa8717fe0a6b080f290
3
+ metadata.gz: b3427d7d435600ebdf7609aaf3fa32a929fcb49c
4
+ data.tar.gz: 6af589c77936c6c98573e129723c27ec4f4facc9
5
5
  SHA512:
6
- metadata.gz: d9dec839f133d29763adab32b68c017ecc12ef960398c8156c84f40e8bb4640c7161dcfa8d0e0f5268ff8e6d82ce7ed2b31e33bab330c3cbf979ada953a224ac
7
- data.tar.gz: ecd202980a60ec146c429a45750dfffeff4337bb6aeac56c94f8a43aa4907f6fc058a59315587b850fcba471c670b4b253f3eedf72e557cd2052bc862b0a20fe
6
+ metadata.gz: 10d40a04e6b406620aa808f615650ae5045ca897b6deefa0d509e59761736c03b9e3985dd1cdbd038957e052046df042eda0ae29d80aa362b6d16fac25f076ac
7
+ data.tar.gz: 6f2498c1fb678cdf6f54f29beb6913c79c4637196db1e0ab436e33931e3503d087fe5cfcebe1985985734f2aa1faffb10c6de086b37b15d58fa7bb63a9c9cfac
data/README.md CHANGED
@@ -4,20 +4,6 @@ Temporality [![Build Status](https://secure.travis-ci.org/davout/temporality.png
4
4
  ## What is Temporality
5
5
  Temporality adds the ability to `ActiveRecord::Base` descendants to validate temporal data on themselves, and their associations.
6
6
 
7
- ## Target features
8
- - Completeness implique prevent_overlap! implique également d'être deferred jusqu'au commit de la transaction
9
- - Enforce this is only used on belong_to associations
10
- - Doit permettre de valider que le parent est rempli
11
- - Doit permettre au parent de valider que les enfants sont inclus
12
- - Doit ne pas permettre l'overlap des records enfants
13
-
14
- - Doit faire l'auto-close des records précédents, avec callbacks
15
- - Doit faire l'auto-close des enfants, avec callbacks
16
- - Doit permettre d'initialiser l'enfant avec les bornes du parent en faisant par exemple parent.children.build
17
- - Scopes AR : intersecting, contained, englobant
18
- - Quand les bornes du parent sont changées on doit jouer les validations de l'enfant, sinon dans les autres cas, c'est l'enfant qu'on valide
19
- - Que se passe-t-il quand on modifie les bornes du parent, mais qu'on modifie en même temps les bornes de l'enfant ?
20
-
21
7
  ## Examples
22
8
 
23
9
  ````ruby
@@ -37,3 +23,4 @@ class Compensation < ActiveRecord::Base
37
23
  belongs_to :contract, temporality: { inclusion: true, auto_close_previous: true, allow_overlap: false, completeness: true }
38
24
  end
39
25
  ````
26
+
@@ -3,7 +3,7 @@ require 'date'
3
3
  require 'temporality/version'
4
4
 
5
5
  require 'temporality/slice_collection'
6
-
6
+ require 'temporality/errors'
7
7
  require 'temporality/validation'
8
8
  require 'temporality/default_boundary_values'
9
9
  require 'temporality/attribute_overrides'
@@ -11,17 +11,66 @@ require 'temporality/associations'
11
11
  require 'temporality/schema'
12
12
  require 'temporality/scopes'
13
13
  require 'temporality/day_count'
14
+ require 'temporality/transaction'
14
15
 
16
+ # = Temporality
17
+ #
18
+ # Root module for temporal functionality, include it in ActiveRecord classes
19
+ # to benefit from the temporality features.
20
+ #
21
+ # This functionality requires a +starts_on+ and +ends_on+ attribute pair defined
22
+ # on the models in which the module is included.
23
+ #
24
+ # == Example
25
+ #
26
+ # This will define three classes with temporality constraints. An employment
27
+ # contract will be required to be temporally within the bounds of the legal entity
28
+ # with which it is made. A contract will also be required to have a temporally
29
+ # complete set of compensation records.
30
+ #
31
+ # class LegalEntity < ActiveRecord::Base
32
+ # include Temporality
33
+ # has_many :employment_contracts
34
+ # end
35
+ #
36
+ # class EmploymentContract < ActiveRecord::Base
37
+ # include Temporality
38
+ # has_many :compensations
39
+ # belongs_to :legal_entity, temporality: {
40
+ # inclusion: true,
41
+ # auto_close: false,
42
+ # completeness: false,
43
+ # prevent_overlap: false
44
+ # }
45
+ # end
46
+ #
47
+ # class Compensation < ActiveRecord::Base
48
+ # include Temporality
49
+ # belongs_to :employment_contract, temporality: {
50
+ # inclusion: true,
51
+ # auto_close: true,
52
+ # completeness: true,
53
+ # prevent_overlap: true
54
+ # }
55
+ # end
56
+ #
15
57
  module Temporality
16
58
 
59
+ extend Transaction
60
+
17
61
  # Used when no start date is defined
18
62
  PAST_INFINITY = Date.new(1500, 1, 1)
19
63
 
20
64
  # Used when no end date is defined
21
65
  FUTURE_INFINITY = Date.new(5000, 1, 1)
22
66
 
67
+ # Prepended modules
23
68
  PREPENDS = [ AttributeOverrides, Validation ]
69
+
70
+ # Extensions to the included class
24
71
  EXTENDS = [ Associations, Scopes ]
72
+
73
+ # Inclusions for the included class
25
74
  INCLUDES = [ DefaultBoundaryValues ]
26
75
 
27
76
  def self.included(base)
@@ -29,6 +78,7 @@ module Temporality
29
78
  EXTENDS.each { |mod| base.extend(mod) }
30
79
  INCLUDES.each { |mod| base.include(mod) }
31
80
 
81
+ # TODO : On va peut-être pas l'inclure 50 fois ce truc...
32
82
  ActiveRecord::Base.send(:include, DayCount)
33
83
  end
34
84
 
@@ -1,9 +1,22 @@
1
1
  module Temporality
2
- module Associations
3
2
 
4
- # TODO : Use class-inheritable instance variables
3
+ # = Temporal associations
4
+ #
5
+ # This module overrides +ActiveRecord::Base.belongs_to+ with the ability to
6
+ # specifiy temporality options on the association.
7
+ #
8
+ # == Inverse associations
9
+ #
10
+ # It is necessary that ActiveRecord knows the inverse association on the
11
+ # +has_many+ side. If it isn't inferred automatically you must specify it
12
+ # using the +:inverse_of+ option on the +has_many+ declaration.
13
+ #
14
+ # @todo Use class-inheritable instance variables
15
+ #
16
+ module Associations
5
17
 
6
- DEFAULTS = { inclusion: true, completeness: false, prevent_overlap: false, auto_close: false }
18
+ # The default temporality options
19
+ DEFAULTS = { inclusion: true, completeness: false, prevent_overlap: false, auto_close: false }.freeze
7
20
 
8
21
  def belongs_to(*args, &block)
9
22
  @temporality ||= {}
@@ -17,13 +30,34 @@ module Temporality
17
30
  end
18
31
  end
19
32
 
20
- @temporality[assoc_name] = DEFAULTS.merge(opts)
33
+ @temporality[assoc_name] = with_implied_options(DEFAULTS.merge(opts))
21
34
  end
22
35
  end
23
36
 
24
37
  super(*args, &block)
25
38
  end
26
39
 
40
+
41
+ private
42
+
43
+ #
44
+ # Sets options implied by other options as follows:
45
+ #
46
+ # - +:auto_close+ implies +:completeness+
47
+ # - +:completeness+ implies +:prevent_overlap+
48
+ # - +:completeness+ implies +:inclusion+
49
+ #
50
+ # @param opts [Hash] The options hash
51
+ # @return [Hash] The options with implied options set
52
+ #
53
+ def with_implied_options(opts)
54
+ res = opts.dup
55
+ res[:completeness] ||= res[:auto_close]
56
+ res[:prevent_overlap] ||= res[:completeness]
57
+ res[:inclusion] ||= res[:completeness]
58
+ res
59
+ end
60
+
27
61
  end
28
62
  end
29
63
 
@@ -3,7 +3,33 @@ require 'temporality/validation_strategy'
3
3
  module Temporality
4
4
  class AutoClose < ValidationStrategy
5
5
 
6
- def validate
6
+ def call
7
+ prev = nil
8
+
9
+ if @model.id
10
+ prev = inverse.order('starts_on ASC').where('id <> ?', @model.id).last
11
+ else
12
+ prev = inverse.order('starts_on ASC').last
13
+ end
14
+
15
+ if prev
16
+ if prev.starts_on >= @model.starts_on
17
+ raise Temporality::AutoCloseError.new("Can't auto-close a previous sibling with a fully overlapping record")
18
+ end
19
+
20
+ if prev.ends_on != Temporality::FUTURE_INFINITY
21
+ raise Temporality::AutoCloseError.new("Can't auto-close previous record if it's end date is finite")
22
+ end
23
+ end
24
+
25
+ if prev
26
+ if Temporality.active_transaction?
27
+ prev.ends_on = @model.starts_on - 1
28
+ prev.save
29
+ else
30
+ raise Temporality::NoTransactionError.new("Auto-closing previous records requires a Temporality transaction")
31
+ end
32
+ end
7
33
  end
8
34
 
9
35
  end
@@ -4,18 +4,28 @@ module Temporality
4
4
  class Completeness < ValidationStrategy
5
5
 
6
6
  def validate
7
+ if Temporality.active_transaction?
8
+ Temporality.defer(defer_key) { perform_validation }
9
+ else
10
+ perform_validation
11
+ end
12
+ end
13
+
14
+ def perform_validation
7
15
  days = @model.day_count + (inverse.where('id <> ?', @model.id || -1).map(&:day_count).inject(&:+) || 0)
8
16
  parent_days = @model.send(@assoc).day_count
9
-
10
17
  raise Temporality::Violation.new(error_message) unless (parent_days == days)
11
18
  end
12
19
 
20
+ def defer_key
21
+ parent = @model.send(@assoc)
22
+ "#{parent.class.name}_#{parent.id}_#{inverse_name}"
23
+ end
24
+
13
25
  def error_message
14
26
  "#{@model.send(@assoc).class} record must have a temporally complete children collection for assocation #{inverse_name}"
15
27
  end
16
28
 
17
- # TODO : Check if in transaction ActiveRecord::Base.connection.open_transactions
18
-
19
29
  end
20
30
  end
21
31
 
@@ -2,7 +2,7 @@ module Temporality
2
2
  module DayCount
3
3
 
4
4
  def day_count
5
- (ends_on - starts_on).to_i
5
+ (ends_on - starts_on).to_i + 1
6
6
  end
7
7
 
8
8
  end
@@ -0,0 +1,6 @@
1
+ module Temporality
2
+ class Violation < RuntimeError; end
3
+ class AutoCloseError < Violation; end
4
+ class NoTransactionError < Violation; end
5
+ end
6
+
@@ -1,8 +1,9 @@
1
- #
2
- # Defines a `temporality` migration helper for use in a `create_table` block as well
3
- # as a `temporality(:table)` helper to be used to alter existing table definitions.
4
- #
5
1
  module Temporality
2
+
3
+ #
4
+ # Defines a +temporality+ migration helper for use in a +create_table+ block as well
5
+ # as a +temporality(:table)+ helper to be used to alter existing table definitions.
6
+ #
6
7
  module Schema
7
8
 
8
9
  def self.include_helpers!
@@ -0,0 +1,42 @@
1
+ module Temporality
2
+ module Transaction
3
+
4
+ QUEUE = :deferred_temporality_constraints
5
+
6
+ def transaction(*args, &block)
7
+ raise RuntimeError.new("There already is a currently active temporality transaction") if active_transaction?
8
+
9
+ ActiveRecord::Base.transaction(*args) do
10
+ begin
11
+ init_queue
12
+ block.call
13
+ process_queue
14
+ ensure
15
+ cleanup_queue
16
+ end
17
+ end
18
+ end
19
+
20
+ def defer(key, &block)
21
+ Thread.current[QUEUE][key] = block
22
+ end
23
+
24
+ def active_transaction?
25
+ !!Thread.current[QUEUE]
26
+ end
27
+
28
+ def process_queue
29
+ Thread.current[QUEUE].values.each(&:call)
30
+ end
31
+
32
+ def init_queue
33
+ Thread.current[QUEUE] = Hash.new
34
+ end
35
+
36
+ def cleanup_queue
37
+ Thread.current[QUEUE] = nil
38
+ end
39
+
40
+ end
41
+ end
42
+
@@ -1,5 +1,3 @@
1
- require 'temporality/violation'
2
-
3
1
  require 'temporality/auto_close'
4
2
  require 'temporality/completeness'
5
3
  require 'temporality/overlap'
@@ -8,11 +6,10 @@ require 'temporality/inclusion'
8
6
  module Temporality
9
7
  module Validation
10
8
 
11
- VALIDATIONS = {
9
+ CONSTRAINTS = {
12
10
  inclusion: Inclusion,
13
11
  prevent_overlap: Overlap,
14
- completeness: Completeness,
15
- auto_close: AutoClose
12
+ completeness: Completeness
16
13
  }
17
14
 
18
15
  DEFAULTS = { inclusion: true, completeness: false, prevent_overlap: false, auto_close: false }
@@ -28,15 +25,21 @@ module Temporality
28
25
  def validate_temporality_contraints!
29
26
  validate_bounds_order
30
27
 
31
- temporal_associations.each do |assoc, constraints|
32
- constraints.select { |k,v| v }.keys.each do |constraint|
28
+ temporal_associations.each do |assoc, constrs|
29
+ constraints = constrs.dup
30
+
31
+ if constraints.delete(:auto_close)
32
+ AutoClose.new(self, assoc).call
33
+ end
34
+
35
+ constraints.map { |constraint, enabled| constraint if enabled }.compact.each do |constraint|
33
36
  validate_constraint(assoc, constraint)
34
37
  end
35
38
  end
36
39
  end
37
40
 
38
- def validate_constraint(assoc, constraint)
39
- VALIDATIONS[constraint].new(self, assoc).validate
41
+ def validate_constraint(assoc, constraint_name)
42
+ CONSTRAINTS[constraint_name].new(self, assoc).validate
40
43
  end
41
44
 
42
45
  def validate_bounds_order
@@ -1,7 +1,7 @@
1
1
  module Temporality
2
2
 
3
3
  # Temporality gem version
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.5'
5
5
 
6
6
  end
7
7
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: temporality
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - David FRANCOIS
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-25 00:00:00.000000000 Z
11
+ date: 2016-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -98,44 +98,44 @@ dependencies:
98
98
  name: activerecord
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '5.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '5.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: sqlite3
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ">="
115
+ - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '0'
117
+ version: '1.3'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ">="
122
+ - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '0'
124
+ version: '1.3'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: byebug
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ">="
129
+ - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '9.0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ">="
136
+ - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '9.0'
139
139
  description: Give records the ability to validate temporal constraints on themselves
140
140
  and associations
141
141
  email:
@@ -153,6 +153,7 @@ files:
153
153
  - lib/temporality/completeness.rb
154
154
  - lib/temporality/day_count.rb
155
155
  - lib/temporality/default_boundary_values.rb
156
+ - lib/temporality/errors.rb
156
157
  - lib/temporality/inclusion.rb
157
158
  - lib/temporality/overlap.rb
158
159
  - lib/temporality/schema.rb
@@ -161,10 +162,10 @@ files:
161
162
  - lib/temporality/slice_collection.rb
162
163
  - lib/temporality/time_span.rb
163
164
  - lib/temporality/time_span_collection.rb
165
+ - lib/temporality/transaction.rb
164
166
  - lib/temporality/validation.rb
165
167
  - lib/temporality/validation_strategy.rb
166
168
  - lib/temporality/version.rb
167
- - lib/temporality/violation.rb
168
169
  homepage: https://paygun.fr
169
170
  licenses:
170
171
  - MIT
@@ -1,5 +0,0 @@
1
- module Temporality
2
- class Violation < RuntimeError
3
- end
4
- end
5
-