activemodel 3.0.0.beta
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.
- 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
|