├── .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 | [](https://github.com/savonrb/gyoku/actions/workflows/ci.yml)
11 | [](http://badge.fury.io/rb/gyoku)
12 | [](https://codeclimate.com/github/savonrb/gyoku)
13 | [](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 |
--------------------------------------------------------------------------------