Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,63 @@ The example above shows an example of how you can use all three at the same time

Notice that we have the attribute "lang" defined twice.
The `@lang` value takes precedence over the `:attribute![:subtitle]["lang"]` value.

## Pretty Print

You can prettify the output XML to make it more readable. Use these options:
* `pretty_print` – controls pretty mode (default: `false`)
* `indent` – specifies indentation in spaces (default: `2`)
* `compact` – controls compact mode (default: `true`)

**This feature is not available for XML documents generated from arrays with unwrap option set to false as such documents are not valid**

**Examples**

``` ruby
puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true)
#<user status='active'>
# <name>John</name>
# <job>
# <title>Programmer</title>
# </job>
#</user>
```

``` ruby
puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, indent: 4)
#<user status='active'>
# <name>John</name>
# <job>
# <title>Programmer</title>
# </job>
#</user>
```

``` ruby
puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, compact: false)
#<user status='active'>
# <name>
# John
# </name>
# <job>
# <title>
# Programmer
# </title>
# </job>
#</user>
```

**Generate XML from an array with `unwrap` option set to `true`**
``` ruby
puts Gyoku::Array.to_xml(["john", "jane"], "user", true, {}, pretty_print: true, unwrap: true)
#<user>
# <user>john</user>
# <user>jane</user>
#</user>
```

**Generate XML from an array with `unwrap` option unset (`false` by default)**
``` ruby
puts Gyoku::Array.to_xml(["john", "jane"], "user", true, {}, pretty_print: true)
#<user>john</user><user>jane</user>
```
22 changes: 17 additions & 5 deletions lib/gyoku/array.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "builder"

require "gyoku/prettifier.rb"
require "gyoku/hash"
require "gyoku/xml_value"

Expand All @@ -8,9 +9,22 @@ class Array

NESTED_ELEMENT_NAME = "element"

# Builds XML and prettifies it if +pretty_print+ option is set to +true+
def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
xml = build_xml(array, key, escape_xml, attributes, options)

if options[:pretty_print] && options[:unwrap]
Prettifier.prettify(xml, options)
else
xml
end
end

private

# Translates a given +array+ to XML. Accepts the XML +key+ to add the elements to,
# whether to +escape_xml+ and an optional Hash of +attributes+.
def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
def self.build_xml(array, key, escape_xml = true, attributes = {}, options = {})

self_closing = options.delete(:self_closing)
unwrap = options[:unwrap] || false
Expand All @@ -24,10 +38,10 @@ def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
if unwrap
xml << Hash.to_xml(item, options)
else
xml.tag!(key, attrs) { xml << Hash.to_xml(item, options) }
xml.tag!(key, attrs) { xml << Hash.build_xml(item, options) }
end
when ::Array then
xml.tag!(key, attrs) { xml << Array.to_xml(item, NESTED_ELEMENT_NAME) }
xml.tag!(key, attrs) { xml << Array.build_xml(item, NESTED_ELEMENT_NAME) }
when NilClass then
xml.tag!(key, "xsi:nil" => "true")
else
Expand All @@ -37,8 +51,6 @@ def self.to_xml(array, key, escape_xml = true, attributes = {}, options = {})
end
end

private

# Iterates over a given +array+ with a Hash of +attributes+ and yields a builder +xml+
# instance, the current +item+, any XML +attributes+ and the current +index+.
def self.iterate_with_xml(array, key, attributes, options, &block)
Expand Down
22 changes: 17 additions & 5 deletions lib/gyoku/hash.rb
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
require "builder"

require "gyoku/prettifier.rb"
require "gyoku/array"
require "gyoku/xml_key"
require "gyoku/xml_value"

module Gyoku
class Hash

# Translates a given +hash+ with +options+ to XML.
# Builds XML and prettifies it if +pretty_print+ option is set to +true+
def self.to_xml(hash, options = {})
xml = build_xml(hash, options)

if options[:pretty_print]
Prettifier.prettify(xml, options)
else
xml
end
end

private

# Translates a given +hash+ with +options+ to XML.
def self.build_xml(hash, options = {})
iterate_with_xml hash do |xml, key, value, attributes|
self_closing = key.to_s[-1, 1] == "/"
escape_xml = key.to_s[-1, 1] != "!"
xml_key = XMLKey.create key, options

case
when :content! === key then xml << XMLValue.create(value, escape_xml, options)
when ::Array === value then xml << Array.to_xml(value, xml_key, escape_xml, attributes, options.merge(:self_closing => self_closing))
when ::Hash === value then xml.tag!(xml_key, attributes) { xml << Hash.to_xml(value, options) }
when ::Array === value then xml << Array.build_xml(value, xml_key, escape_xml, attributes, options.merge(:self_closing => self_closing))
when ::Hash === value then xml.tag!(xml_key, attributes) { xml << build_xml(value, options) }
when self_closing then xml.tag!(xml_key, attributes)
when NilClass === value then xml.tag!(xml_key, "xsi:nil" => "true")
else xml.tag!(xml_key, attributes) { xml << XMLValue.create(value, escape_xml, options) }
end
end
end

private

# Iterates over a given +hash+ and yields a builder +xml+ instance, the current
# Hash +key+ and any XML +attributes+.
#
Expand Down
29 changes: 29 additions & 0 deletions lib/gyoku/prettifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'rexml/document'

module Gyoku
class Prettifier
DEFAULT_INDENT = 2
DEFAULT_COMPACT = true

attr_accessor :indent, :compact

def self.prettify(xml, options = {})
new(options).prettify(xml)
end

def initialize(options = {})
@indent = options[:indent] || DEFAULT_INDENT
@compact = options[:compact].nil? ? DEFAULT_COMPACT : options[:compact]
end

# Adds intendations and newlines to +xml+ to make it more readable
def prettify(xml)
result = ''
formatter = REXML::Formatters::Pretty.new indent
formatter.compact = compact
doc = REXML::Document.new xml
formatter.write doc, result
result
end
end
end
38 changes: 38 additions & 0 deletions spec/gyoku/array_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,44 @@

expect(to_xml(array, "value")).to eq(result)
end

context "when :pretty_print option is set to true" do
context "when :unwrap option is set to true" do
it "returns prettified xml" do
array = ["one", "two", {"three" => "four"}]
options = { pretty_print: true, unwrap: true }
result = "<test>\n <test>one</test>\n <test>two</test>\n <three>four</three>\n</test>"
expect(to_xml(array, "test", true, {}, options)).to eq(result)
end

context "when :indent option is specified" do
it "returns prettified xml with specified indent" do
array = ["one", "two", {"three" => "four"}]
options = { pretty_print: true, indent: 3, unwrap: true }
result = "<test>\n <test>one</test>\n <test>two</test>\n <three>four</three>\n</test>"
expect(to_xml(array, "test", true, {}, options)).to eq(result)
end
end

context "when :compact option is specified" do
it "returns prettified xml with specified compact mode" do
array = ["one", {"two" => "three"}]
options = { pretty_print: true, compact: false, unwrap: true }
result = "<test>\n <test>\n one\n </test>\n <two>\n three \n </two>\n</test>"
expect(to_xml(array, "test", true, {}, options)).to eq(result)
end
end
end

context "when :unwrap option is not set" do
it "returns non-prettified xml" do
array = ["one", "two", {"three" => "four"}]
options = { pretty_print: true }
result = "<test>one</test><test>two</test><test><three>four</three></test>"
expect(to_xml(array, "test", true, {}, options)).to eq(result)
end
end
end
end

def to_xml(*args)
Expand Down
27 changes: 27 additions & 0 deletions spec/gyoku/hash_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,33 @@
expect(to_xml(:some => [{ :new => "user" }, { :old => "gorilla" }])).
to eq("<some><new>user</new></some><some><old>gorilla</old></some>")
end

context "when :pretty_print option is set to true" do
it "returns prettified xml" do
hash = { some: { user: { name: "John", groups: ["admin", "editor"] } } }
options = { pretty_print: true }
result = "<some>\n <user>\n <name>John</name>\n <groups>admin</groups>\n <groups>editor</groups>\n </user>\n</some>"
expect(to_xml(hash, options)).to eq(result)
end

context "when :indent option is specified" do
it "returns prettified xml with specified indent" do
hash = { some: { user: { name: "John" } } }
options = { pretty_print: true, indent: 4 }
result = "<some>\n <user>\n <name>John</name>\n </user>\n</some>"
expect(to_xml(hash, options)).to eq(result)
end
end

context "when :compact option is specified" do
it "returns prettified xml with specified compact mode" do
hash = { some: { user: { name: "John" } } }
options = { pretty_print: true, compact: false }
result = "<some>\n <user>\n <name>\n John\n </name>\n </user>\n</some>"
expect(to_xml(hash, options)).to eq(result)
end
end
end
end

it "converts Hash key Symbols to lowerCamelCase" do
Expand Down
39 changes: 39 additions & 0 deletions spec/gyoku/prettifier_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
require "spec_helper"

describe Gyoku::Prettifier do
describe "#prettify" do
context "when xml is valid" do
let!(:xml) { Gyoku::Hash.build_xml(test: { pretty: "xml" }) }

it "returns prettified xml" do
expect(subject.prettify(xml)).to eql("<test>\n <pretty>xml</pretty>\n</test>")
end

context "when indent option is specified" do
it "returns prettified xml with indent" do
options = { indent: 3 }
subject = Gyoku::Prettifier.new(options)
expect(subject.prettify(xml)).to eql("<test>\n <pretty>xml</pretty>\n</test>")
end
end

context "when compact option is specified" do
it "returns prettified xml with indent" do
options = { compact: false }
subject = Gyoku::Prettifier.new(options)
expect(subject.prettify(xml)).to eql("<test>\n <pretty>\n xml\n </pretty>\n</test>")
end
end
end

context "when xml is not valid" do
let!(:xml) do
Gyoku::Array.build_xml(["one", "two"], "test")
end

it "raises an error" do
expect{ subject.prettify(xml) }.to raise_error REXML::ParseException
end
end
end
end