temporality 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
-