aboutsummaryrefslogtreecommitdiffstats
path: root/activemodel/lib/active_model/serializers/xml.rb
blob: 7cdd28122311684aabaad7da4e8f54dc5d8715b1 (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
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::Attributes

      class Serializer < ActiveModel::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

        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.to_s.underscore).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 serializable_attributes
          serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) }
        end

        def serializable_method_attributes
          Array(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 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|
                proc.call(options)
              end
            end
          end
      end

      def to_xml(options = {}, &block)
        serializer = Serializer.new(self, options)
        block_given? ? serializer.to_s(&block) : serializer.to_s
      end

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