├── .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 | [![Build Status](https://api.travis-ci.org/rails/activemodel-serializers-xml.svg)](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 "", 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{^