gyoku-1.4.0/0000755000004100000410000000000014235144521012703 5ustar www-datawww-datagyoku-1.4.0/.rspec0000644000004100000410000000001014235144521014007 0ustar www-datawww-data--color gyoku-1.4.0/README.md0000644000004100000410000002053214235144521014164 0ustar www-datawww-data# Gyoku Gyoku translates Ruby Hashes to XML. ``` ruby Gyoku.xml(:find_user => { :id => 123, "v1:Key" => "api" }) # => "123api" ``` [![Build status](https://github.com/savonrb/gyoku/actions/workflows/ci.yml/badge.svg)](https://github.com/savonrb/gyoku/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/gyoku.svg)](http://badge.fury.io/rb/gyoku) [![Code Climate](https://codeclimate.com/github/savonrb/gyoku.svg)](https://codeclimate.com/github/savonrb/gyoku) [![Coverage Status](https://coveralls.io/repos/savonrb/gyoku/badge.svg?branch=master)](https://coveralls.io/r/savonrb/gyoku) ## Installation Gyoku is available through [Rubygems](http://rubygems.org/gems/gyoku) and can be installed via: ``` bash $ gem install gyoku ``` or add it to your Gemfile like this: ``` ruby gem 'gyoku', '~> 1.0' ``` ## Hash keys Hash key Symbols are converted to lowerCamelCase Strings. ``` ruby Gyoku.xml(:lower_camel_case => "key") # => "key" ``` You can change the default conversion formula to `:camelcase`, `:upcase` or `:none`. Note that options are passed as a second Hash to the `.xml` method. ``` ruby Gyoku.xml({ :camel_case => "key" }, { :key_converter => :camelcase }) # => "key" ``` Custom key converters. You can use a lambda/Proc to provide customer key converters. This is a great way to leverage active support inflections for domain specific acronyms. ``` ruby # Use camelize lower which will hook into active support if installed. Gyoku.xml({ acronym_abc: "value" }, key_converter: lambda { |key| key.camelize(:lower) }) # => "value" ``` Hash key Strings are not converted and may contain namespaces. ``` ruby Gyoku.xml("XML" => "key") # => "key" ``` ## Hash values * DateTime objects are converted to xs:dateTime Strings * Objects responding to :to_datetime (except Strings) are converted to xs:dateTime Strings * TrueClass and FalseClass objects are converted to "true" and "false" Strings * NilClass objects are converted to xsi:nil tags * These conventions are also applied to the return value of objects responding to :call * All other objects are converted to Strings using :to_s ## Array values Array items are by default wrapped with the containiner tag, which may be unexpected. ``` ruby > Gyoku.xml({languages: [{language: 'ruby'},{language: 'java'}]}) # => "rubyjava" ``` You can set the `unwrap` option to remove this behavior. ``` ruby > Gyoku.xml({languages: [{language: 'ruby'},{language: 'java'}]}, { unwrap: true}) # => "rubyjava" ``` ## Special characters Gyoku escapes special characters unless the Hash key ends with an exclamation mark. ``` ruby Gyoku.xml(:escaped => "", :not_escaped! => "") # => "<tag />" ``` ## Self-closing tags Hash Keys ending with a forward slash create self-closing tags. ``` ruby Gyoku.xml(:"self_closing/" => "", "selfClosing/" => nil) # => "" ``` ## Sort XML tags In case you need the XML tags to be in a specific order, you can specify the order through an additional Array stored under the `:order!` key. ``` ruby Gyoku.xml(:name => "Eve", :id => 1, :order! => [:id, :name]) # => "1Eve" ``` ## XML attributes Adding XML attributes is rather ugly, but it can be done by specifying an additional Hash stored under the`:attributes!` key. ``` ruby Gyoku.xml(:person => "Eve", :attributes! => { :person => { :id => 1 } }) # => "Eve" ``` ## Explicit XML Attributes In addition to using the `:attributes!` key, you may also specify attributes through keys beginning with an "@" sign. Since you'll need to set the attribute within the hash containing the node's contents, a `:content!` key can be used to explicity set the content of the node. The `:content!` value may be a String, Hash, or Array. This is particularly useful for self-closing tags. **Using :attributes!** ``` ruby Gyoku.xml( "foo/" => "", :attributes! => { "foo/" => { "bar" => "1", "biz" => "2", "baz" => "3" } } ) # => "" ``` **Using "@" keys and ":content!"** ``` ruby Gyoku.xml( "foo/" => { :@bar => "1", :@biz => "2", :@baz => "3", :content! => "" }) # => "" ``` **Example using "@" to get Array of parent tags each with @attributes & :content!** ``` ruby Gyoku.xml( "foo" => [ {:@name => "bar", :content! => 'gyoku'} {:@name => "baz", :@some => "attr", :content! => 'rocks!'} ]) # => "gyokurocks!" ``` Unwrapping Arrays. You can specify an optional `unwrap` argument to modify the default Array behavior. `unwrap` accepts a boolean flag (false by default) or an Array whitelist of keys to unwrap. ``` ruby # Default Array behavior Gyoku.xml({ "foo" => [ {:is => 'great' }, {:is => 'awesome'} ] }) # => "greatawesome" # Unwrap Array behavior Gyoku.xml({ "foo" => [ {:is => 'great' }, {:is => 'awesome'} ] }, unwrap: true) # => "greatawesome" # Unwrap Array, whitelist. # foo is not unwrapped, bar is. Gyoku.xml({ "foo" => [ {:is => 'great' }, {:is => 'awesome'} ], "bar" => [ {:is => 'rad' }, {:is => 'cool'} ] }, unwrap: [:bar]) # => "greatawesomeradcool" ``` Naturally, it would ignore :content! if tag is self-closing: ``` ruby Gyoku.xml( "foo/" => [ {:@name => "bar", :content! => 'gyoku'} {:@name => "baz", :@some => "attr", :content! => 'rocks!'} ]) # => "" ``` This seems a bit more explicit with the attributes rather than having to maintain a hash of attributes. For backward compatibility, `:attributes!` will still work. However, "@" keys will override `:attributes!` keys if there is a conflict. ``` ruby Gyoku.xml(:person => {:content! => "Adam", :@id! => 0}) # => "Adam" ``` **Example with ":content!", :attributes! and "@" keys** ``` ruby Gyoku.xml({ :subtitle => { :@lang => "en", :content! => "It's Godzilla!" }, :attributes! => { :subtitle => { "lang" => "jp" } } } # => "It's Godzilla!" ``` 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 ``` gyoku-1.4.0/spec/0000755000004100000410000000000014235144521013635 5ustar www-datawww-datagyoku-1.4.0/spec/gyoku_spec.rb0000644000004100000410000000474214235144521016341 0ustar www-datawww-datarequire "spec_helper" describe Gyoku do describe ".xml_tag" do it "translates Symbols to lowerCamelCase by default" do tag = Gyoku.xml_tag(:user_name) expect(tag).to eq("userName") end it "does not translate Strings" do tag = Gyoku.xml_tag("user_name") expect(tag).to eq("user_name") end it "translates Symbols by a given key_converter" do tag = Gyoku.xml_tag(:user_name, :key_converter => :upcase) expect(tag).to eq("USER_NAME") end it "does not translates Strings with a given key_converter" do tag = Gyoku.xml_tag("user_name", :key_converter => :upcase) expect(tag).to eq("user_name") end end describe ".xml" do it "translates a given Hash to XML" do hash = { :id => 1 } xml = Gyoku.xml(hash, :element_form_default => :qualified) expect(xml).to eq("1") end it "accepts a key_converter for the Hash keys" do hash = { :user_name => "finn", :pass_word => "secret" } xml = Gyoku.xml(hash, {key_converter: :upcase}) expect(xml).to include("finn") expect(xml).to include("secret") end it "don't converts Strings keys" do hash = { :user_name => "finn", "pass_word" => "secret" } xml = Gyoku.xml(hash, {key_converter: :upcase}) expect(xml).to include("finn") expect(xml).to include("secret") end it "when defined key_to_convert only convert this key" do hash = { user_name: "finn", pass_word: "secret" } options = {key_converter: :upcase, key_to_convert: 'user_name'} xml = Gyoku.xml(hash, options) expect(xml).to include("finn") expect(xml).to include("secret") end it "accepts key_converter for nested hash" do hash = { user: { user_name: "finn", pass_word: "secret" }} xml = Gyoku.xml(hash, {key_converter: :upcase}) expect(xml).to include("finn") expect(xml).to include("secret") end it "does not modify the original Hash" do hash = { :person => { :first_name => "Lucy", :last_name => "Sky", :order! => [:first_name, :last_name] }, :attributes! => { :person => { :id => "666" } } } original_hash = hash.dup Gyoku.xml(hash) expect(original_hash).to eq(hash) end end end gyoku-1.4.0/spec/gyoku/0000755000004100000410000000000014235144521014773 5ustar www-datawww-datagyoku-1.4.0/spec/gyoku/prettifier_spec.rb0000644000004100000410000000226414235144521020513 0ustar www-datawww-datarequire "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 gyoku-1.4.0/spec/gyoku/array_spec.rb0000644000004100000410000001055414235144521017455 0ustar www-datawww-datarequire "spec_helper" describe Gyoku::Array do describe ".to_xml" do it "returns the XML for an Array of Hashes" do array = [{ :name => "adam" }, { :name => "eve" }] result = "adameve" expect(to_xml(array, "user")).to eq(result) end it "returns the XML for an Array of Hashes unwrapped" do array = [{ :name => "adam" }, { :name => "eve" }] result = "adameve" expect(to_xml(array, "user", true, {}, :unwrap => true)).to eq(result) end it "returns the XML for an Array of different Objects" do array = [:symbol, "string", 123] result = "symbolstring123" expect(to_xml(array, "value")).to eq(result) end it "defaults to escape special characters" do array = ["", "adam & eve"] result = "<tag />adam & eve" expect(to_xml(array, "value")).to eq(result) end it "does not escape special characters when told to" do array = ["", "adam & eve"] result = "adam & eve" expect(to_xml(array, "value", false)).to eq(result) end it "adds attributes to a given tag" do array = ["adam", "eve"] result = 'adameve' expect(to_xml(array, "value", :escape_xml, :active => true)).to eq(result) end it "adds attributes to tags when :unwrap is true" do array = [{:item=>"abc"}] key = "items" escape_xml = :escape_xml attributes = { "amount"=>"1" } options = { :unwrap => true } result = "abc" expect(to_xml(array, key, escape_xml, attributes, options)).to eq result end it "adds attributes to duplicate tags" do array = ["adam", "eve"] result = 'adameve' expect(to_xml(array, "value", :escape_xml, :id => [1, 2])).to eq(result) end it "skips attribute for element without attributes if there are fewer attributes than elements" do array = ["adam", "eve", "serpent"] result = 'adameveserpent' expect(to_xml(array, "value", :escape_xml, :id => [1, 2])).to eq(result) end it "handles nested Arrays" do array = [["one", "two"]] result = "onetwo" 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) Gyoku::Array.to_xml *args end end gyoku-1.4.0/spec/gyoku/xml_key_spec.rb0000644000004100000410000000545314235144521020011 0ustar www-datawww-datarequire "spec_helper" describe Gyoku::XMLKey do describe ".create" do it "removes exclamation marks from the end of a String" do expect(create("value!")).to eq("value") end it "removes forward slashes from the end of a String" do expect(create("self-closing/")).to eq("self-closing") end it "does not convert snake_case Strings" do expect(create("lower_camel_case")).to eq("lower_camel_case") end it "converts snake_case Symbols to lowerCamelCase Strings" do expect(create(:lower_camel_case)).to eq("lowerCamelCase") expect(create(:lower_camel_case!)).to eq("lowerCamelCase") end context "when the converter option is set to camelcase" do it "should replace / with ::, and turn snake case into camel case" do input = "hello_world_bob/how_are_you|there:foo^bar".to_sym expected_output = "HelloWorldBob::HowAreYou|there:foo^bar" expect(create(input, {key_converter: :camelcase})).to eq(expected_output) end end context "with key_converter" do it "accepts lambda converters" do expect(create(:some_text, {key_converter: lambda { |k| k.reverse }})).to eq("txet_emos") end it "convert symbol to the specified type" do expect(create(:some_text, {key_converter: :camelcase})).to eq("SomeText") expect(create(:some_text, {key_converter: :upcase})).to eq("SOME_TEXT") expect(create(:some_text, {key_converter: :none})).to eq("some_text") end it "when key_to_convert is defined, convert only this key" do options = {key_converter: :camelcase, key_to_convert: 'somekey'} expect(create(:some_key, options)).to eq("someKey") options = {key_converter: :camelcase, key_to_convert: 'some_key'} expect(create(:some_key, options)).to eq("SomeKey") end it "when except is defined, dont convert this key" do options = {key_converter: :camelcase, except: 'some_key'} expect(create(:some_key, options)).to eq("someKey") end end context "with :element_form_default set to :qualified and a :namespace" do it "adds the given namespace" do key = create :qualify, :element_form_default => :qualified, :namespace => :v1 expect(key).to eq("v1:qualify") end it "does not add the given namespace if the key starts with a colon" do key = create ":qualify", :element_form_default => :qualified, :namespace => :v1 expect(key).to eq("qualify") end it "adds a given :namespace after converting the key" do key = create :username, :element_form_default => :qualified, :namespace => :v1, :key_converter => :camelcase expect(key).to eq("v1:Username") end end end def create(key, options = {}) Gyoku::XMLKey.create(key, options) end end gyoku-1.4.0/spec/gyoku/xml_value_spec.rb0000644000004100000410000000410614235144521020327 0ustar www-datawww-datarequire "spec_helper" describe Gyoku::XMLValue do describe ".create" do context "for DateTime objects" do it "returns an xs:dateTime compliant String" do expect(create(DateTime.new(2012, 03, 22, 16, 22, 33))).to eq("2012-03-22T16:22:33+00:00") end end context "for Date objects" do it "returns an xs:date compliant String" do expect(create(Date.new(2012, 03, 22))).to eq("2012-03-22") end end context "for Time objects" do it "returns an xs:time compliant String" do expect(create(Time.local(2012, 03, 22, 16, 22, 33))).to eq("16:22:33") end end it "returns the String value and escapes special characters" do expect(create("string")).to eq("string") expect(create("")).to eq("<tag>") expect(create("at&t")).to eq("at&t") expect(create('"quotes"')).to eq(""quotes"") end it "returns the String value without escaping special characters" do expect(create("", false)).to eq("") end it "returns an xs:dateTime compliant String for Objects responding to #to_datetime" do singleton = Object.new def singleton.to_datetime DateTime.new 2012, 03, 22, 16, 22, 33 end expect(create(singleton)).to eq("2012-03-22T16:22:33+00:00") end it "calls Proc objects and converts their return value" do object = lambda { DateTime.new 2012, 03, 22, 16, 22, 33 } expect(create(object)).to eq("2012-03-22T16:22:33+00:00") end it "hash objects get converted to xml" do object = { document!: { "@version" => "2.0", content!: { key!: "value", other_key: { "@attribute" => 'value', content!: { key: "value" } } } } } expect(create(object)).to eq("valuevalue") end it "calls #to_s unless the Object responds to #to_datetime" do expect(create("value")).to eq("value") end end def create(object, escape_xml = true) Gyoku::XMLValue.create object, escape_xml end end gyoku-1.4.0/spec/gyoku/hash_spec.rb0000644000004100000410000003631714235144521017267 0ustar www-datawww-datarequire "spec_helper" describe Gyoku::Hash do describe ".to_xml" do describe "returns SOAP request compatible XML" do it "for a simple Hash" do expect(to_xml(:some => "user")).to eq("user") end it "for a nested Hash" do expect(to_xml(:some => { :new => "user" })).to eq("user") end context "with key_converter" do it "expect all keys change" do expect(to_xml({:some => { :new => "user" }}, {key_converter: :camelcase})).to eq("user") end it "and key_to_convert option should change only key" do hash = {:some => { :new => "user", :age => 20 }} options = {key_converter: :camelcase, key_to_convert: "some"} result = "user20" expect(to_xml(hash, options)).to eq(result) hash = {:some => { :new => "user", :age => 20 }} options = {key_converter: :camelcase, key_to_convert: "new"} result = "user20" expect(to_xml(hash, options)).to eq(result) end it "with except option, dont convert this key" do hash = {:some => { :new => "user", :age => 20 }} options = {key_converter: :camelcase, except: "some"} result = "user20" expect(to_xml(hash, options)).to eq(result) end end it "for a Hash with multiple keys" do expect(to_xml(:all => "users", :before => "whatever")).to include( "users", "whatever" ) end it "for a Hash containing an Array" do expect(to_xml(:some => ["user", "gorilla"])).to eq("usergorilla") end it "for a Hash containing an Array of Hashes" do 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 expect(to_xml(:find_or_create => "user")).to eq("user") end it "does not convert Hash key Strings" do expect(to_xml("find_or_create" => "user")).to eq("user") end it "converts DateTime objects to xs:dateTime compliant Strings" do expect(to_xml(:before => DateTime.new(2012, 03, 22, 16, 22, 33))). to eq("2012-03-22T16:22:33+00:00") end it "converts Objects responding to to_datetime to xs:dateTime compliant Strings" do singleton = Object.new def singleton.to_datetime DateTime.new(2012, 03, 22, 16, 22, 33) end expect(to_xml(:before => singleton)).to eq("2012-03-22T16:22:33+00:00") end it "calls to_s on Strings even if they respond to to_datetime" do singleton = "gorilla" def singleton.to_datetime DateTime.new(2012, 03, 22, 16, 22, 33) end expect(to_xml(:name => singleton)).to eq("gorilla") end it "properly serializes nil values" do expect(to_xml(:some => nil)).to eq('') end it "creates self-closing tags for Hash keys ending with a forward slash" do expect(to_xml("self-closing/" => nil)).to eq('') end it "calls to_s on any other Object" do [666, true, false].each do |object| expect(to_xml(:some => object)).to eq("#{object}") end end it "defaults to escape special characters" do result = to_xml(:some => { :nested => "" }, :tag => "") expect(result).to include("<tag />") expect(result).to include("<tag />") end it "does not escape special characters for keys marked with an exclamation mark" do result = to_xml(:some => { :nested! => "" }, :tag! => "") expect(result).to include("") expect(result).to include("") end it "preserves the order of Hash keys and values specified through :order!" do hash = { :find_user => { :name => "Lucy", :id => 666, :order! => [:id, :name] } } result = "666Lucy" expect(to_xml(hash)).to eq(result) hash = { :find_user => { :mname => "in the", :lname => "Sky", :fname => "Lucy", :order! => [:fname, :mname, :lname] } } result = "Lucyin theSky" expect(to_xml(hash)).to eq(result) end it "preserves the order of Hash keys and values specified through 'order!' (as a string key)" do hash = { :find_user => { :name => "Lucy", :id => 666, 'order!' => [:id, :name] } } result = "666Lucy" expect(to_xml(hash)).to eq(result) hash = { :find_user => { :mname => "in the", :lname => "Sky", :fname => "Lucy", 'order!' => [:fname, :mname, :lname] } } result = "Lucyin theSky" expect(to_xml(hash)).to eq(result) end it "uses :order! symbol values for ordering but leaves the string key 'order!' if both are present" do hash = { :find_user => { :name => "Lucy", :id => 666, 'order!' => 'value', :order! => [:id, :name, 'order!'] } } result = "666Lucyvalue" expect(to_xml(hash)).to eq(result) end it "raises if the :order! Array is missing Hash keys" do hash = { :name => "Lucy", :id => 666, :order! => [:name] } expect { to_xml(hash) }.to raise_error(ArgumentError, "Missing elements in :order! [:id]") end it "raises if the :order! Array contains missing Hash keys" do hash = { :by_name => { :first_name => "Lucy", :last_name => "Sky", :order! => [:first_name, :middle_name, :last_name] } } expect { to_xml(hash) }.to raise_error(ArgumentError, "Spurious elements in :order! [:middle_name]") end it "adds attributes to Hash keys specified through :attributes!" do hash = { :find_user => { :person => "Lucy", :attributes! => { :person => { :id => 666 } } } } result = 'Lucy' expect(to_xml(hash)).to eq(result) hash = { :find_user => { :person => "Lucy", :attributes! => { :person => { :id => 666, :city => "Hamburg" } } } } expect(to_xml(hash)).to include('id="666"', 'city="Hamburg"') end it "adds attributes to duplicate Hash keys specified through :attributes!" do hash = { :find_user => { :person => ["Lucy", "Anna"], :attributes! => { :person => { :id => [1, 3] } } } } result = 'LucyAnna' expect(to_xml(hash)).to eq(result) hash = { :find_user => { :person => ["Lucy", "Anna"], :attributes! => { :person => { :active => "true" } } } } result = 'LucyAnna' expect(to_xml(hash)).to eq(result) end it "skips attribute for element without attributes if there are fewer attributes than elements" do hash = { :find_user => { :person => ["Lucy", "Anna", "Beth"], :attributes! => { :person => { :id => [1, 3] } } } } result = 'LucyAnnaBeth' expect(to_xml(hash)).to eq(result) end it "adds attributes to self-closing tags" do hash = { "category/" => "", :attributes! => { "category/" => { :id => 1 } } } expect(to_xml(hash)).to eq('') end it "recognizes @attribute => value along :attributes!" do hash = { "category" => { :content! => "users", :@id => 1 } } expect(to_xml(hash)).to eq('users') end it "recognizes @attribute => value along :attributes! in selfclosed tags" do hash = { "category/" => { :@id => 1 } } expect(to_xml(hash)).to eq('') end it ":@attribute => value takes over :attributes!" do hash = { "category/" => { :@id => 1 }, :attributes! => { "category/" => { 'id' => 2, # will be ignored 'type' => 'admins' } } } # attribute order is undefined expect(['','']).to include to_xml(hash) # with symbols hash = { "category/" => { :@id => 1 }, :attributes! => { "category/" => { :id => 2, # will be ignored :type => 'admins' } } } expect(['','']).to include to_xml(hash) end it "recognizes :content! => value as tag content" do hash = { "category" => { :content! => "users" } } expect(to_xml(hash)).to eq("users") end it "recognizes :content! => value as tag content with value Fixnum" do hash = { "category" => { :content! => 666 } } expect(to_xml(hash)).to eq("666") end it "recognizes :content! => value as tag content with value true" do hash = { "category" => { :content! => true } } expect(to_xml(hash)).to eq("true") end it "recognizes :content! => value as tag content with value false" do hash = { "category" => { :content! => false } } expect(to_xml(hash)).to eq("false") end it "recognizes :content! => value as tag content with value DateTime" do hash = { "before" => { :content! => DateTime.new(2012, 03, 22, 16, 22, 33) } } expect(to_xml(hash)).to eq("2012-03-22T16:22:33+00:00") end it "ignores :content! if self-closing mark present" do hash = { "category/" => { :content! => "users" } } expect(to_xml(hash)).to eq("") end it "recognizes array of attributes" do hash = { "category" => [{:@name => 'one'}, {:@name => 'two'}] } expect(to_xml(hash)).to eq('') # issue #31. hash = { :order! => ['foo', 'bar'], 'foo' => { :@foo => 'foo' }, 'bar' => { :@bar => 'bar', 'baz' => { } }, } expect(to_xml(hash)).to eq('') end it "recognizes array of attributes with content in each" do hash = { "foo" => [{:@name => "bar", :content! => 'gyoku'}, {:@name => "baz", :@some => "attr", :content! => 'rocks!'}] } expect([ 'gyokurocks!', 'gyokurocks!' ]).to include to_xml(hash) end it "recognizes array of attributes but ignores content in each if selfclosing" do hash = { "foo/" => [{:@name => "bar", :content! => 'gyoku'}, {:@name => "baz", :@some => "attr", :content! => 'rocks!'}] } expect([ '', '' ]).to include to_xml(hash) end it "recognizes array of attributes with selfclosing tag" do hash = { "category/" => [{:@name => 'one'}, {:@name => 'two'}] } expect(to_xml(hash)).to eq('') end context "with :element_form_default set to :qualified and a :namespace" do it "adds the given :namespace to every element" do hash = { :first => { "first_name" => "Lucy" }, ":second" => { :":first_name" => "Anna" }, "v2:third" => { "v2:firstName" => "Danie" } } result = to_xml hash, :element_form_default => :qualified, :namespace => :v1 expect(result).to include( "Lucy", "Anna", "Danie" ) end it "adds given :namespace to every element in an array" do hash = { :array => [ :first => "Lucy", :second => "Anna" ]} result = to_xml hash, :element_form_default => :qualified, :namespace => :v1 expect(result).to include("", "Lucy", "Anna") end end it "does not remove special keys from the original Hash" do hash = { :persons => { :first => "Lucy", :second => "Anna", :order! => [:second, :first], :attributes! => { :first => { :first => true } } }, :countries => [:de, :us], :order! => [:countries, :persons], :attributes! => { :countries => { :array => true } } } to_xml(hash) expect(hash).to eq({ :persons => { :first => "Lucy", :second => "Anna", :order! => [:second, :first], :attributes! => { :first => { :first => true } } }, :countries => [:de, :us], :order! => [:countries, :persons], :attributes! => { :countries => { :array => true } } }) end end it "doesn't modify original hash parameter by deleting its attribute keys" do hash = { :person => {:name => "Johnny", :surname => "Bravo", :"@xsi:type" => "People"} } to_xml(hash) expect(hash).to eq({:person=>{:name=>"Johnny", :surname=>"Bravo", :"@xsi:type"=>"People"}}) end def to_xml(hash, options = {}) Gyoku::Hash.to_xml hash, options end end gyoku-1.4.0/spec/spec_helper.rb0000644000004100000410000000042314235144521016452 0ustar www-datawww-datarequire 'bundler' Bundler.setup(:default, :development) unless RUBY_PLATFORM =~ /java/ require 'simplecov' require 'coveralls' SimpleCov.formatter = Coveralls::SimpleCov::Formatter SimpleCov.start do add_filter 'spec' end end require 'gyoku' require 'rspec' gyoku-1.4.0/CHANGELOG.md0000644000004100000410000001201514235144521014513 0ustar www-datawww-data# CHANGELOG ## 1.4.0 (2022-04-01) ### Fixed - Fix [Issue #56](https://github.com/savonrb/gyoku/issue/56) with PR [#57](https://github.com/savonrb/gyoku/pull/57). Thanks, [@jpmoral]! - Avoid circular reference [#69](https://github.com/savonrb/gyoku/pull/69), thanks [@ccarruitero]! ### Added - Unwrap specific keys [#54](https://github.com/savonrb/gyoku/pull/54), by [@rlburkes]. Documented by [@mahemoff]. Thanks to you both! - Add `:pretty_print`, `:indent` and `:compact` options to allow prettified XML output. [#59](https://github.com/savonrb/gyoku/pull/59), by [@Jeiwan]. Thanks! ### Changed - Removed Rubinius support, by [@olleolleolle] - Clean-up, CI setup, and changelog authoring, by [@olleolleolle] [@jpmoral]: https://github.com/jpmoral [@ccarruitero]: https://github.com/ccarruitero [@rlburkes]: https://github.com/rlburkes [@mahemoff]: https://github.com/mahemoff [@Jeiwan]: https://github.com/Jeiwan [@olleolleolle]: https://github.com/olleolleolle ## 1.3.1 (2015-04-05) * Feature: [#53](https://github.com/savonrb/gyoku/pull/53) Improved serialization of hashes nested in arrays. Thanks to @riburkes for this! ## 1.3.0 (2015-03-30) * Formally drop support for ruby 1.8.7 ## 1.2.3 (2015-03-10) * 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. ## 1.2.2 (2014-09-22) * Fixed a bug introduced by making Gyoku threadsafe. Who knew that `$1` and the block variable that `#gsub` provides are not the same? ## 1.2.1 (2014-09-22) * 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. ## 1.2.0 (2014-09-18) * Feature: [#44](https://github.com/savonrb/gyoku/pull/44) support for sorting via :order! with a string key ## 1.1.1 (2014-01-02) * Feature: [#38](https://github.com/savonrb/gyoku/pull/38) support for building nested Arrays * Feature: [#36](https://github.com/savonrb/gyoku/pull/36) allow setting any objects content with :content! * Deprecation: Support for ree and ruby 1.8.7 will be going away soon. ## 1.1.0 (2013-07-26) * Feature: [#30](https://github.com/savonrb/gyoku/pull/30) support for building Arrays of parent tags using @attributes. * Fix: [#21](https://github.com/savonrb/gyoku/pull/21) stop modifying the original Hash. The original issue is [savonrb/savon#410](https://github.com/savonrb/savon/issues/410). ## 1.0.0 (2012-12-17) * Refactoring: Removed the global configuration. This should really only affect the `Gyoku.convert_symbols_to` shortcut which was removed as well. If you're using Gyoku with Savon 2.0, there's now an option for that. If you're using Gyoku on itself, you can pass it the `:key_converter` option instead. ## 0.5.0 (2012-12-15) Feature: [#19](https://github.com/savonrb/gyoku/pull/19) adds support for explicit XML attributes. Feature: [#17](https://github.com/savonrb/gyoku/pull/17) adds an `:upcase` formula. ## 0.4.6 (2012-06-28) * Fix: [#16](https://github.com/rubiii/gyoku/issues/16) Date objects were mapped like DateTime objects. Gyoku.xml(date: Date.today) # => "2012-06-28" * Fix: Time objects were also mapped like DateTime objects. Gyoku.xml(time: sunday) # => "" ## 0.4.5 (2012-05-28) * Fix: [issue 8](https://github.com/rubiii/gyoku/issues/8) - Conflict between camelcase methods in Rails. * Fix: [pull request 15](https://github.com/rubiii/gyoku/pull/15) - Gyoku generates blank attribute values if there are fewer attribute values in attributes! than elements. * Fix: [issue 12](https://github.com/rubiii/gyoku/issues/12) - Don't remove special keys from the original Hash. ## 0.4.4 * Fix: [issue 6](https://github.com/rubiii/gyoku/issues/6) - `Gyoku.xml` does not modify the original Hash. ## 0.4.3 * Fix: Make sure `require "date"` when necessary. ## 0.4.2 * Fix: `Array.to_xml` so that the given :namespace is applied to every element in an Array. ## 0.4.1 * Fix: Alternative formulas and namespaces. ## 0.4.0 * Feature: Added alternative Symbol conversion formulas. You can choose between :lower_camelcase (the default), :camelcase and :none. Gyoku.convert_symbols_to :camelcase You can even define your own formula: Gyoku.convert_symbols_to { |key| key.upcase } ## 0.3.1 * Feature: Gyoku now calls Proc objects and converts their return value. ## 0.3.0 * Feature: Now when all Hash keys need to be namespaced (like with elementFormDefault), you can use options to to trigger this behavior. Gyoku.xml hash, :element_form_default => :qualified, :namespace => :v2 ## 0.2.0 * Feature: Added support for self-closing tags. Hash keys ending with a forward slash (regardless of their value) are now converted to self-closing tags. ## 0.1.1 * Fix: Allow people to use new versions of builder. ## 0.1.0 * Initial version. Gyoku was born as a core extension inside the [Savon](http://rubygems.org/gems/savon) library. gyoku-1.4.0/.gitignore0000644000004100000410000000011614235144521014671 0ustar www-datawww-data.DS_Store .yardoc doc coverage tmp *~ *.swp *.gem .bundle Gemfile.lock .rvmrc gyoku-1.4.0/Rakefile0000644000004100000410000000032514235144521014350 0ustar www-datawww-datarequire "bundler" require "bundler/setup" Bundler::GemHelper.install_tasks require "rspec/core/rake_task" RSpec::Core::RakeTask.new do |t| t.rspec_opts = %w(-c) end task :default => :spec task :test => :spec gyoku-1.4.0/lib/0000755000004100000410000000000014235144521013451 5ustar www-datawww-datagyoku-1.4.0/lib/gyoku.rb0000644000004100000410000000051614235144521015136 0ustar www-datawww-datarequire "gyoku/version" require "gyoku/hash" module Gyoku # Converts a given Hash +key+ with +options+ into an XML tag. def self.xml_tag(key, options = {}) XMLKey.create(key, options) end # Translates a given +hash+ with +options+ to XML. def self.xml(hash, options = {}) Hash.to_xml hash.dup, options end end gyoku-1.4.0/lib/gyoku/0000755000004100000410000000000014235144521014607 5ustar www-datawww-datagyoku-1.4.0/lib/gyoku/version.rb0000644000004100000410000000004514235144521016620 0ustar www-datawww-datamodule Gyoku VERSION = '1.4.0' end gyoku-1.4.0/lib/gyoku/xml_value.rb0000644000004100000410000000174114235144521017133 0ustar www-datawww-datarequire "cgi" require "date" module Gyoku module XMLValue class << self # xs:date format XS_DATE_FORMAT = "%Y-%m-%d" # xs:time format XS_TIME_FORMAT = "%H:%M:%S" # xs:dateTime format XS_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%Z" # Converts a given +object+ to an XML value. def create(object, escape_xml = true, options = {}) if Time === object object.strftime XS_TIME_FORMAT elsif DateTime === object object.strftime XS_DATETIME_FORMAT elsif Date === object object.strftime XS_DATE_FORMAT elsif String === object escape_xml ? CGI.escapeHTML(object) : object elsif object.respond_to?(:to_datetime) create object.to_datetime elsif object.respond_to?(:call) create object.call elsif ::Hash === object Gyoku::Hash.to_xml(object, options) else object.to_s end end end end end gyoku-1.4.0/lib/gyoku/array.rb0000644000004100000410000000631114235144521016253 0ustar www-datawww-datarequire "builder" require "gyoku/prettifier.rb" require "gyoku/xml_value" module Gyoku 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.build_xml(array, key, escape_xml = true, attributes = {}, options = {}) self_closing = options.delete(:self_closing) unwrap = unwrap?(options.fetch(:unwrap, false), key) iterate_with_xml array, key, attributes, options do |xml, item, attrs, index| if self_closing xml.tag!(key, attrs) else case item when ::Hash then if unwrap xml << Hash.to_xml(item, options) else xml.tag!(key, attrs) { xml << Hash.build_xml(item, options) } end when ::Array then xml.tag!(key, attrs) { xml << Array.build_xml(item, NESTED_ELEMENT_NAME) } when NilClass then xml.tag!(key, "xsi:nil" => "true") else xml.tag!(key, attrs) { xml << XMLValue.create(item, escape_xml) } end end end end # 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) xml = Builder::XmlMarkup.new unwrap = unwrap?(options.fetch(:unwrap, false), key) if unwrap xml.tag!(key, attributes) { iterate_array(xml, array, attributes, &block) } else iterate_array(xml, array, attributes, &block) end xml.target! end # 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_array(xml, array, attributes, &block) array.each_with_index do |item, index| if item.respond_to?(:keys) attrs = item.reduce({}) do |st, v| k = v[0].to_s st[k[1..-1]] = v[1].to_s if k =~ /^@/ st end else attrs = {} end yield xml, item, tag_attributes(attributes, index).merge(attrs), index end end # Takes a Hash of +attributes+ and the +index+ for which to return attributes # for duplicate tags. def self.tag_attributes(attributes, index) return {} if attributes.empty? attributes.inject({}) do |hash, (key, value)| value = value[index] if value.kind_of? ::Array value ? hash.merge(key => value) : hash end end def self.unwrap?(unwrap, key) unwrap.kind_of?(::Array) ? unwrap.include?(key.to_sym) : unwrap end end end gyoku-1.4.0/lib/gyoku/hash.rb0000644000004100000410000000735014235144521016064 0ustar www-datawww-datarequire "builder" require "gyoku/prettifier.rb" require "gyoku/array" require "gyoku/xml_key" require "gyoku/xml_value" module Gyoku class Hash # 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.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 # Iterates over a given +hash+ and yields a builder +xml+ instance, the current # Hash +key+ and any XML +attributes+. # # Keys beginning with "@" are treated as explicit attributes for their container. # You can use both :attributes! and "@" keys to specify attributes. # In the event of a conflict, the "@" key takes precedence. def self.iterate_with_xml(hash) xml = Builder::XmlMarkup.new attributes = hash[:attributes!] || {} hash_without_attributes = hash.reject { |key, value| key == :attributes! } order(hash_without_attributes).each do |key| node_attr = attributes[key] || {} # node_attr must be kind of ActiveSupport::HashWithIndifferentAccess node_attr = ::Hash[node_attr.map { |k,v| [k.to_s, v] }] node_value = hash[key].respond_to?(:keys) ? hash[key].clone : hash[key] if node_value.respond_to?(:keys) explicit_keys = node_value.keys.select{|k| k.to_s =~ /^@/ } explicit_attr = {} explicit_keys.each{|k| explicit_attr[k.to_s[1..-1]] = node_value[k]} node_attr.merge!(explicit_attr) explicit_keys.each{|k| node_value.delete(k) } tmp_node_value = node_value.delete(:content!) node_value = tmp_node_value unless tmp_node_value.nil? node_value = "" if node_value.respond_to?(:empty?) && node_value.empty? end yield xml, key, node_value, node_attr end xml.target! end # Deletes and returns an Array of keys stored under the :order! key of a given +hash+. # Defaults to return the actual keys of the Hash if no :order! key could be found. # Raises an ArgumentError in case the :order! Array does not match the Hash keys. def self.order(hash) order = hash[:order!] || hash.delete('order!') hash_without_order = hash.reject { |key, value| key == :order! } order = hash_without_order.keys unless order.kind_of? ::Array # Ignore Explicit Attributes orderable = order.delete_if{|k| k.to_s =~ /^@/ } hashable = hash_without_order.keys.select{|k| !(k.to_s =~ /^@/) } missing, spurious = hashable - orderable, orderable - hashable raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty? raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty? order end end end gyoku-1.4.0/lib/gyoku/xml_key.rb0000644000004100000410000000435714235144521016615 0ustar www-datawww-datamodule Gyoku module XMLKey class << self CAMELCASE = lambda { |key| key.gsub(/\/(.?)/) { |m| "::#{m.split('').last.upcase}" }.gsub(/(?:^|_)(.)/) { |m| m.split('').last.upcase } } LOWER_CAMELCASE = lambda { |key| key[0].chr.downcase + CAMELCASE.call(key)[1..-1] } UPCASE = lambda { |key| key.upcase } FORMULAS = { :lower_camelcase => lambda { |key| LOWER_CAMELCASE.call(key) }, :camelcase => lambda { |key| CAMELCASE.call(key) }, :upcase => lambda { |key| UPCASE.call(key) }, :none => lambda { |key| key } } # Converts a given +object+ with +options+ to an XML key. def create(key, options = {}) xml_key = chop_special_characters key.to_s if unqualified = unqualify?(xml_key) xml_key = xml_key.split(":").last end xml_key = key_converter(options, xml_key).call(xml_key) if Symbol === key if !unqualified && qualify?(options) && !xml_key.include?(":") xml_key = "#{options[:namespace]}:#{xml_key}" end xml_key end private # Returns the formula for converting Symbol keys. def key_converter(options, xml_key) return options[:key_converter] if options[:key_converter].is_a? Proc defined_key = options[:key_to_convert] if (defined_key != nil) && (defined_key == xml_key) key_converter = options[:key_converter] elsif defined_key != nil key_converter = :lower_camelcase elsif (options[:except] == xml_key) key_converter = :lower_camelcase else key_converter = options[:key_converter] || :lower_camelcase end FORMULAS[key_converter] end # Chops special characters from the end of a given +string+. def chop_special_characters(string) ["!", "/"].include?(string[-1, 1]) ? string.chop : string end # Returns whether to remove the namespace from a given +key+. def unqualify?(key) key[0, 1] == ":" end # Returns whether to namespace all keys (elementFormDefault). def qualify?(options) options[:element_form_default] == :qualified && options[:namespace] end end end end gyoku-1.4.0/lib/gyoku/prettifier.rb0000644000004100000410000000127514235144521017316 0ustar www-datawww-datarequire '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 gyoku-1.4.0/gyoku.gemspec0000644000004100000410000000144314235144521015410 0ustar www-datawww-data$:.push File.expand_path("../lib", __FILE__) require "gyoku/version" Gem::Specification.new do |s| s.name = "gyoku" s.version = Gyoku::VERSION s.platform = Gem::Platform::RUBY s.authors = "Daniel Harrington" s.email = "me@rubiii.com" s.homepage = "https://github.com/savonrb/#{s.name}" s.summary = "Translates Ruby Hashes to XML" s.description = "Gyoku translates Ruby Hashes to XML" s.required_ruby_version = '>= 1.9.2' s.license = "MIT" s.add_dependency "builder", ">= 2.1.2" s.add_dependency "rexml", "~> 3.0" s.add_development_dependency "rake" s.add_development_dependency "rspec" s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.require_paths = ["lib"] end gyoku-1.4.0/Gemfile0000644000004100000410000000015614235144521014200 0ustar www-datawww-datasource 'https://rubygems.org' gemspec gem 'simplecov', :require => false gem 'coveralls', :require => false gyoku-1.4.0/.github/0000755000004100000410000000000014235144521014243 5ustar www-datawww-datagyoku-1.4.0/.github/workflows/0000755000004100000410000000000014235144521016300 5ustar www-datawww-datagyoku-1.4.0/.github/workflows/ci.yml0000644000004100000410000000112114235144521017411 0ustar www-datawww-dataname: CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: test: runs-on: ubuntu-latest strategy: matrix: ruby-version: - 3.1 - "3.0" - 2.7 - 2.6 steps: - uses: actions/checkout@v3 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: Run tests run: bundle exec rake gyoku-1.4.0/MIT-LICENSE0000644000004100000410000000204514235144521014340 0ustar www-datawww-dataCopyright (c) 2010 Daniel Harrington Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.