aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model/serializers/xml.rb
blob: c226359ea7d591fc0876fceb88fafc682258b485 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/hash/conversions'

module ActiveModel
  module Serializers
    module Xml
      extend ActiveSupport::Concern
      include ActiveModel::Serialization

      class Serializer #:nodoc:
        class Attribute #:nodoc:
          attr_reader :name, :value, :type

          def initialize(name, serializable)
            @name, @serializable = name, serializable
            @type  = compute_type
            @value = compute_value
          end

          # There is a significant speed improvement if the value
          # does not need to be escaped, as <tt>tag!</tt> escapes all values
          # to ensure that valid XML is generated. For known binary
          # values, it is at least an order of magnitude faster to
          # Base64 encode binary values and directly put them in the
          # output XML than to pass the original value or the Base64
          # encoded value to the <tt>tag!</tt> method. It definitely makes
          # no sense to Base64 encode the value and then give it to
          # <tt>tag!</tt>, since that just adds additional overhead.
          def needs_encoding?
            ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type)
          end

          def decorations(include_types = true)
            decorations = {}

            if type == :binary
              decorations[:encoding] = 'base64'
            end

            if include_types && type != :string
              decorations[:type] = type
            end

            if value.nil?
              decorations[:nil] = true
            end

            decorations
          end

          protected
            def compute_type
              value = @serializable.send(name)
              type = Hash::XML_TYPE_NAMES[value.class.name]
              type ||= :string if value.respond_to?(:to_str)
              type ||= :yaml
              type
            end

            def compute_value
              value = @serializable.send(name)

              if formatter = Hash::XML_FORMATTING[type.to_s]
                value ? formatter.call(value) : nil
              else
                value
              end
            end
        end

        class MethodAttribute < Attribute #:nodoc:
          protected
            def compute_type
              Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string
            end
        end

        attr_reader :options

        def initialize(serializable, options = nil)
          @serializable = serializable
          @options = options ? options.dup : {}

          @options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s }
          @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s }
        end

        # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> 
        # takes precedence over <tt>:only</tt>.  If <tt>:only</tt> is not set
        # for a N level model but is set for the N+1 level models,
        # then because <tt>:except</tt> is set to a default value, the second
        # level model can have both <tt>:except</tt> and <tt>:only</tt> set.  So if
        # <tt>:only</tt> is set, always delete <tt>:except</tt>.
        def serializable_attribute_names
          attribute_names = @serializable.attributes.keys.sort

          if options[:only].any?
            attribute_names &= options[:only]
          elsif options[:except].any?
            attribute_names -= options[:except]
          end

          attribute_names
        end

        def serializable_attributes
          serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) }
        end

        def serializable_method_attributes
          Array.wrap(options[:methods]).inject([]) do |methods, name|
            methods << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s)
            methods
          end
        end

        def serialize
          args = [root]

          if options[:namespace]
            args << {:xmlns => options[:namespace]}
          end

          if options[:type]
            args << {:type => options[:type]}
          end

          builder.tag!(*args) do
            add_attributes
            procs = options.delete(:procs)
            options[:procs] = procs
            add_procs
            yield builder if block_given?
          end
        end

        private
          def builder
            @builder ||= begin
              require 'builder' unless defined? ::Builder
              options[:indent] ||= 2
              builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])

              unless options[:skip_instruct]
                builder.instruct!
                options[:skip_instruct] = true
              end

              builder
            end
          end

          def root
            root = (options[:root] || @serializable.class.model_name.singular).to_s
            reformat_name(root)
          end

          def dasherize?
            !options.has_key?(:dasherize) || options[:dasherize]
          end

          def camelize?
            options.has_key?(:camelize) && options[:camelize]
          end

          def reformat_name(name)
            name = name.camelize if camelize?
            dasherize? ? name.dasherize : name
          end

          def add_attributes
            (serializable_attributes + serializable_method_attributes).each do |attribute|
              builder.tag!(
                reformat_name(attribute.name),
                attribute.value.to_s,
                attribute.decorations(!options[:skip_types])
              )
            end
          end

          def add_procs
            if procs = options.delete(:procs)
              [ *procs ].each do |proc|
                if proc.arity > 1
                  proc.call(options, @serializable)
                else
                  proc.call(options)
                end
              end
            end
          end
      end

      def to_xml(options = {}, &block)
        Serializer.new(self, options).serialize(&block)
      end

      def from_xml(xml)
        self.attributes = Hash.from_xml(xml).values.first
        self
      end
    end
  end
end