activerecord 1.14.4 → 1.15.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +400 -1
- data/README +2 -2
- data/RUNNING_UNIT_TESTS +21 -3
- data/Rakefile +55 -10
- data/lib/active_record.rb +10 -4
- data/lib/active_record/acts/list.rb +15 -4
- data/lib/active_record/acts/nested_set.rb +11 -12
- data/lib/active_record/acts/tree.rb +13 -14
- data/lib/active_record/aggregations.rb +46 -22
- data/lib/active_record/associations.rb +213 -162
- data/lib/active_record/associations/association_collection.rb +45 -15
- data/lib/active_record/associations/association_proxy.rb +32 -13
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +18 -18
- data/lib/active_record/associations/has_many_association.rb +37 -17
- data/lib/active_record/associations/has_many_through_association.rb +120 -30
- data/lib/active_record/associations/has_one_association.rb +1 -1
- data/lib/active_record/attribute_methods.rb +75 -0
- data/lib/active_record/base.rb +282 -203
- data/lib/active_record/calculations.rb +95 -54
- data/lib/active_record/callbacks.rb +13 -24
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +12 -1
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb.rej +21 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -4
- data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -9
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +121 -37
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +55 -23
- data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -0
- data/lib/active_record/connection_adapters/db2_adapter.rb +1 -11
- data/lib/active_record/connection_adapters/firebird_adapter.rb +364 -50
- data/lib/active_record/connection_adapters/frontbase_adapter.rb +861 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -33
- data/lib/active_record/connection_adapters/openbase_adapter.rb +4 -3
- data/lib/active_record/connection_adapters/oracle_adapter.rb +151 -127
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +125 -48
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +38 -10
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +183 -155
- data/lib/active_record/connection_adapters/sybase_adapter.rb +190 -212
- data/lib/active_record/deprecated_associations.rb +24 -10
- data/lib/active_record/deprecated_finders.rb +4 -1
- data/lib/active_record/fixtures.rb +37 -23
- data/lib/active_record/locking/optimistic.rb +106 -0
- data/lib/active_record/locking/pessimistic.rb +77 -0
- data/lib/active_record/migration.rb +8 -5
- data/lib/active_record/observer.rb +73 -34
- data/lib/active_record/reflection.rb +21 -7
- data/lib/active_record/schema_dumper.rb +33 -5
- data/lib/active_record/timestamp.rb +23 -34
- data/lib/active_record/transactions.rb +37 -30
- data/lib/active_record/validations.rb +46 -30
- data/lib/active_record/vendor/mysql.rb +20 -5
- data/lib/active_record/version.rb +2 -2
- data/lib/active_record/wrappings.rb +1 -2
- data/lib/active_record/xml_serialization.rb +308 -0
- data/test/aaa_create_tables_test.rb +5 -1
- data/test/abstract_unit.rb +18 -8
- data/test/{active_schema_mysql.rb → active_schema_test_mysql.rb} +2 -2
- data/test/adapter_test.rb +9 -7
- data/test/adapter_test_sqlserver.rb +81 -0
- data/test/aggregations_test.rb +29 -0
- data/test/{association_callbacks_test.rb → associations/callbacks_test.rb} +10 -8
- data/test/{associations_cascaded_eager_loading_test.rb → associations/cascaded_eager_loading_test.rb} +35 -3
- data/test/{associations_go_eager_test.rb → associations/eager_test.rb} +36 -2
- data/test/{associations_extensions_test.rb → associations/extension_test.rb} +5 -0
- data/test/{associations_join_model_test.rb → associations/join_model_test.rb} +118 -8
- data/test/associations_test.rb +339 -45
- data/test/attribute_methods_test.rb +49 -0
- data/test/base_test.rb +321 -67
- data/test/calculations_test.rb +48 -10
- data/test/callbacks_test.rb +13 -0
- data/test/connection_test_firebird.rb +8 -0
- data/test/connections/native_db2/connection.rb +18 -17
- data/test/connections/native_firebird/connection.rb +19 -17
- data/test/connections/native_frontbase/connection.rb +27 -0
- data/test/connections/native_mysql/connection.rb +18 -15
- data/test/connections/native_openbase/connection.rb +14 -15
- data/test/connections/native_oracle/connection.rb +16 -12
- data/test/connections/native_postgresql/connection.rb +16 -17
- data/test/connections/native_sqlite/connection.rb +3 -6
- data/test/connections/native_sqlite3/connection.rb +3 -6
- data/test/connections/native_sqlserver/connection.rb +16 -17
- data/test/connections/native_sqlserver_odbc/connection.rb +18 -19
- data/test/connections/native_sybase/connection.rb +16 -17
- data/test/datatype_test_postgresql.rb +52 -0
- data/test/defaults_test.rb +52 -10
- data/test/deprecated_associations_test.rb +151 -107
- data/test/deprecated_finder_test.rb +83 -66
- data/test/empty_date_time_test.rb +25 -0
- data/test/finder_test.rb +118 -11
- data/test/fixtures/accounts.yml +6 -1
- data/test/fixtures/author.rb +27 -4
- data/test/fixtures/categorizations.yml +8 -2
- data/test/fixtures/category.rb +1 -2
- data/test/fixtures/comments.yml +0 -6
- data/test/fixtures/companies.yml +6 -1
- data/test/fixtures/company.rb +23 -1
- data/test/fixtures/company_in_module.rb +8 -10
- data/test/fixtures/customer.rb +2 -2
- data/test/fixtures/customers.yml +9 -0
- data/test/fixtures/db_definitions/db2.drop.sql +1 -0
- data/test/fixtures/db_definitions/db2.sql +9 -0
- data/test/fixtures/db_definitions/firebird.drop.sql +3 -0
- data/test/fixtures/db_definitions/firebird.sql +13 -1
- data/test/fixtures/db_definitions/frontbase.drop.sql +31 -0
- data/test/fixtures/db_definitions/frontbase.sql +262 -0
- data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
- data/test/fixtures/db_definitions/frontbase2.sql +4 -0
- data/test/fixtures/db_definitions/mysql.drop.sql +1 -0
- data/test/fixtures/db_definitions/mysql.sql +23 -14
- data/test/fixtures/db_definitions/openbase.sql +13 -1
- data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
- data/test/fixtures/db_definitions/oracle.sql +29 -2
- data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
- data/test/fixtures/db_definitions/postgresql.sql +13 -3
- data/test/fixtures/db_definitions/schema.rb +29 -1
- data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
- data/test/fixtures/db_definitions/sqlite.sql +12 -3
- data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
- data/test/fixtures/db_definitions/sqlserver.sql +35 -0
- data/test/fixtures/db_definitions/sybase.drop.sql +2 -0
- data/test/fixtures/db_definitions/sybase.sql +13 -4
- data/test/fixtures/developer.rb +12 -0
- data/test/fixtures/edge.rb +5 -0
- data/test/fixtures/edges.yml +6 -0
- data/test/fixtures/funny_jokes.yml +3 -7
- data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
- data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
- data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
- data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
- data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
- data/test/fixtures/mixin.rb +15 -0
- data/test/fixtures/mixins.yml +38 -0
- data/test/fixtures/post.rb +3 -2
- data/test/fixtures/project.rb +3 -1
- data/test/fixtures/topic.rb +6 -1
- data/test/fixtures/topics.yml +4 -4
- data/test/fixtures/vertex.rb +9 -0
- data/test/fixtures/vertices.yml +4 -0
- data/test/fixtures_test.rb +45 -0
- data/test/inheritance_test.rb +67 -6
- data/test/lifecycle_test.rb +40 -19
- data/test/locking_test.rb +170 -26
- data/test/method_scoping_test.rb +2 -2
- data/test/migration_test.rb +387 -110
- data/test/migration_test_firebird.rb +124 -0
- data/test/mixin_nested_set_test.rb +14 -2
- data/test/mixin_test.rb +56 -18
- data/test/modules_test.rb +8 -2
- data/test/multiple_db_test.rb +2 -2
- data/test/pk_test.rb +1 -0
- data/test/reflection_test.rb +8 -2
- data/test/schema_authorization_test_postgresql.rb +75 -0
- data/test/schema_dumper_test.rb +40 -4
- data/test/table_name_test_sqlserver.rb +23 -0
- data/test/threaded_connections_test.rb +19 -16
- data/test/transactions_test.rb +86 -72
- data/test/validations_test.rb +126 -56
- data/test/xml_serialization_test.rb +125 -0
- metadata +45 -11
- data/lib/active_record/locking.rb +0 -79
@@ -87,6 +87,7 @@ module ActiveRecord
|
|
87
87
|
end
|
88
88
|
|
89
89
|
alias :add_on_boundry_breaking :add_on_boundary_breaking
|
90
|
+
deprecate :add_on_boundary_breaking => :validates_length_of, :add_on_boundry_breaking => :validates_length_of
|
90
91
|
|
91
92
|
# Returns true if the specified +attribute+ has errors associated with it.
|
92
93
|
def invalid?(attribute)
|
@@ -97,13 +98,9 @@ module ActiveRecord
|
|
97
98
|
# * Returns the error message, if one error is associated with the specified +attribute+.
|
98
99
|
# * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
|
99
100
|
def on(attribute)
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
@errors[attribute.to_s].first
|
104
|
-
else
|
105
|
-
@errors[attribute.to_s]
|
106
|
-
end
|
101
|
+
errors = @errors[attribute.to_s]
|
102
|
+
return nil if errors.nil?
|
103
|
+
errors.size == 1 ? errors.first : errors
|
107
104
|
end
|
108
105
|
|
109
106
|
alias :[] :on
|
@@ -139,13 +136,12 @@ module ActiveRecord
|
|
139
136
|
end
|
140
137
|
end
|
141
138
|
end
|
142
|
-
|
143
|
-
return full_messages
|
139
|
+
full_messages
|
144
140
|
end
|
145
141
|
|
146
142
|
# Returns true if no errors have been added.
|
147
143
|
def empty?
|
148
|
-
|
144
|
+
@errors.empty?
|
149
145
|
end
|
150
146
|
|
151
147
|
# Removes all the errors that have been added.
|
@@ -156,13 +152,23 @@ module ActiveRecord
|
|
156
152
|
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
|
157
153
|
# with this as well.
|
158
154
|
def size
|
159
|
-
error_count
|
160
|
-
@errors.each_value { |attribute| error_count += attribute.length }
|
161
|
-
error_count
|
155
|
+
@errors.values.inject(0) { |error_count, attribute| error_count + attribute.size }
|
162
156
|
end
|
163
157
|
|
164
158
|
alias_method :count, :size
|
165
159
|
alias_method :length, :size
|
160
|
+
|
161
|
+
# Return an XML representation of this error object.
|
162
|
+
def to_xml(options={})
|
163
|
+
options[:root] ||= "errors"
|
164
|
+
options[:indent] ||= 2
|
165
|
+
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
166
|
+
|
167
|
+
options[:builder].instruct! unless options.delete(:skip_instruct)
|
168
|
+
options[:builder].errors do |e|
|
169
|
+
full_messages.each { |msg| e.error(msg) }
|
170
|
+
end
|
171
|
+
end
|
166
172
|
end
|
167
173
|
|
168
174
|
|
@@ -209,18 +215,12 @@ module ActiveRecord
|
|
209
215
|
module Validations
|
210
216
|
VALIDATIONS = %w( validate validate_on_create validate_on_update )
|
211
217
|
|
212
|
-
def self.
|
213
|
-
super
|
218
|
+
def self.included(base) # :nodoc:
|
214
219
|
base.extend ClassMethods
|
215
220
|
base.class_eval do
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
alias_method :save_without_validation!, :save!
|
220
|
-
alias_method :save!, :save_with_validation!
|
221
|
-
|
222
|
-
alias_method :update_attribute_without_validation_skipping, :update_attribute
|
223
|
-
alias_method :update_attribute, :update_attribute_with_validation_skipping
|
221
|
+
alias_method_chain :save, :validation
|
222
|
+
alias_method_chain :save!, :validation
|
223
|
+
alias_method_chain :update_attribute, :validation_skipping
|
224
224
|
end
|
225
225
|
end
|
226
226
|
|
@@ -290,7 +290,7 @@ module ActiveRecord
|
|
290
290
|
# method, proc or string should return or evaluate to a true or false value.
|
291
291
|
def validates_each(*attrs)
|
292
292
|
options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
|
293
|
-
attrs
|
293
|
+
attrs = attrs.flatten
|
294
294
|
|
295
295
|
# Declare the validation.
|
296
296
|
send(validation_method(options[:on] || :save)) do |record|
|
@@ -375,6 +375,10 @@ module ActiveRecord
|
|
375
375
|
#
|
376
376
|
# The first_name attribute must be in the object and it cannot be blank.
|
377
377
|
#
|
378
|
+
# If you want to validate the presence of a boolean field (where the real values are true and false),
|
379
|
+
# you will want to use validates_inclusion_of :field_name, :in => [true, false]
|
380
|
+
# This is due to the way Object#blank? handles boolean values. false.blank? # => true
|
381
|
+
#
|
378
382
|
# Configuration options:
|
379
383
|
# * <tt>message</tt> - A custom error message (default is: "can't be blank")
|
380
384
|
# * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
|
@@ -515,17 +519,24 @@ module ActiveRecord
|
|
515
519
|
# Configuration options:
|
516
520
|
# * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
|
517
521
|
# * <tt>scope</tt> - One or more columns by which to limit the scope of the uniquness constraint.
|
522
|
+
# * <tt>case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (true by default).
|
523
|
+
# * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
|
518
524
|
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
519
525
|
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
|
520
526
|
# method, proc or string should return or evaluate to a true or false value.
|
521
527
|
|
522
528
|
def validates_uniqueness_of(*attr_names)
|
523
|
-
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
|
529
|
+
configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true }
|
524
530
|
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
|
525
531
|
|
526
532
|
validates_each(attr_names,configuration) do |record, attr_name, value|
|
527
|
-
|
528
|
-
|
533
|
+
if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?)
|
534
|
+
condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
|
535
|
+
condition_params = [value]
|
536
|
+
else
|
537
|
+
condition_sql = "UPPER(#{record.class.table_name}.#{attr_name}) #{attribute_condition(value)}"
|
538
|
+
condition_params = [value.upcase]
|
539
|
+
end
|
529
540
|
if scope = configuration[:scope]
|
530
541
|
Array(scope).map do |scope_item|
|
531
542
|
scope_value = record.send(scope_item)
|
@@ -543,13 +554,17 @@ module ActiveRecord
|
|
543
554
|
end
|
544
555
|
end
|
545
556
|
|
557
|
+
|
558
|
+
|
546
559
|
# Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
|
547
560
|
# provided.
|
548
561
|
#
|
549
562
|
# class Person < ActiveRecord::Base
|
550
|
-
# validates_format_of :email, :with =>
|
563
|
+
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
|
551
564
|
# end
|
552
565
|
#
|
566
|
+
# Note: use \A and \Z to match the start and end of the string, ^ and $ match the start/end of a line.
|
567
|
+
#
|
553
568
|
# A regular expression must be provided or else an exception will be raised.
|
554
569
|
#
|
555
570
|
# Configuration options:
|
@@ -663,7 +678,7 @@ module ActiveRecord
|
|
663
678
|
|
664
679
|
# Validates whether the value of the specified attribute is numeric by trying to convert it to
|
665
680
|
# a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression
|
666
|
-
# <tt
|
681
|
+
# <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>integer</tt> is set to true).
|
667
682
|
#
|
668
683
|
# class Person < ActiveRecord::Base
|
669
684
|
# validates_numericality_of :value, :on => :create
|
@@ -684,7 +699,7 @@ module ActiveRecord
|
|
684
699
|
|
685
700
|
if configuration[:only_integer]
|
686
701
|
validates_each(attr_names,configuration) do |record, attr_name,value|
|
687
|
-
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~
|
702
|
+
record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_before_type_cast").to_s =~ /\A[+-]?\d+\Z/
|
688
703
|
end
|
689
704
|
else
|
690
705
|
validates_each(attr_names,configuration) do |record, attr_name,value|
|
@@ -705,6 +720,7 @@ module ActiveRecord
|
|
705
720
|
if attributes.is_a?(Array)
|
706
721
|
attributes.collect { |attr| create!(attr) }
|
707
722
|
else
|
723
|
+
attributes ||= {}
|
708
724
|
attributes.reverse_merge!(scope(:create)) if scoped?(:create)
|
709
725
|
|
710
726
|
object = new(attributes)
|
@@ -6,7 +6,7 @@
|
|
6
6
|
|
7
7
|
class Mysql
|
8
8
|
|
9
|
-
VERSION = "4.0-ruby-0.2.
|
9
|
+
VERSION = "4.0-ruby-0.2.6-plus-changes"
|
10
10
|
|
11
11
|
require "socket"
|
12
12
|
require "digest/sha1"
|
@@ -18,6 +18,9 @@ class Mysql
|
|
18
18
|
MYSQL_PORT = 3306
|
19
19
|
PROTOCOL_VERSION = 10
|
20
20
|
|
21
|
+
SCRAMBLE_LENGTH = 20
|
22
|
+
SCRAMBLE_LENGTH_323 = 8
|
23
|
+
|
21
24
|
# Command
|
22
25
|
COM_SLEEP = 0
|
23
26
|
COM_QUIT = 1
|
@@ -147,12 +150,23 @@ class Mysql
|
|
147
150
|
@db = db.dup
|
148
151
|
end
|
149
152
|
write data
|
150
|
-
read
|
153
|
+
pkt = read
|
154
|
+
handle_auth_fallback(pkt, passwd)
|
151
155
|
ObjectSpace.define_finalizer(self, Mysql.finalizer(@net))
|
152
156
|
self
|
153
157
|
end
|
154
158
|
alias :connect :real_connect
|
155
159
|
|
160
|
+
def handle_auth_fallback(pkt, passwd)
|
161
|
+
# A packet like this means that we need to send an old-format password
|
162
|
+
if pkt.size == 1 and pkt[0] == 254 and
|
163
|
+
@server_capabilities & CLIENT_SECURE_CONNECTION != 0 then
|
164
|
+
data = scramble(passwd, @scramble_buff, @protocol_version == 9)
|
165
|
+
write data + "\0"
|
166
|
+
read
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
156
170
|
def escape_string(str)
|
157
171
|
Mysql::escape_string str
|
158
172
|
end
|
@@ -208,7 +222,8 @@ class Mysql
|
|
208
222
|
else
|
209
223
|
data = user+"\0"+scramble41(passwd, @scramble_buff)+db
|
210
224
|
end
|
211
|
-
command COM_CHANGE_USER, data
|
225
|
+
pkt = command COM_CHANGE_USER, data
|
226
|
+
handle_auth_fallback(pkt, passwd)
|
212
227
|
@user = user
|
213
228
|
@passwd = passwd
|
214
229
|
@db = db
|
@@ -534,10 +549,10 @@ class Mysql
|
|
534
549
|
return "" if password == nil or password == ""
|
535
550
|
raise "old version password is not implemented" if old_ver
|
536
551
|
hash_pass = hash_password password
|
537
|
-
hash_message = hash_password message
|
552
|
+
hash_message = hash_password message.slice(0,SCRAMBLE_LENGTH_323)
|
538
553
|
rnd = Random::new hash_pass[0] ^ hash_message[0], hash_pass[1] ^ hash_message[1]
|
539
554
|
to = []
|
540
|
-
1.upto(
|
555
|
+
1.upto(SCRAMBLE_LENGTH_323) do
|
541
556
|
to << ((rnd.rnd*31)+64).floor
|
542
557
|
end
|
543
558
|
extra = (rnd.rnd*31).floor
|
@@ -0,0 +1,308 @@
|
|
1
|
+
module ActiveRecord #:nodoc:
|
2
|
+
module XmlSerialization
|
3
|
+
# Builds an XML document to represent the model. Some configuration is
|
4
|
+
# availble through +options+, however more complicated cases should use
|
5
|
+
# override ActiveRecord's to_xml.
|
6
|
+
#
|
7
|
+
# By default the generated XML document will include the processing
|
8
|
+
# instruction and all object's attributes. For example:
|
9
|
+
#
|
10
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
11
|
+
# <topic>
|
12
|
+
# <title>The First Topic</title>
|
13
|
+
# <author-name>David</author-name>
|
14
|
+
# <id type="integer">1</id>
|
15
|
+
# <approved type="boolean">false</approved>
|
16
|
+
# <replies-count type="integer">0</replies-count>
|
17
|
+
# <bonus-time type="datetime">2000-01-01T08:28:00+12:00</bonus-time>
|
18
|
+
# <written-on type="datetime">2003-07-16T09:28:00+1200</written-on>
|
19
|
+
# <content>Have a nice day</content>
|
20
|
+
# <author-email-address>david@loudthinking.com</author-email-address>
|
21
|
+
# <parent-id></parent-id>
|
22
|
+
# <last-read type="date">2004-04-15</last-read>
|
23
|
+
# </topic>
|
24
|
+
#
|
25
|
+
# This behavior can be controlled with :only, :except,
|
26
|
+
# :skip_instruct, :skip_types and :dasherize. The :only and
|
27
|
+
# :except options are the same as for the #attributes method.
|
28
|
+
# The default is to dasherize all column names, to disable this,
|
29
|
+
# set :dasherize to false. To not have the column type included
|
30
|
+
# in the XML output, set :skip_types to false.
|
31
|
+
#
|
32
|
+
# For instance:
|
33
|
+
#
|
34
|
+
# topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
|
35
|
+
#
|
36
|
+
# <topic>
|
37
|
+
# <title>The First Topic</title>
|
38
|
+
# <author-name>David</author-name>
|
39
|
+
# <approved type="boolean">false</approved>
|
40
|
+
# <content>Have a nice day</content>
|
41
|
+
# <author-email-address>david@loudthinking.com</author-email-address>
|
42
|
+
# <parent-id></parent-id>
|
43
|
+
# <last-read type="date">2004-04-15</last-read>
|
44
|
+
# </topic>
|
45
|
+
#
|
46
|
+
# To include first level associations use :include
|
47
|
+
#
|
48
|
+
# firm.to_xml :include => [ :account, :clients ]
|
49
|
+
#
|
50
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
51
|
+
# <firm>
|
52
|
+
# <id type="integer">1</id>
|
53
|
+
# <rating type="integer">1</rating>
|
54
|
+
# <name>37signals</name>
|
55
|
+
# <clients>
|
56
|
+
# <client>
|
57
|
+
# <rating type="integer">1</rating>
|
58
|
+
# <name>Summit</name>
|
59
|
+
# </client>
|
60
|
+
# <client>
|
61
|
+
# <rating type="integer">1</rating>
|
62
|
+
# <name>Microsoft</name>
|
63
|
+
# </client>
|
64
|
+
# </clients>
|
65
|
+
# <account>
|
66
|
+
# <id type="integer">1</id>
|
67
|
+
# <credit-limit type="integer">50</credit-limit>
|
68
|
+
# </account>
|
69
|
+
# </firm>
|
70
|
+
#
|
71
|
+
# To include any methods on the object(s) being called use :methods
|
72
|
+
#
|
73
|
+
# firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
|
74
|
+
#
|
75
|
+
# <firm>
|
76
|
+
# # ... normal attributes as shown above ...
|
77
|
+
# <calculated-earnings>100000000000000000</calculated-earnings>
|
78
|
+
# <real-earnings>5</real-earnings>
|
79
|
+
# </firm>
|
80
|
+
#
|
81
|
+
# To call any Proc's on the object(s) use :procs. The Proc's
|
82
|
+
# are passed a modified version of the options hash that was
|
83
|
+
# given to #to_xml.
|
84
|
+
#
|
85
|
+
# proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
|
86
|
+
# firm.to_xml :procs => [ proc ]
|
87
|
+
#
|
88
|
+
# <firm>
|
89
|
+
# # ... normal attributes as shown above ...
|
90
|
+
# <abc>def</abc>
|
91
|
+
# </firm>
|
92
|
+
#
|
93
|
+
# You may override the to_xml method in your ActiveRecord::Base
|
94
|
+
# subclasses if you need to. The general form of doing this is
|
95
|
+
#
|
96
|
+
# class IHaveMyOwnXML < ActiveRecord::Base
|
97
|
+
# def to_xml(options = {})
|
98
|
+
# options[:indent] ||= 2
|
99
|
+
# xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
100
|
+
# xml.instruct! unless options[:skip_instruct]
|
101
|
+
# xml.level_one do
|
102
|
+
# xml.tag!(:second_level, 'content')
|
103
|
+
# end
|
104
|
+
# end
|
105
|
+
# end
|
106
|
+
def to_xml(options = {})
|
107
|
+
XmlSerializer.new(self, options).to_s
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class XmlSerializer #:nodoc:
|
112
|
+
attr_reader :options
|
113
|
+
|
114
|
+
def initialize(record, options = {})
|
115
|
+
@record, @options = record, options.dup
|
116
|
+
end
|
117
|
+
|
118
|
+
def builder
|
119
|
+
@builder ||= begin
|
120
|
+
options[:indent] ||= 2
|
121
|
+
builder = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
122
|
+
|
123
|
+
unless options[:skip_instruct]
|
124
|
+
builder.instruct!
|
125
|
+
options[:skip_instruct] = true
|
126
|
+
end
|
127
|
+
|
128
|
+
builder
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def root
|
133
|
+
root = (options[:root] || @record.class.to_s.underscore).to_s
|
134
|
+
dasherize? ? root.dasherize : root
|
135
|
+
end
|
136
|
+
|
137
|
+
def dasherize?
|
138
|
+
!options.has_key?(:dasherize) || options[:dasherize]
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
# To replicate the behavior in ActiveRecord#attributes,
|
143
|
+
# :except takes precedence over :only. If :only is not set
|
144
|
+
# for a N level model but is set for the N+1 level models,
|
145
|
+
# then because :except is set to a default value, the second
|
146
|
+
# level model can have both :except and :only set. So if
|
147
|
+
# :only is set, always delete :except.
|
148
|
+
def serializable_attributes
|
149
|
+
attribute_names = @record.attribute_names
|
150
|
+
|
151
|
+
if options[:only]
|
152
|
+
options.delete(:except)
|
153
|
+
attribute_names = attribute_names & Array(options[:only]).collect { |n| n.to_s }
|
154
|
+
else
|
155
|
+
options[:except] = Array(options[:except]) | Array(@record.class.inheritance_column)
|
156
|
+
attribute_names = attribute_names - options[:except].collect { |n| n.to_s }
|
157
|
+
end
|
158
|
+
|
159
|
+
attribute_names.collect { |name| Attribute.new(name, @record) }
|
160
|
+
end
|
161
|
+
|
162
|
+
def serializable_method_attributes
|
163
|
+
Array(options[:methods]).collect { |name| MethodAttribute.new(name.to_s, @record) }
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def add_attributes
|
168
|
+
(serializable_attributes + serializable_method_attributes).each do |attribute|
|
169
|
+
add_tag(attribute)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def add_includes
|
174
|
+
if include_associations = options.delete(:include)
|
175
|
+
root_only_or_except = { :except => options[:except],
|
176
|
+
:only => options[:only] }
|
177
|
+
|
178
|
+
include_has_options = include_associations.is_a?(Hash)
|
179
|
+
|
180
|
+
for association in include_has_options ? include_associations.keys : Array(include_associations)
|
181
|
+
association_options = include_has_options ? include_associations[association] : root_only_or_except
|
182
|
+
|
183
|
+
opts = options.merge(association_options)
|
184
|
+
|
185
|
+
case @record.class.reflect_on_association(association).macro
|
186
|
+
when :has_many, :has_and_belongs_to_many
|
187
|
+
records = @record.send(association).to_a
|
188
|
+
unless records.empty?
|
189
|
+
tag = records.first.class.to_s.underscore.pluralize
|
190
|
+
tag = tag.dasherize if dasherize?
|
191
|
+
|
192
|
+
builder.tag!(tag) do
|
193
|
+
records.each { |r| r.to_xml(opts.merge(:root => association.to_s.singularize)) }
|
194
|
+
end
|
195
|
+
end
|
196
|
+
when :has_one, :belongs_to
|
197
|
+
if record = @record.send(association)
|
198
|
+
record.to_xml(opts.merge(:root => association))
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
options[:include] = include_associations
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def add_procs
|
208
|
+
if procs = options.delete(:procs)
|
209
|
+
[ *procs ].each do |proc|
|
210
|
+
proc.call(options)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
def add_tag(attribute)
|
217
|
+
builder.tag!(
|
218
|
+
dasherize? ? attribute.name.dasherize : attribute.name,
|
219
|
+
attribute.value.to_s,
|
220
|
+
attribute.decorations(!options[:skip_types])
|
221
|
+
)
|
222
|
+
end
|
223
|
+
|
224
|
+
def serialize
|
225
|
+
args = [root]
|
226
|
+
if options[:namespace]
|
227
|
+
args << {:xmlns=>options[:namespace]}
|
228
|
+
end
|
229
|
+
|
230
|
+
builder.tag!(*args) do
|
231
|
+
add_attributes
|
232
|
+
add_includes
|
233
|
+
add_procs
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
alias_method :to_s, :serialize
|
238
|
+
|
239
|
+
class Attribute #:nodoc:
|
240
|
+
attr_reader :name, :value, :type
|
241
|
+
|
242
|
+
def initialize(name, record)
|
243
|
+
@name, @record = name, record
|
244
|
+
|
245
|
+
@type = compute_type
|
246
|
+
@value = compute_value
|
247
|
+
end
|
248
|
+
|
249
|
+
# There is a significant speed improvement if the value
|
250
|
+
# does not need to be escaped, as #tag! escapes all values
|
251
|
+
# to ensure that valid XML is generated. For known binary
|
252
|
+
# values, it is at least an order of magnitude faster to
|
253
|
+
# Base64 encode binary values and directly put them in the
|
254
|
+
# output XML than to pass the original value or the Base64
|
255
|
+
# encoded value to the #tag! method. It definitely makes
|
256
|
+
# no sense to Base64 encode the value and then give it to
|
257
|
+
# #tag!, since that just adds additional overhead.
|
258
|
+
def needs_encoding?
|
259
|
+
![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
|
260
|
+
end
|
261
|
+
|
262
|
+
def decorations(include_types = true)
|
263
|
+
decorations = {}
|
264
|
+
|
265
|
+
if type == :binary
|
266
|
+
decorations[:encoding] = 'base64'
|
267
|
+
end
|
268
|
+
|
269
|
+
if include_types && type != :string
|
270
|
+
decorations[:type] = type
|
271
|
+
end
|
272
|
+
|
273
|
+
decorations
|
274
|
+
end
|
275
|
+
|
276
|
+
protected
|
277
|
+
def compute_type
|
278
|
+
type = @record.class.columns_hash[name].type
|
279
|
+
|
280
|
+
case type
|
281
|
+
when :text
|
282
|
+
:string
|
283
|
+
when :time
|
284
|
+
:datetime
|
285
|
+
else
|
286
|
+
type
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def compute_value
|
291
|
+
value = @record.send(name)
|
292
|
+
|
293
|
+
if formatter = Hash::XML_FORMATTING[type.to_s]
|
294
|
+
value ? formatter.call(value) : nil
|
295
|
+
else
|
296
|
+
value
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
class MethodAttribute < Attribute #:nodoc:
|
302
|
+
protected
|
303
|
+
def compute_type
|
304
|
+
Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|