diff --git a/README.md b/README.md index d64345e..3542e3b 100644 --- a/README.md +++ b/README.md @@ -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) +# +# John +# +# Programmer +# +# +``` + +``` ruby +puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, indent: 4) +# +# John +# +# Programmer +# +# +``` + +``` ruby +puts Gyoku.xml({user: { name: 'John', job: { title: 'Programmer' }, :@status => 'active' }}, pretty_print: true, compact: false) +# +# +# John +# +# +# +# Programmer +# +# +# +``` + +**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) +# +# john +# jane +# +``` + +**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) +#johnjane +``` diff --git a/lib/gyoku/array.rb b/lib/gyoku/array.rb index 46dd739..045d397 100644 --- a/lib/gyoku/array.rb +++ b/lib/gyoku/array.rb @@ -1,5 +1,6 @@ require "builder" +require "gyoku/prettifier.rb" require "gyoku/hash" require "gyoku/xml_value" @@ -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 @@ -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 @@ -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) diff --git a/lib/gyoku/hash.rb b/lib/gyoku/hash.rb index 9d7915e..ddd77f1 100644 --- a/lib/gyoku/hash.rb +++ b/lib/gyoku/hash.rb @@ -1,5 +1,6 @@ require "builder" +require "gyoku/prettifier.rb" require "gyoku/array" require "gyoku/xml_key" require "gyoku/xml_value" @@ -7,8 +8,21 @@ 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] != "!" @@ -16,8 +30,8 @@ def self.to_xml(hash, 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) } @@ -25,8 +39,6 @@ def self.to_xml(hash, options = {}) end end - private - # Iterates over a given +hash+ and yields a builder +xml+ instance, the current # Hash +key+ and any XML +attributes+. # diff --git a/lib/gyoku/prettifier.rb b/lib/gyoku/prettifier.rb new file mode 100644 index 0000000..3ca337d --- /dev/null +++ b/lib/gyoku/prettifier.rb @@ -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 diff --git a/spec/gyoku/array_spec.rb b/spec/gyoku/array_spec.rb index 5667274..4caff92 100644 --- a/spec/gyoku/array_spec.rb +++ b/spec/gyoku/array_spec.rb @@ -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 = "\n one\n two\n four\n" + 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 = "\n one\n two\n four\n" + 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 = "\n \n one\n \n \n three \n \n" + 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 = "onetwofour" + expect(to_xml(array, "test", true, {}, options)).to eq(result) + end + end + end end def to_xml(*args) diff --git a/spec/gyoku/hash_spec.rb b/spec/gyoku/hash_spec.rb index 91fa958..3a877f5 100644 --- a/spec/gyoku/hash_spec.rb +++ b/spec/gyoku/hash_spec.rb @@ -52,6 +52,33 @@ expect(to_xml(:some => [{ :new => "user" }, { :old => "gorilla" }])). to eq("usergorilla") 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 = "\n \n John\n admin\n editor\n \n" + 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 = "\n \n John\n \n" + 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 = "\n \n \n John\n \n \n" + expect(to_xml(hash, options)).to eq(result) + end + end + end end it "converts Hash key Symbols to lowerCamelCase" do diff --git a/spec/gyoku/prettifier_spec.rb b/spec/gyoku/prettifier_spec.rb new file mode 100644 index 0000000..467035e --- /dev/null +++ b/spec/gyoku/prettifier_spec.rb @@ -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("\n xml\n") + 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("\n xml\n") + 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("\n \n xml\n \n") + 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