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