themis 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f56efba984b1bd0789799d9dc814f19bbaa3fc17
4
+ data.tar.gz: 1849671bff67b2cf9a1fa3ed0e6a13721e08af5e
5
+ SHA512:
6
+ metadata.gz: 085e50ffaada519d96163830581c7c8c4e586edf2a4e97223cb649d41105c1e87fdde42f177a6b7353f19393de5c20d9c286841b012344bb0ee91066f06c3112
7
+ data.tar.gz: dc74d323204022de865f3f607e554d18065126e368b67129b6ae46377d979476aa9a43ebe95b473c888248a7bcc9999d32b7cebadee444f44313568de592f68f
data/README.markdown ADDED
@@ -0,0 +1,213 @@
1
+ # Themis
2
+
3
+ [![Build Status](https://secure.travis-ci.org/TMXCredit/themis.png)](http://travis-ci.org/TMXCredit/themis)
4
+ [![Dependency Status](https://gemnasium.com/TMXCredit/themis.png)](https://gemnasium.com/TMXCredit/themis)
5
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/TMXCredit/themis)
6
+
7
+ Modular and switchable validations for ActiveRecord models.
8
+
9
+ ## Usage
10
+
11
+ ### Define validation module
12
+
13
+ ```ruby
14
+ module PersonValidation
15
+ extend Themis::Validation
16
+
17
+ validates_presence_of :first_name
18
+ validates_presence_of :last_name
19
+ end
20
+ ```
21
+
22
+ ### Mix validation modules
23
+
24
+ ```ruby
25
+ module UserValidation
26
+ extend Themis::Validation
27
+
28
+ # use all validators defined in PersonValidation
29
+ include PersonValidation
30
+
31
+ validates :email, :format => {:with /\A.*@.*\z/ }, :presence => true
32
+ validate_presence_of :spouse, :if => :married?
33
+ end
34
+ ```
35
+
36
+ ### ActiveRecord model
37
+
38
+ #### Including validation modules in models:
39
+
40
+ You can include a validation module in a ActiveRecord model to apply to the model all validators
41
+ defined in the module:
42
+
43
+ ```ruby
44
+ class User < ActiveRecord::Base
45
+ include UserValidation
46
+ end
47
+ ```
48
+
49
+ It's equivalent to:
50
+
51
+ ```ruby
52
+ class User < ActiveRecord::Base
53
+ validates_presence_of :first_name
54
+ validates_presence_of :last_name
55
+ validates :email, :format => {:with /\A.*@.*\z/ }, :presence => true
56
+ validate_presence_of :spouse, :if => :married?
57
+ end
58
+ ```
59
+
60
+ #### Using has\_validation and use\_validation methods
61
+
62
+ You can define a number of validator sets for a model using the `.has_validation` method. So you can
63
+ choose with the `#use_validation` method which validator set to use depending on the context.
64
+
65
+ ```ruby
66
+ class User < ActiveRecord::Base
67
+ has_validation :soft, PersonValidation
68
+ has_validation :hard, UserValidation
69
+ end
70
+
71
+ user = User.new
72
+ user.valid? # no validators are used
73
+ user.use_validation(:soft)
74
+ user.valid? # validate first_name and last_name
75
+ user.use_validation(:hard)
76
+ user.valid? # validate first_name, last_name, email and spouse(if user is married)
77
+
78
+ user.use_no_validation
79
+ user.valid? # no validators are used
80
+ ```
81
+
82
+ #### has\_validation syntax
83
+
84
+ ##### With module:
85
+
86
+ ```ruby
87
+ has_validation :soft, SoftValidation
88
+ ```
89
+
90
+ ##### With block:
91
+
92
+ ```ruby
93
+ has_validation :hard do |model|
94
+ # you can include validation module within block as well
95
+ model.include SoftValidation
96
+ model.validate_presence_of :email
97
+ end
98
+ ```
99
+
100
+ ##### With module and block:
101
+
102
+ ```ruby
103
+ # It's equivalent to the example above
104
+ has_validation :hard, SoftValidation do |model|
105
+ model.validate_presence_of :email
106
+ end
107
+ ```
108
+
109
+ ##### Multiple validations with one block or module:
110
+
111
+ ```ruby
112
+ # declare :soft and :hard validation
113
+ has_validation :soft, :hard, SoftValidation
114
+
115
+ # extended :hard validation
116
+ has_validation :hard do |model|
117
+ model.validate_presence_of :email
118
+ end
119
+ ```
120
+
121
+
122
+ ##### Option `:default`:
123
+
124
+ ```ruby
125
+ class User < ActiveRecord::Base
126
+ has_validation :soft, PersonValidation, :default => true
127
+ end
128
+
129
+ user = User.new
130
+ user.themis_validation # => :soft
131
+ ```
132
+
133
+ ##### Option `:nested`:
134
+
135
+ The `:nested` option causes the `use_validation` method to be called recursively on associations passed to it.
136
+ It receives a symbol or array of symbols with association names.
137
+
138
+ ```ruby
139
+ class User < ActiveRecord::Base
140
+ has_one :account
141
+ has_validation :soft, PersonValidation, :nested => :account
142
+ end
143
+
144
+ class Account < ActiveRecord::Base
145
+ has_validation :soft do |model|
146
+ model.validates_presence_of :nickname
147
+ end
148
+ end
149
+
150
+ user = User.first
151
+ user.themis_validation # => nil
152
+ user.account.themis_validation # => nil
153
+ user.use_validation(:soft)
154
+ user.themis_validation # => :soft
155
+ user.account.themis_validation # => :soft
156
+ ```
157
+
158
+ #### Using use\_nested\_validation\_on method
159
+
160
+ If you don't want to repeat yourself with the `:nested` option:
161
+
162
+ ```ruby
163
+ class User
164
+ has_validation :none, NoneValidation, :nested => [:accounts, :preferences, :info]
165
+ has_validation :soft, SoftValidation, :nested => [:accounts, :preferences, :info]
166
+ has_validation :hard, HardValidation, :nested => [:accounts, :preferences, :info]
167
+ end
168
+ ```
169
+
170
+ You can use `use_nested_validation_on` method:
171
+
172
+ ```ruby
173
+ class User
174
+ use_nested_validation_on :accounts, :preferences, :info
175
+ has_validation :none, NoneValidation
176
+ has_validation :soft, SoftValidation
177
+ has_validation :hard, HardValidation
178
+ end
179
+ ```
180
+
181
+ Also `use_nested_validation_on` supports deep nesting:
182
+
183
+ ```ruby
184
+ class User
185
+ use_nested_validation_on :preferences, :info => [:email, :history]
186
+ end
187
+ ```
188
+
189
+ # Running specs
190
+
191
+ To run specs:
192
+
193
+ ```
194
+ rake spec
195
+ ```
196
+
197
+ To verify test coverage use SimpleCov with Ruby 1.9.3:
198
+
199
+ ```
200
+ rvm use 1.9.3
201
+ bundle install
202
+ rake spec
203
+ $BROWSER ./coverage/index.html
204
+ ```
205
+
206
+
207
+ ## Credits
208
+
209
+ * [Potapov Sergey](https://github.com/greyblake)
210
+
211
+ ## Copyright
212
+
213
+ Copyright (c) 2013 TMX Credit.
@@ -0,0 +1,25 @@
1
+ module Themis
2
+ module AR
3
+ # Extends ActiveRecord::Associations::Association
4
+ # Hooks load_target method with to process after_association_loaded callback.
5
+ module AssociationExtension
6
+ extend ActiveSupport::Concern
7
+
8
+ # Run original load_target method and process after_association_loaded
9
+ # callback.
10
+ def load_target_with_after_association_loaded(*args, &block)
11
+ result = load_target_without_after_association_loaded(*args, &block)
12
+
13
+ if callback = self.owner._after_association_loaded_callbacks[self.reflection.name]
14
+ callback.call(self)
15
+ end
16
+
17
+ result
18
+ end
19
+
20
+ included do
21
+ alias_method_chain :load_target, :after_association_loaded
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,115 @@
1
+ module Themis
2
+ module AR
3
+ # Extends ActiveRecord::Base to make it support has_validation and use_validation
4
+ # methods.
5
+ # It adds some class attributes to model:
6
+ # * themis_validation - name of validation, symbol or nil
7
+ # * themis_validation_sets - hash where key is symbol(validation name) and value is {ValidationSet}.
8
+ # * themis_default_validation - name of default validation.
9
+ # * themis_default_nested - default value for :nested option
10
+ module BaseExtension
11
+ extend ActiveSupport::Autoload
12
+
13
+ # :nodoc:
14
+ def self.included(base)
15
+ base.extend ClassMethods
16
+ base.send :include, InstanceMethods
17
+ base.send :include, Callbacks
18
+
19
+ base.class_eval(<<-eoruby, __FILE__, __LINE__+1)
20
+ attr_reader :themis_validation
21
+
22
+ class_attribute :themis_validation_sets
23
+ class_attribute :themis_default_validation
24
+ class_attribute :themis_default_nested
25
+
26
+ delegate :has_themis_validation?, :to => "self.class"
27
+ eoruby
28
+ end
29
+
30
+ # :nodoc:
31
+ module ClassMethods
32
+ # @overload has_validation(name, options, &block)
33
+ # Declare validation set using block
34
+ # @example
35
+ # has_validation :soft, :nested => :account, :default => true do |validation|
36
+ # validation.validates_presence_of :some_date
37
+ # end
38
+ # @param [Symbol] name name of validation set
39
+ # @param [Hash] options options: :default, :nested
40
+ # @param [Proc] block proc which receives {ModelProxy} and defines validators
41
+ # @option options [Boolean] :default make it validation be used by default
42
+ # @option options [Symbol, Array<Symbol>] :nested association which should be affected when validation {#use_validation} is called
43
+ #
44
+ # @overload has_validation(name_1, name_2, options, &block)
45
+ # Declare validation in 2 sets using a block:
46
+ # @example
47
+ # has_validation :soft, :hard :nested => :account, :default => true do |validation|
48
+ # validation.validates_presence_of :some_date
49
+ # end
50
+ #
51
+ # @overload has_validation(name, validation_module, options, &block)
52
+ # Declare validation set on model using {Themis::Validation validation module} or(and) block.
53
+ # @example
54
+ # has_validation :soft, SoftValidation, :default => true
55
+ # @param [Symbol] name name of validation set
56
+ # @param [Module] validation_module module extended by {Themis::Validation}.
57
+ # @param [Hash] options options: :default, :nested
58
+ # @param [Proc] block proc which receives {ModelProxy} and defines validators
59
+ # @option options [Boolean] :default make it validation be used by default
60
+ # @option options [Symbol, Array<Symbol>] :nested association which should be affect when validation {#use_validation} is called
61
+ def has_validation(*args_and_options, &block)
62
+ options = args_and_options.extract_options!
63
+ names, args = args_and_options.partition { |obj| obj.class.in?([String, Symbol]) }
64
+ validation_module = args.first
65
+ Themis::AR::HasValidationMethod.new(self, names, validation_module, options, block).execute!
66
+ end
67
+
68
+ # Verify that model has {ValidationSet validation set} with passed name.
69
+ # @param [Symbol] name name of validation set
70
+ def has_themis_validation?(name)
71
+ themis_validation_sets.keys.include?(name.to_sym)
72
+ end
73
+
74
+ # Set the default value of the +:nested+ option for validations.
75
+ # @example
76
+ # use_nested_validation_on :author
77
+ #
78
+ # @example
79
+ # use_nested_validation_on :author, :comments
80
+ #
81
+ # @example
82
+ # use_nested_validation_on :author => {:posts => :comments }
83
+ #
84
+ # @param [Array<Symbol>, Hash] args an association or associations which should be effected
85
+ def use_nested_validation_on(*args)
86
+ if themis_default_nested
87
+ warn "WARNING: default nested validation is already defined: " \
88
+ "`#{themis_default_nested.inspect}` on #{self}"
89
+ end
90
+
91
+ args = args.flatten
92
+ deep_nested = args.extract_options!
93
+ associations = args + deep_nested.keys
94
+
95
+ UseNestedValidationOnMethod.new(self, associations, deep_nested).execute
96
+ end
97
+ end # module ClassMethods
98
+
99
+ # :nodoc:
100
+ module InstanceMethods
101
+ # Switch validation.
102
+ # @param [Symbol] validation_name name of {ValidationSet validation set}
103
+ def use_validation(validation_name)
104
+ Themis::AR::UseValidationMethod.new(self, validation_name).execute!
105
+ end
106
+
107
+ # Do not use any of {ValidationSet validation sets}.
108
+ def use_no_validation
109
+ @themis_validation = nil
110
+ end
111
+ end # module InstanceMethods
112
+
113
+ end # module BaseExtension
114
+ end # module AR
115
+ end # module Themis
@@ -0,0 +1,40 @@
1
+ module Themis
2
+ module AR
3
+ # Adds after_association_loaded callback to ActiveRecord::Base
4
+ module Callbacks
5
+ # :nodoc:
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+
9
+ base.class_eval(<<-eoruby, __FILE__, __LINE__+1)
10
+ class_attribute :_after_association_loaded_callbacks
11
+ self._after_association_loaded_callbacks = {}
12
+ eoruby
13
+ end
14
+
15
+ # :nodoc:
16
+ module ClassMethods
17
+ # Save callback in appropriate callback collection
18
+ #
19
+ # @example
20
+ # class User < ActiveRecord::Base
21
+ # has_many :accounts
22
+ #
23
+ # # List accounts after loading
24
+ # after_association_loaded(:accounts) do |association|
25
+ # association.target.each do |account|
26
+ # puts account.inspect
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # @param [Symbol] association_name association name as a symbol
32
+ # @yield [ActiveRecord::Associations::Association] a block which receives association
33
+ def after_association_loaded(association_name, &block)
34
+ self._after_association_loaded_callbacks[association_name] = block
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,108 @@
1
+ module Themis
2
+ module AR
3
+ # Encapsulates implementation of
4
+ # {AR::BaseExtension::ClassMethods#has_validation has_validation} method.
5
+ class HasValidationMethod
6
+ # @param [ActiveRecord::Base] model_class
7
+ # @param [Symbol] names names of validation sets
8
+ # @param [Module, nil] validation_module
9
+ # @param [Hash, nil] options
10
+ # @param [Proc, nil] block
11
+ def initialize(model_class, names, validation_module, options, block)
12
+ @model_class = model_class
13
+ @names = names
14
+ @module = validation_module
15
+ @default = options[:default] || false
16
+ @nested = options[:nested]
17
+ @block = block
18
+ end
19
+
20
+ # Execute the method.
21
+ def execute!
22
+ preinitialize_model_class!
23
+ validate!
24
+ register_validation_sets!
25
+ add_conditional_validators!
26
+ add_after_initialize_hook! if @default
27
+ add_before_validation_hook!
28
+ end
29
+
30
+
31
+ # Unless themis_validation_sets and themis_default_validation
32
+ # is set, then set them. This is necessary to do in every inheritor
33
+ # of ActiveRecord::Base to avoid overriding values for the entire
34
+ # ActiveRecord::Base hierarchy.
35
+ def preinitialize_model_class!
36
+ @model_class.themis_validation_sets ||= {}
37
+ @model_class.themis_default_validation ||= nil
38
+ end
39
+ private :preinitialize_model_class!
40
+
41
+
42
+ # Add {ValidationSet validation sets} to themis_validation_sets collection.
43
+ def register_validation_sets!
44
+ @names.each do |name|
45
+ @model_class.themis_validation_sets[name] ||= ValidationSet.new(
46
+ :name => name,
47
+ :module => @module,
48
+ :default => @default,
49
+ :nested => @nested,
50
+ :block => @block
51
+ )
52
+ end
53
+ end
54
+ private :register_validation_sets!
55
+
56
+
57
+ # Add conditional validation to ActiveRecord model.
58
+ def add_conditional_validators!
59
+ # Define local variable to have ability to pass its value to lambda
60
+ validation_names = @names
61
+
62
+ condition = lambda { |obj| obj.themis_validation.in?(validation_names) }
63
+ model_proxy = ModelProxy.new(@model_class, condition)
64
+
65
+ if @module
66
+ @module.validators.each do |validator|
67
+ model_proxy.send(validator.name, *validator.args)
68
+ end
69
+ end
70
+ @block.call(model_proxy) if @block
71
+ end
72
+ private :add_conditional_validators!
73
+
74
+
75
+ # Add after_initialize hook to set default validation.
76
+ def add_after_initialize_hook!
77
+ if @names.size > 1
78
+ raise "Can not set default to multiple validations"
79
+ end
80
+
81
+ # Define local variable to have ability to pass its value to proc
82
+ validation_name = @names.first
83
+ @model_class.themis_default_validation = validation_name
84
+ @model_class.after_initialize { use_validation(validation_name) }
85
+ end
86
+ private :add_after_initialize_hook!
87
+
88
+ # Add before_validation hook to make all nested models use same
89
+ # validation set.
90
+ def add_before_validation_hook!
91
+ @model_class.before_validation do
92
+ themis_validation ? use_validation(themis_validation) : use_no_validation
93
+ end
94
+ end
95
+ private :add_before_validation_hook!
96
+
97
+ # Run validation to be sure that minimum of necessary parameters were passed.
98
+ def validate!
99
+ if @default && @model_class.themis_default_validation
100
+ warn "WARNING: validation `#{@model_class.themis_default_validation}` " \
101
+ "is already used as default on #{@model_class}"
102
+ end
103
+ end
104
+ private :validate!
105
+
106
+ end # class HasValidationMethod
107
+ end # module AR
108
+ end # module Themis
@@ -0,0 +1,82 @@
1
+ module Themis
2
+ module AR
3
+ # It wraps a model class to override validation parameters
4
+ # and add(update) :if option.
5
+ # Also it provides some DSL syntax to include validation modules.
6
+ #
7
+ # See where exactly it does its job:
8
+ # class User
9
+ # has_validation :soft do |validation|
10
+ # validation.class # => ModelProxy
11
+ # end
12
+ # end
13
+ class ModelProxy
14
+ # @param [ActiveRecord::Base] model_class
15
+ # @param [Proc] condition lambda which is passed as :if option for conditional validation
16
+ def initialize(model_class, condition)
17
+ @model_class = model_class
18
+ @condition = condition
19
+ end
20
+
21
+ # Defines conditional validations from module.
22
+ # @param [Themis::Validation] validation_module any module extended by {Themis::Validation}
23
+ def include(validation_module)
24
+ validation_module.validators.each do |validator|
25
+ cargs = args_with_condition(validator.args)
26
+ @model_class.send(validator.name, *cargs)
27
+ end
28
+ end
29
+
30
+ # Defines conditional validation by adding(updating) :if option
31
+ # to original method call.
32
+ def method_missing(*args)
33
+ cargs = args_with_condition(args)
34
+ @model_class.send(*cargs)
35
+ end
36
+
37
+
38
+ # Build new arguments with modified :if option to make validation
39
+ # conditional.
40
+ # @param [Array] arguments validation method and its options
41
+ def args_with_condition(arguments)
42
+ # Ala deep duplication to not override original array.
43
+ args = arguments.map { |v| v.is_a?(Symbol) ? v : v.dup }
44
+
45
+ if args.last.is_a?(Hash)
46
+ old_opts = args.pop
47
+ args << build_opts(old_opts)
48
+ else
49
+ args << build_opts
50
+ end
51
+
52
+ args
53
+ end
54
+
55
+ # Build options for validator with :if option to make validation conditional
56
+ # so it would be used depending on `@themis_validation` value.
57
+ # If :if option already was passed then merge lambdas to create new one and
58
+ # use it as :if option.
59
+ # @param [Hash] old_opts old validator options
60
+ def build_opts(old_opts = nil)
61
+ # define local variable so its value can be addressed in lambda
62
+ condition = @condition
63
+ new_opts = old_opts || {}
64
+
65
+ if old_opts && old_opts.has_key?(:if)
66
+ old_if = old_opts[:if]
67
+ final_condition = lambda do
68
+ instance_eval(&old_if) &&
69
+ instance_eval(&condition)
70
+ end
71
+ else
72
+ final_condition = condition
73
+ end
74
+
75
+ new_opts[:if] = final_condition
76
+ new_opts
77
+ end
78
+ private :build_opts
79
+
80
+ end # class ModelProxy
81
+ end # module AR
82
+ end # module Themis
@@ -0,0 +1,55 @@
1
+ module Themis
2
+ module AR
3
+ # Encapsulates {Themis::AR::BaseExtension#use_nested_validation_on} method
4
+ class UseNestedValidationOnMethod
5
+
6
+ # @param [ActiveRecord::Base] model base mdoel
7
+ # @param [Array<Symbol>] associations associations on base model
8
+ # @param [Hash] nested_associations deep nested associations
9
+ def initialize(model, associations, nested_associations)
10
+ @model = model
11
+ @associations = associations
12
+ @nested_associations = nested_associations
13
+ end
14
+
15
+ # Trigger calling use_nested_validation_on on associations and adds
16
+ # after_association_loaded hooks.
17
+ def execute
18
+ # Set themis_default_nested for current model
19
+ @model.themis_default_nested = @associations unless @associations.empty?
20
+
21
+ process_nested_validations
22
+ add_after_association_loaded_hooks
23
+ end
24
+
25
+ # Iterate over associations and recursively call #use_nested_validation_on
26
+ def process_nested_validations
27
+ @nested_associations.each do |association_name, nested|
28
+ reflection = @model.reflect_on_association(association_name)
29
+ model_class = reflection.class_name.constantize
30
+ model_class.use_nested_validation_on(nested)
31
+ end
32
+ end
33
+
34
+ # Add after_association_loaded hooks to associations so when association
35
+ # is loaded it would have same validation as base model.
36
+ def add_after_association_loaded_hooks
37
+ @associations.each do |association_name|
38
+ @model.after_association_loaded(association_name) do |association|
39
+ validation = association.owner.themis_validation
40
+ target = association.target
41
+
42
+ if validation
43
+ if target.respond_to?(:each)
44
+ target.each { |model| model.use_validation(validation) }
45
+ elsif target.is_a? ActiveRecord::Base
46
+ target.use_validation(validation) if validation
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,69 @@
1
+ module Themis
2
+ module AR
3
+ # It encapsulates # {AR::BaseExtension::InstanceMethods#use_validation use_validation}
4
+ # method.
5
+ # It makes a model and its nested associations use another validation.
6
+ class UseValidationMethod
7
+ # @param [ActiveRecord::Base] model instance of model
8
+ # @param [Symbol, String] new_name name of new validation set
9
+ def initialize(model, new_name)
10
+ @model = model
11
+ @new_name = new_name.to_sym
12
+
13
+ unless @model.has_themis_validation?(@new_name)
14
+ raise ArgumentError.new("Unknown validation: `#{@new_name.inspect}` for #{model.class}")
15
+ end
16
+
17
+ @new_validation_set = @model.themis_validation_sets[@new_name]
18
+ end
19
+
20
+ # Affect model and its nested associations.
21
+ # Make them use the new validation set by assigning `@themis_validation`
22
+ # instance variable.
23
+ def execute!
24
+ # NOTE: It breaks encapsulation of ActiveRecord model.
25
+ # We do it because we don't wanna public "themis_validation=" method.
26
+ # -- sergey.potapov 2012-08-14
27
+ @model.instance_variable_set("@themis_validation", @new_name)
28
+
29
+ nested = @new_validation_set.nested || @model.class.themis_default_nested
30
+ if nested
31
+ association_names = Array.wrap(nested)
32
+ affect_associations(association_names)
33
+ end
34
+ end
35
+
36
+
37
+ # Make associations use new validation set.
38
+ # @param [Array<Symbol>] association_names names of associations
39
+ def affect_associations(association_names)
40
+ association_names.each { |name| affect_association(name) }
41
+ end
42
+ private :affect_associations
43
+
44
+ # Make an association use new validation set.
45
+ # Affect associations that are already loaded.
46
+ # @param [Symbol] association_name
47
+ def affect_association(association_name)
48
+ unless @model.reflections.has_key?(association_name)
49
+ raise("`#{association_name}` is not an association on #{@model.class}")
50
+ end
51
+
52
+ association = @model.association(association_name)
53
+ return if (!association.loaded? && !@model.new_record?)
54
+
55
+ target = association.target
56
+ case target
57
+ when Array, ActiveRecord::Associations::CollectionProxy
58
+ target.each {|obj| obj.send(:use_validation, @new_name) }
59
+ when ActiveRecord::Base
60
+ target.send(:use_validation, @new_name)
61
+ else
62
+ # do nothing
63
+ end
64
+ end
65
+ private :affect_association
66
+
67
+ end # class UseValidationMethod
68
+ end # module AR
69
+ end # module Themis
@@ -0,0 +1,14 @@
1
+ module Themis
2
+ module AR
3
+
4
+ # Used to store options about validation sets on every single ActiveRecord model class.
5
+ class ValidationSet < Struct.new(:name, :module, :default, :nested, :block)
6
+ # Redefine `new` to initialize structure with hash
7
+ def initialize(hash)
8
+ members = self.class.members.map!(&:to_sym)
9
+ super(*hash.values_at(*members))
10
+ end
11
+ end
12
+
13
+ end # module AR
14
+ end # module Themis
data/lib/themis/ar.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Themis
2
+ # All stuff related to ActiveRecord.
3
+ # Mainly it provides {AR::BaseExtension} to extend ActiveRecord::Base.
4
+ module AR
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Callbacks
8
+ autoload :AssociationExtension
9
+ autoload :BaseExtension
10
+ autoload :ModelProxy
11
+ autoload :ValidationSet
12
+ autoload :HasValidationMethod
13
+ autoload :UseValidationMethod
14
+ autoload :UseNestedValidationOnMethod
15
+ end # module AR
16
+ end # module Themis
@@ -0,0 +1,24 @@
1
+ module Themis
2
+ # :nodoc
3
+ class Engine < Rails::Engine
4
+
5
+ initializer 'themis' do
6
+ ActiveSupport.on_load(:active_record) do
7
+
8
+ ::ActiveRecord::Associations::CollectionAssociation.class_eval do
9
+ include Themis::AR::AssociationExtension
10
+ end
11
+
12
+ ::ActiveRecord::Associations::Association.class_eval do
13
+ include Themis::AR::AssociationExtension
14
+ end
15
+
16
+ ::ActiveRecord::Base.class_eval do
17
+ include Themis::AR::BaseExtension
18
+ end
19
+
20
+ end # on_load
21
+ end # initializer
22
+
23
+ end # Engine
24
+ end # Themis
@@ -0,0 +1,18 @@
1
+ module Themis
2
+ module Validation
3
+
4
+ # Simple structure to store information about methods called on
5
+ # {Themis::Validation validation module}. It saves name and arguments
6
+ # of validation method in order to apply it on model later.
7
+ class Validator
8
+ attr_reader :name, :args
9
+
10
+ # @param [Symbol, String] name validation method name, e.g. "validates_presence_of"
11
+ # @param [Array] args arguments of method
12
+ def initialize(name, args)
13
+ @name, @args = name, args
14
+ end
15
+ end
16
+
17
+ end # Validation
18
+ end # Themis
@@ -0,0 +1,58 @@
1
+ module Themis
2
+ # Extends other modules to make them be validation modules.
3
+ # Consider it as the "parent module" for all validation modules.
4
+ #
5
+ # @example define UserValidation
6
+ #
7
+ # module UserValidation
8
+ # extend Themis::Validation
9
+ #
10
+ # validates :email , :presence => true
11
+ # validates :nickname, :presence => true
12
+ # end
13
+ module Validation
14
+ extend ActiveSupport::Autoload
15
+
16
+ autoload :Validator
17
+
18
+ # Array {Validator validators} defined in module.
19
+ # @return [Array<Validator>] array of module's validators
20
+ def validators
21
+ @validators ||= []
22
+ end
23
+
24
+ # When included in another module: copy {Validator validators} to another module.
25
+ # When included in ActiveRecord model: define validators on model.
26
+ # @param [Module, ActiveRecord::Base] base another validation module or ActiveRecord model.
27
+ def included(base)
28
+ if base.instance_of?(Module) && base.respond_to?(:validators)
29
+ base.validators.concat(validators)
30
+ elsif base.ancestors.include? ::ActiveRecord::Base
31
+ apply_to_model!(base)
32
+ else
33
+ raise "Validation module `#{self.inspect}` can be included only in another validation module or in ActiveRecord model"
34
+ end
35
+ end
36
+
37
+ # Save all calls of validation methods as array of validators
38
+ def method_missing(method_name, *args)
39
+ if method_name.to_s =~ /\Avalidate/
40
+ self.validators << Themis::Validation::Validator.new(method_name, args)
41
+ else
42
+ super
43
+ end
44
+ end
45
+ private :method_missing
46
+
47
+ # Add validators to model
48
+ # @param [ActiveRecord::Base] model_class
49
+ def apply_to_model!(model_class)
50
+ validators.each do |validator|
51
+ method, args = validator.name, validator.args
52
+ model_class.send(method, *args)
53
+ end
54
+ end
55
+ private :apply_to_model!
56
+
57
+ end # module Validation
58
+ end # module Themis
@@ -0,0 +1,4 @@
1
+ module Themis
2
+ # :nodoc:
3
+ VERSION = "0.1.0"
4
+ end
data/lib/themis.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'themis/engine'
2
+
3
+ # Extends ActiveRecord to provide switchable validations.
4
+ module Themis
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Validation
8
+ autoload :AR
9
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: themis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - TMX Credit
8
+ - Potapov Sergey
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: '3.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: '3.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec-rails
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: '2.11'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ version: '2.11'
42
+ - !ruby/object:Gem::Dependency
43
+ name: sqlite3
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: pry
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: jeweler
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '1.8'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ~>
82
+ - !ruby/object:Gem::Version
83
+ version: '1.8'
84
+ - !ruby/object:Gem::Dependency
85
+ name: yard
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ description: Flexible and modular validations for ActiveRecord models
99
+ email:
100
+ - rubygems@tmxcredit.com
101
+ - blake131313@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files:
105
+ - README.markdown
106
+ files:
107
+ - README.markdown
108
+ - lib/themis.rb
109
+ - lib/themis/ar.rb
110
+ - lib/themis/ar/association_extension.rb
111
+ - lib/themis/ar/base_extension.rb
112
+ - lib/themis/ar/callbacks.rb
113
+ - lib/themis/ar/has_validation_method.rb
114
+ - lib/themis/ar/model_proxy.rb
115
+ - lib/themis/ar/use_nested_validation_on_method.rb
116
+ - lib/themis/ar/use_validation_method.rb
117
+ - lib/themis/ar/validation_set.rb
118
+ - lib/themis/engine.rb
119
+ - lib/themis/validation.rb
120
+ - lib/themis/validation/validator.rb
121
+ - lib/themis/version.rb
122
+ homepage:
123
+ licenses: []
124
+ metadata: {}
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ requirements: []
140
+ rubyforge_project:
141
+ rubygems_version: 2.0.3
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Flexible and modular validations for ActiveRecord models
145
+ test_files: []