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,96 @@
1
+ require 'active_support/json'
2
+ require 'active_support/core_ext/class/attribute_accessors'
3
+
4
+ module ActiveModel
5
+ module Serializers
6
+ module JSON
7
+ extend ActiveSupport::Concern
8
+ include ActiveModel::Serialization
9
+
10
+ included do
11
+ extend ActiveModel::Naming
12
+
13
+ cattr_accessor :include_root_in_json, :instance_writer => true
14
+ end
15
+
16
+ # Returns a JSON string representing the model. Some configuration is
17
+ # available through +options+.
18
+ #
19
+ # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the
20
+ # top-level behavior of to_json. It is true by default. When it is <tt>true</tt>,
21
+ # to_json will emit a single root node named after the object's type. For example:
22
+ #
23
+ # konata = User.find(1)
24
+ # konata.to_json
25
+ # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
26
+ # "created_at": "2006/08/01", "awesome": true} }
27
+ #
28
+ # ActiveRecord::Base.include_root_in_json = false
29
+ # konata.to_json
30
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
31
+ # "created_at": "2006/08/01", "awesome": true}
32
+ #
33
+ # The remainder of the examples in this section assume include_root_in_json is set to
34
+ # <tt>false</tt>.
35
+ #
36
+ # Without any +options+, the returned JSON string will include all
37
+ # the model's attributes. For example:
38
+ #
39
+ # konata = User.find(1)
40
+ # konata.to_json
41
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
42
+ # "created_at": "2006/08/01", "awesome": true}
43
+ #
44
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
45
+ # included, and work similar to the +attributes+ method. For example:
46
+ #
47
+ # konata.to_json(:only => [ :id, :name ])
48
+ # # => {"id": 1, "name": "Konata Izumi"}
49
+ #
50
+ # konata.to_json(:except => [ :id, :created_at, :age ])
51
+ # # => {"name": "Konata Izumi", "awesome": true}
52
+ #
53
+ # To include any methods on the model, use <tt>:methods</tt>.
54
+ #
55
+ # konata.to_json(:methods => :permalink)
56
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
57
+ # "created_at": "2006/08/01", "awesome": true,
58
+ # "permalink": "1-konata-izumi"}
59
+ #
60
+ # To include associations, use <tt>:include</tt>.
61
+ #
62
+ # konata.to_json(:include => :posts)
63
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
64
+ # "created_at": "2006/08/01", "awesome": true,
65
+ # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
66
+ # {"id": 2, author_id: 1, "title": "So I was thinking"}]}
67
+ #
68
+ # 2nd level and higher order associations work as well:
69
+ #
70
+ # konata.to_json(:include => { :posts => {
71
+ # :include => { :comments => {
72
+ # :only => :body } },
73
+ # :only => :title } })
74
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
75
+ # "created_at": "2006/08/01", "awesome": true,
76
+ # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
77
+ # "title": "Welcome to the weblog"},
78
+ # {"comments": [{"body": "Don't think too hard"}],
79
+ # "title": "So I was thinking"}]}
80
+ def encode_json(encoder)
81
+ hash = serializable_hash(encoder.options)
82
+ hash = { self.class.model_name.element => hash } if include_root_in_json
83
+ ActiveSupport::JSON.encode(hash)
84
+ end
85
+
86
+ def as_json(options = nil)
87
+ self
88
+ end
89
+
90
+ def from_json(json)
91
+ self.attributes = ActiveSupport::JSON.decode(json)
92
+ self
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,204 @@
1
+ require 'active_support/core_ext/class/attribute_accessors'
2
+ require 'active_support/core_ext/hash/conversions'
3
+
4
+ module ActiveModel
5
+ module Serializers
6
+ module Xml
7
+ extend ActiveSupport::Concern
8
+ include ActiveModel::Serialization
9
+
10
+ class Serializer #:nodoc:
11
+ class Attribute #:nodoc:
12
+ attr_reader :name, :value, :type
13
+
14
+ def initialize(name, serializable)
15
+ @name, @serializable = name, serializable
16
+ @type = compute_type
17
+ @value = compute_value
18
+ end
19
+
20
+ # There is a significant speed improvement if the value
21
+ # does not need to be escaped, as <tt>tag!</tt> escapes all values
22
+ # to ensure that valid XML is generated. For known binary
23
+ # values, it is at least an order of magnitude faster to
24
+ # Base64 encode binary values and directly put them in the
25
+ # output XML than to pass the original value or the Base64
26
+ # encoded value to the <tt>tag!</tt> method. It definitely makes
27
+ # no sense to Base64 encode the value and then give it to
28
+ # <tt>tag!</tt>, since that just adds additional overhead.
29
+ def needs_encoding?
30
+ ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
31
+ end
32
+
33
+ def decorations(include_types = true)
34
+ decorations = {}
35
+
36
+ if type == :binary
37
+ decorations[:encoding] = 'base64'
38
+ end
39
+
40
+ if include_types && type != :string
41
+ decorations[:type] = type
42
+ end
43
+
44
+ if value.nil?
45
+ decorations[:nil] = true
46
+ end
47
+
48
+ decorations
49
+ end
50
+
51
+ protected
52
+ def compute_type
53
+ value = @serializable.send(name)
54
+ type = Hash::XML_TYPE_NAMES[value.class.name]
55
+ type ||= :string if value.respond_to?(:to_str)
56
+ type ||= :yaml
57
+ type
58
+ end
59
+
60
+ def compute_value
61
+ value = @serializable.send(name)
62
+
63
+ if formatter = Hash::XML_FORMATTING[type.to_s]
64
+ value ? formatter.call(value) : nil
65
+ else
66
+ value
67
+ end
68
+ end
69
+ end
70
+
71
+ class MethodAttribute < Attribute #:nodoc:
72
+ protected
73
+ def compute_type
74
+ Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string
75
+ end
76
+ end
77
+
78
+ attr_reader :options
79
+
80
+ def initialize(serializable, options = nil)
81
+ @serializable = serializable
82
+ @options = options ? options.dup : {}
83
+
84
+ @options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s }
85
+ @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s }
86
+ end
87
+
88
+ # To replicate the behavior in ActiveRecord#attributes,
89
+ # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set
90
+ # for a N level model but is set for the N+1 level models,
91
+ # then because <tt>:except</tt> is set to a default value, the second
92
+ # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if
93
+ # <tt>:only</tt> is set, always delete <tt>:except</tt>.
94
+ def serializable_attribute_names
95
+ attribute_names = @serializable.attributes.keys.sort
96
+
97
+ if options[:only].any?
98
+ attribute_names &= options[:only]
99
+ elsif options[:except].any?
100
+ attribute_names -= options[:except]
101
+ end
102
+
103
+ attribute_names
104
+ end
105
+
106
+ def serializable_attributes
107
+ serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) }
108
+ end
109
+
110
+ def serializable_method_attributes
111
+ Array(options[:methods]).inject([]) do |methods, name|
112
+ methods << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
113
+ methods
114
+ end
115
+ end
116
+
117
+ def serialize
118
+ args = [root]
119
+
120
+ if options[:namespace]
121
+ args << {:xmlns => options[:namespace]}
122
+ end
123
+
124
+ if options[:type]
125
+ args << {:type => options[:type]}
126
+ end
127
+
128
+ builder.tag!(*args) do
129
+ add_attributes
130
+ procs = options.delete(:procs)
131
+ options[:procs] = procs
132
+ add_procs
133
+ yield builder if block_given?
134
+ end
135
+ end
136
+
137
+ private
138
+ def builder
139
+ @builder ||= begin
140
+ require 'builder' unless defined? ::Builder
141
+ options[:indent] ||= 2
142
+ builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
143
+
144
+ unless options[:skip_instruct]
145
+ builder.instruct!
146
+ options[:skip_instruct] = true
147
+ end
148
+
149
+ builder
150
+ end
151
+ end
152
+
153
+ def root
154
+ root = (options[:root] || @serializable.class.model_name.singular).to_s
155
+ reformat_name(root)
156
+ end
157
+
158
+ def dasherize?
159
+ !options.has_key?(:dasherize) || options[:dasherize]
160
+ end
161
+
162
+ def camelize?
163
+ options.has_key?(:camelize) && options[:camelize]
164
+ end
165
+
166
+ def reformat_name(name)
167
+ name = name.camelize if camelize?
168
+ dasherize? ? name.dasherize : name
169
+ end
170
+
171
+ def add_attributes
172
+ (serializable_attributes + serializable_method_attributes).each do |attribute|
173
+ builder.tag!(
174
+ reformat_name(attribute.name),
175
+ attribute.value.to_s,
176
+ attribute.decorations(!options[:skip_types])
177
+ )
178
+ end
179
+ end
180
+
181
+ def add_procs
182
+ if procs = options.delete(:procs)
183
+ [ *procs ].each do |proc|
184
+ if proc.arity > 1
185
+ proc.call(options, @serializable)
186
+ else
187
+ proc.call(options)
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def to_xml(options = {}, &block)
195
+ Serializer.new(self, options).serialize(&block)
196
+ end
197
+
198
+ def from_xml(xml)
199
+ self.attributes = Hash.from_xml(xml).values.first
200
+ self
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveModel #:nodoc:
2
+ class TestCase < ActiveSupport::TestCase #:nodoc:
3
+ def with_kcode(kcode)
4
+ if RUBY_VERSION < '1.9'
5
+ orig_kcode, $KCODE = $KCODE, kcode
6
+ begin
7
+ yield
8
+ ensure
9
+ $KCODE = orig_kcode
10
+ end
11
+ else
12
+ yield
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,60 @@
1
+ require 'active_support/core_ext/hash/reverse_merge'
2
+
3
+ module ActiveModel
4
+
5
+ # ActiveModel::Translation provides integration between your object and
6
+ # the Rails internationalization (i18n) framework.
7
+ #
8
+ # A minimal implementation could be:
9
+ #
10
+ # class TranslatedPerson
11
+ # extend ActiveModel::Translation
12
+ # end
13
+ #
14
+ # TranslatedPerson.human_attribute_name('my_attribue')
15
+ # #=> "My attribute"
16
+ #
17
+ # This also provides the required class methods for hooking into the
18
+ # Rails internationalization API, including being able to define a
19
+ # class based i18n_scope and lookup_ancestors to find translations in
20
+ # parent classes.
21
+ module Translation
22
+ include ActiveModel::Naming
23
+
24
+ # Returns the i18n_scope for the class. Overwrite if you want custom lookup.
25
+ def i18n_scope
26
+ :activemodel
27
+ end
28
+
29
+ # When localizing a string, goes through the lookup returned by this method.
30
+ # Used in ActiveModel::Name#human, ActiveModel::Errors#full_messages and
31
+ # ActiveModel::Translation#human_attribute_name.
32
+ def lookup_ancestors
33
+ self.ancestors.select { |x| x.respond_to?(:model_name) }
34
+ end
35
+
36
+ # Transforms attributes names into a more human format, such as "First name" instead of "first_name".
37
+ #
38
+ # Person.human_attribute_name("first_name") # => "First name"
39
+ #
40
+ # Specify +options+ with additional translating options.
41
+ def human_attribute_name(attribute, options = {})
42
+ defaults = lookup_ancestors.map do |klass|
43
+ :"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}"
44
+ end
45
+
46
+ defaults << :"attributes.#{attribute}"
47
+ defaults << options.delete(:default) if options[:default]
48
+ defaults << attribute.to_s.humanize
49
+
50
+ options.reverse_merge! :count => 1, :default => defaults
51
+ I18n.translate(defaults.shift, options)
52
+ end
53
+
54
+ # Model.human_name is deprecated. Use Model.model_name.human instead.
55
+ def human_name(*args)
56
+ ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,5])
57
+ model_name.human(*args)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,168 @@
1
+ require 'active_support/core_ext/array/extract_options'
2
+ require 'active_support/core_ext/hash/keys'
3
+ require 'active_model/errors'
4
+
5
+ module ActiveModel
6
+
7
+ # Provides a full validation framework to your objects.
8
+ #
9
+ # A minimal implementation could be:
10
+ #
11
+ # class Person
12
+ # include ActiveModel::Validations
13
+ #
14
+ # attr_accessor :first_name, :last_name
15
+ #
16
+ # validates_each :first_name, :last_name do |record, attr, value|
17
+ # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
18
+ # end
19
+ # end
20
+ #
21
+ # Which provides you with the full standard validation stack that you
22
+ # know from ActiveRecord.
23
+ #
24
+ # person = Person.new
25
+ # person.valid?
26
+ # #=> true
27
+ # person.invalid?
28
+ # #=> false
29
+ # person.first_name = 'zoolander'
30
+ # person.valid?
31
+ # #=> false
32
+ # person.invalid?
33
+ # #=> true
34
+ # person.errors
35
+ # #=> #<OrderedHash {:first_name=>["starts with z."]}>
36
+ #
37
+ # Note that ActiveModel::Validations automatically adds an +errors+ method
38
+ # to your instances initialized with a new ActiveModel::Errors object, so
39
+ # there is no need for you to add this manually.
40
+ #
41
+ module Validations
42
+ extend ActiveSupport::Concern
43
+ include ActiveSupport::Callbacks
44
+
45
+ included do
46
+ extend ActiveModel::Translation
47
+ define_callbacks :validate, :scope => :name
48
+ end
49
+
50
+ module ClassMethods
51
+ # Validates each attribute against a block.
52
+ #
53
+ # class Person
54
+ # include ActiveModel::Validations
55
+ #
56
+ # attr_accessor :first_name, :last_name
57
+ #
58
+ # validates_each :first_name, :last_name do |record, attr, value|
59
+ # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
60
+ # end
61
+ # end
62
+ #
63
+ # Options:
64
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>,
65
+ # other options <tt>:create</tt>, <tt>:update</tt>).
66
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
67
+ # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
68
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
69
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or
70
+ # <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
71
+ # method, proc or string should return or evaluate to a true or false value.
72
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
73
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or
74
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
75
+ # method, proc or string should return or evaluate to a true or false value.
76
+ def validates_each(*attr_names, &block)
77
+ options = attr_names.extract_options!.symbolize_keys
78
+ validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block
79
+ end
80
+
81
+ # Adds a validation method or block to the class. This is useful when
82
+ # overriding the +validate+ instance method becomes too unwieldly and
83
+ # you're looking for more descriptive declaration of your validations.
84
+ #
85
+ # This can be done with a symbol pointing to a method:
86
+ #
87
+ # class Comment
88
+ # include ActiveModel::Validations
89
+ #
90
+ # validate :must_be_friends
91
+ #
92
+ # def must_be_friends
93
+ # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
94
+ # end
95
+ # end
96
+ #
97
+ # Or with a block which is passed the current record to be validated:
98
+ #
99
+ # class Comment
100
+ # include ActiveModel::Validations
101
+ #
102
+ # validate do |comment|
103
+ # comment.must_be_friends
104
+ # end
105
+ #
106
+ # def must_be_friends
107
+ # errors.add_to_base("Must be friends to leave a comment") unless commenter.friend_of?(commentee)
108
+ # end
109
+ # end
110
+ #
111
+ # This usage applies to +validate_on_create+ and +validate_on_update as well+.
112
+ def validate(*args, &block)
113
+ options = args.last
114
+ if options.is_a?(Hash) && options.key?(:on)
115
+ options[:if] = Array(options[:if])
116
+ options[:if] << "@_on_validate == :#{options[:on]}"
117
+ end
118
+ set_callback(:validate, *args, &block)
119
+ end
120
+
121
+ private
122
+
123
+ def _merge_attributes(attr_names)
124
+ options = attr_names.extract_options!
125
+ options.merge(:attributes => attr_names)
126
+ end
127
+ end
128
+
129
+ # Returns the Errors object that holds all information about attribute error messages.
130
+ def errors
131
+ @errors ||= Errors.new(self)
132
+ end
133
+
134
+ # Runs all the specified validations and returns true if no errors were added otherwise false.
135
+ def valid?
136
+ errors.clear
137
+ _run_validate_callbacks
138
+ errors.empty?
139
+ end
140
+
141
+ # Performs the opposite of <tt>valid?</tt>. Returns true if errors were added, false otherwise.
142
+ def invalid?
143
+ !valid?
144
+ end
145
+
146
+ # Hook method defining how an attribute value should be retieved. By default this is assumed
147
+ # to be an instance named after the attribute. Override this method in subclasses should you
148
+ # need to retrieve the value for a given attribute differently e.g.
149
+ # class MyClass
150
+ # include ActiveModel::Validations
151
+ #
152
+ # def initialize(data = {})
153
+ # @data = data
154
+ # end
155
+ #
156
+ # def read_attribute_for_validation(key)
157
+ # @data[key]
158
+ # end
159
+ # end
160
+ #
161
+ alias :read_attribute_for_validation :send
162
+ end
163
+ end
164
+
165
+ Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path|
166
+ filename = File.basename(path)
167
+ require "active_model/validations/#{filename}"
168
+ end