activemodel 3.0.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +13 -0
- data/MIT-LICENSE +21 -0
- data/README +216 -0
- data/lib/active_model.rb +61 -0
- data/lib/active_model/attribute_methods.rb +391 -0
- data/lib/active_model/callbacks.rb +133 -0
- data/lib/active_model/conversion.rb +19 -0
- data/lib/active_model/deprecated_error_methods.rb +33 -0
- data/lib/active_model/dirty.rb +164 -0
- data/lib/active_model/errors.rb +277 -0
- data/lib/active_model/lint.rb +89 -0
- data/lib/active_model/locale/en.yml +26 -0
- data/lib/active_model/naming.rb +60 -0
- data/lib/active_model/observing.rb +196 -0
- data/lib/active_model/railtie.rb +2 -0
- data/lib/active_model/serialization.rb +87 -0
- data/lib/active_model/serializers/json.rb +96 -0
- data/lib/active_model/serializers/xml.rb +204 -0
- data/lib/active_model/test_case.rb +16 -0
- data/lib/active_model/translation.rb +60 -0
- data/lib/active_model/validations.rb +168 -0
- data/lib/active_model/validations/acceptance.rb +51 -0
- data/lib/active_model/validations/confirmation.rb +49 -0
- data/lib/active_model/validations/exclusion.rb +40 -0
- data/lib/active_model/validations/format.rb +64 -0
- data/lib/active_model/validations/inclusion.rb +40 -0
- data/lib/active_model/validations/length.rb +98 -0
- data/lib/active_model/validations/numericality.rb +111 -0
- data/lib/active_model/validations/presence.rb +41 -0
- data/lib/active_model/validations/validates.rb +108 -0
- data/lib/active_model/validations/with.rb +70 -0
- data/lib/active_model/validator.rb +160 -0
- data/lib/active_model/version.rb +9 -0
- metadata +96 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
# You can test whether an object is compliant with the ActiveModel API by
|
2
|
+
# including ActiveModel::Lint::Tests in your TestCase. It will included
|
3
|
+
# tests that tell you whether your object is fully compliant, or if not,
|
4
|
+
# which aspects of the API are not implemented.
|
5
|
+
#
|
6
|
+
# These tests do not attempt to determine the semantic correctness of the
|
7
|
+
# returned values. For instance, you could implement valid? to always
|
8
|
+
# return true, and the tests would pass. It is up to you to ensure that
|
9
|
+
# the values are semantically meaningful.
|
10
|
+
#
|
11
|
+
# Objects you pass in are expected to return a compliant object from a
|
12
|
+
# call to to_model. It is perfectly fine for to_model to return self.
|
13
|
+
module ActiveModel
|
14
|
+
module Lint
|
15
|
+
module Tests
|
16
|
+
# == Responds to <tt>valid?</tt>
|
17
|
+
#
|
18
|
+
# Returns a boolean that specifies whether the object is in a valid or invalid
|
19
|
+
# state.
|
20
|
+
def test_valid?
|
21
|
+
assert model.respond_to?(:valid?), "The model should respond to valid?"
|
22
|
+
assert_boolean model.valid?, "valid?"
|
23
|
+
end
|
24
|
+
|
25
|
+
# == Responds to <tt>new_record?</tt>
|
26
|
+
#
|
27
|
+
# Returns a boolean that specifies whether the object has been persisted yet.
|
28
|
+
# This is used when calculating the URL for an object. If the object is
|
29
|
+
# not persisted, a form for that object, for instance, will be POSTed to the
|
30
|
+
# collection. If it is persisted, a form for the object will put PUTed to the
|
31
|
+
# URL for the object.
|
32
|
+
def test_new_record?
|
33
|
+
assert model.respond_to?(:new_record?), "The model should respond to new_record?"
|
34
|
+
assert_boolean model.new_record?, "new_record?"
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_destroyed?
|
38
|
+
assert model.respond_to?(:destroyed?), "The model should respond to destroyed?"
|
39
|
+
assert_boolean model.destroyed?, "destroyed?"
|
40
|
+
end
|
41
|
+
|
42
|
+
# == Naming
|
43
|
+
#
|
44
|
+
# Model.model_name must returns a string with some convenience methods as
|
45
|
+
# :human and :partial_path. Check ActiveModel::Naming for more information.
|
46
|
+
#
|
47
|
+
def test_model_naming
|
48
|
+
assert model.class.respond_to?(:model_name), "The model should respond to model_name"
|
49
|
+
model_name = model.class.model_name
|
50
|
+
assert_kind_of String, model_name
|
51
|
+
assert_kind_of String, model_name.human
|
52
|
+
assert_kind_of String, model_name.partial_path
|
53
|
+
assert_kind_of String, model_name.singular
|
54
|
+
assert_kind_of String, model_name.plural
|
55
|
+
end
|
56
|
+
|
57
|
+
# == Errors Testing
|
58
|
+
#
|
59
|
+
# Returns an object that has :[] and :full_messages defined on it. See below
|
60
|
+
# for more details.
|
61
|
+
#
|
62
|
+
# Returns an Array of Strings that are the errors for the attribute in
|
63
|
+
# question. If localization is used, the Strings should be localized
|
64
|
+
# for the current locale. If no error is present, this method should
|
65
|
+
# return an empty Array.
|
66
|
+
def test_errors_aref
|
67
|
+
assert model.respond_to?(:errors), "The model should respond to errors"
|
68
|
+
assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns an Array of all error messages for the object. Each message
|
72
|
+
# should contain information about the field, if applicable.
|
73
|
+
def test_errors_full_messages
|
74
|
+
assert model.respond_to?(:errors), "The model should respond to errors"
|
75
|
+
assert model.errors.full_messages.is_a?(Array), "errors#full_messages should return an Array"
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
def model
|
80
|
+
assert @model.respond_to?(:to_model), "The object should respond_to to_model"
|
81
|
+
@model.to_model
|
82
|
+
end
|
83
|
+
|
84
|
+
def assert_boolean(result, name)
|
85
|
+
assert result == true || result == false, "#{name} should be a boolean"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
en:
|
2
|
+
errors:
|
3
|
+
# The default format use in full error messages.
|
4
|
+
format: "{{attribute}} {{message}}"
|
5
|
+
|
6
|
+
# The values :model, :attribute and :value are always available for interpolation
|
7
|
+
# The value :count is available when applicable. Can be used for pluralization.
|
8
|
+
messages:
|
9
|
+
inclusion: "is not included in the list"
|
10
|
+
exclusion: "is reserved"
|
11
|
+
invalid: "is invalid"
|
12
|
+
confirmation: "doesn't match confirmation"
|
13
|
+
accepted: "must be accepted"
|
14
|
+
empty: "can't be empty"
|
15
|
+
blank: "can't be blank"
|
16
|
+
too_long: "is too long (maximum is {{count}} characters)"
|
17
|
+
too_short: "is too short (minimum is {{count}} characters)"
|
18
|
+
wrong_length: "is the wrong length (should be {{count}} characters)"
|
19
|
+
not_a_number: "is not a number"
|
20
|
+
greater_than: "must be greater than {{count}}"
|
21
|
+
greater_than_or_equal_to: "must be greater than or equal to {{count}}"
|
22
|
+
equal_to: "must be equal to {{count}}"
|
23
|
+
less_than: "must be less than {{count}}"
|
24
|
+
less_than_or_equal_to: "must be less than or equal to {{count}}"
|
25
|
+
odd: "must be odd"
|
26
|
+
even: "must be even"
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module ActiveModel
|
4
|
+
class Name < String
|
5
|
+
attr_reader :singular, :plural, :element, :collection, :partial_path
|
6
|
+
alias_method :cache_key, :collection
|
7
|
+
|
8
|
+
def initialize(klass)
|
9
|
+
super(klass.name)
|
10
|
+
@klass = klass
|
11
|
+
@singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze
|
12
|
+
@plural = ActiveSupport::Inflector.pluralize(@singular).freeze
|
13
|
+
@element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
|
14
|
+
@human = ActiveSupport::Inflector.humanize(@element).freeze
|
15
|
+
@collection = ActiveSupport::Inflector.tableize(self).freeze
|
16
|
+
@partial_path = "#{@collection}/#{@element}".freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
# Transform the model name into a more humane format, using I18n. By default,
|
20
|
+
# it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post").
|
21
|
+
# Specify +options+ with additional translating options.
|
22
|
+
def human(options={})
|
23
|
+
return @human unless @klass.respond_to?(:lookup_ancestors) &&
|
24
|
+
@klass.respond_to?(:i18n_scope)
|
25
|
+
|
26
|
+
defaults = @klass.lookup_ancestors.map do |klass|
|
27
|
+
klass.model_name.underscore.to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
defaults << options.delete(:default) if options[:default]
|
31
|
+
defaults << @human
|
32
|
+
|
33
|
+
options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults
|
34
|
+
I18n.translate(defaults.shift, options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# ActiveModel::Naming is a module that creates a +model_name+ method on your
|
39
|
+
# object.
|
40
|
+
#
|
41
|
+
# To implement, just extend ActiveModel::Naming in your object:
|
42
|
+
#
|
43
|
+
# class BookCover
|
44
|
+
# exten ActiveModel::Naming
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# BookCover.model_name #=> "BookCover"
|
48
|
+
# BookCover.model_name.human #=> "Book cover"
|
49
|
+
#
|
50
|
+
# Providing the functionality that ActiveModel::Naming provides in your object
|
51
|
+
# is required to pass the ActiveModel Lint test. So either extending the provided
|
52
|
+
# method below, or rolling your own is required..
|
53
|
+
module Naming
|
54
|
+
# Returns an ActiveModel::Name object for module. It can be
|
55
|
+
# used to retrieve all kinds of naming-related information.
|
56
|
+
def model_name
|
57
|
+
@_model_name ||= ActiveModel::Name.new(self)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
require 'observer'
|
2
|
+
require 'singleton'
|
3
|
+
require 'active_support/core_ext/array/wrap'
|
4
|
+
require 'active_support/core_ext/module/aliasing'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
|
7
|
+
module ActiveModel
|
8
|
+
module Observing
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
extend Observable
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Activates the observers assigned. Examples:
|
17
|
+
#
|
18
|
+
# # Calls PersonObserver.instance
|
19
|
+
# ActiveRecord::Base.observers = :person_observer
|
20
|
+
#
|
21
|
+
# # Calls Cacher.instance and GarbageCollector.instance
|
22
|
+
# ActiveRecord::Base.observers = :cacher, :garbage_collector
|
23
|
+
#
|
24
|
+
# # Same as above, just using explicit class references
|
25
|
+
# ActiveRecord::Base.observers = Cacher, GarbageCollector
|
26
|
+
#
|
27
|
+
# Note: Setting this does not instantiate the observers yet.
|
28
|
+
# +instantiate_observers+ is called during startup, and before
|
29
|
+
# each development request.
|
30
|
+
def observers=(*values)
|
31
|
+
@observers = values.flatten
|
32
|
+
end
|
33
|
+
|
34
|
+
# Gets the current observers.
|
35
|
+
def observers
|
36
|
+
@observers ||= []
|
37
|
+
end
|
38
|
+
|
39
|
+
# Instantiate the global Active Record observers.
|
40
|
+
def instantiate_observers
|
41
|
+
observers.each { |o| instantiate_observer(o) }
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
def instantiate_observer(observer) #:nodoc:
|
46
|
+
# string/symbol
|
47
|
+
if observer.respond_to?(:to_sym)
|
48
|
+
observer = observer.to_s.camelize.constantize.instance
|
49
|
+
elsif observer.respond_to?(:instance)
|
50
|
+
observer.instance
|
51
|
+
else
|
52
|
+
raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Notify observers when the observed class is subclassed.
|
57
|
+
def inherited(subclass)
|
58
|
+
super
|
59
|
+
changed
|
60
|
+
notify_observers :observed_class_inherited, subclass
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
# Fires notifications to model's observers
|
66
|
+
#
|
67
|
+
# def save
|
68
|
+
# notify_observers(:before_save)
|
69
|
+
# ...
|
70
|
+
# notify_observers(:after_save)
|
71
|
+
# end
|
72
|
+
def notify_observers(method)
|
73
|
+
self.class.changed
|
74
|
+
self.class.notify_observers(method, self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Observer classes respond to lifecycle callbacks to implement trigger-like
|
79
|
+
# behavior outside the original class. This is a great way to reduce the
|
80
|
+
# clutter that normally comes when the model class is burdened with
|
81
|
+
# functionality that doesn't pertain to the core responsibility of the
|
82
|
+
# class. Example:
|
83
|
+
#
|
84
|
+
# class CommentObserver < ActiveModel::Observer
|
85
|
+
# def after_save(comment)
|
86
|
+
# Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# This Observer sends an email when a Comment#save is finished.
|
91
|
+
#
|
92
|
+
# class ContactObserver < ActiveModel::Observer
|
93
|
+
# def after_create(contact)
|
94
|
+
# contact.logger.info('New contact added!')
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# def after_destroy(contact)
|
98
|
+
# contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# This Observer uses logger to log when specific callbacks are triggered.
|
103
|
+
#
|
104
|
+
# == Observing a class that can't be inferred
|
105
|
+
#
|
106
|
+
# Observers will by default be mapped to the class with which they share a
|
107
|
+
# name. So CommentObserver will be tied to observing Comment, ProductManagerObserver
|
108
|
+
# to ProductManager, and so on. If you want to name your observer differently than
|
109
|
+
# the class you're interested in observing, you can use the Observer.observe class
|
110
|
+
# method which takes either the concrete class (Product) or a symbol for that
|
111
|
+
# class (:product):
|
112
|
+
#
|
113
|
+
# class AuditObserver < ActiveModel::Observer
|
114
|
+
# observe :account
|
115
|
+
#
|
116
|
+
# def after_update(account)
|
117
|
+
# AuditTrail.new(account, "UPDATED")
|
118
|
+
# end
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# If the audit observer needs to watch more than one kind of object, this can be
|
122
|
+
# specified with multiple arguments:
|
123
|
+
#
|
124
|
+
# class AuditObserver < ActiveModel::Observer
|
125
|
+
# observe :account, :balance
|
126
|
+
#
|
127
|
+
# def after_update(record)
|
128
|
+
# AuditTrail.new(record, "UPDATED")
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# The AuditObserver will now act on both updates to Account and Balance by treating
|
133
|
+
# them both as records.
|
134
|
+
#
|
135
|
+
class Observer
|
136
|
+
include Singleton
|
137
|
+
|
138
|
+
class << self
|
139
|
+
# Attaches the observer to the supplied model classes.
|
140
|
+
def observe(*models)
|
141
|
+
models.flatten!
|
142
|
+
models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
|
143
|
+
define_method(:observed_classes) { models }
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns an array of Classes to observe.
|
147
|
+
#
|
148
|
+
# You can override this instead of using the +observe+ helper.
|
149
|
+
#
|
150
|
+
# class AuditObserver < ActiveModel::Observer
|
151
|
+
# def self.observed_classes
|
152
|
+
# [Account, Balance]
|
153
|
+
# end
|
154
|
+
# end
|
155
|
+
def observed_classes
|
156
|
+
Array.wrap(observed_class)
|
157
|
+
end
|
158
|
+
|
159
|
+
# The class observed by default is inferred from the observer's class name:
|
160
|
+
# assert_equal Person, PersonObserver.observed_class
|
161
|
+
def observed_class
|
162
|
+
if observed_class_name = name[/(.*)Observer/, 1]
|
163
|
+
observed_class_name.constantize
|
164
|
+
else
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Start observing the declared classes and their subclasses.
|
171
|
+
def initialize
|
172
|
+
observed_classes.each { |klass| add_observer!(klass) }
|
173
|
+
end
|
174
|
+
|
175
|
+
def observed_classes #:nodoc:
|
176
|
+
self.class.observed_classes
|
177
|
+
end
|
178
|
+
|
179
|
+
# Send observed_method(object) if the method exists.
|
180
|
+
def update(observed_method, object) #:nodoc:
|
181
|
+
send(observed_method, object) if respond_to?(observed_method)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Special method sent by the observed class when it is inherited.
|
185
|
+
# Passes the new subclass.
|
186
|
+
def observed_class_inherited(subclass) #:nodoc:
|
187
|
+
self.class.observe(observed_classes + [subclass])
|
188
|
+
add_observer!(subclass)
|
189
|
+
end
|
190
|
+
|
191
|
+
protected
|
192
|
+
def add_observer!(klass) #:nodoc:
|
193
|
+
klass.add_observer(self)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'active_support/core_ext/hash/except'
|
2
|
+
require 'active_support/core_ext/hash/slice'
|
3
|
+
|
4
|
+
module ActiveModel
|
5
|
+
# Provides a basic serialization to a serializable_hash for your object.
|
6
|
+
#
|
7
|
+
# A minimal implementation could be:
|
8
|
+
#
|
9
|
+
# class Person
|
10
|
+
#
|
11
|
+
# include ActiveModel::Serialization
|
12
|
+
#
|
13
|
+
# attr_accessor :name
|
14
|
+
#
|
15
|
+
# def attributes
|
16
|
+
# @attributes ||= {'name' => 'nil'}
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# Which would provide you with:
|
22
|
+
#
|
23
|
+
# person = Person.new
|
24
|
+
# person.serializable_hash # => {"name"=>nil}
|
25
|
+
# person.name = "Bob"
|
26
|
+
# person.serializable_hash # => {"name"=>"Bob"}
|
27
|
+
#
|
28
|
+
# You need to declare some sort of attributes hash which contains the attributes
|
29
|
+
# you want to serialize and their current value.
|
30
|
+
#
|
31
|
+
# Most of the time though, you will want to include the JSON or XML
|
32
|
+
# serializations. Both of these modules automatically include the
|
33
|
+
# ActiveModel::Serialization module, so there is no need to explicitly
|
34
|
+
# include it.
|
35
|
+
#
|
36
|
+
# So a minimal implementation including XML and JSON would be:
|
37
|
+
#
|
38
|
+
# class Person
|
39
|
+
#
|
40
|
+
# include ActiveModel::Serializers::JSON
|
41
|
+
# include ActiveModel::Serializers::Xml
|
42
|
+
#
|
43
|
+
# attr_accessor :name
|
44
|
+
#
|
45
|
+
# def attributes
|
46
|
+
# @attributes ||= {'name' => 'nil'}
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# Which would provide you with:
|
52
|
+
#
|
53
|
+
# person = Person.new
|
54
|
+
# person.serializable_hash # => {"name"=>nil}
|
55
|
+
# person.to_json # => "{\"name\":null}"
|
56
|
+
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
|
57
|
+
#
|
58
|
+
# person.name = "Bob"
|
59
|
+
# person.serializable_hash # => {"name"=>"Bob"}
|
60
|
+
# person.to_json # => "{\"name\":\"Bob\"}"
|
61
|
+
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
|
62
|
+
module Serialization
|
63
|
+
def serializable_hash(options = nil)
|
64
|
+
options ||= {}
|
65
|
+
|
66
|
+
options[:only] = Array.wrap(options[:only]).map { |n| n.to_s }
|
67
|
+
options[:except] = Array.wrap(options[:except]).map { |n| n.to_s }
|
68
|
+
|
69
|
+
attribute_names = attributes.keys.sort
|
70
|
+
if options[:only].any?
|
71
|
+
attribute_names &= options[:only]
|
72
|
+
elsif options[:except].any?
|
73
|
+
attribute_names -= options[:except]
|
74
|
+
end
|
75
|
+
|
76
|
+
method_names = Array.wrap(options[:methods]).inject([]) do |methods, name|
|
77
|
+
methods << name if respond_to?(name.to_s)
|
78
|
+
methods
|
79
|
+
end
|
80
|
+
|
81
|
+
(attribute_names + method_names).inject({}) { |hash, name|
|
82
|
+
hash[name] = send(name)
|
83
|
+
hash
|
84
|
+
}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|