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 +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
|
+
[](http://travis-ci.org/TMXCredit/themis)
|
4
|
+
[](https://gemnasium.com/TMXCredit/themis)
|
5
|
+
[](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: []
|