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.
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