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.

Files changed (159) hide show
  1. data/CHANGELOG +400 -1
  2. data/README +2 -2
  3. data/RUNNING_UNIT_TESTS +21 -3
  4. data/Rakefile +55 -10
  5. data/lib/active_record.rb +10 -4
  6. data/lib/active_record/acts/list.rb +15 -4
  7. data/lib/active_record/acts/nested_set.rb +11 -12
  8. data/lib/active_record/acts/tree.rb +13 -14
  9. data/lib/active_record/aggregations.rb +46 -22
  10. data/lib/active_record/associations.rb +213 -162
  11. data/lib/active_record/associations/association_collection.rb +45 -15
  12. data/lib/active_record/associations/association_proxy.rb +32 -13
  13. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +18 -18
  14. data/lib/active_record/associations/has_many_association.rb +37 -17
  15. data/lib/active_record/associations/has_many_through_association.rb +120 -30
  16. data/lib/active_record/associations/has_one_association.rb +1 -1
  17. data/lib/active_record/attribute_methods.rb +75 -0
  18. data/lib/active_record/base.rb +282 -203
  19. data/lib/active_record/calculations.rb +95 -54
  20. data/lib/active_record/callbacks.rb +13 -24
  21. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +12 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb.rej +21 -0
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -4
  24. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -9
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +121 -37
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +55 -23
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -0
  28. data/lib/active_record/connection_adapters/db2_adapter.rb +1 -11
  29. data/lib/active_record/connection_adapters/firebird_adapter.rb +364 -50
  30. data/lib/active_record/connection_adapters/frontbase_adapter.rb +861 -0
  31. data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -33
  32. data/lib/active_record/connection_adapters/openbase_adapter.rb +4 -3
  33. data/lib/active_record/connection_adapters/oracle_adapter.rb +151 -127
  34. data/lib/active_record/connection_adapters/postgresql_adapter.rb +125 -48
  35. data/lib/active_record/connection_adapters/sqlite_adapter.rb +38 -10
  36. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +183 -155
  37. data/lib/active_record/connection_adapters/sybase_adapter.rb +190 -212
  38. data/lib/active_record/deprecated_associations.rb +24 -10
  39. data/lib/active_record/deprecated_finders.rb +4 -1
  40. data/lib/active_record/fixtures.rb +37 -23
  41. data/lib/active_record/locking/optimistic.rb +106 -0
  42. data/lib/active_record/locking/pessimistic.rb +77 -0
  43. data/lib/active_record/migration.rb +8 -5
  44. data/lib/active_record/observer.rb +73 -34
  45. data/lib/active_record/reflection.rb +21 -7
  46. data/lib/active_record/schema_dumper.rb +33 -5
  47. data/lib/active_record/timestamp.rb +23 -34
  48. data/lib/active_record/transactions.rb +37 -30
  49. data/lib/active_record/validations.rb +46 -30
  50. data/lib/active_record/vendor/mysql.rb +20 -5
  51. data/lib/active_record/version.rb +2 -2
  52. data/lib/active_record/wrappings.rb +1 -2
  53. data/lib/active_record/xml_serialization.rb +308 -0
  54. data/test/aaa_create_tables_test.rb +5 -1
  55. data/test/abstract_unit.rb +18 -8
  56. data/test/{active_schema_mysql.rb → active_schema_test_mysql.rb} +2 -2
  57. data/test/adapter_test.rb +9 -7
  58. data/test/adapter_test_sqlserver.rb +81 -0
  59. data/test/aggregations_test.rb +29 -0
  60. data/test/{association_callbacks_test.rb → associations/callbacks_test.rb} +10 -8
  61. data/test/{associations_cascaded_eager_loading_test.rb → associations/cascaded_eager_loading_test.rb} +35 -3
  62. data/test/{associations_go_eager_test.rb → associations/eager_test.rb} +36 -2
  63. data/test/{associations_extensions_test.rb → associations/extension_test.rb} +5 -0
  64. data/test/{associations_join_model_test.rb → associations/join_model_test.rb} +118 -8
  65. data/test/associations_test.rb +339 -45
  66. data/test/attribute_methods_test.rb +49 -0
  67. data/test/base_test.rb +321 -67
  68. data/test/calculations_test.rb +48 -10
  69. data/test/callbacks_test.rb +13 -0
  70. data/test/connection_test_firebird.rb +8 -0
  71. data/test/connections/native_db2/connection.rb +18 -17
  72. data/test/connections/native_firebird/connection.rb +19 -17
  73. data/test/connections/native_frontbase/connection.rb +27 -0
  74. data/test/connections/native_mysql/connection.rb +18 -15
  75. data/test/connections/native_openbase/connection.rb +14 -15
  76. data/test/connections/native_oracle/connection.rb +16 -12
  77. data/test/connections/native_postgresql/connection.rb +16 -17
  78. data/test/connections/native_sqlite/connection.rb +3 -6
  79. data/test/connections/native_sqlite3/connection.rb +3 -6
  80. data/test/connections/native_sqlserver/connection.rb +16 -17
  81. data/test/connections/native_sqlserver_odbc/connection.rb +18 -19
  82. data/test/connections/native_sybase/connection.rb +16 -17
  83. data/test/datatype_test_postgresql.rb +52 -0
  84. data/test/defaults_test.rb +52 -10
  85. data/test/deprecated_associations_test.rb +151 -107
  86. data/test/deprecated_finder_test.rb +83 -66
  87. data/test/empty_date_time_test.rb +25 -0
  88. data/test/finder_test.rb +118 -11
  89. data/test/fixtures/accounts.yml +6 -1
  90. data/test/fixtures/author.rb +27 -4
  91. data/test/fixtures/categorizations.yml +8 -2
  92. data/test/fixtures/category.rb +1 -2
  93. data/test/fixtures/comments.yml +0 -6
  94. data/test/fixtures/companies.yml +6 -1
  95. data/test/fixtures/company.rb +23 -1
  96. data/test/fixtures/company_in_module.rb +8 -10
  97. data/test/fixtures/customer.rb +2 -2
  98. data/test/fixtures/customers.yml +9 -0
  99. data/test/fixtures/db_definitions/db2.drop.sql +1 -0
  100. data/test/fixtures/db_definitions/db2.sql +9 -0
  101. data/test/fixtures/db_definitions/firebird.drop.sql +3 -0
  102. data/test/fixtures/db_definitions/firebird.sql +13 -1
  103. data/test/fixtures/db_definitions/frontbase.drop.sql +31 -0
  104. data/test/fixtures/db_definitions/frontbase.sql +262 -0
  105. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  106. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  107. data/test/fixtures/db_definitions/mysql.drop.sql +1 -0
  108. data/test/fixtures/db_definitions/mysql.sql +23 -14
  109. data/test/fixtures/db_definitions/openbase.sql +13 -1
  110. data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
  111. data/test/fixtures/db_definitions/oracle.sql +29 -2
  112. data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
  113. data/test/fixtures/db_definitions/postgresql.sql +13 -3
  114. data/test/fixtures/db_definitions/schema.rb +29 -1
  115. data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
  116. data/test/fixtures/db_definitions/sqlite.sql +12 -3
  117. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  118. data/test/fixtures/db_definitions/sqlserver.sql +35 -0
  119. data/test/fixtures/db_definitions/sybase.drop.sql +2 -0
  120. data/test/fixtures/db_definitions/sybase.sql +13 -4
  121. data/test/fixtures/developer.rb +12 -0
  122. data/test/fixtures/edge.rb +5 -0
  123. data/test/fixtures/edges.yml +6 -0
  124. data/test/fixtures/funny_jokes.yml +3 -7
  125. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  126. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  127. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  128. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  129. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  130. data/test/fixtures/mixin.rb +15 -0
  131. data/test/fixtures/mixins.yml +38 -0
  132. data/test/fixtures/post.rb +3 -2
  133. data/test/fixtures/project.rb +3 -1
  134. data/test/fixtures/topic.rb +6 -1
  135. data/test/fixtures/topics.yml +4 -4
  136. data/test/fixtures/vertex.rb +9 -0
  137. data/test/fixtures/vertices.yml +4 -0
  138. data/test/fixtures_test.rb +45 -0
  139. data/test/inheritance_test.rb +67 -6
  140. data/test/lifecycle_test.rb +40 -19
  141. data/test/locking_test.rb +170 -26
  142. data/test/method_scoping_test.rb +2 -2
  143. data/test/migration_test.rb +387 -110
  144. data/test/migration_test_firebird.rb +124 -0
  145. data/test/mixin_nested_set_test.rb +14 -2
  146. data/test/mixin_test.rb +56 -18
  147. data/test/modules_test.rb +8 -2
  148. data/test/multiple_db_test.rb +2 -2
  149. data/test/pk_test.rb +1 -0
  150. data/test/reflection_test.rb +8 -2
  151. data/test/schema_authorization_test_postgresql.rb +75 -0
  152. data/test/schema_dumper_test.rb +40 -4
  153. data/test/table_name_test_sqlserver.rb +23 -0
  154. data/test/threaded_connections_test.rb +19 -16
  155. data/test/transactions_test.rb +86 -72
  156. data/test/validations_test.rb +126 -56
  157. data/test/xml_serialization_test.rb +125 -0
  158. metadata +45 -11
  159. 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
- if @errors[attribute.to_s].nil?
101
- nil
102
- elsif @errors[attribute.to_s].length == 1
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
- return @errors.empty?
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 = 0
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.append_features(base) # :nodoc:
213
- super
218
+ def self.included(base) # :nodoc:
214
219
  base.extend ClassMethods
215
220
  base.class_eval do
216
- alias_method :save_without_validation, :save
217
- alias_method :save, :save_with_validation
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 = attrs.flatten
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
- condition_sql = "#{record.class.table_name}.#{attr_name} #{attribute_condition(value)}"
528
- condition_params = [value]
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 => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :on => :create
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>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true).
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 =~ /^[+-]?\d+$/
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.5"
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(message.length) do
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
@@ -1,8 +1,8 @@
1
1
  module ActiveRecord
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1
4
- MINOR = 14
5
- TINY = 4
4
+ MINOR = 15
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -9,8 +9,7 @@ module ActiveRecord
9
9
  end
10
10
  end
11
11
 
12
- def self.append_features(base)
13
- super
12
+ def self.included(base)
14
13
  base.extend(ClassMethods)
15
14
  end
16
15
 
@@ -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