activemodel 3.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/CHANGELOG +13 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +216 -0
  4. data/lib/active_model.rb +61 -0
  5. data/lib/active_model/attribute_methods.rb +391 -0
  6. data/lib/active_model/callbacks.rb +133 -0
  7. data/lib/active_model/conversion.rb +19 -0
  8. data/lib/active_model/deprecated_error_methods.rb +33 -0
  9. data/lib/active_model/dirty.rb +164 -0
  10. data/lib/active_model/errors.rb +277 -0
  11. data/lib/active_model/lint.rb +89 -0
  12. data/lib/active_model/locale/en.yml +26 -0
  13. data/lib/active_model/naming.rb +60 -0
  14. data/lib/active_model/observing.rb +196 -0
  15. data/lib/active_model/railtie.rb +2 -0
  16. data/lib/active_model/serialization.rb +87 -0
  17. data/lib/active_model/serializers/json.rb +96 -0
  18. data/lib/active_model/serializers/xml.rb +204 -0
  19. data/lib/active_model/test_case.rb +16 -0
  20. data/lib/active_model/translation.rb +60 -0
  21. data/lib/active_model/validations.rb +168 -0
  22. data/lib/active_model/validations/acceptance.rb +51 -0
  23. data/lib/active_model/validations/confirmation.rb +49 -0
  24. data/lib/active_model/validations/exclusion.rb +40 -0
  25. data/lib/active_model/validations/format.rb +64 -0
  26. data/lib/active_model/validations/inclusion.rb +40 -0
  27. data/lib/active_model/validations/length.rb +98 -0
  28. data/lib/active_model/validations/numericality.rb +111 -0
  29. data/lib/active_model/validations/presence.rb +41 -0
  30. data/lib/active_model/validations/validates.rb +108 -0
  31. data/lib/active_model/validations/with.rb +70 -0
  32. data/lib/active_model/validator.rb +160 -0
  33. data/lib/active_model/version.rb +9 -0
  34. 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,2 @@
1
+ require "active_model"
2
+ require "rails"
@@ -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