themis 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.markdown +213 -0
- data/lib/themis/ar/association_extension.rb +25 -0
- data/lib/themis/ar/base_extension.rb +115 -0
- data/lib/themis/ar/callbacks.rb +40 -0
- data/lib/themis/ar/has_validation_method.rb +108 -0
- data/lib/themis/ar/model_proxy.rb +82 -0
- data/lib/themis/ar/use_nested_validation_on_method.rb +55 -0
- data/lib/themis/ar/use_validation_method.rb +69 -0
- data/lib/themis/ar/validation_set.rb +14 -0
- data/lib/themis/ar.rb +16 -0
- data/lib/themis/engine.rb +24 -0
- data/lib/themis/validation/validator.rb +18 -0
- data/lib/themis/validation.rb +58 -0
- data/lib/themis/version.rb +4 -0
- data/lib/themis.rb +9 -0
- metadata +145 -0
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
|
data/lib/themis.rb
ADDED
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: []
|