activeobject 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. data/CHANGE +10 -0
  2. data/Interface_desc +21 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README +72 -0
  5. data/Rakefile.rb +9 -0
  6. data/active-object.gemspec +50 -0
  7. data/examples/account.rb +69 -0
  8. data/examples/data.tch +0 -0
  9. data/examples/light_cloud.yml +18 -0
  10. data/examples/test.rb +3 -0
  11. data/examples/user.rb +112 -0
  12. data/init.rb +4 -0
  13. data/lib/active-object.rb +23 -0
  14. data/lib/active_object/adapters/light_cloud.rb +40 -0
  15. data/lib/active_object/adapters/tokyo_cabinet.rb +48 -0
  16. data/lib/active_object/adapters/tokyo_tyrant.rb +14 -0
  17. data/lib/active_object/associations.rb +200 -0
  18. data/lib/active_object/base.rb +415 -0
  19. data/lib/active_object/callbacks.rb +180 -0
  20. data/lib/active_object/observer.rb +180 -0
  21. data/lib/active_object/serialization.rb +99 -0
  22. data/lib/active_object/serializers/json_serializer.rb +75 -0
  23. data/lib/active_object/serializers/xml_serializer.rb +325 -0
  24. data/lib/active_object/validations.rb +687 -0
  25. data/lib/active_support/callbacks.rb +303 -0
  26. data/lib/active_support/core_ext/array/access.rb +53 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +183 -0
  28. data/lib/active_support/core_ext/array/extract_options.rb +20 -0
  29. data/lib/active_support/core_ext/array/grouping.rb +106 -0
  30. data/lib/active_support/core_ext/array/random_access.rb +12 -0
  31. data/lib/active_support/core_ext/array.rb +13 -0
  32. data/lib/active_support/core_ext/blank.rb +58 -0
  33. data/lib/active_support/core_ext/class/attribute_accessors.rb +54 -0
  34. data/lib/active_support/core_ext/class/inheritable_attributes.rb +140 -0
  35. data/lib/active_support/core_ext/class/removal.rb +50 -0
  36. data/lib/active_support/core_ext/class.rb +3 -0
  37. data/lib/active_support/core_ext/duplicable.rb +43 -0
  38. data/lib/active_support/core_ext/enumerable.rb +72 -0
  39. data/lib/active_support/core_ext/hash/conversions.rb +259 -0
  40. data/lib/active_support/core_ext/hash/keys.rb +52 -0
  41. data/lib/active_support/core_ext/hash.rb +8 -0
  42. data/lib/active_support/core_ext/module/aliasing.rb +74 -0
  43. data/lib/active_support/core_ext/module/attr_accessor_with_default.rb +31 -0
  44. data/lib/active_support/core_ext/module/attribute_accessors.rb +58 -0
  45. data/lib/active_support/core_ext/module.rb +16 -0
  46. data/lib/active_support/core_ext/object/conversions.rb +14 -0
  47. data/lib/active_support/core_ext/object/extending.rb +80 -0
  48. data/lib/active_support/core_ext/object/instance_variables.rb +74 -0
  49. data/lib/active_support/core_ext/object/metaclass.rb +13 -0
  50. data/lib/active_support/core_ext/object/misc.rb +43 -0
  51. data/lib/active_support/core_ext/object.rb +5 -0
  52. data/lib/active_support/core_ext/string/inflections.rb +167 -0
  53. data/lib/active_support/core_ext/string.rb +7 -0
  54. data/lib/active_support/core_ext.rb +4 -0
  55. data/lib/active_support/inflections.rb +55 -0
  56. data/lib/active_support/inflector.rb +348 -0
  57. data/lib/active_support/vendor/builder-2.1.2/blankslate.rb +113 -0
  58. data/lib/active_support/vendor/builder-2.1.2/builder/blankslate.rb +20 -0
  59. data/lib/active_support/vendor/builder-2.1.2/builder/css.rb +250 -0
  60. data/lib/active_support/vendor/builder-2.1.2/builder/xchar.rb +115 -0
  61. data/lib/active_support/vendor/builder-2.1.2/builder/xmlbase.rb +139 -0
  62. data/lib/active_support/vendor/builder-2.1.2/builder/xmlevents.rb +63 -0
  63. data/lib/active_support/vendor/builder-2.1.2/builder/xmlmarkup.rb +328 -0
  64. data/lib/active_support/vendor/builder-2.1.2/builder.rb +13 -0
  65. data/lib/active_support/vendor/xml-simple-1.0.11/xmlsimple.rb +1021 -0
  66. data/lib/active_support/vendor.rb +14 -0
  67. data/lib/active_support.rb +6 -0
  68. data/spec/case/association_test.rb +97 -0
  69. data/spec/case/base_test.rb +74 -0
  70. data/spec/case/callbacks_observers_test.rb +38 -0
  71. data/spec/case/callbacks_test.rb +424 -0
  72. data/spec/case/serialization_test.rb +87 -0
  73. data/spec/case/validations_test.rb +1482 -0
  74. data/spec/data.tch +0 -0
  75. data/spec/helper.rb +15 -0
  76. data/spec/light_cloud.yml +18 -0
  77. data/spec/model/account.rb +4 -0
  78. data/spec/model/topic.rb +26 -0
  79. data/spec/model/user.rb +8 -0
  80. metadata +173 -0
@@ -0,0 +1,58 @@
1
+ class Object
2
+ # An object is blank if it's false, empty, or a whitespace string.
3
+ # For example, "", " ", +nil+, [], and {} are blank.
4
+ #
5
+ # This simplifies
6
+ #
7
+ # if !address.nil? && !address.empty?
8
+ #
9
+ # to
10
+ #
11
+ # if !address.blank?
12
+ def blank?
13
+ respond_to?(:empty?) ? empty? : !self
14
+ end
15
+
16
+ # An object is present if it's not blank.
17
+ def present?
18
+ !blank?
19
+ end
20
+ end
21
+
22
+ class NilClass #:nodoc:
23
+ def blank?
24
+ true
25
+ end
26
+ end
27
+
28
+ class FalseClass #:nodoc:
29
+ def blank?
30
+ true
31
+ end
32
+ end
33
+
34
+ class TrueClass #:nodoc:
35
+ def blank?
36
+ false
37
+ end
38
+ end
39
+
40
+ class Array #:nodoc:
41
+ alias_method :blank?, :empty?
42
+ end
43
+
44
+ class Hash #:nodoc:
45
+ alias_method :blank?, :empty?
46
+ end
47
+
48
+ class String #:nodoc:
49
+ def blank?
50
+ self !~ /\S/
51
+ end
52
+ end
53
+
54
+ class Numeric #:nodoc:
55
+ def blank?
56
+ false
57
+ end
58
+ end
@@ -0,0 +1,54 @@
1
+ # Extends the class object with class and instance accessors for class attributes,
2
+ # just like the native attr* accessors for instance attributes.
3
+ #
4
+ # class Person
5
+ # cattr_accessor :hair_colors
6
+ # end
7
+ #
8
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
9
+ class Class
10
+ def cattr_reader(*syms)
11
+ syms.flatten.each do |sym|
12
+ next if sym.is_a?(Hash)
13
+ class_eval(<<-EOS, __FILE__, __LINE__)
14
+ unless defined? @@#{sym}
15
+ @@#{sym} = nil
16
+ end
17
+
18
+ def self.#{sym}
19
+ @@#{sym}
20
+ end
21
+
22
+ def #{sym}
23
+ @@#{sym}
24
+ end
25
+ EOS
26
+ end
27
+ end
28
+
29
+ def cattr_writer(*syms)
30
+ options = syms.extract_options!
31
+ syms.flatten.each do |sym|
32
+ class_eval(<<-EOS, __FILE__, __LINE__)
33
+ unless defined? @@#{sym}
34
+ @@#{sym} = nil
35
+ end
36
+
37
+ def self.#{sym}=(obj)
38
+ @@#{sym} = obj
39
+ end
40
+
41
+ #{"
42
+ def #{sym}=(obj)
43
+ @@#{sym} = obj
44
+ end
45
+ " unless options[:instance_writer] == false }
46
+ EOS
47
+ end
48
+ end
49
+
50
+ def cattr_accessor(*syms)
51
+ cattr_reader(*syms)
52
+ cattr_writer(*syms)
53
+ end
54
+ end
@@ -0,0 +1,140 @@
1
+ # Retain for backward compatibility. Methods are now included in Class.
2
+ module ClassInheritableAttributes # :nodoc:
3
+ end
4
+
5
+ # Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
6
+ # their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
7
+ # to, for example, an array without those additions being shared with either their parent, siblings, or
8
+ # children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
9
+ class Class # :nodoc:
10
+ def class_inheritable_reader(*syms)
11
+ syms.each do |sym|
12
+ next if sym.is_a?(Hash)
13
+ class_eval <<-EOS
14
+ def self.#{sym}
15
+ read_inheritable_attribute(:#{sym})
16
+ end
17
+
18
+ def #{sym}
19
+ self.class.#{sym}
20
+ end
21
+ EOS
22
+ end
23
+ end
24
+
25
+ def class_inheritable_writer(*syms)
26
+ options = syms.extract_options!
27
+ syms.each do |sym|
28
+ class_eval <<-EOS
29
+ def self.#{sym}=(obj)
30
+ write_inheritable_attribute(:#{sym}, obj)
31
+ end
32
+
33
+ #{"
34
+ def #{sym}=(obj)
35
+ self.class.#{sym} = obj
36
+ end
37
+ " unless options[:instance_writer] == false }
38
+ EOS
39
+ end
40
+ end
41
+
42
+ def class_inheritable_array_writer(*syms)
43
+ options = syms.extract_options!
44
+ syms.each do |sym|
45
+ class_eval <<-EOS
46
+ def self.#{sym}=(obj)
47
+ write_inheritable_array(:#{sym}, obj)
48
+ end
49
+
50
+ #{"
51
+ def #{sym}=(obj)
52
+ self.class.#{sym} = obj
53
+ end
54
+ " unless options[:instance_writer] == false }
55
+ EOS
56
+ end
57
+ end
58
+
59
+ def class_inheritable_hash_writer(*syms)
60
+ options = syms.extract_options!
61
+ syms.each do |sym|
62
+ class_eval <<-EOS
63
+ def self.#{sym}=(obj)
64
+ write_inheritable_hash(:#{sym}, obj)
65
+ end
66
+
67
+ #{"
68
+ def #{sym}=(obj)
69
+ self.class.#{sym} = obj
70
+ end
71
+ " unless options[:instance_writer] == false }
72
+ EOS
73
+ end
74
+ end
75
+
76
+ def class_inheritable_accessor(*syms)
77
+ class_inheritable_reader(*syms)
78
+ class_inheritable_writer(*syms)
79
+ end
80
+
81
+ def class_inheritable_array(*syms)
82
+ class_inheritable_reader(*syms)
83
+ class_inheritable_array_writer(*syms)
84
+ end
85
+
86
+ def class_inheritable_hash(*syms)
87
+ class_inheritable_reader(*syms)
88
+ class_inheritable_hash_writer(*syms)
89
+ end
90
+
91
+ def inheritable_attributes
92
+ @inheritable_attributes ||= EMPTY_INHERITABLE_ATTRIBUTES
93
+ end
94
+
95
+ def write_inheritable_attribute(key, value)
96
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
97
+ @inheritable_attributes = {}
98
+ end
99
+ inheritable_attributes[key] = value
100
+ end
101
+
102
+ def write_inheritable_array(key, elements)
103
+ write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
104
+ write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
105
+ end
106
+
107
+ def write_inheritable_hash(key, hash)
108
+ write_inheritable_attribute(key, {}) if read_inheritable_attribute(key).nil?
109
+ write_inheritable_attribute(key, read_inheritable_attribute(key).merge(hash))
110
+ end
111
+
112
+ def read_inheritable_attribute(key)
113
+ inheritable_attributes[key]
114
+ end
115
+
116
+ def reset_inheritable_attributes
117
+ @inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
118
+ end
119
+
120
+ private
121
+ # Prevent this constant from being created multiple times
122
+ EMPTY_INHERITABLE_ATTRIBUTES = {}.freeze unless const_defined?(:EMPTY_INHERITABLE_ATTRIBUTES)
123
+
124
+ def inherited_with_inheritable_attributes(child)
125
+ inherited_without_inheritable_attributes(child) if respond_to?(:inherited_without_inheritable_attributes)
126
+
127
+ if inheritable_attributes.equal?(EMPTY_INHERITABLE_ATTRIBUTES)
128
+ new_inheritable_attributes = EMPTY_INHERITABLE_ATTRIBUTES
129
+ else
130
+ new_inheritable_attributes = inheritable_attributes.inject({}) do |memo, (key, value)|
131
+ memo.update(key => value.duplicable? ? value.dup : value)
132
+ end
133
+ end
134
+
135
+ child.instance_variable_set('@inheritable_attributes', new_inheritable_attributes)
136
+ end
137
+
138
+ alias inherited_without_inheritable_attributes inherited
139
+ alias inherited inherited_with_inheritable_attributes
140
+ end
@@ -0,0 +1,50 @@
1
+ class Class #:nodoc:
2
+
3
+ # Unassociates the class with its subclasses and removes the subclasses
4
+ # themselves.
5
+ #
6
+ # Integer.remove_subclasses # => [Bignum, Fixnum]
7
+ # Fixnum # => NameError: uninitialized constant Fixnum
8
+ def remove_subclasses
9
+ Object.remove_subclasses_of(self)
10
+ end
11
+
12
+ # Returns an array with the names of the subclasses of +self+ as strings.
13
+ #
14
+ # Integer.subclasses # => ["Bignum", "Fixnum"]
15
+ def subclasses
16
+ Object.subclasses_of(self).map { |o| o.to_s }
17
+ end
18
+
19
+ # Removes the classes in +klasses+ from their parent module.
20
+ #
21
+ # Ordinary classes belong to some module via a constant. This method computes
22
+ # that constant name from the class name and removes it from the module it
23
+ # belongs to.
24
+ #
25
+ # Object.remove_class(Integer) # => [Integer]
26
+ # Integer # => NameError: uninitialized constant Integer
27
+ #
28
+ # Take into account that in general the class object could be still stored
29
+ # somewhere else.
30
+ #
31
+ # i = Integer # => Integer
32
+ # Object.remove_class(Integer) # => [Integer]
33
+ # Integer # => NameError: uninitialized constant Integer
34
+ # i.subclasses # => ["Bignum", "Fixnum"]
35
+ # Fixnum.superclass # => Integer
36
+ def remove_class(*klasses)
37
+ klasses.flatten.each do |klass|
38
+ # Skip this class if there is nothing bound to this name
39
+ next unless defined?(klass.name)
40
+
41
+ basename = klass.to_s.split("::").last
42
+ parent = klass.parent
43
+
44
+ # Skip this class if it does not match the current one bound to this name
45
+ next unless parent.const_defined?(basename) && klass = parent.const_get(basename)
46
+
47
+ parent.instance_eval { remove_const basename } unless parent == klass
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_support/core_ext/class/attribute_accessors'
2
+ require 'active_support/core_ext/class/inheritable_attributes'
3
+ require 'active_support/core_ext/class/removal'
@@ -0,0 +1,43 @@
1
+ class Object
2
+ # Can you safely .dup this object?
3
+ # False for nil, false, true, symbols, and numbers; true otherwise.
4
+ def duplicable?
5
+ true
6
+ end
7
+ end
8
+
9
+ class NilClass #:nodoc:
10
+ def duplicable?
11
+ false
12
+ end
13
+ end
14
+
15
+ class FalseClass #:nodoc:
16
+ def duplicable?
17
+ false
18
+ end
19
+ end
20
+
21
+ class TrueClass #:nodoc:
22
+ def duplicable?
23
+ false
24
+ end
25
+ end
26
+
27
+ class Symbol #:nodoc:
28
+ def duplicable?
29
+ false
30
+ end
31
+ end
32
+
33
+ class Numeric #:nodoc:
34
+ def duplicable?
35
+ false
36
+ end
37
+ end
38
+
39
+ class Class #:nodoc:
40
+ def duplicable?
41
+ false
42
+ end
43
+ end
@@ -0,0 +1,72 @@
1
+ module Enumerable
2
+
3
+ # Calculates a sum from the elements. Examples:
4
+ #
5
+ # payments.sum { |p| p.price * p.tax_rate }
6
+ # payments.sum(&:price)
7
+ #
8
+ # The latter is a shortcut for:
9
+ #
10
+ # payments.inject { |sum, p| sum + p.price }
11
+ #
12
+ # It can also calculate the sum without the use of a block.
13
+ #
14
+ # [5, 15, 10].sum # => 30
15
+ # ["foo", "bar"].sum # => "foobar"
16
+ # [[1, 2], [3, 1, 5]].sum => [1, 2, 3, 1, 5]
17
+ #
18
+ # The default sum of an empty list is zero. You can override this default:
19
+ #
20
+ # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
21
+ #
22
+ def sum(identity = 0, &block)
23
+ return identity unless size > 0
24
+
25
+ if block_given?
26
+ map(&block).sum
27
+ else
28
+ inject { |sum, element| sum + element }
29
+ end
30
+ end
31
+
32
+ # Iterates over a collection, passing the current element *and* the
33
+ # +memo+ to the block. Handy for building up hashes or
34
+ # reducing collections down to one object. Examples:
35
+ #
36
+ # %w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } #=> {'foo' => 'FOO', 'bar' => 'BAR'}
37
+ #
38
+ # *Note* that you can't use immutable objects like numbers, true or false as
39
+ # the memo. You would think the following returns 120, but since the memo is
40
+ # never changed, it does not.
41
+ #
42
+ # (1..5).each_with_object(1) { |value, memo| memo *= value } # => 1
43
+ #
44
+ def each_with_object(memo, &block)
45
+ returning memo do |m|
46
+ each do |element|
47
+ block.call(element, m)
48
+ end
49
+ end
50
+ end unless [].respond_to?(:each_with_object)
51
+
52
+ # Convert an enumerable to a hash. Examples:
53
+ #
54
+ # people.index_by(&:login)
55
+ # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
56
+ # people.index_by { |person| "#{person.first_name} #{person.last_name}" }
57
+ # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
58
+ #
59
+ def index_by
60
+ inject({}) do |accum, elem|
61
+ accum[yield(elem)] = elem
62
+ accum
63
+ end
64
+ end
65
+
66
+ # Returns true if the collection has more than 1 element. Functionally equivalent to collection.size > 1.
67
+ # Works with a block too ala any?, so people.many? { |p| p.age > 26 } # => returns true if more than 1 person is over 26.
68
+ def many?(&block)
69
+ size = block_given? ? select(&block).size : self.size
70
+ size > 1
71
+ end
72
+ end
@@ -0,0 +1,259 @@
1
+ require 'date'
2
+ require 'cgi'
3
+ require 'builder'
4
+ require 'xmlsimple'
5
+
6
+ # Locked down XmlSimple#xml_in_string
7
+ class XmlSimple
8
+ # Same as xml_in but doesn't try to smartly shoot itself in the foot.
9
+ def xml_in_string(string, options = nil)
10
+ handle_options('in', options)
11
+
12
+ @doc = parse(string)
13
+ result = collapse(@doc.root)
14
+
15
+ if @options['keeproot']
16
+ merge({}, @doc.root.name, result)
17
+ else
18
+ result
19
+ end
20
+ end
21
+
22
+ def self.xml_in_string(string, options = nil)
23
+ new.xml_in_string(string, options)
24
+ end
25
+ end
26
+
27
+ # This module exists to decorate files deserialized using Hash.from_xml with
28
+ # the <tt>original_filename</tt> and <tt>content_type</tt> methods.
29
+ module FileLike #:nodoc:
30
+ attr_writer :original_filename, :content_type
31
+
32
+ def original_filename
33
+ @original_filename || 'untitled'
34
+ end
35
+
36
+ def content_type
37
+ @content_type || 'application/octet-stream'
38
+ end
39
+ end
40
+
41
+ module ActiveSupport #:nodoc:
42
+ module CoreExtensions #:nodoc:
43
+ module Hash #:nodoc:
44
+ module Conversions
45
+
46
+ XML_TYPE_NAMES = {
47
+ "Symbol" => "symbol",
48
+ "Fixnum" => "integer",
49
+ "Bignum" => "integer",
50
+ "BigDecimal" => "decimal",
51
+ "Float" => "float",
52
+ "Date" => "date",
53
+ "DateTime" => "datetime",
54
+ "Time" => "datetime",
55
+ "TrueClass" => "boolean",
56
+ "FalseClass" => "boolean"
57
+ } unless defined?(XML_TYPE_NAMES)
58
+
59
+ XML_FORMATTING = {
60
+ "symbol" => Proc.new { |symbol| symbol.to_s },
61
+ "date" => Proc.new { |date| date.to_s(:db) },
62
+ "datetime" => Proc.new { |time| time.xmlschema },
63
+ "binary" => Proc.new { |binary| ActiveSupport::Base64.encode64(binary) },
64
+ "yaml" => Proc.new { |yaml| yaml.to_yaml }
65
+ } unless defined?(XML_FORMATTING)
66
+
67
+ # TODO: use Time.xmlschema instead of Time.parse;
68
+ # use regexp instead of Date.parse
69
+ unless defined?(XML_PARSING)
70
+ XML_PARSING = {
71
+ "symbol" => Proc.new { |symbol| symbol.to_sym },
72
+ "date" => Proc.new { |date| ::Date.parse(date) },
73
+ "datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc },
74
+ "integer" => Proc.new { |integer| integer.to_i },
75
+ "float" => Proc.new { |float| float.to_f },
76
+ "decimal" => Proc.new { |number| BigDecimal(number) },
77
+ "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
78
+ "string" => Proc.new { |string| string.to_s },
79
+ "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
80
+ "base64Binary" => Proc.new { |bin| ActiveSupport::Base64.decode64(bin) },
81
+ "file" => Proc.new do |file, entity|
82
+ f = StringIO.new(ActiveSupport::Base64.decode64(file))
83
+ f.extend(FileLike)
84
+ f.original_filename = entity['name']
85
+ f.content_type = entity['content_type']
86
+ f
87
+ end
88
+ }
89
+
90
+ XML_PARSING.update(
91
+ "double" => XML_PARSING["float"],
92
+ "dateTime" => XML_PARSING["datetime"]
93
+ )
94
+ end
95
+
96
+ def self.included(klass)
97
+ klass.extend(ClassMethods)
98
+ end
99
+
100
+ # Converts a hash into a string suitable for use as a URL query string. An optional <tt>namespace</tt> can be
101
+ # passed to enclose the param names (see example below).
102
+ #
103
+ # ==== Example:
104
+ # { :name => 'David', :nationality => 'Danish' }.to_query # => "name=David&nationality=Danish"
105
+ #
106
+ # { :name => 'David', :nationality => 'Danish' }.to_query('user') # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
107
+ def to_query(namespace = nil)
108
+ collect do |key, value|
109
+ value.to_query(namespace ? "#{namespace}[#{key}]" : key)
110
+ end.sort * '&'
111
+ end
112
+
113
+ alias_method :to_param, :to_query
114
+
115
+ def to_xml(options = {})
116
+ options[:indent] ||= 2
117
+ options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
118
+ :root => "hash" })
119
+ options[:builder].instruct! unless options.delete(:skip_instruct)
120
+ dasherize = !options.has_key?(:dasherize) || options[:dasherize]
121
+ root = dasherize ? options[:root].to_s.dasherize : options[:root].to_s
122
+
123
+ options[:builder].__send__(:method_missing, root) do
124
+ each do |key, value|
125
+ case value
126
+ when ::Hash
127
+ value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
128
+ when ::Array
129
+ value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true}))
130
+ when ::Method, ::Proc
131
+ # If the Method or Proc takes two arguments, then
132
+ # pass the suggested child element name. This is
133
+ # used if the Method or Proc will be operating over
134
+ # multiple records and needs to create an containing
135
+ # element that will contain the objects being
136
+ # serialized.
137
+ if 1 == value.arity
138
+ value.call(options.merge({ :root => key, :skip_instruct => true }))
139
+ else
140
+ value.call(options.merge({ :root => key, :skip_instruct => true }), key.to_s.singularize)
141
+ end
142
+ else
143
+ if value.respond_to?(:to_xml)
144
+ value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
145
+ else
146
+ type_name = XML_TYPE_NAMES[value.class.name]
147
+
148
+ key = dasherize ? key.to_s.dasherize : key.to_s
149
+
150
+ attributes = options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name }
151
+ if value.nil?
152
+ attributes[:nil] = true
153
+ end
154
+
155
+ options[:builder].tag!(key,
156
+ XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
157
+ attributes
158
+ )
159
+ end
160
+ end
161
+ end
162
+
163
+ yield options[:builder] if block_given?
164
+ end
165
+
166
+ end
167
+
168
+ module ClassMethods
169
+ def from_xml(xml)
170
+ # TODO: Refactor this into something much cleaner that doesn't rely on XmlSimple
171
+ typecast_xml_value(undasherize_keys(XmlSimple.xml_in_string(xml,
172
+ 'forcearray' => false,
173
+ 'forcecontent' => true,
174
+ 'keeproot' => true,
175
+ 'contentkey' => '__content__')
176
+ ))
177
+ end
178
+
179
+ private
180
+ def typecast_xml_value(value)
181
+ case value.class.to_s
182
+ when 'Hash'
183
+ if value['type'] == 'array'
184
+ child_key, entries = value.detect { |k,v| k != 'type' } # child_key is throwaway
185
+ if entries.nil? || (c = value['__content__'] && c.blank?)
186
+ []
187
+ else
188
+ case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
189
+ when "Array"
190
+ entries.collect { |v| typecast_xml_value(v) }
191
+ when "Hash"
192
+ [typecast_xml_value(entries)]
193
+ else
194
+ raise "can't typecast #{entries.inspect}"
195
+ end
196
+ end
197
+ elsif value.has_key?("__content__")
198
+ content = value["__content__"]
199
+ if parser = XML_PARSING[value["type"]]
200
+ if parser.arity == 2
201
+ XML_PARSING[value["type"]].call(content, value)
202
+ else
203
+ XML_PARSING[value["type"]].call(content)
204
+ end
205
+ else
206
+ content
207
+ end
208
+ elsif value['type'] == 'string' && value['nil'] != 'true'
209
+ ""
210
+ # blank or nil parsed values are represented by nil
211
+ elsif value.blank? || value['nil'] == 'true'
212
+ nil
213
+ # If the type is the only element which makes it then
214
+ # this still makes the value nil, except if type is
215
+ # a XML node(where type['value'] is a Hash)
216
+ elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash)
217
+ nil
218
+ else
219
+ xml_value = value.inject({}) do |h,(k,v)|
220
+ h[k] = typecast_xml_value(v)
221
+ h
222
+ end
223
+
224
+ # Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
225
+ # how multipart uploaded files from HTML appear
226
+ xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
227
+ end
228
+ when 'Array'
229
+ value.map! { |i| typecast_xml_value(i) }
230
+ case value.length
231
+ when 0 then nil
232
+ when 1 then value.first
233
+ else value
234
+ end
235
+ when 'String'
236
+ value
237
+ else
238
+ raise "can't typecast #{value.class.name} - #{value.inspect}"
239
+ end
240
+ end
241
+
242
+ def undasherize_keys(params)
243
+ case params.class.to_s
244
+ when "Hash"
245
+ params.inject({}) do |h,(k,v)|
246
+ h[k.to_s.tr("-", "_")] = undasherize_keys(v)
247
+ h
248
+ end
249
+ when "Array"
250
+ params.map { |v| undasherize_keys(v) }
251
+ else
252
+ params
253
+ end
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end