├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── gyoku.gemspec ├── lib ├── gyoku.rb └── gyoku │ ├── array.rb │ ├── hash.rb │ ├── prettifier.rb │ ├── version.rb │ ├── xml_key.rb │ └── xml_value.rb └── spec ├── gyoku ├── array_spec.rb ├── hash_spec.rb ├── prettifier_spec.rb ├── xml_key_spec.rb └── xml_value_spec.rb ├── gyoku_spec.rb └── spec_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | test: 13 | name: Ruby ${{ matrix.ruby }} 14 | 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | ruby: 19 | - '3.2' 20 | - '3.1' 21 | - '3.0' 22 | - 'jruby' 23 | 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v3 29 | 30 | - name: Install Ruby ${{ matrix.ruby }} 31 | uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{ matrix.ruby }} 34 | bundler-cache: true # 'bundle install' and cache 35 | 36 | - name: Run tests 37 | run: bundle exec rake --trace 38 | continue-on-error: false 39 | 40 | coveralls: 41 | name: Coveralls 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v3 46 | 47 | - name: Set up Ruby 48 | uses: ruby/setup-ruby@v1 49 | with: 50 | ruby-version: 3.2 51 | bundler-cache: true 52 | 53 | - name: Install dependencies 54 | run: bundle install 55 | 56 | - name: Run tests 57 | run: bundle exec rake 58 | 59 | - name: Report coverage 60 | uses: coverallsapp/github-action@v2 61 | with: 62 | flag-name: ruby-3.2 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .yardoc 3 | doc 4 | coverage 5 | tmp 6 | *~ 7 | *.swp 8 | *.gem 9 | .bundle 10 | Gemfile.lock 11 | .rvmrc 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.4.0 (2022-04-01) 4 | 5 | ### Fixed 6 | 7 | - Fix [Issue #56](https://github.com/savonrb/gyoku/issues/56) with PR [#57](https://github.com/savonrb/gyoku/pull/57). Thanks, [@jpmoral]! 8 | - Avoid circular reference [#69](https://github.com/savonrb/gyoku/pull/69), thanks [@ccarruitero]! 9 | 10 | ### Added 11 | 12 | - Unwrap specific keys [#54](https://github.com/savonrb/gyoku/pull/54), by [@rlburkes]. Documented by [@mahemoff]. Thanks to you both! 13 | - Add `:pretty_print`, `:indent` and `:compact` options to allow prettified XML output. [#59](https://github.com/savonrb/gyoku/pull/59), by [@Jeiwan]. Thanks! 14 | 15 | ### Changed 16 | 17 | - Removed Rubinius support, by [@olleolleolle] 18 | - Clean-up, CI setup, and changelog authoring, by [@olleolleolle] 19 | 20 | [@jpmoral]: https://github.com/jpmoral 21 | [@ccarruitero]: https://github.com/ccarruitero 22 | [@rlburkes]: https://github.com/rlburkes 23 | [@mahemoff]: https://github.com/mahemoff 24 | [@Jeiwan]: https://github.com/Jeiwan 25 | [@olleolleolle]: https://github.com/olleolleolle 26 | 27 | ## 1.3.1 (2015-04-05) 28 | 29 | * Feature: [#53](https://github.com/savonrb/gyoku/pull/53) Improved serialization of hashes nested in arrays. Thanks to @riburkes for this! 30 | 31 | ## 1.3.0 (2015-03-30) 32 | 33 | * Formally drop support for ruby 1.8.7 34 | 35 | ## 1.2.3 (2015-03-10) 36 | 37 | * Feature: [#52](https://github.com/savonrb/gyoku/pull/52) Adds an :unwrap option that allows an array of hashes to be unwrapped into a single array xml node, rather than one per hash. 38 | 39 | ## 1.2.2 (2014-09-22) 40 | 41 | * Fixed a bug introduced by making Gyoku threadsafe. Who knew that `$1` and the block variable that `#gsub` provides are not the same? 42 | 43 | ## 1.2.1 (2014-09-22) 44 | 45 | * Fix : [#46](https://github.com/savonrb/gyoku/pull/46) Fixed an issue where Gyoku was not threadsafe. Gyoku should now be relatively more threadsafe due to less usage of global variables. 46 | 47 | ## 1.2.0 (2014-09-18) 48 | 49 | * Feature: [#44](https://github.com/savonrb/gyoku/pull/44) support for sorting via :order! with a string key 50 | 51 | ## 1.1.1 (2014-01-02) 52 | 53 | * Feature: [#38](https://github.com/savonrb/gyoku/pull/38) support for building nested Arrays 54 | * Feature: [#36](https://github.com/savonrb/gyoku/pull/36) allow setting any objects content with :content! 55 | * Deprecation: Support for ree and ruby 1.8.7 will be going away soon. 56 | 57 | ## 1.1.0 (2013-07-26) 58 | 59 | * Feature: [#30](https://github.com/savonrb/gyoku/pull/30) support for building Arrays 60 | of parent tags using @attributes. 61 | 62 | * Fix: [#21](https://github.com/savonrb/gyoku/pull/21) stop modifying the original Hash. 63 | The original issue is [savonrb/savon#410](https://github.com/savonrb/savon/issues/410). 64 | 65 | ## 1.0.0 (2012-12-17) 66 | 67 | * Refactoring: Removed the global configuration. This should really only affect the 68 | `Gyoku.convert_symbols_to` shortcut which was removed as well. If you're using Gyoku 69 | with Savon 2.0, there's now an option for that. If you're using Gyoku on itself, 70 | you can pass it the `:key_converter` option instead. 71 | 72 | ## 0.5.0 (2012-12-15) 73 | 74 | Feature: [#19](https://github.com/savonrb/gyoku/pull/19) adds support for explicit XML attributes. 75 | 76 | Feature: [#17](https://github.com/savonrb/gyoku/pull/17) adds an `:upcase` formula. 77 | 78 | ## 0.4.6 (2012-06-28) 79 | 80 | * Fix: [#16](https://github.com/rubiii/gyoku/issues/16) Date objects were mapped like DateTime objects. 81 | 82 | Gyoku.xml(date: Date.today) # => "2012-06-28" 83 | 84 | * Fix: Time objects were also mapped like DateTime objects. 85 | 86 | Gyoku.xml(time: sunday) # => "" 87 | 88 | ## 0.4.5 (2012-05-28) 89 | 90 | * Fix: [issue 8](https://github.com/rubiii/gyoku/issues/8) - 91 | Conflict between camelcase methods in Rails. 92 | 93 | * Fix: [pull request 15](https://github.com/rubiii/gyoku/pull/15) - 94 | Gyoku generates blank attribute values if there are fewer attribute 95 | values in attributes! than elements. 96 | 97 | * Fix: [issue 12](https://github.com/rubiii/gyoku/issues/12) - 98 | Don't remove special keys from the original Hash. 99 | 100 | ## 0.4.4 101 | 102 | * Fix: [issue 6](https://github.com/rubiii/gyoku/issues/6) - 103 | `Gyoku.xml` does not modify the original Hash. 104 | 105 | ## 0.4.3 106 | 107 | * Fix: Make sure `require "date"` when necessary. 108 | 109 | ## 0.4.2 110 | 111 | * Fix: `Array.to_xml` so that the given :namespace is applied to every element 112 | in an Array. 113 | 114 | ## 0.4.1 115 | 116 | * Fix: Alternative formulas and namespaces. 117 | 118 | ## 0.4.0 119 | 120 | * Feature: Added alternative Symbol conversion formulas. You can choose between 121 | :lower_camelcase (the default), :camelcase and :none. 122 | 123 | Gyoku.convert_symbols_to :camelcase 124 | 125 | You can even define your own formula: 126 | 127 | Gyoku.convert_symbols_to { |key| key.upcase } 128 | 129 | ## 0.3.1 130 | 131 | * Feature: Gyoku now calls Proc objects and converts their return value. 132 | 133 | ## 0.3.0 134 | 135 | * Feature: Now when all Hash keys need to be namespaced (like with 136 | elementFormDefault), you can use options to to trigger this behavior. 137 | 138 | Gyoku.xml hash, 139 | :element_form_default => :qualified, 140 | :namespace => :v2 141 | 142 | ## 0.2.0 143 | 144 | * Feature: Added support for self-closing tags. Hash keys ending with a forward 145 | slash (regardless of their value) are now converted to self-closing tags. 146 | 147 | ## 0.1.1 148 | 149 | * Fix: Allow people to use new versions of builder. 150 | 151 | ## 0.1.0 152 | 153 | * Initial version. Gyoku was born as a core extension inside the 154 | [Savon](http://rubygems.org/gems/savon) library. 155 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "simplecov", require: false 6 | gem "coveralls", require: false 7 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Daniel Harrington 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gyoku 2 | 3 | Gyoku translates Ruby Hashes to XML. 4 | 5 | ``` ruby 6 | Gyoku.xml(:find_user => { :id => 123, "v1:Key" => "api" }) 7 | # => "123api" 8 | ``` 9 | 10 | [![Build status](https://github.com/savonrb/gyoku/actions/workflows/ci.yml/badge.svg)](https://github.com/savonrb/gyoku/actions/workflows/ci.yml) 11 | [![Gem Version](https://badge.fury.io/rb/gyoku.svg)](http://badge.fury.io/rb/gyoku) 12 | [![Code Climate](https://codeclimate.com/github/savonrb/gyoku.svg)](https://codeclimate.com/github/savonrb/gyoku) 13 | [![Coverage Status](https://coveralls.io/repos/savonrb/gyoku/badge.svg?branch=master)](https://coveralls.io/r/savonrb/gyoku) 14 | 15 | 16 | ## Installation 17 | 18 | Gyoku is available through [Rubygems](http://rubygems.org/gems/gyoku) and can be installed via: 19 | 20 | ``` bash 21 | $ gem install gyoku 22 | ``` 23 | 24 | or add it to your Gemfile like this: 25 | 26 | ``` ruby 27 | gem 'gyoku', '~> 1.0' 28 | ``` 29 | 30 | 31 | ## Hash keys 32 | 33 | Hash key Symbols are converted to lowerCamelCase Strings. 34 | 35 | ``` ruby 36 | Gyoku.xml(:lower_camel_case => "key") 37 | # => "key" 38 | ``` 39 | 40 | You can change the default conversion formula to `:camelcase`, `:upcase` or `:none`. 41 | Note that options are passed as a second Hash to the `.xml` method. 42 | 43 | ``` ruby 44 | Gyoku.xml({ :camel_case => "key" }, { :key_converter => :camelcase }) 45 | # => "key" 46 | ``` 47 | 48 | Custom key converters. You can use a lambda/Proc to provide customer key converters. 49 | This is a great way to leverage active support inflections for domain specific acronyms. 50 | 51 | ``` ruby 52 | # Use camelize lower which will hook into active support if installed. 53 | Gyoku.xml({ acronym_abc: "value" }, key_converter: lambda { |key| key.camelize(:lower) }) 54 | # => "value" 55 | 56 | ``` 57 | 58 | Hash key Strings are not converted and may contain namespaces. 59 | 60 | ``` ruby 61 | Gyoku.xml("XML" => "key") 62 | # => "key" 63 | ``` 64 | 65 | 66 | ## Hash values 67 | 68 | * DateTime objects are converted to xs:dateTime Strings 69 | * Objects responding to :to_datetime (except Strings) are converted to xs:dateTime Strings 70 | * TrueClass and FalseClass objects are converted to "true" and "false" Strings 71 | * NilClass objects are converted to xsi:nil tags 72 | * These conventions are also applied to the return value of objects responding to :call 73 | * All other objects are converted to Strings using :to_s 74 | 75 | ## Array values 76 | 77 | Array items are by default wrapped with the containiner tag, which may be unexpected. 78 | 79 | ``` ruby 80 | > Gyoku.xml({languages: [{language: 'ruby'},{language: 'java'}]}) 81 | # => "rubyjava" 82 | ``` 83 | 84 | You can set the `unwrap` option to remove this behavior. 85 | 86 | ``` ruby 87 | > Gyoku.xml({languages: [{language: 'ruby'},{language: 'java'}]}, { unwrap: true}) 88 | # => "rubyjava" 89 | ``` 90 | 91 | ## Special characters 92 | 93 | Gyoku escapes special characters unless the Hash key ends with an exclamation mark. 94 | 95 | ``` ruby 96 | Gyoku.xml(:escaped => "", :not_escaped! => "") 97 | # => "<tag />" 98 | ``` 99 | 100 | 101 | ## Self-closing tags 102 | 103 | Hash Keys ending with a forward slash create self-closing tags. 104 | 105 | ``` ruby 106 | Gyoku.xml(:"self_closing/" => "", "selfClosing/" => nil) 107 | # => "" 108 | ``` 109 | 110 | 111 | ## Sort XML tags 112 | 113 | In case you need the XML tags to be in a specific order, you can specify the order 114 | through an additional Array stored under the `:order!` key. 115 | 116 | ``` ruby 117 | Gyoku.xml(:name => "Eve", :id => 1, :order! => [:id, :name]) 118 | # => "1Eve" 119 | ``` 120 | 121 | 122 | ## XML attributes 123 | 124 | Adding XML attributes is rather ugly, but it can be done by specifying an additional 125 | Hash stored under the`:attributes!` key. 126 | 127 | ``` ruby 128 | Gyoku.xml(:person => "Eve", :attributes! => { :person => { :id => 1 } }) 129 | # => "Eve" 130 | ``` 131 | 132 | ## Explicit XML Attributes 133 | 134 | In addition to using the `:attributes!` key, you may also specify attributes through keys beginning with an "@" sign. 135 | Since you'll need to set the attribute within the hash containing the node's contents, a `:content!` key can be used 136 | to explicity set the content of the node. The `:content!` value may be a String, Hash, or Array. 137 | 138 | This is particularly useful for self-closing tags. 139 | 140 | **Using :attributes!** 141 | 142 | ``` ruby 143 | Gyoku.xml( 144 | "foo/" => "", 145 | :attributes! => { 146 | "foo/" => { 147 | "bar" => "1", 148 | "biz" => "2", 149 | "baz" => "3" 150 | } 151 | } 152 | ) 153 | # => "" 154 | ``` 155 | 156 | **Using "@" keys and ":content!"** 157 | 158 | ``` ruby 159 | Gyoku.xml( 160 | "foo/" => { 161 | :@bar => "1", 162 | :@biz => "2", 163 | :@baz => "3", 164 | :content! => "" 165 | }) 166 | # => "" 167 | ``` 168 | 169 | **Example using "@" to get Array of parent tags each with @attributes & :content!** 170 | 171 | ``` ruby 172 | Gyoku.xml( 173 | "foo" => [ 174 | {:@name => "bar", :content! => 'gyoku'} 175 | {:@name => "baz", :@some => "attr", :content! => 'rocks!'} 176 | ]) 177 | # => "gyokurocks!" 178 | ``` 179 | 180 | Unwrapping Arrays. You can specify an optional `unwrap` argument to modify the default Array 181 | behavior. `unwrap` accepts a boolean flag (false by default) or an Array whitelist of keys to unwrap. 182 | ``` ruby 183 | # Default Array behavior 184 | Gyoku.xml({ 185 | "foo" => [ 186 | {:is => 'great' }, 187 | {:is => 'awesome'} 188 | ] 189 | }) 190 | # => "greatawesome" 191 | 192 | # Unwrap Array behavior 193 | Gyoku.xml({ 194 | "foo" => [ 195 | {:is => 'great' }, 196 | {:is => 'awesome'} 197 | ] 198 | }, unwrap: true) 199 | # => "greatawesome" 200 | 201 | # Unwrap Array, whitelist. 202 | # foo is not unwrapped, bar is. 203 | Gyoku.xml({ 204 | "foo" => [ 205 | {:is => 'great' }, 206 | {:is => 'awesome'} 207 | ], 208 | "bar" => [ 209 | {:is => 'rad' }, 210 | {:is => 'cool'} 211 | ] 212 | }, unwrap: [:bar]) 213 | # => "greatawesomeradcool" 214 | ``` 215 | 216 | Naturally, it would ignore :content! if tag is self-closing: 217 | 218 | ``` ruby 219 | Gyoku.xml( 220 | "foo/" => [ 221 | {:@name => "bar", :content! => 'gyoku'} 222 | {:@name => "baz", :@some => "attr", :content! => 'rocks!'} 223 | ]) 224 | # => "" 225 | ``` 226 | 227 | This seems a bit more explicit with the attributes rather than having to maintain a hash of attributes. 228 | 229 | For backward compatibility, `:attributes!` will still work. However, "@" keys will override `:attributes!` keys 230 | if there is a conflict. 231 | 232 | ``` ruby 233 | Gyoku.xml(:person => {:content! => "Adam", :@id! => 0}) 234 | # => "Adam" 235 | ``` 236 | 237 | **Example with ":content!", :attributes! and "@" keys** 238 | 239 | ``` ruby 240 | Gyoku.xml({ 241 | :subtitle => { 242 | :@lang => "en", 243 | :content! => "It's Godzilla!" 244 | }, 245 | :attributes! => { :subtitle => { "lang" => "jp" } } 246 | } 247 | # => "It's Godzilla!" 248 | ``` 249 | 250 | The example above shows an example of how you can use all three at the same time. 251 | 252 | Notice that we have the attribute "lang" defined twice. 253 | The `@lang` value takes precedence over the `:attribute![:subtitle]["lang"]` value. 254 | 255 | ## Pretty Print 256 | 257 | You can prettify the output XML to make it more readable. Use these options: 258 | * `pretty_print` – controls pretty mode (default: `false`) 259 | * `indent` – specifies indentation in spaces (default: `2`) 260 | * `compact` – controls compact mode (default: `true`) 261 | 262 | **This feature is not available for XML documents generated from arrays with unwrap option set to false as such documents are not valid** 263 | 264 | **Examples** 265 | 266 | ``` ruby 267 | puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true) 268 | # 269 | # John 270 | # 271 | # Programmer 272 | # 273 | # 274 | ``` 275 | 276 | ``` ruby 277 | puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, indent: 4) 278 | # 279 | # John 280 | # 281 | # Programmer 282 | # 283 | # 284 | ``` 285 | 286 | ``` ruby 287 | puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, compact: false) 288 | # 289 | # 290 | # John 291 | # 292 | # 293 | # 294 | # Programmer 295 | # 296 | # 297 | # 298 | ``` 299 | 300 | **Generate XML from an array with `unwrap` option set to `true`** 301 | ``` ruby 302 | puts Gyoku::Array.to_xml(["john", "jane"], "user", true, {}, pretty_print: true, unwrap: true) 303 | # 304 | # john 305 | # jane 306 | # 307 | ``` 308 | 309 | **Generate XML from an array with `unwrap` option unset (`false` by default)** 310 | ``` ruby 311 | puts Gyoku::Array.to_xml(["john", "jane"], "user", true, {}, pretty_print: true) 312 | #johnjane 313 | ``` 314 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | require "bundler/setup" 3 | Bundler::GemHelper.install_tasks 4 | 5 | require "rspec/core/rake_task" 6 | 7 | RSpec::Core::RakeTask.new do |t| 8 | t.rspec_opts = %w[-c] 9 | end 10 | 11 | task default: :spec 12 | task test: :spec 13 | -------------------------------------------------------------------------------- /gyoku.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | require "gyoku/version" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "gyoku" 6 | s.version = Gyoku::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = "Daniel Harrington" 9 | s.email = "me@rubiii.com" 10 | s.homepage = "https://github.com/savonrb/#{s.name}" 11 | s.summary = "Translates Ruby Hashes to XML" 12 | s.description = "Gyoku translates Ruby Hashes to XML" 13 | s.required_ruby_version = ">= 3.0" 14 | 15 | s.license = "MIT" 16 | 17 | s.add_dependency "builder", ">= 2.1.2" 18 | s.add_dependency "rexml", "~> 3.0" 19 | 20 | s.add_development_dependency "rake" 21 | s.add_development_dependency "rspec" 22 | s.add_development_dependency "standard" 23 | 24 | s.files = `git ls-files`.split("\n") 25 | 26 | s.require_paths = ["lib"] 27 | end 28 | -------------------------------------------------------------------------------- /lib/gyoku.rb: -------------------------------------------------------------------------------- 1 | require "gyoku/version" 2 | require "gyoku/hash" 3 | 4 | module Gyoku 5 | # Converts a given Hash +key+ with +options+ into an XML tag. 6 | def self.xml_tag(key, options = {}) 7 | XMLKey.create(key, options) 8 | end 9 | 10 | # Translates a given +hash+ with +options+ to XML. 11 | def self.xml(hash, options = {}) 12 | Hash.to_xml hash.dup, options 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/gyoku/array.rb: -------------------------------------------------------------------------------- 1 | module Gyoku 2 | module Array 3 | module_function 4 | 5 | NESTED_ELEMENT_NAME = "element" 6 | 7 | # Builds XML and prettifies it if +pretty_print+ option is set to +true+ 8 | def to_xml(array, key, escape_xml = true, attributes = {}, options = {}) 9 | xml = build_xml(array, key, escape_xml, attributes, options) 10 | 11 | if options[:pretty_print] && options[:unwrap] 12 | Prettifier.prettify(xml, options) 13 | else 14 | xml 15 | end 16 | end 17 | 18 | # Translates a given +array+ to XML. Accepts the XML +key+ to add the elements to, 19 | # whether to +escape_xml+ and an optional Hash of +attributes+. 20 | def self.build_xml(array, key, escape_xml = true, attributes = {}, options = {}) 21 | self_closing = options.delete(:self_closing) 22 | unwrap = unwrap?(options.fetch(:unwrap, false), key) 23 | 24 | iterate_with_xml array, key, attributes, options do |xml, item, attrs, index| 25 | if self_closing 26 | xml.tag!(key, attrs) 27 | else 28 | case item 29 | when ::Hash 30 | if unwrap 31 | xml << Hash.to_xml(item, options) 32 | else 33 | xml.tag!(key, attrs) { xml << Hash.build_xml(item, options) } 34 | end 35 | when ::Array 36 | xml.tag!(key, attrs) { xml << Array.build_xml(item, NESTED_ELEMENT_NAME) } 37 | when NilClass 38 | xml.tag!(key, "xsi:nil" => "true") 39 | else 40 | xml.tag!(key, attrs) { xml << XMLValue.create(item, escape_xml) } 41 | end 42 | end 43 | end 44 | end 45 | 46 | # Iterates over a given +array+ with a Hash of +attributes+ and yields a builder +xml+ 47 | # instance, the current +item+, any XML +attributes+ and the current +index+. 48 | def iterate_with_xml(array, key, attributes, options, &block) 49 | xml = Builder::XmlMarkup.new 50 | unwrap = unwrap?(options.fetch(:unwrap, false), key) 51 | 52 | if unwrap 53 | xml.tag!(key, attributes) { iterate_array(xml, array, attributes, &block) } 54 | else 55 | iterate_array(xml, array, attributes, &block) 56 | end 57 | 58 | xml.target! 59 | end 60 | private_class_method :iterate_with_xml 61 | 62 | # Iterates over a given +array+ with a Hash of +attributes+ and yields a builder +xml+ 63 | # instance, the current +item+, any XML +attributes+ and the current +index+. 64 | def iterate_array(xml, array, attributes, &block) 65 | array.each_with_index do |item, index| 66 | attrs = if item.respond_to?(:keys) 67 | item.each_with_object({}) do |v, st| 68 | k = v[0].to_s 69 | st[k[1..]] = v[1].to_s if Hash.explicit_attribute?(k) 70 | end 71 | else 72 | {} 73 | end 74 | yield xml, item, tag_attributes(attributes, index).merge(attrs), index 75 | end 76 | end 77 | private_class_method :iterate_array 78 | 79 | # Takes a Hash of +attributes+ and the +index+ for which to return attributes 80 | # for duplicate tags. 81 | def tag_attributes(attributes, index) 82 | return {} if attributes.empty? 83 | 84 | attributes.inject({}) do |hash, (key, value)| 85 | value = value[index] if value.is_a? ::Array 86 | value ? hash.merge(key => value) : hash 87 | end 88 | end 89 | private_class_method :tag_attributes 90 | 91 | def unwrap?(unwrap, key) 92 | unwrap.is_a?(::Array) ? unwrap.include?(key.to_sym) : unwrap 93 | end 94 | private_class_method :unwrap? 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/gyoku/hash.rb: -------------------------------------------------------------------------------- 1 | require "builder" 2 | require "gyoku/prettifier" 3 | require "gyoku/array" 4 | require "gyoku/xml_key" 5 | require "gyoku/xml_value" 6 | 7 | module Gyoku 8 | module Hash 9 | module_function 10 | 11 | # Builds XML and prettifies it if +pretty_print+ option is set to +true+ 12 | def to_xml(hash, options = {}) 13 | xml = build_xml(hash, options) 14 | 15 | if options[:pretty_print] 16 | Prettifier.prettify(xml, options) 17 | else 18 | xml 19 | end 20 | end 21 | 22 | # Translates a given +hash+ with +options+ to XML. 23 | def build_xml(hash, options = {}) 24 | iterate_with_xml hash do |xml, key, value, attributes| 25 | self_closing = key.to_s[-1, 1] == "/" 26 | escape_xml = key.to_s[-1, 1] != "!" 27 | xml_key = XMLKey.create key, options 28 | 29 | if :content! === key 30 | xml << XMLValue.create(value, escape_xml, options) 31 | elsif ::Array === value 32 | xml << Array.build_xml(value, xml_key, escape_xml, attributes, options.merge(self_closing: self_closing)) 33 | elsif ::Hash === value 34 | xml.tag!(xml_key, attributes) { xml << build_xml(value, options) } 35 | elsif self_closing 36 | xml.tag!(xml_key, attributes) 37 | elsif NilClass === value 38 | xml.tag!(xml_key, "xsi:nil" => "true") 39 | else 40 | xml.tag!(xml_key, attributes) { xml << XMLValue.create(value, escape_xml, options) } 41 | end 42 | end 43 | end 44 | 45 | def explicit_attribute?(key) 46 | key.to_s.start_with?("@") 47 | end 48 | 49 | # Iterates over a given +hash+ and yields a builder +xml+ instance, the current 50 | # Hash +key+ and any XML +attributes+. 51 | # 52 | # Keys beginning with "@" are treated as explicit attributes for their container. 53 | # You can use both :attributes! and "@" keys to specify attributes. 54 | # In the event of a conflict, the "@" key takes precedence. 55 | def iterate_with_xml(hash) 56 | xml = Builder::XmlMarkup.new 57 | attributes = hash[:attributes!] || {} 58 | hash_without_attributes = hash.except(:attributes!) 59 | 60 | order(hash_without_attributes).each do |key| 61 | node_attr = attributes[key] || {} 62 | # node_attr must be kind of ActiveSupport::HashWithIndifferentAccess 63 | node_attr = node_attr.map { |k, v| [k.to_s, v] }.to_h 64 | node_value = hash[key].respond_to?(:keys) ? hash[key].clone : hash[key] 65 | 66 | if node_value.respond_to?(:keys) 67 | explicit_keys = node_value.keys.select { |k| explicit_attribute?(k) } 68 | explicit_attr = {} 69 | explicit_keys.each { |k| explicit_attr[k.to_s[1..]] = node_value[k] } 70 | node_attr.merge!(explicit_attr) 71 | explicit_keys.each { |k| node_value.delete(k) } 72 | 73 | tmp_node_value = node_value.delete(:content!) 74 | node_value = tmp_node_value unless tmp_node_value.nil? 75 | node_value = "" if node_value.respond_to?(:empty?) && node_value.empty? 76 | end 77 | 78 | yield xml, key, node_value, node_attr 79 | end 80 | 81 | xml.target! 82 | end 83 | private_class_method :iterate_with_xml 84 | 85 | # Deletes and returns an Array of keys stored under the :order! key of a given +hash+. 86 | # Defaults to return the actual keys of the Hash if no :order! key could be found. 87 | # Raises an ArgumentError in case the :order! Array does not match the Hash keys. 88 | def order(hash) 89 | order = hash[:order!] || hash.delete("order!") 90 | hash_without_order = hash.except(:order!) 91 | order = hash_without_order.keys unless order.is_a? ::Array 92 | 93 | # Ignore Explicit Attributes 94 | orderable = order.delete_if { |k| explicit_attribute?(k) } 95 | hashable = hash_without_order.keys.select { |k| !explicit_attribute?(k) } 96 | 97 | missing, spurious = hashable - orderable, orderable - hashable 98 | raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty? 99 | raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty? 100 | 101 | order 102 | end 103 | private_class_method :order 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/gyoku/prettifier.rb: -------------------------------------------------------------------------------- 1 | require "rexml/document" 2 | 3 | module Gyoku 4 | class Prettifier 5 | DEFAULT_INDENT = 2 6 | DEFAULT_COMPACT = true 7 | 8 | attr_accessor :indent, :compact 9 | 10 | def self.prettify(xml, options = {}) 11 | new(options).prettify(xml) 12 | end 13 | 14 | def initialize(options = {}) 15 | @indent = options[:indent] || DEFAULT_INDENT 16 | @compact = options[:compact].nil? ? DEFAULT_COMPACT : options[:compact] 17 | end 18 | 19 | # Adds intendations and newlines to +xml+ to make it more readable 20 | def prettify(xml) 21 | result = "" 22 | formatter = REXML::Formatters::Pretty.new indent 23 | formatter.compact = compact 24 | doc = REXML::Document.new xml 25 | formatter.write doc, result 26 | result 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/gyoku/version.rb: -------------------------------------------------------------------------------- 1 | module Gyoku 2 | VERSION = "1.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/gyoku/xml_key.rb: -------------------------------------------------------------------------------- 1 | module Gyoku 2 | module XMLKey 3 | class << self 4 | CAMELCASE = lambda { |key| key.gsub(/\/(.?)/) { |m| "::#{m[-1].upcase}" }.gsub(/(?:^|_)(.)/) { |m| m[-1].upcase } } 5 | LOWER_CAMELCASE = lambda { |key| key[0].chr.downcase + CAMELCASE.call(key)[1..] } 6 | UPCASE = lambda { |key| key.upcase } 7 | 8 | FORMULAS = { 9 | lower_camelcase: lambda { |key| LOWER_CAMELCASE.call(key) }, 10 | camelcase: lambda { |key| CAMELCASE.call(key) }, 11 | upcase: lambda { |key| UPCASE.call(key) }, 12 | none: lambda { |key| key } 13 | } 14 | 15 | # Converts a given +object+ with +options+ to an XML key. 16 | def create(key, options = {}) 17 | xml_key = chop_special_characters key.to_s 18 | 19 | if unqualified = unqualify?(xml_key) 20 | xml_key = xml_key.split(":").last 21 | end 22 | 23 | xml_key = key_converter(options, xml_key).call(xml_key) if Symbol === key 24 | 25 | if !unqualified && qualify?(options) && !xml_key.include?(":") 26 | xml_key = "#{options[:namespace]}:#{xml_key}" 27 | end 28 | 29 | xml_key 30 | end 31 | 32 | private 33 | 34 | # Returns the formula for converting Symbol keys. 35 | def key_converter(options, xml_key) 36 | return options[:key_converter] if options[:key_converter].is_a? Proc 37 | 38 | defined_key = options[:key_to_convert] 39 | key_converter = if !defined_key.nil? && (defined_key == xml_key) 40 | options[:key_converter] 41 | elsif !defined_key.nil? 42 | :lower_camelcase 43 | elsif options[:except] == xml_key 44 | :lower_camelcase 45 | else 46 | options[:key_converter] || :lower_camelcase 47 | end 48 | FORMULAS[key_converter] 49 | end 50 | 51 | # Chops special characters from the end of a given +string+. 52 | def chop_special_characters(string) 53 | ["!", "/"].include?(string[-1, 1]) ? string.chop : string 54 | end 55 | 56 | # Returns whether to remove the namespace from a given +key+. 57 | def unqualify?(key) 58 | key[0, 1] == ":" 59 | end 60 | 61 | # Returns whether to namespace all keys (elementFormDefault). 62 | def qualify?(options) 63 | options[:element_form_default] == :qualified && options[:namespace] 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/gyoku/xml_value.rb: -------------------------------------------------------------------------------- 1 | require "cgi" 2 | require "date" 3 | 4 | module Gyoku 5 | module XMLValue 6 | class << self 7 | # xs:date format 8 | XS_DATE_FORMAT = "%Y-%m-%d" 9 | 10 | # xs:time format 11 | XS_TIME_FORMAT = "%H:%M:%S" 12 | 13 | # xs:dateTime format 14 | XS_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%Z" 15 | 16 | # Converts a given +object+ to an XML value. 17 | def create(object, escape_xml = true, options = {}) 18 | case object 19 | when Time 20 | object.strftime XS_TIME_FORMAT 21 | when DateTime 22 | object.strftime XS_DATETIME_FORMAT 23 | when Date 24 | object.strftime XS_DATE_FORMAT 25 | when String 26 | escape_xml ? CGI.escapeHTML(object) : object 27 | when ::Hash 28 | Gyoku::Hash.to_xml(object, options) 29 | else 30 | if object.respond_to?(:to_datetime) 31 | create object.to_datetime 32 | elsif object.respond_to?(:call) 33 | create object.call 34 | else 35 | object.to_s 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/gyoku/array_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gyoku::Array do 4 | describe ".to_xml" do 5 | it "returns the XML for an Array of Hashes" do 6 | array = [{name: "adam"}, {name: "eve"}] 7 | result = "adameve" 8 | 9 | expect(to_xml(array, "user")).to eq(result) 10 | end 11 | 12 | it "returns the XML for an Array of Hashes unwrapped" do 13 | array = [{name: "adam"}, {name: "eve"}] 14 | result = "adameve" 15 | 16 | expect(to_xml(array, "user", true, {}, unwrap: true)).to eq(result) 17 | end 18 | 19 | it "returns the XML for an Array of different Objects" do 20 | array = [:symbol, "string", 123] 21 | result = "symbolstring123" 22 | 23 | expect(to_xml(array, "value")).to eq(result) 24 | end 25 | 26 | it "defaults to escape special characters" do 27 | array = ["", "adam & eve"] 28 | result = "<tag />adam & eve" 29 | 30 | expect(to_xml(array, "value")).to eq(result) 31 | end 32 | 33 | it "does not escape special characters when told to" do 34 | array = ["", "adam & eve"] 35 | result = "adam & eve" 36 | 37 | expect(to_xml(array, "value", false)).to eq(result) 38 | end 39 | 40 | it "adds attributes to a given tag" do 41 | array = ["adam", "eve"] 42 | result = 'adameve' 43 | 44 | expect(to_xml(array, "value", :escape_xml, active: true)).to eq(result) 45 | end 46 | 47 | it "adds attributes to tags when :unwrap is true" do 48 | array = [{item: "abc"}] 49 | key = "items" 50 | escape_xml = :escape_xml 51 | attributes = {"amount" => "1"} 52 | options = {unwrap: true} 53 | result = "abc" 54 | 55 | expect(to_xml(array, key, escape_xml, attributes, options)).to eq result 56 | end 57 | 58 | it "adds attributes to duplicate tags" do 59 | array = ["adam", "eve"] 60 | result = 'adameve' 61 | 62 | expect(to_xml(array, "value", :escape_xml, id: [1, 2])).to eq(result) 63 | end 64 | 65 | it "skips attribute for element without attributes if there are fewer attributes than elements" do 66 | array = ["adam", "eve", "serpent"] 67 | result = 'adameveserpent' 68 | 69 | expect(to_xml(array, "value", :escape_xml, id: [1, 2])).to eq(result) 70 | end 71 | 72 | it "handles nested Arrays" do 73 | array = [["one", "two"]] 74 | result = "onetwo" 75 | 76 | expect(to_xml(array, "value")).to eq(result) 77 | end 78 | 79 | context "when :pretty_print option is set to true" do 80 | context "when :unwrap option is set to true" do 81 | it "returns prettified xml" do 82 | array = ["one", "two", {"three" => "four"}] 83 | options = {pretty_print: true, unwrap: true} 84 | result = "\n one\n two\n four\n" 85 | expect(to_xml(array, "test", true, {}, options)).to eq(result) 86 | end 87 | 88 | context "when :indent option is specified" do 89 | it "returns prettified xml with specified indent" do 90 | array = ["one", "two", {"three" => "four"}] 91 | options = {pretty_print: true, indent: 3, unwrap: true} 92 | result = "\n one\n two\n four\n" 93 | expect(to_xml(array, "test", true, {}, options)).to eq(result) 94 | end 95 | end 96 | 97 | context "when :compact option is specified" do 98 | it "returns prettified xml with specified compact mode" do 99 | array = ["one", {"two" => "three"}] 100 | options = {pretty_print: true, compact: false, unwrap: true} 101 | result = "\n \n one\n \n \n three \n \n" 102 | expect(to_xml(array, "test", true, {}, options)).to eq(result) 103 | end 104 | end 105 | end 106 | 107 | context "when :unwrap option is not set" do 108 | it "returns non-prettified xml" do 109 | array = ["one", "two", {"three" => "four"}] 110 | options = {pretty_print: true} 111 | result = "onetwofour" 112 | expect(to_xml(array, "test", true, {}, options)).to eq(result) 113 | end 114 | end 115 | end 116 | end 117 | 118 | def to_xml(*args) 119 | Gyoku::Array.to_xml(*args) 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/gyoku/hash_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gyoku::Hash do 4 | describe ".to_xml" do 5 | describe "returns SOAP request compatible XML" do 6 | it "for a simple Hash" do 7 | expect(to_xml(some: "user")).to eq("user") 8 | end 9 | 10 | it "for a nested Hash" do 11 | expect(to_xml(some: {new: "user"})).to eq("user") 12 | end 13 | 14 | context "with key_converter" do 15 | it "expect all keys change" do 16 | expect(to_xml({some: {new: "user"}}, {key_converter: :camelcase})).to eq("user") 17 | end 18 | 19 | it "and key_to_convert option should change only key" do 20 | hash = {some: {new: "user", age: 20}} 21 | options = {key_converter: :camelcase, key_to_convert: "some"} 22 | result = "user20" 23 | expect(to_xml(hash, options)).to eq(result) 24 | 25 | hash = {some: {new: "user", age: 20}} 26 | options = {key_converter: :camelcase, key_to_convert: "new"} 27 | result = "user20" 28 | expect(to_xml(hash, options)).to eq(result) 29 | end 30 | 31 | it "with except option, dont convert this key" do 32 | hash = {some: {new: "user", age: 20}} 33 | options = {key_converter: :camelcase, except: "some"} 34 | result = "user20" 35 | expect(to_xml(hash, options)).to eq(result) 36 | end 37 | end 38 | 39 | it "for a Hash with multiple keys" do 40 | expect(to_xml(all: "users", before: "whatever")).to include( 41 | "users", 42 | "whatever" 43 | ) 44 | end 45 | 46 | it "for a Hash containing an Array" do 47 | expect(to_xml(some: ["user", "gorilla"])).to eq("usergorilla") 48 | end 49 | 50 | it "for a Hash containing an Array of Hashes" do 51 | expect(to_xml(some: [{new: "user"}, {old: "gorilla"}])) 52 | .to eq("usergorilla") 53 | end 54 | 55 | context "when :pretty_print option is set to true" do 56 | it "returns prettified xml" do 57 | hash = {some: {user: {name: "John", groups: ["admin", "editor"]}}} 58 | options = {pretty_print: true} 59 | result = "\n \n John\n admin\n editor\n \n" 60 | expect(to_xml(hash, options)).to eq(result) 61 | end 62 | 63 | context "when :indent option is specified" do 64 | it "returns prettified xml with specified indent" do 65 | hash = {some: {user: {name: "John"}}} 66 | options = {pretty_print: true, indent: 4} 67 | result = "\n \n John\n \n" 68 | expect(to_xml(hash, options)).to eq(result) 69 | end 70 | end 71 | 72 | context "when :compact option is specified" do 73 | it "returns prettified xml with specified compact mode" do 74 | hash = {some: {user: {name: "John"}}} 75 | options = {pretty_print: true, compact: false} 76 | result = "\n \n \n John\n \n \n" 77 | expect(to_xml(hash, options)).to eq(result) 78 | end 79 | end 80 | end 81 | end 82 | 83 | it "converts Hash key Symbols to lowerCamelCase" do 84 | expect(to_xml(find_or_create: "user")).to eq("user") 85 | end 86 | 87 | it "does not convert Hash key Strings" do 88 | expect(to_xml("find_or_create" => "user")).to eq("user") 89 | end 90 | 91 | it "converts DateTime objects to xs:dateTime compliant Strings" do 92 | expect(to_xml(before: DateTime.new(2012, 0o3, 22, 16, 22, 33))) 93 | .to eq("2012-03-22T16:22:33+00:00") 94 | end 95 | 96 | it "converts Objects responding to to_datetime to xs:dateTime compliant Strings" do 97 | singleton = Object.new 98 | def singleton.to_datetime 99 | DateTime.new(2012, 0o3, 22, 16, 22, 33) 100 | end 101 | 102 | expect(to_xml(before: singleton)).to eq("2012-03-22T16:22:33+00:00") 103 | end 104 | 105 | it "calls to_s on Strings even if they respond to to_datetime" do 106 | singleton = "gorilla" 107 | def singleton.to_datetime 108 | DateTime.new(2012, 0o3, 22, 16, 22, 33) 109 | end 110 | 111 | expect(to_xml(name: singleton)).to eq("gorilla") 112 | end 113 | 114 | it "properly serializes nil values" do 115 | expect(to_xml(some: nil)).to eq('') 116 | end 117 | 118 | it "creates self-closing tags for Hash keys ending with a forward slash" do 119 | expect(to_xml("self-closing/" => nil)).to eq("") 120 | end 121 | 122 | it "calls to_s on any other Object" do 123 | [666, true, false].each do |object| 124 | expect(to_xml(some: object)).to eq("#{object}") 125 | end 126 | end 127 | 128 | it "defaults to escape special characters" do 129 | result = to_xml(some: {nested: ""}, tag: "") 130 | expect(result).to include("<tag />") 131 | expect(result).to include("<tag />") 132 | end 133 | 134 | it "does not escape special characters for keys marked with an exclamation mark" do 135 | result = to_xml(some: {nested!: ""}, tag!: "") 136 | expect(result).to include("") 137 | expect(result).to include("") 138 | end 139 | 140 | it "preserves the order of Hash keys and values specified through :order!" do 141 | hash = {find_user: {name: "Lucy", id: 666, order!: [:id, :name]}} 142 | result = "666Lucy" 143 | expect(to_xml(hash)).to eq(result) 144 | 145 | hash = {find_user: {mname: "in the", lname: "Sky", fname: "Lucy", order!: [:fname, :mname, :lname]}} 146 | result = "Lucyin theSky" 147 | expect(to_xml(hash)).to eq(result) 148 | end 149 | 150 | it "preserves the order of Hash keys and values specified through 'order!' (as a string key)" do 151 | hash = {find_user: {:name => "Lucy", :id => 666, "order!" => [:id, :name]}} 152 | result = "666Lucy" 153 | expect(to_xml(hash)).to eq(result) 154 | 155 | hash = {find_user: {:mname => "in the", :lname => "Sky", :fname => "Lucy", "order!" => [:fname, :mname, :lname]}} 156 | result = "Lucyin theSky" 157 | expect(to_xml(hash)).to eq(result) 158 | end 159 | 160 | it "uses :order! symbol values for ordering but leaves the string key 'order!' if both are present" do 161 | hash = {find_user: {:name => "Lucy", :id => 666, "order!" => "value", :order! => [:id, :name, "order!"]}} 162 | result = "666Lucyvalue" 163 | expect(to_xml(hash)).to eq(result) 164 | end 165 | 166 | it "raises if the :order! Array is missing Hash keys" do 167 | hash = {name: "Lucy", id: 666, order!: [:name]} 168 | expect { to_xml(hash) }.to raise_error(ArgumentError, "Missing elements in :order! [:id]") 169 | end 170 | 171 | it "raises if the :order! Array contains missing Hash keys" do 172 | hash = {by_name: {first_name: "Lucy", last_name: "Sky", order!: [:first_name, :middle_name, :last_name]}} 173 | expect { to_xml(hash) }.to raise_error(ArgumentError, "Spurious elements in :order! [:middle_name]") 174 | end 175 | 176 | it "adds attributes to Hash keys specified through :attributes!" do 177 | hash = {find_user: {person: "Lucy", attributes!: {person: {id: 666}}}} 178 | result = 'Lucy' 179 | expect(to_xml(hash)).to eq(result) 180 | 181 | hash = {find_user: {person: "Lucy", attributes!: {person: {id: 666, city: "Hamburg"}}}} 182 | expect(to_xml(hash)).to include('id="666"', 'city="Hamburg"') 183 | end 184 | 185 | it "adds attributes to duplicate Hash keys specified through :attributes!" do 186 | hash = {find_user: {person: ["Lucy", "Anna"], attributes!: {person: {id: [1, 3]}}}} 187 | result = 'LucyAnna' 188 | expect(to_xml(hash)).to eq(result) 189 | 190 | hash = {find_user: {person: ["Lucy", "Anna"], attributes!: {person: {active: "true"}}}} 191 | result = 'LucyAnna' 192 | expect(to_xml(hash)).to eq(result) 193 | end 194 | 195 | it "skips attribute for element without attributes if there are fewer attributes than elements" do 196 | hash = {find_user: {person: ["Lucy", "Anna", "Beth"], attributes!: {person: {id: [1, 3]}}}} 197 | result = 'LucyAnnaBeth' 198 | expect(to_xml(hash)).to eq(result) 199 | end 200 | 201 | it "adds attributes to self-closing tags" do 202 | hash = { 203 | "category/" => "", 204 | :attributes! => {"category/" => {id: 1}} 205 | } 206 | 207 | expect(to_xml(hash)).to eq('') 208 | end 209 | 210 | it "recognizes @attribute => value along :attributes!" do 211 | hash = { 212 | "category" => { 213 | :content! => "users", 214 | :@id => 1 215 | } 216 | } 217 | expect(to_xml(hash)).to eq('users') 218 | end 219 | 220 | it "recognizes @attribute => value along :attributes! in selfclosed tags" do 221 | hash = { 222 | "category/" => { 223 | :@id => 1 224 | } 225 | } 226 | expect(to_xml(hash)).to eq('') 227 | end 228 | 229 | it ":@attribute => value takes over :attributes!" do 230 | hash = { 231 | "category/" => { 232 | :@id => 1 233 | }, 234 | :attributes! => { 235 | "category/" => { 236 | "id" => 2, # will be ignored 237 | "type" => "admins" 238 | } 239 | } 240 | } 241 | # attribute order is undefined 242 | expect(['', '']).to include to_xml(hash) 243 | 244 | # with symbols 245 | hash = { 246 | "category/" => { 247 | :@id => 1 248 | }, 249 | :attributes! => { 250 | "category/" => { 251 | id: 2, # will be ignored 252 | type: "admins" 253 | } 254 | } 255 | } 256 | expect(['', '']).to include to_xml(hash) 257 | end 258 | 259 | it "recognizes :content! => value as tag content" do 260 | hash = { 261 | "category" => { 262 | content!: "users" 263 | } 264 | } 265 | expect(to_xml(hash)).to eq("users") 266 | end 267 | 268 | it "recognizes :content! => value as tag content with value Fixnum" do 269 | hash = { 270 | "category" => { 271 | content!: 666 272 | } 273 | } 274 | expect(to_xml(hash)).to eq("666") 275 | end 276 | 277 | it "recognizes :content! => value as tag content with value true" do 278 | hash = { 279 | "category" => { 280 | content!: true 281 | } 282 | } 283 | expect(to_xml(hash)).to eq("true") 284 | end 285 | 286 | it "recognizes :content! => value as tag content with value false" do 287 | hash = { 288 | "category" => { 289 | content!: false 290 | } 291 | } 292 | expect(to_xml(hash)).to eq("false") 293 | end 294 | 295 | it "recognizes :content! => value as tag content with value DateTime" do 296 | hash = { 297 | "before" => { 298 | content!: DateTime.new(2012, 0o3, 22, 16, 22, 33) 299 | } 300 | } 301 | expect(to_xml(hash)).to eq("2012-03-22T16:22:33+00:00") 302 | end 303 | 304 | it "ignores :content! if self-closing mark present" do 305 | hash = { 306 | "category/" => { 307 | content!: "users" 308 | } 309 | } 310 | expect(to_xml(hash)).to eq("") 311 | end 312 | 313 | it "recognizes array of attributes" do 314 | hash = { 315 | "category" => [{:@name => "one"}, {:@name => "two"}] 316 | } 317 | expect(to_xml(hash)).to eq('') 318 | 319 | # issue #31. 320 | hash = { 321 | :order! => ["foo", "bar"], 322 | "foo" => {:@foo => "foo"}, 323 | "bar" => {:@bar => "bar", "baz" => {}} 324 | } 325 | expect(to_xml(hash)).to eq('') 326 | end 327 | 328 | it "recognizes array of attributes with content in each" do 329 | hash = { 330 | "foo" => [{:@name => "bar", :content! => "gyoku"}, {:@name => "baz", :@some => "attr", :content! => "rocks!"}] 331 | } 332 | 333 | expect([ 334 | 'gyokurocks!', 335 | 'gyokurocks!' 336 | ]).to include to_xml(hash) 337 | end 338 | 339 | it "recognizes array of attributes but ignores content in each if selfclosing" do 340 | hash = { 341 | "foo/" => [{:@name => "bar", :content! => "gyoku"}, {:@name => "baz", :@some => "attr", :content! => "rocks!"}] 342 | } 343 | 344 | expect([ 345 | '', 346 | '' 347 | ]).to include to_xml(hash) 348 | end 349 | 350 | it "recognizes array of attributes with selfclosing tag" do 351 | hash = { 352 | "category/" => [{:@name => "one"}, {:@name => "two"}] 353 | } 354 | expect(to_xml(hash)).to eq('') 355 | end 356 | 357 | context "with :element_form_default set to :qualified and a :namespace" do 358 | it "adds the given :namespace to every element" do 359 | hash = {:first => {"first_name" => "Lucy"}, ":second" => {":first_name": "Anna"}, "v2:third" => {"v2:firstName" => "Danie"}} 360 | result = to_xml hash, element_form_default: :qualified, namespace: :v1 361 | 362 | expect(result).to include( 363 | "Lucy", 364 | "Anna", 365 | "Danie" 366 | ) 367 | end 368 | 369 | it "adds given :namespace to every element in an array" do 370 | hash = {array: [first: "Lucy", second: "Anna"]} 371 | result = to_xml hash, element_form_default: :qualified, namespace: :v1 372 | 373 | expect(result).to include("", "Lucy", "Anna") 374 | end 375 | end 376 | 377 | it "does not remove special keys from the original Hash" do 378 | hash = { 379 | persons: { 380 | first: "Lucy", 381 | second: "Anna", 382 | order!: [:second, :first], 383 | attributes!: {first: {first: true}} 384 | }, 385 | countries: [:de, :us], 386 | order!: [:countries, :persons], 387 | attributes!: {countries: {array: true}} 388 | } 389 | 390 | to_xml(hash) 391 | 392 | expect(hash).to eq({ 393 | persons: { 394 | first: "Lucy", 395 | second: "Anna", 396 | order!: [:second, :first], 397 | attributes!: {first: {first: true}} 398 | }, 399 | countries: [:de, :us], 400 | order!: [:countries, :persons], 401 | attributes!: {countries: {array: true}} 402 | }) 403 | end 404 | end 405 | 406 | it "doesn't modify original hash parameter by deleting its attribute keys" do 407 | hash = {person: {name: "Johnny", surname: "Bravo", "@xsi:type": "People"}} 408 | to_xml(hash) 409 | expect(hash).to eq({person: {name: "Johnny", surname: "Bravo", "@xsi:type": "People"}}) 410 | end 411 | 412 | describe ".explicit_attribute?" do 413 | subject { described_class.explicit_attribute?(key) } 414 | 415 | context "when key starts with an @" do 416 | let(:key) { "@" } 417 | 418 | it { is_expected.to eq(true) } 419 | end 420 | 421 | context "when key does not start with an @" do 422 | let(:key) { "NOT@" } 423 | 424 | it { is_expected.to eq(false) } 425 | end 426 | end 427 | 428 | def to_xml(hash, options = {}) 429 | Gyoku::Hash.to_xml hash, options 430 | end 431 | end 432 | -------------------------------------------------------------------------------- /spec/gyoku/prettifier_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gyoku::Prettifier do 4 | describe "#prettify" do 5 | context "when xml is valid" do 6 | let!(:xml) { Gyoku::Hash.build_xml(test: {pretty: "xml"}) } 7 | 8 | it "returns prettified xml" do 9 | expect(subject.prettify(xml)).to eql("\n xml\n") 10 | end 11 | 12 | context "when indent option is specified" do 13 | it "returns prettified xml with indent" do 14 | options = {indent: 3} 15 | subject = Gyoku::Prettifier.new(options) 16 | expect(subject.prettify(xml)).to eql("\n xml\n") 17 | end 18 | end 19 | 20 | context "when compact option is specified" do 21 | it "returns prettified xml with indent" do 22 | options = {compact: false} 23 | subject = Gyoku::Prettifier.new(options) 24 | expect(subject.prettify(xml)).to eql("\n \n xml\n \n") 25 | end 26 | end 27 | end 28 | 29 | context "when xml is not valid" do 30 | let!(:xml) do 31 | Gyoku::Array.build_xml(["one", "two"], "test") 32 | end 33 | 34 | it "raises an error" do 35 | expect { subject.prettify(xml) }.to raise_error REXML::ParseException 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/gyoku/xml_key_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gyoku::XMLKey do 4 | describe ".create" do 5 | it "removes exclamation marks from the end of a String" do 6 | expect(create("value!")).to eq("value") 7 | end 8 | 9 | it "removes forward slashes from the end of a String" do 10 | expect(create("self-closing/")).to eq("self-closing") 11 | end 12 | 13 | it "does not convert snake_case Strings" do 14 | expect(create("lower_camel_case")).to eq("lower_camel_case") 15 | end 16 | 17 | it "converts snake_case Symbols to lowerCamelCase Strings" do 18 | expect(create(:lower_camel_case)).to eq("lowerCamelCase") 19 | expect(create(:lower_camel_case!)).to eq("lowerCamelCase") 20 | end 21 | 22 | context "when the converter option is set to camelcase" do 23 | it "should replace / with ::, and turn snake case into camel case" do 24 | input = :"hello_world_bob/how_are_you|there:foo^bar" 25 | expected_output = "HelloWorldBob::HowAreYou|there:foo^bar" 26 | expect(create(input, {key_converter: :camelcase})).to eq(expected_output) 27 | end 28 | end 29 | 30 | context "with key_converter" do 31 | it "accepts lambda converters" do 32 | expect(create(:some_text, {key_converter: lambda { |k| k.reverse }})).to eq("txet_emos") 33 | end 34 | 35 | it "convert symbol to the specified type" do 36 | expect(create(:some_text, {key_converter: :camelcase})).to eq("SomeText") 37 | expect(create(:some_text, {key_converter: :upcase})).to eq("SOME_TEXT") 38 | expect(create(:some_text, {key_converter: :none})).to eq("some_text") 39 | end 40 | 41 | it "when key_to_convert is defined, convert only this key" do 42 | options = {key_converter: :camelcase, key_to_convert: "somekey"} 43 | expect(create(:some_key, options)).to eq("someKey") 44 | 45 | options = {key_converter: :camelcase, key_to_convert: "some_key"} 46 | expect(create(:some_key, options)).to eq("SomeKey") 47 | end 48 | 49 | it "when except is defined, dont convert this key" do 50 | options = {key_converter: :camelcase, except: "some_key"} 51 | expect(create(:some_key, options)).to eq("someKey") 52 | end 53 | end 54 | 55 | context "with :element_form_default set to :qualified and a :namespace" do 56 | it "adds the given namespace" do 57 | key = create :qualify, element_form_default: :qualified, namespace: :v1 58 | expect(key).to eq("v1:qualify") 59 | end 60 | 61 | it "does not add the given namespace if the key starts with a colon" do 62 | key = create ":qualify", element_form_default: :qualified, namespace: :v1 63 | expect(key).to eq("qualify") 64 | end 65 | 66 | it "adds a given :namespace after converting the key" do 67 | key = create :username, element_form_default: :qualified, namespace: :v1, key_converter: :camelcase 68 | expect(key).to eq("v1:Username") 69 | end 70 | end 71 | end 72 | 73 | def create(key, options = {}) 74 | Gyoku::XMLKey.create(key, options) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/gyoku/xml_value_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gyoku::XMLValue do 4 | describe ".create" do 5 | context "for DateTime objects" do 6 | it "returns an xs:dateTime compliant String" do 7 | expect(create(DateTime.new(2012, 3, 22, 16, 22, 33))).to eq("2012-03-22T16:22:33+00:00") 8 | end 9 | end 10 | 11 | context "for Date objects" do 12 | it "returns an xs:date compliant String" do 13 | expect(create(Date.new(2012, 3, 22))).to eq("2012-03-22") 14 | end 15 | end 16 | 17 | context "for Time objects" do 18 | it "returns an xs:time compliant String" do 19 | expect(create(Time.local(2012, 3, 22, 16, 22, 33))).to eq("16:22:33") 20 | end 21 | end 22 | 23 | it "returns the String value and escapes special characters" do 24 | expect(create("string")).to eq("string") 25 | expect(create("")).to eq("<tag>") 26 | expect(create("at&t")).to eq("at&t") 27 | expect(create('"quotes"')).to eq(""quotes"") 28 | end 29 | 30 | it "returns the String value without escaping special characters" do 31 | expect(create("", false)).to eq("") 32 | end 33 | 34 | it "returns an xs:dateTime compliant String for Objects responding to #to_datetime" do 35 | singleton = Object.new 36 | def singleton.to_datetime 37 | DateTime.new 2012, 3, 22, 16, 22, 33 38 | end 39 | 40 | expect(create(singleton)).to eq("2012-03-22T16:22:33+00:00") 41 | end 42 | 43 | it "calls Proc objects and converts their return value" do 44 | object = lambda { DateTime.new 2012, 3, 22, 16, 22, 33 } 45 | expect(create(object)).to eq("2012-03-22T16:22:33+00:00") 46 | end 47 | 48 | it "hash objects get converted to xml" do 49 | object = {document!: {"@version" => "2.0", :content! => {key!: "value", other_key: {"@attribute" => "value", :content! => {key: "value"}}}}} 50 | expect(create(object)).to eq("valuevalue") 51 | end 52 | 53 | it "calls #to_s unless the Object responds to #to_datetime" do 54 | expect(create("value")).to eq("value") 55 | end 56 | end 57 | 58 | def create(object, escape_xml = true) 59 | Gyoku::XMLValue.create object, escape_xml 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/gyoku_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gyoku do 4 | describe ".xml_tag" do 5 | it "translates Symbols to lowerCamelCase by default" do 6 | tag = Gyoku.xml_tag(:user_name) 7 | expect(tag).to eq("userName") 8 | end 9 | 10 | it "does not translate Strings" do 11 | tag = Gyoku.xml_tag("user_name") 12 | expect(tag).to eq("user_name") 13 | end 14 | 15 | it "translates Symbols by a given key_converter" do 16 | tag = Gyoku.xml_tag(:user_name, key_converter: :upcase) 17 | expect(tag).to eq("USER_NAME") 18 | end 19 | 20 | it "does not translates Strings with a given key_converter" do 21 | tag = Gyoku.xml_tag("user_name", key_converter: :upcase) 22 | expect(tag).to eq("user_name") 23 | end 24 | end 25 | 26 | describe ".xml" do 27 | it "translates a given Hash to XML" do 28 | hash = {id: 1} 29 | xml = Gyoku.xml(hash, element_form_default: :qualified) 30 | 31 | expect(xml).to eq("1") 32 | end 33 | 34 | it "accepts a key_converter for the Hash keys" do 35 | hash = {user_name: "finn", pass_word: "secret"} 36 | xml = Gyoku.xml(hash, {key_converter: :upcase}) 37 | 38 | expect(xml).to include("finn") 39 | expect(xml).to include("secret") 40 | end 41 | 42 | it "don't converts Strings keys" do 43 | hash = {:user_name => "finn", "pass_word" => "secret"} 44 | xml = Gyoku.xml(hash, {key_converter: :upcase}) 45 | 46 | expect(xml).to include("finn") 47 | expect(xml).to include("secret") 48 | end 49 | 50 | it "when defined key_to_convert only convert this key" do 51 | hash = {user_name: "finn", pass_word: "secret"} 52 | options = {key_converter: :upcase, key_to_convert: "user_name"} 53 | xml = Gyoku.xml(hash, options) 54 | 55 | expect(xml).to include("finn") 56 | expect(xml).to include("secret") 57 | end 58 | 59 | it "accepts key_converter for nested hash" do 60 | hash = {user: {user_name: "finn", pass_word: "secret"}} 61 | xml = Gyoku.xml(hash, {key_converter: :upcase}) 62 | 63 | expect(xml).to include("finn") 64 | expect(xml).to include("secret") 65 | end 66 | 67 | it "does not modify the original Hash" do 68 | hash = { 69 | person: { 70 | first_name: "Lucy", 71 | last_name: "Sky", 72 | order!: [:first_name, :last_name] 73 | }, 74 | attributes!: {person: {id: "666"}} 75 | } 76 | original_hash = hash.dup 77 | 78 | Gyoku.xml(hash) 79 | expect(original_hash).to eq(hash) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.setup(:default, :development) 3 | 4 | unless RUBY_PLATFORM.match?(/java/) 5 | require "simplecov" 6 | require "coveralls" 7 | 8 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 9 | SimpleCov.start do 10 | add_filter "spec" 11 | end 12 | end 13 | 14 | require "gyoku" 15 | require "rspec" 16 | --------------------------------------------------------------------------------