├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── activemodel-serializers-xml.gemspec
├── lib
├── active_model
│ ├── serializers.rb
│ └── serializers
│ │ ├── version.rb
│ │ └── xml.rb
├── active_record
│ └── serializers
│ │ └── xml_serializer.rb
└── activemodel-serializers-xml.rb
└── test
├── active_model
└── xml_serialization_test.rb
├── active_record
└── xml_serialization_test.rb
├── fixtures
├── accounts.yml
├── authors.yml
├── companies.yml
├── posts.yml
├── projects.yml
└── topics.yml
├── helper.rb
└── models
├── arcontact.rb
├── author.rb
├── comment.rb
├── company.rb
├── company_in_module.rb
├── contact.rb
├── post.rb
├── project.rb
├── reply.rb
├── topic.rb
└── toy.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | sudo: false
3 | rvm:
4 | - 2.2.4
5 | - 2.3.0
6 | - ruby-head
7 | matrix:
8 | allow_failures:
9 | - rvm: ruby-head
10 | fast_finish: true
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing to ActiveModel::Serializers::Xml
2 | =====================
3 |
4 | [](https://travis-ci.org/rails/activemodel-serializers-xml)
5 |
6 | ActiveModel::Serializers::Xml is work of [many contributors](https://github.com/rails/activemodel-serializers-xml/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/rails/activemodel-serializers-xml/pulls), [propose features and discuss issues](https://github.com/rails/activemodel-serializers-xml/issues).
7 |
8 | #### Fork the Project
9 |
10 | Fork the [project on GitHub](https://github.com/rails/activemodel-serializers-xml) and check out your copy.
11 |
12 | ```
13 | git clone https://github.com/contributor/activemodel-serializers-xml.git
14 | cd activemodel-serializers-xml
15 | git remote add upstream https://github.com/rails/activemodel-serializers-xml.git
16 | ```
17 |
18 | #### Create a Topic Branch
19 |
20 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix.
21 |
22 | ```
23 | git checkout master
24 | git pull upstream master
25 | git checkout -b my-feature-branch
26 | ```
27 |
28 | #### Bundle Install and Test
29 |
30 | Ensure that you can build the project and run tests.
31 |
32 | ```
33 | bundle install
34 | bundle exec rake test
35 | ```
36 |
37 | #### Write Tests
38 |
39 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [test](test).
40 |
41 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix.
42 |
43 | #### Write Code
44 |
45 | Implement your feature or bug fix.
46 |
47 | Make sure that `bundle exec rake test` completes without errors.
48 |
49 | #### Write Documentation
50 |
51 | Document any external behavior in the [README](README.md).
52 |
53 | #### Commit Changes
54 |
55 | Make sure git knows your name and email address:
56 |
57 | ```
58 | git config --global user.name "Your Name"
59 | git config --global user.email "contributor@example.com"
60 | ```
61 |
62 | Writing good commit logs is important. A commit log should describe what changed and why.
63 |
64 | ```
65 | git add ...
66 | git commit
67 | ```
68 |
69 | #### Push
70 |
71 | ```
72 | git push origin my-feature-branch
73 | ```
74 |
75 | #### Make a Pull Request
76 |
77 | Go to https://github.com/contributor/activemodel-serializers-xml and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days.
78 |
79 | #### Rebase
80 |
81 | If you've been working on a change for a while, rebase with upstream/master.
82 |
83 | ```
84 | git fetch upstream
85 | git rebase upstream/master
86 | git push origin my-feature-branch -f
87 | ```
88 |
89 | #### Check on Your Pull Request
90 |
91 | Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above.
92 |
93 | #### Be Patient
94 |
95 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there!
96 |
97 | #### Thank You
98 |
99 | Please do know that we really appreciate and value your time and work. We love you, really.
100 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in activemodel-serializers-xml.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Zachary Scott
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ActiveModel::Serializers::Xml
2 |
3 | This gem provides XML serialization for your Active Model objects and Active Record models.
4 |
5 | ## Installation
6 |
7 | Add this line to your application's Gemfile:
8 |
9 | ```ruby
10 | gem 'activemodel-serializers-xml'
11 | ```
12 |
13 | And then execute:
14 |
15 | ```
16 | $ bundle
17 | ```
18 |
19 | Or install it yourself as:
20 |
21 | ```
22 | $ gem install activemodel-serializers-xml
23 | ```
24 |
25 | ## Usage
26 |
27 | ### ActiveModel::Serializers::Xml
28 |
29 | To use the `ActiveModel::Serializers::Xml` you only need to change from
30 | `ActiveModel::Serialization` to `ActiveModel::Serializers::Xml`.
31 |
32 | ```ruby
33 | class Person
34 | include ActiveModel::Serializers::Xml
35 |
36 | attr_accessor :name
37 |
38 | def attributes
39 | {'name' => nil}
40 | end
41 | end
42 | ```
43 |
44 | With the `to_xml` you have an XML representing the model.
45 |
46 | ```ruby
47 | person = Person.new
48 | person.to_xml # => "\n\n \n\n"
49 | person.name = "Bob"
50 | person.to_xml # => "\n\n Bob\n\n"
51 | ```
52 |
53 | From an XML string you define the attributes of the model.
54 | You need to have the `attributes=` method defined on your class:
55 |
56 | ```ruby
57 | class Person
58 | include ActiveModel::Serializers::Xml
59 |
60 | attr_accessor :name
61 |
62 | def attributes=(hash)
63 | hash.each do |key, value|
64 | send("#{key}=", value)
65 | end
66 | end
67 |
68 | def attributes
69 | {'name' => nil}
70 | end
71 | end
72 | ```
73 |
74 | Now it is possible to create an instance of person and set the attributes using `from_xml`.
75 |
76 | ```ruby
77 | xml = { name: 'Bob' }.to_xml
78 | person = Person.new
79 | person.from_xml(xml) # => #
80 | person.name # => "Bob"
81 | ```
82 |
83 | ### ActiveRecord::XmlSerializer
84 |
85 | This gem also provides serialization to XML for Active Record.
86 |
87 | Please see ActiveRecord::Serialization#to_xml for more information.
88 |
89 | ## Contributing to ActiveModel::Serializers::Xml
90 |
91 | ActiveModel::Serializers::Xml is work of many contributors. You're encouraged to submit pull requests, propose features and discuss issues.
92 |
93 | See [CONTRIBUTING](CONTRIBUTING.md)
94 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | Rake::TestTask.new do |t|
5 | t.libs << "test"
6 | t.test_files = FileList['test/**/*_test.rb']
7 | t.verbose = true
8 | end
9 |
10 | task :default => :test
11 |
--------------------------------------------------------------------------------
/activemodel-serializers-xml.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'active_model/serializers/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "activemodel-serializers-xml"
8 | spec.version = ActiveModel::Serializers::VERSION
9 | spec.authors = ["Rails team"]
10 | spec.email = ["security@rubyonrails.com"]
11 |
12 | spec.summary = "XML serialization for your Active Model objects and Active Record models - extracted from Rails"
13 | spec.homepage = "http://github.com/rails/activemodel-serializers-xml"
14 | spec.license = "MIT"
15 |
16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17 | spec.require_paths = ["lib"]
18 | spec.add_dependency "activesupport", ">= 5.0.0.a"
19 | spec.add_dependency "activemodel", ">= 5.0.0.a"
20 | spec.add_dependency "builder", "~> 3.1"
21 |
22 | spec.add_development_dependency "rake"
23 | spec.add_development_dependency "activerecord"
24 | spec.add_development_dependency "sqlite3"
25 | spec.add_development_dependency "rexml"
26 | end
27 |
--------------------------------------------------------------------------------
/lib/active_model/serializers.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'active_support/lazy_load_hooks'
3 | require 'active_model'
4 | require "active_model/serializers/version"
5 |
6 | ActiveSupport.on_load(:active_record) do
7 | require "active_record/serializers/xml_serializer"
8 | end
9 |
10 | module ActiveModel
11 | module Serializers
12 | extend ActiveSupport::Autoload
13 |
14 | eager_autoload do
15 | autoload :Xml
16 | end
17 |
18 | module EagerLoading
19 | def eager_load!
20 | super
21 | ActiveModel::Serializers.eager_load!
22 | end
23 | end
24 | end
25 |
26 | extend Serializers::EagerLoading
27 | end
28 |
--------------------------------------------------------------------------------
/lib/active_model/serializers/version.rb:
--------------------------------------------------------------------------------
1 | module ActiveModel
2 | module Serializers
3 | VERSION = "1.0.3"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/active_model/serializers/xml.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/deprecation'
2 | require 'active_support/core_ext/module/attribute_accessors'
3 | require 'active_support/core_ext/array/conversions'
4 | require 'active_support/core_ext/hash/conversions'
5 | require 'active_support/core_ext/hash/slice'
6 | require 'active_support/core_ext/time/acts_like'
7 |
8 | module ActiveModel
9 | module Serializers
10 | module Xml
11 | extend ActiveSupport::Concern
12 | include ActiveModel::Serialization
13 |
14 | included do
15 | extend ActiveModel::Naming
16 | end
17 |
18 | class Serializer #:nodoc:
19 | class Attribute #:nodoc:
20 | attr_reader :name, :value, :type
21 |
22 | def initialize(name, serializable, value)
23 | @name, @serializable = name, serializable
24 |
25 | if value.acts_like?(:time) && value.respond_to?(:in_time_zone)
26 | value = value.in_time_zone
27 | end
28 |
29 | @value = value
30 | @type = compute_type
31 | end
32 |
33 | def decorations
34 | decorations = {}
35 | decorations[:encoding] = 'base64' if type == :binary
36 | decorations[:type] = (type == :string) ? nil : type
37 | decorations[:nil] = true if value.nil?
38 | decorations
39 | end
40 |
41 | protected
42 |
43 | def compute_type
44 | return if value.nil?
45 | type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
46 | type ||= :string if value.respond_to?(:to_str)
47 | type ||= :yaml
48 | type
49 | end
50 | end
51 |
52 | class MethodAttribute < Attribute #:nodoc:
53 | end
54 |
55 | attr_reader :options
56 |
57 | def initialize(serializable, options = nil)
58 | @serializable = serializable
59 | @options = options ? options.dup : {}
60 | end
61 |
62 | def serializable_hash
63 | @serializable.serializable_hash(@options.except(:include))
64 | end
65 |
66 | def serializable_collection
67 | methods = Array(options[:methods]).map(&:to_s)
68 | serializable_hash.map do |name, value|
69 | name = name.to_s
70 | if methods.include?(name)
71 | self.class::MethodAttribute.new(name, @serializable, value)
72 | else
73 | self.class::Attribute.new(name, @serializable, value)
74 | end
75 | end
76 | end
77 |
78 | def serialize
79 | require 'builder' unless defined? ::Builder
80 |
81 | options[:indent] ||= 2
82 | options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
83 |
84 | @builder = options[:builder]
85 | @builder.instruct! unless options[:skip_instruct]
86 |
87 | root = (options[:root] || @serializable.model_name.element).to_s
88 | root = ActiveSupport::XmlMini.rename_key(root, options)
89 |
90 | args = [root]
91 | args << { xmlns: options[:namespace] } if options[:namespace]
92 | args << { type: options[:type] } if options[:type] && !options[:skip_types]
93 |
94 | @builder.tag!(*args) do
95 | add_attributes_and_methods
96 | add_includes
97 | add_extra_behavior
98 | add_procs
99 | yield @builder if block_given?
100 | end
101 | end
102 |
103 | private
104 |
105 | def add_extra_behavior
106 | end
107 |
108 | def add_attributes_and_methods
109 | serializable_collection.each do |attribute|
110 | _options = options.except(:methods)
111 | key = ActiveSupport::XmlMini.rename_key(attribute.name, _options)
112 | ActiveSupport::XmlMini.to_tag(key, attribute.value,
113 | _options.merge(attribute.decorations))
114 | end
115 | end
116 |
117 | def add_includes
118 | @serializable.send(:serializable_add_includes, options) do |association, records, opts|
119 | add_associations(association, records, opts)
120 | end
121 | end
122 |
123 | # TODO: This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
124 | def add_associations(association, records, opts)
125 | merged_options = opts.merge(options.slice(:builder, :indent))
126 | merged_options[:skip_instruct] = true
127 |
128 | [:skip_types, :dasherize, :camelize].each do |key|
129 | merged_options[key] = options[key] if merged_options[key].nil? && !options[key].nil?
130 | end
131 |
132 | if records.respond_to?(:to_ary)
133 | records = records.to_ary
134 |
135 | tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
136 | type = options[:skip_types] ? { } : { type: "array" }
137 | association_name = association.to_s.singularize
138 | merged_options[:root] = association_name
139 |
140 | if records.empty?
141 | @builder.tag!(tag, type)
142 | else
143 | @builder.tag!(tag, type) do
144 | records.each do |record|
145 | if options[:skip_types]
146 | record_type = {}
147 | else
148 | record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
149 | record_type = { type: record_class }
150 | end
151 |
152 | record.to_xml merged_options.merge(record_type)
153 | end
154 | end
155 | end
156 | else
157 | merged_options[:root] = association.to_s
158 |
159 | unless records.class.to_s.underscore == association.to_s
160 | merged_options[:type] = records.class.name
161 | end
162 |
163 | records.to_xml merged_options
164 | end
165 | end
166 |
167 | def add_procs
168 | if procs = options.delete(:procs)
169 | Array(procs).each do |proc|
170 | if proc.arity == 1
171 | proc.call(options)
172 | else
173 | proc.call(options, @serializable)
174 | end
175 | end
176 | end
177 | end
178 | end
179 |
180 | # Returns XML representing the model. Configuration can be
181 | # passed through +options+.
182 | #
183 | # Without any +options+, the returned XML string will include all the
184 | # model's attributes.
185 | #
186 | # user = User.find(1)
187 | # user.to_xml
188 | #
189 | #
190 | #
191 | # 1
192 | # David
193 | # 16
194 | # 2011-01-30T22:29:23Z
195 | #
196 | #
197 | # The :only and :except options can be used to limit the
198 | # attributes included, and work similar to the +attributes+ method.
199 | #
200 | # To include the result of some method calls on the model use :methods.
201 | #
202 | # To include associations use :include.
203 | #
204 | # For further documentation, see ActiveRecord::Serialization#to_xml
205 | def to_xml(options = {}, &block)
206 | Serializer.new(self, options).serialize(&block)
207 | end
208 |
209 | # Sets the model +attributes+ from an XML string. Returns +self+.
210 | #
211 | # class Person
212 | # include ActiveModel::Serializers::Xml
213 | #
214 | # attr_accessor :name, :age, :awesome
215 | #
216 | # def attributes=(hash)
217 | # hash.each do |key, value|
218 | # instance_variable_set("@#{key}", value)
219 | # end
220 | # end
221 | #
222 | # def attributes
223 | # instance_values
224 | # end
225 | # end
226 | #
227 | # xml = { name: 'bob', age: 22, awesome:true }.to_xml
228 | # person = Person.new
229 | # person.from_xml(xml) # => #
230 | # person.name # => "bob"
231 | # person.age # => 22
232 | # person.awesome # => true
233 | def from_xml(xml)
234 | self.attributes = Hash.from_xml(xml).values.first
235 | self
236 | end
237 | end
238 | end
239 | end
240 |
--------------------------------------------------------------------------------
/lib/active_record/serializers/xml_serializer.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/hash/conversions'
2 | require 'active_model/serializers/xml'
3 |
4 | module ActiveRecord #:nodoc:
5 | module Serialization
6 | include ActiveModel::Serializers::Xml
7 |
8 | # Builds an XML document to represent the model. Some configuration is
9 | # available through +options+. However more complicated cases should
10 | # override ActiveRecord::Base#to_xml.
11 | #
12 | # By default the generated XML document will include the processing
13 | # instruction and all the object's attributes. For example:
14 | #
15 | #
16 | #
17 | # The First Topic
18 | # David
19 | # 1
20 | # false
21 | # 0
22 | # 2000-01-01T08:28:00+12:00
23 | # 2003-07-16T09:28:00+1200
24 | # Have a nice day
25 | # david@loudthinking.com
26 | #
27 | # 2004-04-15
28 | #
29 | #
30 | # This behavior can be controlled with :only, :except,
31 | # :skip_instruct, :skip_types, :dasherize and :camelize .
32 | # The :only and :except options are the same as for the
33 | # +attributes+ method. The default is to dasherize all column names, but you
34 | # can disable this setting :dasherize to +false+. Setting :camelize
35 | # to +true+ will camelize all column names - this also overrides :dasherize.
36 | # To not have the column type included in the XML output set :skip_types to +true+.
37 | #
38 | # For instance:
39 | #
40 | # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
41 | #
42 | #
43 | # The First Topic
44 | # David
45 | # false
46 | # Have a nice day
47 | # david@loudthinking.com
48 | #
49 | # 2004-04-15
50 | #
51 | #
52 | # To include first level associations use :include:
53 | #
54 | # firm.to_xml include: [ :account, :clients ]
55 | #
56 | #
57 | #
58 | # 1
59 | # 1
60 | # 37signals
61 | #
62 | #
63 | # 1
64 | # Summit
65 | #
66 | #
67 | # 1
68 | # Microsoft
69 | #
70 | #
71 | #
72 | # 1
73 | # 50
74 | #
75 | #
76 | #
77 | # Additionally, the record being serialized will be passed to a Proc's second
78 | # parameter. This allows for ad hoc additions to the resultant document that
79 | # incorporate the context of the record being serialized. And by leveraging the
80 | # closure created by a Proc, to_xml can be used to add elements that normally fall
81 | # outside of the scope of the model -- for example, generating and appending URLs
82 | # associated with models.
83 | #
84 | # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
85 | # firm.to_xml procs: [ proc ]
86 | #
87 | #
88 | # # ... normal attributes as shown above ...
89 | # slangis73
90 | #
91 | #
92 | # To include deeper levels of associations pass a hash like this:
93 | #
94 | # firm.to_xml include: {account: {}, clients: {include: :address}}
95 | #
96 | #
97 | # 1
98 | # 1
99 | # 37signals
100 | #
101 | #
102 | # 1
103 | # Summit
104 | #
105 | # ...
106 | #
107 | #
108 | #
109 | # 1
110 | # Microsoft
111 | #
112 | # ...
113 | #
114 | #
115 | #
116 | #
117 | # 1
118 | # 50
119 | #
120 | #
121 | #
122 | # To include any methods on the model being called use :methods:
123 | #
124 | # firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
125 | #
126 | #
127 | # # ... normal attributes as shown above ...
128 | # 100000000000000000
129 | # 5
130 | #
131 | #
132 | # To call any additional Procs use :procs. The Procs are passed a
133 | # modified version of the options hash that was given to +to_xml+:
134 | #
135 | # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
136 | # firm.to_xml procs: [ proc ]
137 | #
138 | #
139 | # # ... normal attributes as shown above ...
140 | # def
141 | #
142 | #
143 | # Alternatively, you can yield the builder object as part of the +to_xml+ call:
144 | #
145 | # firm.to_xml do |xml|
146 | # xml.creator do
147 | # xml.first_name "David"
148 | # xml.last_name "Heinemeier Hansson"
149 | # end
150 | # end
151 | #
152 | #
153 | # # ... normal attributes as shown above ...
154 | #
155 | # David
156 | # Heinemeier Hansson
157 | #
158 | #
159 | #
160 | # As noted above, you may override +to_xml+ in your ActiveRecord::Base
161 | # subclasses to have complete control about what's generated. The general
162 | # form of doing this is:
163 | #
164 | # class IHaveMyOwnXML < ActiveRecord::Base
165 | # def to_xml(options = {})
166 | # require 'builder'
167 | # options[:indent] ||= 2
168 | # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
169 | # xml.instruct! unless options[:skip_instruct]
170 | # xml.level_one do
171 | # xml.tag!(:second_level, 'content')
172 | # end
173 | # end
174 | # end
175 | def to_xml(options = {}, &block)
176 | XmlSerializer.new(self, options).serialize(&block)
177 | end
178 | end
179 |
180 | class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
181 | class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
182 | def compute_type
183 | klass = @serializable.class
184 | cast_type = klass.type_for_attribute(name)
185 |
186 | type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type
187 |
188 | { :text => :string,
189 | :time => :datetime }[type] || type
190 | end
191 | protected :compute_type
192 | end
193 | end
194 | end
195 |
--------------------------------------------------------------------------------
/lib/activemodel-serializers-xml.rb:
--------------------------------------------------------------------------------
1 | require 'active_model/serializers'
2 |
--------------------------------------------------------------------------------
/test/active_model/xml_serialization_test.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'models/contact'
3 | require 'active_support/core_ext/object/instance_variables'
4 | require 'ostruct'
5 | require 'yaml'
6 |
7 | module Admin
8 | class Contact < ::Contact
9 | end
10 | end
11 |
12 | class Customer < Struct.new(:name)
13 | end
14 |
15 | class Address
16 | include ActiveModel::Serializers::Xml
17 |
18 | attr_accessor :street, :city, :state, :zip, :apt_number
19 |
20 | def attributes
21 | instance_values
22 | end
23 | end
24 |
25 | class SerializableContact < Contact
26 | def serializable_hash(options={})
27 | super(options.merge(only: [:name, :age]))
28 | end
29 | end
30 |
31 | class Role
32 | include ActiveModel::Serializers::Xml
33 | attr_accessor :title
34 |
35 | def initialize(title)
36 | @title = title
37 | end
38 |
39 | def attributes
40 | instance_values
41 | end
42 | end
43 |
44 | class Human
45 | include ActiveModel::Serializers::Xml
46 |
47 | attr_accessor :first_name, :last_name, :role
48 |
49 | def initialize(first_name, last_name, role)
50 | @first_name = first_name
51 | @last_name = last_name
52 | @role = role
53 | end
54 |
55 | def full_name
56 | first_name + ' ' + last_name
57 | end
58 |
59 | def attributes
60 | instance_values
61 | end
62 | end
63 |
64 | class AMXmlSerializationTest < ActiveSupport::TestCase
65 | def setup
66 | @contact = Contact.new
67 | @contact.name = 'aaron stack'
68 | @contact.age = 25
69 | @contact.created_at = Time.utc(2006, 8, 1)
70 | @contact.awesome = false
71 | customer = Customer.new
72 | customer.name = "John"
73 | @contact.preferences = customer
74 | @contact.address = Address.new
75 | @contact.address.city = "Springfield"
76 | @contact.address.apt_number = 35
77 | @contact.friends = [Contact.new, Contact.new]
78 | @contact.contact = SerializableContact.new
79 | end
80 |
81 | test "should serialize default root" do
82 | xml = @contact.to_xml
83 | assert_match %r{^}, xml
84 | assert_match %r{$}, xml
85 | end
86 |
87 | test "should serialize namespaced root" do
88 | xml = Admin::Contact.new(@contact.attributes).to_xml
89 | assert_match %r{^}, xml
90 | assert_match %r{$}, xml
91 | end
92 |
93 | test "should serialize default root with namespace" do
94 | xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact"
95 | assert_match %r{^}, xml
96 | assert_match %r{$}, xml
97 | end
98 |
99 | test "should serialize custom root" do
100 | xml = @contact.to_xml root: 'xml_contact'
101 | assert_match %r{^}, xml
102 | assert_match %r{$}, xml
103 | end
104 |
105 | test "should allow undasherized tags" do
106 | xml = @contact.to_xml root: 'xml_contact', dasherize: false
107 | assert_match %r{^}, xml
108 | assert_match %r{$}, xml
109 | assert_match %r{}, xml
115 | assert_match %r{$}, xml
116 | assert_match %r{}, xml
122 | assert_match %r{$}, xml
123 | assert_match %r{aaron stack}, xml
133 | assert_match %r{25}, xml
134 | assert_no_match %r{}, xml
135 | end
136 |
137 | test "should allow skipped types" do
138 | xml = @contact.to_xml skip_types: true
139 | assert_match %r{25}, xml
140 | end
141 |
142 | test "should include yielded additions" do
143 | xml_output = @contact.to_xml do |xml|
144 | xml.creator "David"
145 | end
146 | assert_match %r{David}, xml_output
147 | end
148 |
149 | test "should serialize string" do
150 | assert_match %r{aaron stack}, @contact.to_xml
151 | end
152 |
153 | test "should serialize nil" do
154 | assert_match %r{}, @contact.to_xml(methods: :pseudonyms)
155 | end
156 |
157 | test "should serialize integer" do
158 | assert_match %r{25}, @contact.to_xml
159 | end
160 |
161 | test "should serialize datetime" do
162 | assert_match %r{2006-08-01T00:00:00Z}, @contact.to_xml
163 | end
164 |
165 | test "should serialize boolean" do
166 | assert_match %r{false}, @contact.to_xml
167 | end
168 |
169 | test "should serialize array" do
170 | assert_match %r{\s*twitter\s*github\s*}, @contact.to_xml(methods: :social)
171 | end
172 |
173 | test "should serialize hash" do
174 | assert_match %r{\s*github\s*}, @contact.to_xml(methods: :network)
175 | end
176 |
177 | test "should serialize yaml" do
178 | assert_match %r{--- !ruby/struct:Customer(\s*)\nname: John\n}, @contact.to_xml
179 | end
180 |
181 | test "should call proc on object" do
182 | proc = Proc.new { |options| options[:builder].tag!('nationality', 'unknown') }
183 | xml = @contact.to_xml(procs: [ proc ])
184 | assert_match %r{unknown}, xml
185 | end
186 |
187 | test "should supply serializable to second proc argument" do
188 | proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
189 | xml = @contact.to_xml(procs: [ proc ])
190 | assert_match %r{kcats noraa}, xml
191 | end
192 |
193 | test "should serialize string correctly when type passed" do
194 | xml = @contact.to_xml type: 'Contact'
195 | assert_match %r{}, xml
196 | assert_match %r{aaron stack}, xml
197 | end
198 |
199 | test "include option with singular association" do
200 | xml = @contact.to_xml include: :address, indent: 0
201 | assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true))
202 | end
203 |
204 | test "include option with plural association" do
205 | xml = @contact.to_xml include: :friends, indent: 0
206 | assert_match %r{}, xml
207 | assert_match %r{}, xml
208 | end
209 |
210 | class FriendList
211 | def initialize(friends)
212 | @friends = friends
213 | end
214 |
215 | def to_ary
216 | @friends
217 | end
218 | end
219 |
220 | test "include option with ary" do
221 | @contact.friends = FriendList.new(@contact.friends)
222 | xml = @contact.to_xml include: :friends, indent: 0
223 | assert_match %r{}, xml
224 | assert_match %r{}, xml
225 | end
226 |
227 | test "multiple includes" do
228 | xml = @contact.to_xml indent: 0, skip_instruct: true, include: [ :address, :friends ]
229 | assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true))
230 | assert_match %r{}, xml
231 | assert_match %r{}, xml
232 | end
233 |
234 | test "include with options" do
235 | xml = @contact.to_xml indent: 0, skip_instruct: true, include: { address: { only: :city } }
236 | assert xml.include?(%(>Springfield))
237 | end
238 |
239 | test "propagates skip_types option to included associations" do
240 | xml = @contact.to_xml include: :friends, indent: 0, skip_types: true
241 | assert_match %r{}, xml
242 | assert_match %r{}, xml
243 | end
244 |
245 | test "propagates skip-types option to included associations and attributes" do
246 | xml = @contact.to_xml skip_types: true, include: :address, indent: 0
247 | assert_match %r{}, xml
248 | assert_match %r{}, xml
249 | end
250 |
251 | test "propagates camelize option to included associations and attributes" do
252 | xml = @contact.to_xml camelize: true, include: :address, indent: 0
253 | assert_match %r{}, xml
254 | assert_match %r{}, xml
255 | end
256 |
257 | test "propagates dasherize option to included associations and attributes" do
258 | xml = @contact.to_xml dasherize: false, include: :address, indent: 0
259 | assert_match %r{}, xml
260 | end
261 |
262 | test "don't propagate skip_types if skip_types is defined at the included association level" do
263 | xml = @contact.to_xml skip_types: true, include: { address: { skip_types: false } }, indent: 0
264 | assert_match %r{}, xml
265 | assert_match %r{}, xml
266 | end
267 |
268 | test "don't propagate camelize if camelize is defined at the included association level" do
269 | xml = @contact.to_xml camelize: true, include: { address: { camelize: false } }, indent: 0
270 | assert_match %r{}, xml
271 | assert_match %r{}, xml
272 | end
273 |
274 | test "don't propagate dasherize if dasherize is defined at the included association level" do
275 | xml = @contact.to_xml dasherize: false, include: { address: { dasherize: true } }, indent: 0
276 | assert_match %r{}, xml
277 | assert_match %r{}, xml
278 | end
279 |
280 | test "association with sti" do
281 | xml = @contact.to_xml(include: :contact)
282 | assert xml.include?(%())
283 | end
284 |
285 | test "computed property applies only to root" do
286 | role = Role.new("manager")
287 | human = Human.new("Jane", "Air", role)
288 | xml = human.to_xml(methods: :full_name)
289 | assert_match %r{}, xml
290 | end
291 | end
292 |
--------------------------------------------------------------------------------
/test/active_record/xml_serialization_test.rb:
--------------------------------------------------------------------------------
1 | require "helper"
2 | require "rexml/document"
3 | require 'models/arcontact'
4 | require 'models/post'
5 | require 'models/author'
6 | require 'models/comment'
7 | require 'models/company_in_module'
8 | require 'models/toy'
9 | require 'models/topic'
10 | require 'models/reply'
11 | require 'models/company'
12 |
13 | class ARXmlSerializationTest < ActiveRecord::TestCase
14 | def test_should_serialize_default_root
15 | @xml = ARContact.new.to_xml
16 | assert_match %r{^}, @xml
17 | assert_match %r{$}, @xml
18 | end
19 |
20 | def test_should_serialize_default_root_with_namespace
21 | @xml = ARContact.new.to_xml :namespace=>"http://xml.rubyonrails.org/ar-contact"
22 | assert_match %r{^}, @xml
23 | assert_match %r{$}, @xml
24 | end
25 |
26 | def test_should_serialize_custom_root
27 | @xml = ARContact.new.to_xml :root => 'xml_contact'
28 | assert_match %r{^}, @xml
29 | assert_match %r{$}, @xml
30 | end
31 |
32 | def test_should_allow_undasherized_tags
33 | @xml = ARContact.new.to_xml :root => 'xml_contact', :dasherize => false
34 | assert_match %r{^}, @xml
35 | assert_match %r{$}, @xml
36 | assert_match %r{ 'xml_contact', :camelize => true
41 | assert_match %r{^}, @xml
42 | assert_match %r{$}, @xml
43 | assert_match %r{ 25).to_xml :skip_types => true
48 | assert %r{25}.match(@xml)
49 | end
50 |
51 | def test_should_include_yielded_additions
52 | @xml = ARContact.new.to_xml do |xml|
53 | xml.creator "David"
54 | end
55 | assert_match %r{David}, @xml
56 | end
57 |
58 | def test_to_xml_with_block
59 | value = "Rockin' the block"
60 | xml = ARContact.new.to_xml(:skip_instruct => true) do |_xml|
61 | _xml.tag! "arbitrary-element", value
62 | end
63 | assert_equal "", xml.first(12)
64 | assert xml.include?(%(#{value}))
65 | end
66 |
67 | def test_should_skip_instruct_for_included_records
68 | @contact = ARContact.new
69 | @contact.alternative = ARContact.new(:name => 'Copa Cabana')
70 | @xml = @contact.to_xml(:include => [ :alternative ])
71 | assert_equal @xml.index(' 'aaron stack',
80 | :age => 25,
81 | :avatar => 'binarydata',
82 | :created_at => Time.utc(2006, 8, 1),
83 | :awesome => false,
84 | :preferences => { :gem => 'ruby' }
85 | )
86 | end
87 |
88 | def test_should_serialize_string
89 | assert_match %r{aaron stack}, @contact.to_xml
90 | end
91 |
92 | def test_should_serialize_integer
93 | assert_match %r{25}, @contact.to_xml
94 | end
95 |
96 | def test_should_serialize_binary
97 | xml = @contact.to_xml
98 | assert_match %r{YmluYXJ5ZGF0YQ==\n}, xml
99 | assert_match %r{2006-08-01T00:00:00Z}, @contact.to_xml
105 | end
106 |
107 | def test_should_serialize_boolean
108 | assert_match %r{false}, @contact.to_xml
109 | end
110 |
111 | def test_should_serialize_hash
112 | assert_match %r{\s*ruby\s*}m, @contact.to_xml
113 | end
114 |
115 | def test_uses_serializable_hash_with_only_option
116 | def @contact.serializable_hash(options=nil)
117 | super(only: %w(name))
118 | end
119 |
120 | xml = @contact.to_xml
121 | assert_match %r{aaron stack}, xml
122 | assert_no_match %r{age}, xml
123 | assert_no_match %r{awesome}, xml
124 | end
125 |
126 | def test_uses_serializable_hash_with_except_option
127 | def @contact.serializable_hash(options=nil)
128 | super(except: %w(age))
129 | end
130 |
131 | xml = @contact.to_xml
132 | assert_match %r{aaron stack}, xml
133 | assert_match %r{false}, xml
134 | assert_no_match %r{age}, xml
135 | end
136 |
137 | def test_does_not_include_inheritance_column_from_sti
138 | @contact = ContactSti.new(@contact.attributes)
139 | assert_equal 'ContactSti', @contact.type
140 |
141 | xml = @contact.to_xml
142 | assert_match %r{aaron stack}, xml
143 | assert_no_match %r{aaron stack}, xml
157 | assert_no_match %r{age}, xml
158 | assert_no_match %r{ 'Mickey', :updated_at => Time.utc(2006, 8, 1))
167 | assert_match %r{2006-07-31T17:00:00-07:00}, toy.to_xml
168 | end
169 | end
170 |
171 | def test_should_serialize_datetime_with_timezone_reloaded
172 | with_timezone_config zone: "Pacific Time (US & Canada)" do
173 | contact = ARContact.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
174 | assert_match %r{2006-07-31T17:00:00-07:00}, contact.to_xml
175 | end
176 | end
177 | end
178 |
179 | class NilXmlSerializationTest < ActiveRecord::TestCase
180 | def setup
181 | @xml = ARContact.new.to_xml(:root => 'xml_contact')
182 | end
183 |
184 | def test_should_serialize_string
185 | assert_match %r{}, @xml
186 | end
187 |
188 | def test_should_serialize_integer
189 | assert %r{}.match(@xml)
190 | attributes = $1
191 | assert_match %r{nil="true"}, attributes
192 | assert_match %r{type="integer"}, attributes
193 | end
194 |
195 | def test_should_serialize_binary
196 | assert %r{}.match(@xml)
197 | attributes = $1
198 | assert_match %r{type="binary"}, attributes
199 | assert_match %r{encoding="base64"}, attributes
200 | assert_match %r{nil="true"}, attributes
201 | end
202 |
203 | def test_should_serialize_datetime
204 | assert %r{}.match(@xml)
205 | attributes = $1
206 | assert_match %r{nil="true"}, attributes
207 | assert_match %r{type="dateTime"}, attributes
208 | end
209 |
210 | def test_should_serialize_boolean
211 | assert %r{}.match(@xml)
212 | attributes = $1
213 | assert_match %r{type="boolean"}, attributes
214 | assert_match %r{nil="true"}, attributes
215 | end
216 |
217 | def test_should_serialize_yaml
218 | assert_match %r{}, @xml
219 | end
220 | end
221 |
222 | class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
223 | fixtures :topics, :companies, :accounts, :authors, :posts, :projects
224 |
225 | def test_to_xml
226 | xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
227 | bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
228 | written_on_in_current_timezone = topics(:first).written_on.xmlschema
229 |
230 | assert_equal "topic", xml.root.name
231 | assert_equal "The First Topic" , xml.elements["//title"].text
232 | assert_equal "David" , xml.elements["//author-name"].text
233 | assert_match "Have a nice day", xml.elements["//content"].text
234 |
235 | assert_equal "1", xml.elements["//id"].text
236 | assert_equal "integer" , xml.elements["//id"].attributes['type']
237 |
238 | assert_equal "1", xml.elements["//replies-count"].text
239 | assert_equal "integer" , xml.elements["//replies-count"].attributes['type']
240 |
241 | assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text
242 | assert_equal "dateTime" , xml.elements["//written-on"].attributes['type']
243 |
244 | assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text
245 |
246 | assert_equal nil, xml.elements["//parent-id"].text
247 | assert_equal "integer", xml.elements["//parent-id"].attributes['type']
248 | assert_equal "true", xml.elements["//parent-id"].attributes['nil']
249 |
250 | # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
251 | assert_equal "2004-04-15", xml.elements["//last-read"].text
252 | assert_equal "date" , xml.elements["//last-read"].attributes['type']
253 |
254 | assert_equal "false", xml.elements["//approved"].text
255 | assert_equal "boolean" , xml.elements["//approved"].attributes['type']
256 |
257 | assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text
258 | assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type']
259 | end
260 |
261 | def test_except_option
262 | xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count])
263 | assert_equal "", xml.first(7)
264 | assert !xml.include?(%(The First Topic))
265 | assert xml.include?(%(David))
266 |
267 | xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count])
268 | assert !xml.include?(%(The First Topic))
269 | assert !xml.include?(%(David))
270 | end
271 |
272 | # to_xml used to mess with the hash the user provided which
273 | # caused the builder to be reused. This meant the document kept
274 | # getting appended to.
275 |
276 | def test_modules
277 | projects = MyApplication::Business::Project.all
278 | xml = projects.to_xml
279 | root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize
280 | assert_match "<#{root} type=\"array\">", xml
281 | assert_match "#{root}>", xml
282 | end
283 |
284 | def test_passing_hash_shouldnt_reuse_builder
285 | options = {:include=>:posts}
286 | david = authors(:david)
287 | first_xml_size = david.to_xml(options).size
288 | second_xml_size = david.to_xml(options).size
289 | assert_equal first_xml_size, second_xml_size
290 | end
291 |
292 | def test_include_uses_association_name
293 | xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0
294 | assert_match %r{}, xml
295 | assert_match %r{}, xml
296 | assert_match %r{}, xml
297 | end
298 |
299 | def test_included_associations_should_skip_types
300 | xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true
301 | assert_match %r{}, xml
302 | assert_match %r{}, xml
303 | assert_match %r{}, xml
304 | end
305 |
306 | def test_including_has_many_association
307 | xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count)
308 | assert_equal "", xml.first(7)
309 | assert xml.include?(%())
310 | assert xml.include?(%(The Second Topic of the day))
311 | end
312 |
313 | def test_including_belongs_to_association
314 | xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
315 | assert !xml.include?("")
316 |
317 | xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm)
318 | assert xml.include?("")
319 | end
320 |
321 | def test_including_multiple_associations
322 | xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ])
323 | assert_equal "", xml.first(6)
324 | assert xml.include?(%())
325 | assert xml.include?(%())
326 | end
327 |
328 | def test_including_association_with_options
329 | xml = companies(:first_firm).to_xml(
330 | :indent => 0, :skip_instruct => true,
331 | :include => { :clients => { :only => :name } }
332 | )
333 |
334 | assert_equal "", xml.first(6)
335 | assert xml.include?(%(Summit))
336 | assert xml.include?(%())
337 | end
338 |
339 | def test_methods_are_called_on_object
340 | xml = authors(:david).to_xml :methods => :label, :indent => 0
341 | assert_match %r{}, xml
342 | end
343 |
344 | def test_should_not_call_methods_on_associations_that_dont_respond
345 | xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2
346 | assert !authors(:david).hello_posts.first.respond_to?(:label)
347 | assert_match %r{^ }, xml
348 | assert_no_match %r{^