Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

version 0.1.0

  • Loading branch information...
commit 2b0ae53d8f66871d09b311fdad418c9c62ad0ff0 0 parents
Ryan Johnson authored
19 .gitignore
... ... @@ -0,0 +1,19 @@
  1 +*.gem
  2 +*.rbc
  3 +*.swp
  4 +.rvmrc
  5 +.bundle
  6 +.config
  7 +.yardoc
  8 +Gemfile.lock
  9 +InstalledFiles
  10 +_yardoc
  11 +coverage
  12 +doc/
  13 +lib/bundler/man
  14 +pkg
  15 +rdoc
  16 +spec/reports
  17 +test/tmp
  18 +test/version_tmp
  19 +tmp
8 CHANGELOG.md
Source Rendered
... ... @@ -0,0 +1,8 @@
  1 +### 0.1.0
  2 +
  3 +* Initial version. born as a replacement of
  4 + [Gyoku](http://www.rubygems.org/gems/gyoku)
  5 + with corrected assumptions about Array values and
  6 + no need for meta tags such as:
  7 + * :order!
  8 + * :attributes!
4 Gemfile
... ... @@ -0,0 +1,4 @@
  1 +source 'https://rubygems.org'
  2 +
  3 +# Specify your gem's dependencies in xml-fu.gemspec
  4 +gemspec
23 LICENSE
... ... @@ -0,0 +1,23 @@
  1 +Copyright (c) 2012 Ryan Johnson
  2 +Copyright (c) 2010 Daniel Harrington (for logic inspired by Gyoku)
  3 +
  4 +MIT License
  5 +
  6 +Permission is hereby granted, free of charge, to any person obtaining
  7 +a copy of this software and associated documentation files (the
  8 +"Software"), to deal in the Software without restriction, including
  9 +without limitation the rights to use, copy, modify, merge, publish,
  10 +distribute, sublicense, and/or sell copies of the Software, and to
  11 +permit persons to whom the Software is furnished to do so, subject to
  12 +the following conditions:
  13 +
  14 +The above copyright notice and this permission notice shall be
  15 +included in all copies or substantial portions of the Software.
  16 +
  17 +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  18 +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  19 +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  20 +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  21 +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  22 +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  23 +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
291 README.md
Source Rendered
... ... @@ -0,0 +1,291 @@
  1 +# Xml::Fu
  2 +
  3 +Convert Ruby Hashes to XML
  4 +
  5 +A hash is meant to be a structured set of data. So is XML. The two are very similar in that they have
  6 +the capability of nesting information within a tree structure. With XML you have nodes. With Hashes, you
  7 +have key/value pairs. The value of an XML node is referenced by its parent's name. A hash value is referenced
  8 +by its key. This basic lesson tells the majority of what you need to know about creating XML via Hashes in
  9 +Ruby using the XmlFu gem.
  10 +
  11 +
  12 +## Installation
  13 +
  14 +Add this line to your application's Gemfile:
  15 +
  16 + gem 'xml-fu'
  17 +
  18 +And then execute:
  19 +
  20 + $ bundle
  21 +
  22 +Or install it yourself as:
  23 +
  24 + $ gem install xml-fu
  25 +
  26 +
  27 +## Hash Keys
  28 +
  29 +Hash keys are translated into XML nodes (whether it be a document node or attribute node).
  30 +
  31 +
  32 +### Translation
  33 +
  34 +With Ruby, a hash key may be a string or a symbol. XmlFu will convert the symbols into an XML safe name
  35 +by lower camel-casing them. So :foo\_bar will become "fooBar". You may change the conversion algorithm to your
  36 +liking by setting the XmlFu.symbol\_conversion\_algorithm to a lambda or proc of your liking.
  37 +
  38 +
  39 +#### Built-In Algorithms
  40 +
  41 +* :lower\_camelcase **(default)**
  42 +* :camelcase
  43 +* :none (result of :sym.to\_s)
  44 +* :snake\_case (alias for :none)
  45 +
  46 +```ruby
  47 +# Built-in Algorithm
  48 +XmlFu.symbol_conversion_algorithm(:camelcase)
  49 +
  50 +XmlFu.xml( :foo => "bar" ) #=> "<foo>bar</foo>"
  51 +XmlFu.xml( "foo" => "bar" ) #=> "<foo>bar</foo>"
  52 +# :foo and "foo" both translate to <foo>
  53 +
  54 +# Custom Algorithm
  55 +XmlFu.symbol_conversion_algorithm {|sym| sym.downcase }
  56 +```
  57 +
  58 +### Types of Nodes
  59 +
  60 +Because there are multiple types of XML nodes, there are also multiple types of keys to denote them.
  61 +
  62 +
  63 +#### Self-Closing Nodes (key/)
  64 +
  65 +By default, XmlFu assumes that all XML nodes will contain closing tags. However, if you want to explicitly
  66 +create a self-closing node, use the following syntax when you define the key.
  67 +
  68 +``` ruby
  69 +XmlFu.xml("foo/" => "bar") #=> <foo/>
  70 +```
  71 +
  72 +One thing to take note of this syntax is that XmlFu will ignore ANY value you throw at it if the key syntax
  73 +denotes a self-closing tag. This is because a self-closing tag cannot have any contents (hence the use for
  74 +a self-closing tag).
  75 +
  76 +
  77 +#### Unescaped Content Nodes (key!)
  78 +
  79 +By default, if you pass a pure string as a value, special characters will be escaped to keep the XML compliant.
  80 +If you know that the string is valid XML and can be trusted, you can add the exclamation point to the end of
  81 +the key name to denote that XmlFu should NOT escape special characters in the value.
  82 +
  83 +```ruby
  84 +# Default Functionality (Escaped Characters)
  85 +XmlFu.xml("foo" => "<bar/>") #=> "<foo>&lt;bar/&gt;</foo>"
  86 +
  87 +# Unescaped Characters
  88 +XmlFu.xml("foo!" => "<bar/>") #=> "<foo><bar/></foo>"
  89 +```
  90 +
  91 +
  92 +#### Attribute Node (@key)
  93 +
  94 +Yes, the attributes of an XML node are nodes themselves, so we need a way of defining them. Since XPath syntax
  95 +uses @ to denote an attribute, so does XmlFu.
  96 +
  97 +``` ruby
  98 +XmlFu.xml(:agent => {
  99 + "@id" => "007",
  100 + "FirstName" => "James",
  101 + "LastName" => "Bond"
  102 +})
  103 +#=> <agent id="007"><FirstName>James</FirstName><LastName>Bond</LastName></agent>
  104 +```
  105 +
  106 +
  107 +## Hash Values
  108 +
  109 +The value in a key/value pair describes the key/node. Different value types determine the extent of this description.
  110 +
  111 +
  112 +### Simple Values
  113 +
  114 +Simple value types describe the contents of the XML node.
  115 +
  116 +
  117 +#### Strings
  118 +
  119 +``` ruby
  120 +XmlFu.xml( :foo => "bar" ) #=> "<foo>bar</foo>"
  121 +XmlFu.xml( "foo" => "bar" ) #=> "<foo>bar</foo>"
  122 +```
  123 +
  124 +
  125 +#### Numbers
  126 +
  127 +``` ruby
  128 +XmlFu.xml( :foo => 0 ) #=> "<foo>0</foo>"
  129 +XmlFu.xml( :pi => 3.14159 ) #=> "<pi>3.14159</pi>"
  130 +```
  131 +
  132 +
  133 +#### Nil
  134 +
  135 +``` ruby
  136 +XmlFu.xml( :foo => nil ) #=> "<foo xsi:nil=\"true\"/>"
  137 +```
  138 +
  139 +
  140 +### Hashes
  141 +
  142 +Hash are parsed for their translated values prior to returning a XmlFu value.
  143 +
  144 +```ruby
  145 +XmlFu.xml(:foo => {:bar => {:biz => "bang"} })
  146 +#=> "<foo><bar><biz>bang</biz></bar></foo>"
  147 +```
  148 +
  149 +#### Content in Hash (=)
  150 +
  151 +Should you require setting node attributes as well as setting the value of the XML node, you may use the "="
  152 +key in a nested hash to denote explicit content.
  153 +
  154 +```ruby
  155 +XmlFu.xml(:agent => {"@id" => "007", "=" => "James Bond"})
  156 +#=> "<agent id=\"007\">James Bond</agent>"
  157 +```
  158 +
  159 +This key will not get around the self-closing node rule. The only nodes that will be used in this case will be
  160 +attribute nodes and additional content will be ignored.
  161 +
  162 +```ruby
  163 +XmlFu.xml("foo/" => {"@id" => "123", "=" => "You can't see me."})
  164 +#=> "<foo id=\"123\"/>"
  165 +```
  166 +
  167 +
  168 +### Arrays
  169 +
  170 +Since the value in a key/value pair is (for the most part) used as the contents of a key/node, there are some
  171 +assumptions that XmlFu makes when dealing with Array values.
  172 +
  173 +* For a typical key, the contents of the array are considered to be nodes to be contained within the <key> node.
  174 +
  175 +
  176 +#### Array of Hashes
  177 +
  178 +``` ruby
  179 +XmlFu.xml( "SecretAgents" => [
  180 + { "agent/" => { "@id"=>"006", "@name"=>"Alec Trevelyan" } },
  181 + { "agent/" => { "@id"=>"007", "@name"=>"James Bond" } }
  182 +])
  183 +#=> "<SecretAgents><agent name=\"Alec Trevelyan\" id=\"006\"/><agent name=\"James Bond\" id=\"007\"/></SecretAgents>"
  184 +```
  185 +
  186 +
  187 +#### Alternate Array of Hashes (key\*)
  188 +
  189 +There comes a time that you may want to declare the contents of an array as a collection of items denoted by the
  190 +key name. Using the asterisk (also known for multiplication --- hence multiple keys) we denote that we want a
  191 +collection of &lt;key&gt; nodes.
  192 +
  193 +```ruby
  194 +XmlFu.xml( "person*" => ["Bob", "Sheila"] )
  195 +#=> "<person>Bob</person><person>Sheila</person>"
  196 +```
  197 +
  198 +In this case, the value of "person*" is an array of two names. These names are to be the contents of multiple
  199 +&lt;person&gt; nodes and the result is a set of sibling XML nodes with no parent.
  200 +
  201 +How about a more complex example:
  202 +
  203 +```ruby
  204 +XmlFu.xml(
  205 + "person*" => {
  206 + "@foo" => "bar",
  207 + "=" => [
  208 + {"@foo" => "nope", "=" => "Bob"},
  209 + "Sheila"
  210 + ]
  211 + }
  212 +)
  213 +#=> "<person foo=\"nope\">Bob</person><person foo=\"bar\">Sheila</person>"
  214 +```
  215 +
  216 +*This is getting interesting, isn't it?* In this example, we are setting a default "foo" attribute on each of the
  217 +items in the collection of &lt;person&gt; nodes. However, you'll notice that we overwrote the default "foo" with Bob.
  218 +
  219 +
  220 +#### Array of Arrays
  221 +
  222 +Array values are flattened prior to translation, to reduce the need to iterate over nested arrays.
  223 +
  224 +```ruby
  225 +XmlFu.xml(
  226 + :foo => [
  227 + [{"a/" => nil}, {"b/" => nil}],
  228 + {"c/" => nil},
  229 + [
  230 + [{"d/" => nil}, {"e/" => nil}],
  231 + {"f/" => nil}
  232 + ]
  233 + ]
  234 +)
  235 +#=> "<foo><a/><b/><c/><d/><e/><f/></foo>"
  236 +```
  237 +
  238 +:foo in this case, is the parent node of it's contents
  239 +
  240 +
  241 +#### Array of Mixed Types
  242 +
  243 +Since with simple values, you cannot infer the value of their node container purely on their value, simple values
  244 +are currently ignored in arrays and only Hashes are translated.
  245 +
  246 +``` ruby
  247 + "foo" => [
  248 + {:bar => "biz"},
  249 + nil, # ignored
  250 + true, # ignored
  251 + false, # ignored
  252 + 42, # ignored
  253 + 3.14, # ignored
  254 + "simple string", # ignored
  255 + ['another','array','of','values'] # ignored
  256 + ]
  257 + #=> "<foo><bar>biz</bar></foo>"
  258 +```
  259 +
  260 +
  261 +### Cheat Sheet
  262 +
  263 +#### Key
  264 + 1. if key denotes self-closing node (key/)
  265 + * attributes are preserved with Hash values
  266 + * value and "=" values are ignored
  267 + 2. if key denotes collection (key*) with Array value
  268 + * Array is flattened
  269 + * Only Hash and Simple values are translated
  270 + * Hashes may override default attributes set by parent
  271 + * **(applies to Array values only)**
  272 + 3. if key denotes contents (key) with Array value
  273 + * Array is flattened
  274 + * Only Hash items in array are translated
  275 +
  276 +#### Value
  277 + 1. if value is Hash:
  278 + * "@" keys are attributes of the node
  279 + * "=" key can be used in conjunction with any "@" keys to specify content of node
  280 + 3. if value is simple value:
  281 + * it is content of <key> node
  282 + * **unless:** key denotes a self-closing node
  283 +
  284 +
  285 +## Contributing
  286 +
  287 +1. Fork it
  288 +2. Create your feature branch (`git checkout -b my-new-feature`)
  289 +3. Commit your changes (`git commit -am 'Added some feature'`)
  290 +4. Push to the branch (`git push origin my-new-feature`)
  291 +5. Create new Pull Request
2  Rakefile
... ... @@ -0,0 +1,2 @@
  1 +#!/usr/bin/env rake
  2 +require "bundler/gem_tasks"
53 lib/xml-fu.rb
... ... @@ -0,0 +1,53 @@
  1 +#require "xml-fu/version"
  2 +require "xml-fu/hash"
  3 +require "xml-fu/array"
  4 +
  5 +module XmlFu
  6 + class << self
  7 +
  8 + @@infer_simple_value_nodes = false
  9 +
  10 + # Convert construct into XML
  11 + def xml(construct, options={})
  12 + case construct
  13 + when ::Hash then Hash.to_xml( construct.dup, options )
  14 + when ::Array then Array.to_xml( construct.dup, options )
  15 + end
  16 + end#convert
  17 +
  18 + # @todo Add Nori-like parsing capability to convert XML back into XmlFu-compatible Hash/Array
  19 + # Parse XML into array of hashes. If XML used as input contains only sibling nodes, output
  20 + # will be array of hashes corresponding to those sibling nodes.
  21 + #
  22 + # <foo/><bar/> => [{"foo/" => ""}, {"bar/" => ""}]
  23 + #
  24 + # If XML used as input contains a full document with root node, output will be
  25 + # an array of one hash (the root node hash)
  26 + #
  27 + # <foo><bar/><baz/></foo> => [{"foo" => [{"bar/" => ""},{"baz/" => ""}] }]
  28 + def hash(xml, options)
  29 + end
  30 +
  31 + def configure
  32 + yield self
  33 + end
  34 +
  35 + # Set configuration option to be used with future releases
  36 + def infer_simple_value_nodes=(val)
  37 + @@infer_simple_value_nodes = val
  38 + end
  39 +
  40 + # Configuration option to be used with future releases
  41 + # This option should allow for the inferrance of parent node names of simple value types
  42 + #
  43 + # Example:
  44 + # 1 => <Integer>1</Integer>
  45 + # true => <Boolean>true</Boolean>
  46 + #
  47 + # This is disabled by default as it is conflicting with working logic.
  48 + def infer_simple_value_nodes
  49 + return @@infer_simple_value_nodes
  50 + end
  51 +
  52 + end#class<<self
  53 +end#XmlFu
BIN  lib/xml-fu/.FuNode.rb.swp
Binary file not shown
98 lib/xml-fu/array.rb
... ... @@ -0,0 +1,98 @@
  1 +require 'builder'
  2 +require 'xml-fu/hash'
  3 +require 'xml-fu/node'
  4 +
  5 +module XmlFu
  6 +
  7 + # Convert Array to XML String
  8 + class Array
  9 +
  10 + # Custom exception class
  11 + class MissingKeyException < Exception; end
  12 +
  13 + # Convert Array to XML String of sibling XML nodes
  14 + # @param [Array] array
  15 + # @param [Hash] options
  16 + # @option options [String] :content_type Either "collection" or "content". (defaults to "content")
  17 + # @option options [String, Symbol] :key Name of node.
  18 + # @option options [Hash] :attributes Possible hash of attributes to assign to nodes in array.
  19 + # @return String
  20 + def self.to_xml(array, options={})
  21 + each_with_xml(array, options) do |xml, key, item, attributes|
  22 + case options[:content_type].to_s
  23 + when "collection"
  24 + raise(MissingKeyException, "Key name missing for collection") if key.empty?
  25 +
  26 + case
  27 + when key[-1,1] == "/"
  28 + xml << Node.new(key, nil, attributes).to_xml
  29 + when ::Hash === item
  30 + xml.tag!(key, attributes) { xml << Hash.to_xml(item,options) }
  31 + when ::Array === item
  32 + xml << Array.to_xml(item.flatten,options)
  33 + else
  34 + xml << Node.new(key, item, attributes).to_xml
  35 + end
  36 + else
  37 + # Array is content of node rather than collection of node elements
  38 + case
  39 + when ::Hash === item
  40 + xml << Hash.to_xml(item, options)
  41 + when ::Array === item
  42 + xml << Array.to_xml(item, options)
  43 + when XmlFu.infer_simple_value_nodes == true
  44 + xml << infer_node(item, attributes)
  45 + else
  46 + # only act on item if it responds to to_xml
  47 + xml << item.to_xml if item.respond_to?(:to_xml)
  48 + end
  49 + end
  50 + end
  51 + end#self.to_xml
  52 +
  53 + # Future Functionality - VERY ALPHA STAGE!!!
  54 + # @todo Add node inferrance functionality
  55 + # @note Do not use if you want stable functionality
  56 + # @param item Simple Value
  57 + # @param [Hash] attributes Hash of attributes to assign to inferred node.
  58 + def self.infer_node(item, attributes={})
  59 + node_name = case item.class
  60 + when "TrueClass"
  61 + when "FalseClass"
  62 + "Boolean"
  63 + else
  64 + item.class
  65 + end
  66 + Node.new(node_name, item, attributes).to_xml
  67 + end#self.infer_node
  68 +
  69 + # Convenience function to iterate over array items as well as
  70 + # providing a single location for logic
  71 + # @param [Array] arr Array to iterate over
  72 + # @param [Hash] opts Hash of options to pass to the iteration
  73 + def self.each_with_xml(arr, opts={})
  74 + xml = Builder::XmlMarkup.new
  75 +
  76 + arr.each do |item|
  77 + key = opts.fetch(:key, "")
  78 + item_content = item
  79 +
  80 + # Attributes reuires duplicate or child elements will
  81 + # contain attributes of their siblings.
  82 + attributes = (opts[:attributes] ? opts[:attributes].dup : {})
  83 +
  84 + if item.respond_to?(:keys)
  85 + filtered = Hash.filter(item)
  86 + attributes = filtered.last
  87 + item_content = filtered.first
  88 + end
  89 +
  90 + yield xml, key, item_content, attributes
  91 + end
  92 +
  93 + xml.target!
  94 + end#self.each_with_xml
  95 +
  96 + end#Array
  97 +
  98 +end#XmlFu
14 lib/xml-fu/core_ext/string.rb
... ... @@ -0,0 +1,14 @@
  1 +# Redefined parts of the String class to suit our needs in the gem.
  2 +class String
  3 +
  4 + # Returns the string in camelcase (with first character lowercase)
  5 + def lower_camelcase
  6 + self[0].chr.downcase + self.camelcase[1..-1]
  7 + end#lower_camelcase
  8 +
  9 + # Returns the string in camelcase (with the first character uppercase)
  10 + def camelcase
  11 + self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
  12 + end#camelcase
  13 +
  14 +end#String
87 lib/xml-fu/hash.rb
... ... @@ -0,0 +1,87 @@
  1 +require 'builder'
  2 +require 'xml-fu/array'
  3 +require 'xml-fu/node'
  4 +
  5 +module XmlFu
  6 +
  7 + # By definition, a hash is an UNORDERED list of key/value pairs
  8 + # There's no sense in trying to order the keys.
  9 + # If order is of concern, use Array.to_xml
  10 + class Hash
  11 +
  12 + # Convert Hash to XML String
  13 + def self.to_xml(hash, options={})
  14 + each_with_xml hash do |xml, key, value, attributes|
  15 + # Use symbol conversion algorithm to set tag name
  16 + tag_name = ( Symbol === key ?
  17 + XmlFu::Node.symbol_conversion_algorithm.call(key) :
  18 + key.to_s )
  19 +
  20 + case
  21 + when tag_name[-1,1] == "/"
  22 + xml << Node.new(tag_name, nil, attributes).to_xml
  23 + when ::Array === value
  24 + if tag_name[-1,1] == '*'
  25 + options.merge!({
  26 + :content_type => "collection",
  27 + :key => tag_name.chop,
  28 + :attributes => attributes
  29 + })
  30 + # Collection is merely a set of sibling nodes
  31 + xml << Array.to_xml(value.flatten, options)
  32 + else
  33 + # Contents will contain a parent node
  34 + xml.tag!(tag_name, attributes) { xml << Array.to_xml(value, options) }
  35 + end
  36 + when ::Hash === value
  37 + xml.tag!(tag_name, attributes) { xml << Hash.to_xml(value, options) }
  38 + else
  39 + xml << Node.new(tag_name, value, attributes).to_xml
  40 + end
  41 + end
  42 + end#self.to_xml
  43 +
  44 +
  45 + # Class method to filter out attributes and content
  46 + # from a given hash
  47 + def self.filter(hash)
  48 + attribs = {}
  49 + content = hash.dup
  50 +
  51 + content.keys.select{|k| k =~ /^@/ }.each do |k|
  52 + attribs[k[1..-1]] = content.delete(k)
  53 + end
  54 +
  55 + # Use _content value if defined
  56 + content = content.delete("=") || content
  57 +
  58 + return [content, attribs]
  59 + end#self.filter
  60 +
  61 + private
  62 +
  63 + # Provides a convenience function to iterate over the hash
  64 + # Logic will filter out attribute and content keys from hash values
  65 + def self.each_with_xml(hash)
  66 + xml = Builder::XmlMarkup.new
  67 +
  68 + hash.each do |key,value|
  69 + node_value = value
  70 + node_attrs = {}
  71 +
  72 + # yank the attribute keys into their own hash
  73 + if value.respond_to?(:keys)
  74 + filtered = Hash.filter(value)
  75 + node_attrs = filtered.last
  76 + node_value = filtered.first
  77 + end
  78 +
  79 + yield xml, key, node_value, node_attrs
  80 + end
  81 +
  82 + xml.target!
  83 + end#self.each_with_xml
  84 +
  85 + end#Hash
  86 +
  87 +end#XmlFu
143 lib/xml-fu/node.rb
... ... @@ -0,0 +1,143 @@
  1 +require 'builder'
  2 +require 'cgi'
  3 +require 'date'
  4 +
  5 +require 'xml-fu/core_ext/string'
  6 +
  7 +module XmlFu
  8 +
  9 + # Class to contain logic for converting a key/value pair into an XML node
  10 + class Node
  11 +
  12 + # Custom exception class
  13 + class InvalidAttributesException < Exception; end
  14 +
  15 + # xs:dateTime format.
  16 + XS_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
  17 +
  18 + # default set of algorithms to choose from
  19 + ALGORITHMS = {
  20 + :lower_camelcase => lambda { |sym| sym.to_s.lower_camelcase },
  21 + :camelcase => lambda { |sym| sym.to_s.camelcase },
  22 + :snakecase => lambda { |sym| sym.to_s },
  23 + :none => lambda { |sym| sym.to_s }
  24 + }
  25 +
  26 + # Class method for retrieving global Symbol-to-string conversion algorithm
  27 + # @return [lambda]
  28 + def self.symbol_conversion_algorithm
  29 + @symbol_conversion_algorithm ||= ALGORITHMS[:lower_camelcase]
  30 + end#self.symbol_conversion_algorithm
  31 +
  32 + # Class method for setting global Symbol-to-string conversion algorithm
  33 + # @param [lambda] algorithm Should accept a symbol as an argument and return a string
  34 + def self.symbol_conversion_algorithm=(algorithm)
  35 + algorithm = ALGORITHMS[algorithm] unless algorithm.respond_to?(:call)
  36 + raise(ArgumentError, "Invalid symbol conversion algorithm") unless algorithm
  37 + @symbol_conversion_algorithm = algorithm
  38 + end#self.symbol_conversion_algorithm=
  39 +
  40 + attr_accessor :escape_xml
  41 + attr_accessor :self_closing
  42 +
  43 + # Create XmlFu::Node object
  44 + # @param [String, Symbol] name Name of node
  45 + # @param value Simple Value or nil
  46 + # @param [Hash] attributes Optional hash of attributes to apply to XML Node
  47 + def initialize(name, value, attributes={})
  48 + @escape_xml = true
  49 + @self_closing = false
  50 + self.attributes = attributes
  51 + self.value = value
  52 + self.name = name
  53 + end#initialize
  54 +
  55 + attr_reader :attributes
  56 + def attributes=(val)
  57 + if ::Hash === val
  58 + @attributes = val
  59 + else
  60 + raise(InvalidAttributesException, "Attempted to set attributes to non-hash value")
  61 + end
  62 + end
  63 +
  64 + attr_reader :name
  65 + def name=(val)
  66 + use_name = val.dup
  67 +
  68 + use_name = name_parse_special_characters(use_name)
  69 +
  70 + # TODO: Add additional logic that Gyoku XmlKey puts in place
  71 +
  72 + # remove ":" if name begins with ":" (i.e. no namespace)
  73 + use_name = use_name[1..-1] if use_name[0,1] == ":"
  74 +
  75 + if Symbol === val
  76 + use_name = self.class.symbol_conversion_algorithm.call(use_name)
  77 + end
  78 +
  79 + # Set name to remaining value
  80 + @name = "#{use_name}"
  81 + end#name=
  82 +
  83 + # Converts name into proper XML node name
  84 + # @param [String, Symbol] val Raw name
  85 + def name_parse_special_characters(val)
  86 + use_this = val.dup
  87 +
  88 + # Will this be a self closing node?
  89 + if use_this.to_s[-1,1] == '/'
  90 + @self_closing = true
  91 + use_this.chop!
  92 + end
  93 +
  94 + # Will this node contain escaped XML?
  95 + if use_this.to_s[-1,1] == '!'
  96 + @escape_xml = false
  97 + use_this.chop!
  98 + end
  99 +
  100 + # Ensure that we don't have special characters at end of name
  101 + while ["!","/","*"].include?(use_this.to_s[-1,1]) do
  102 + use_this.chop!
  103 + end
  104 +
  105 + return use_this
  106 + end#name_parse_special_characters
  107 +
  108 + # Custom Setter for @value instance method
  109 + def value=(val)
  110 + if DateTime === val || Time === val || Date === val
  111 + @value = val.strftime XS_DATETIME_FORMAT
  112 + elsif val.respond_to?(:to_datetime)
  113 + @value = val.to_datetime
  114 + elsif val.respond_to?(:call)
  115 + @value = val.call
  116 + elsif val.nil?
  117 + @value = nil
  118 + else
  119 + @value = val.to_s
  120 + end
  121 + end#value=
  122 +
  123 + # @return [String, nil]
  124 + # Value can be nil, else it should return a String value.
  125 + def value
  126 + return CGI.escapeHTML(@value) if String === @value && @escape_xml
  127 + return @value
  128 + end#value
  129 +
  130 + # Create XML String from XmlFu::Node object
  131 + def to_xml
  132 + xml = Builder::XmlMarkup.new
  133 + case
  134 + when @self_closing then xml.tag!(@name, @attributes)
  135 + when @value.nil? then xml.tag!(@name, @attributes.merge!("xsi:nil" => "true"))
  136 + else xml.tag!(@name, @attributes) { xml << self.value }
  137 + end
  138 + xml.target!
  139 + end#to_xml
  140 +
  141 + end#Node
  142 +
  143 +end#XmlFu
3  lib/xml-fu/version.rb
... ... @@ -0,0 +1,3 @@
  1 +module XmlFu
  2 + VERSION = "0.1.0"
  3 +end
77 spec/lib/array_spec.rb
... ... @@ -0,0 +1,77 @@
  1 +require 'spec_helper'
  2 +
  3 +describe XmlFu::Array do
  4 +
  5 + describe ".to_xml" do
  6 + #after do
  7 + # XmlFu.infer_simple_value_nodes = false
  8 + #end
  9 +
  10 + #it "should infer simple value type nodes with configuration turned on" do
  11 + # mixed_hash = {
  12 + # "foo" => [
  13 + # {:bar => "biz"},
  14 + # nil,
  15 + # true,
  16 + # false,
  17 + # 3.14,
  18 + # "simple string",
  19 + # ["another","array","of","values"]
  20 + # ]
  21 + # }
  22 + # XmlFu.infer_simple_value_nodes = true
  23 + # XmlFu.xml(mixed_hash).should_not == "<foo><bar>biz</bar></foo>"
  24 + #
  25 + # XmlFu.infer_simple_value_nodes = false
  26 + # XmlFu.xml(mixed_hash).should == "<foo><bar>biz</bar></foo>"
  27 + #
  28 + # XmlFu.infer_simple_value_nodes = false
  29 + #end
  30 +
  31 + it "should flatten nested arrays properly" do
  32 + hash = {
  33 + :foo => [
  34 + [{"a/" => nil}, {"b/" => nil}],
  35 + {"c/" => nil},
  36 + [
  37 + [{"d/" => nil}, {"e/" => nil}],
  38 + {"f/" => nil}
  39 + ]
  40 + ]
  41 + }
  42 + expected = "<foo><a/><b/><c/><d/><e/><f/></foo>"
  43 + XmlFu.xml(hash).should == expected
  44 + end
  45 +
  46 + describe "creating siblings with special key character (*)" do
  47 + it "should create siblings with special key character" do
  48 + hash = { "person*" => ["Bob", "Sheila"] }
  49 + XmlFu.xml(hash).should == "<person>Bob</person><person>Sheila</person>"
  50 + end
  51 +
  52 + it "should create siblings with mixed attributes" do
  53 + hash = {
  54 + "person*" => {
  55 + "@foo" => "bar",
  56 + "=" => ["Bob", "Sheila"]
  57 + }
  58 + }
  59 + XmlFu.xml(hash).should == "<person foo=\"bar\">Bob</person><person foo=\"bar\">Sheila</person>"
  60 + end
  61 +
  62 + it "should create siblings with complex mixed attributes" do
  63 + hash = {
  64 + "person*" => {
  65 + "@foo" => "bar",
  66 + "=" => [
  67 + {"@foo" => "nope", "=" => "Bob"},
  68 + "Sheila"
  69 + ]
  70 + }
  71 + }
  72 + XmlFu.xml(hash).should == "<person foo=\"nope\">Bob</person><person foo=\"bar\">Sheila</person>"
  73 + end
  74 + end
  75 +
  76 + end
  77 +end
61 spec/lib/hash_spec.rb
... ... @@ -0,0 +1,61 @@
  1 +require 'spec_helper'
  2 +
  3 +describe XmlFu::Hash do
  4 +
  5 + describe ".to_xml" do
  6 + it "for a simple Hash" do
  7 + XmlFu::Hash.to_xml(:some => "body").should == "<some>body</some>"
  8 + end
  9 +
  10 + it "for a nested Hash" do
  11 + XmlFu::Hash.to_xml(:foo => {:bar => "biz" }).should == "<foo><bar>biz</bar></foo>"
  12 + end
  13 +
  14 + it "for a hash with multiple keys" do
  15 + XmlFu::Hash.to_xml(:first => "myself", :second => "the world").should include(
  16 + "<first>myself</first>",
  17 + "<second>the world</second>"
  18 + )
  19 + end
  20 +
  21 +
  22 + describe "for a hash value with '=' key defined" do
  23 + it "should ignore '=' for self-closing tag" do
  24 + hash = {"foo/" => {"@id" => "1", "=" => "PEEKABOO"}}
  25 + XmlFu.xml(hash).should == "<foo id=\"1\"/>"
  26 + end
  27 +
  28 + it "should set additional content using '=' key" do
  29 + hash = {:foo => {"@id" => "1", "=" => "Hello"}}
  30 + XmlFu.xml(hash).should == "<foo id=\"1\">Hello</foo>"
  31 + end
  32 + end
  33 +
  34 + describe "with a key that will contain multiple nodes" do
  35 + describe "when key explicitly denotes value is a collection" do
  36 + hash = { "foo*" => ["bar", "biz"] }
  37 + XmlFu::Hash.to_xml(hash).should == "<foo>bar</foo><foo>biz</foo>"
  38 + end
  39 +
  40 + describe "when key denotes value contains children" do
  41 + it "for array consisting entirely of simple values" do
  42 + XmlFu::Hash.to_xml(:foo => ["bar", "biz"]).should == "<foo></foo>"
  43 + end
  44 +
  45 + it "for array containing mix of simple and complex values" do
  46 + XmlFu::Hash.to_xml(:foo => ["bar", {:biz => "bang"}]).should == "<foo><biz>bang</biz></foo>"
  47 + end
  48 +
  49 + it "for array containing complex values" do
  50 + hash1 = {:foo => "bar"}
  51 + hash2 = {:bar => "biz"}
  52 + XmlFu::Hash.to_xml(:lol => [hash1, hash2]).should == "<lol><foo>bar</foo><bar>biz</bar></lol>"
  53 + end
  54 +
  55 + it "for array containing nil values" do
  56 + XmlFu::Hash.to_xml(:foo => [nil, {:bar => "biz"}]).should == "<foo><bar>biz</bar></foo>"
  57 + end
  58 + end
  59 + end
  60 + end
  61 +end
112 spec/lib/node_spec.rb
... ... @@ -0,0 +1,112 @@
  1 +require 'spec_helper'
  2 +
  3 +describe XmlFu::Node do
  4 +
  5 + describe "setting instance variables" do
  6 + it "should correctly remove special characters from a name" do
  7 + node = XmlFu::Node.new("foo/", "something")
  8 + node.name.should == "foo"
  9 +
  10 + node = XmlFu::Node.new("foo*", "something")
  11 + node.name.should == "foo"
  12 +
  13 + node = XmlFu::Node.new("foo!", "something")
  14 + node.name.should == "foo"
  15 + end
  16 +
  17 + it "should set self-closing with special name character" do
  18 + node = XmlFu::Node.new("foo/", "something")
  19 + node.self_closing.should == true
  20 + end
  21 +
  22 + it "should set escape_xml with special name character" do
  23 + node = XmlFu::Node.new("foo!", "something")
  24 + node.escape_xml.should == false
  25 + end
  26 +
  27 + it "should set attributes with a hash" do
  28 + node = XmlFu::Node.new("foo", "bar", {:this => "that"})
  29 + node.attributes.should == {:this => "that"}
  30 +
  31 + lambda { node.attributes = "foo" }.should raise_error(XmlFu::Node::InvalidAttributesException)
  32 + end
  33 +
  34 + it "should be able to set a nil value" do
  35 + node = XmlFu::Node.new("foo", nil)
  36 + node.value.should == nil
  37 + end
  38 +
  39 + it "should format a Data/Time value to acceptable string value" do
  40 + formatted_regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/
  41 +
  42 + node = XmlFu::Node.new("now", Time.now)
  43 + node.value.should match formatted_regex
  44 +
  45 + node = XmlFu::Node.new("today", Date.today)
  46 + node.value.should match formatted_regex
  47 + end
  48 +
  49 + it "should properly convert names without proper namespacing" do
  50 + node = XmlFu::Node.new(":foo", "bar")
  51 + node.name.should == "foo"
  52 + end
  53 +
  54 + it "should properly preserve namespaceds names" do
  55 + node = XmlFu::Node.new("foo:bar", "biz")
  56 + node.name.should == "foo:bar"
  57 + end
  58 + end
  59 +
  60 + describe "to_xml" do
  61 +
  62 + describe "should return self-closing nil XML node for nil value" do
  63 +
  64 + it "provided ANY non-blank name" do
  65 + nil_foo = "<foo xsi:nil=\"true\"/>"
  66 + node = XmlFu::Node.new("foo", nil)
  67 + node.to_xml.should == nil_foo
  68 +
  69 + node = XmlFu::Node.new("foo!", nil)
  70 + node.to_xml.should == nil_foo
  71 +
  72 + node = XmlFu::Node.new("foo*", nil)
  73 + node.to_xml.should == nil_foo
  74 + end
  75 +
  76 + it "with additional attributes provided" do
  77 + node = XmlFu::Node.new("foo", nil, {:this => "that"})
  78 + node.to_xml.should == "<foo this=\"that\" xsi:nil=\"true\"/>"
  79 + end
  80 +
  81 + end
  82 +
  83 + it "should escape values by default" do
  84 + node = XmlFu::Node.new("foo", "<bar/>")
  85 + node.to_xml.should == "<foo>&lt;bar/&gt;</foo>"
  86 + end
  87 +
  88 + it "should not escape values when provided with a special name" do
  89 + node = XmlFu::Node.new("foo!", "<bar/>")
  90 + node.to_xml.should == "<foo><bar/></foo>"
  91 + end
  92 +
  93 + it "should ignore starred key (key*) for simple values" do
  94 + node = XmlFu::Node.new("foo*", "bar")
  95 + node.to_xml.should == "<foo>bar</foo>"
  96 +
  97 + node = XmlFu::Node.new("pi*", 3.14159)
  98 + node.to_xml.should == "<pi>3.14159</pi>"
  99 + end
  100 +
  101 + describe "when name denotes a self-closing XML node" do
  102 + it "should ignore tag content/value if it isn't a hash" do
  103 + node = XmlFu::Node.new("foo/", nil)
  104 + node.to_xml.should == "<foo/>"
  105 +
  106 + node = XmlFu::Node.new("foo/", "bar")
  107 + node.to_xml.should == "<foo/>"
  108 + end
  109 + end
  110 + end
  111 +
  112 +end
6 spec/spec_helper.rb
... ... @@ -0,0 +1,6 @@
  1 +require 'rspec'
  2 +require 'xml-fu'
  3 +
  4 +RSpec.configure do |config|
  5 + config.color_enabled = true
  6 +end
75 spec/xmlfu_spec.rb
... ... @@ -0,0 +1,75 @@
  1 +require 'spec_helper'
  2 +
  3 +describe XmlFu do
  4 + describe ".xml" do
  5 + it "translates a given Hash to XML" do
  6 + XmlFu.xml( :id => 1 ).should == "<id>1</id>"
  7 + end
  8 +
  9 + it "doesn't modify the input hash" do
  10 + the_hash = {
  11 + :person => {
  12 + "@id" => "007",
  13 + :first_name => "James",
  14 + :last_name => "Bond"
  15 + }
  16 + }
  17 + original_hash = the_hash.dup
  18 +
  19 + XmlFu.xml(the_hash)
  20 + original_hash.should == the_hash
  21 + end
  22 +
  23 + it "should return correct value based on nested array of hashes" do
  24 + hash = {
  25 + "SecretAgents" => [
  26 + {"agent/" => {"@id"=>"006", "@name"=>"Alec Trevelyan"}},
  27 + {"agent/" => {"@id"=>"007", "@name"=>"James Bond"}}
  28 + ]
  29 + }
  30 + expected = "<SecretAgents><agent name=\"Alec Trevelyan\" id=\"006\"/><agent name=\"James Bond\" id=\"007\"/></SecretAgents>"
  31 + XmlFu.xml(hash).should == expected
  32 + end
  33 +
  34 + it "should return correct value for nested collection of hashes" do
  35 + hash = {
  36 + "foo*" => [
  37 + {"@bar" => "biz"},
  38 + {"@biz" => "bang"}
  39 + ]
  40 + }
  41 + XmlFu.xml(hash).should == "<foo bar=\"biz\"></foo><foo biz=\"bang\"></foo>"
  42 + end
  43 +
  44 + it "should ignore nested values for content array" do
  45 + output = XmlFu.xml("foo/" => [{:bar => "biz"}, {:bar => "biz"}])
  46 + output.should == "<foo/>"
  47 + end
  48 +
  49 + it "should ignore nested keys if they aren't attributes" do
  50 + output = XmlFu.xml("foo/" => {"bar" => "biz"})
  51 + output.should == "<foo/>"
  52 +
  53 + output = XmlFu.xml("foo/" => {"@id" => "0"})
  54 + output.should == "<foo id=\"0\"/>"
  55 + end
  56 +
  57 + end
  58 +
  59 + describe "configure" do
  60 + it "yields the XmlFu module" do
  61 + XmlFu.configure do |xf|
  62 + xf.should respond_to(:infer_simple_value_nodes)
  63 + end
  64 + end
  65 + end
  66 +
  67 + it "should set XmlFu Module variable 'infer_simple_value_nodes'" do
  68 + XmlFu.infer_simple_value_nodes.should == false
  69 + XmlFu.infer_simple_value_nodes = true
  70 + XmlFu.infer_simple_value_nodes.should == true
  71 + XmlFu.infer_simple_value_nodes = false
  72 + XmlFu.infer_simple_value_nodes.should == false
  73 + end
  74 +
  75 +end
29 xml-fu.gemspec
... ... @@ -0,0 +1,29 @@
  1 +# -*- encoding: utf-8 -*-
  2 +require File.expand_path('../lib/xml-fu/version', __FILE__)
  3 +
  4 +Gem::Specification.new do |gem|
  5 + gem.name = "xml-fu"
  6 + gem.version = XmlFu::VERSION
  7 + gem.authors = ["Ryan Johnson"]
  8 + gem.email = ["rhino.citguy@gmail.com"]
  9 + gem.homepage = "http://github.com/CITguy/#{gem.name}"
  10 + gem.summary = %q{Simple Hash/Array to XML generation}
  11 + gem.description = %q{
  12 + Inspired by the Gyoku gem for hash to xml conversion,
  13 + XmlFu is designed to require no meta tagging for
  14 + node attributes and content. (i.e. no :attributes! and no :order!)
  15 + }
  16 +
  17 + gem.rubyforge_project = 'xml-fu'
  18 +
  19 + gem.add_dependency "builder", ">= 2.1.2"
  20 +
  21 + gem.add_development_dependency "rspec", ">= 2.4.0"
  22 + gem.add_development_dependency "autotest"
  23 + gem.add_development_dependency "mocha", "~> 0.9.9"
  24 +
  25 + gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  26 + gem.files = `git ls-files`.split("\n")
  27 + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
  28 + gem.require_paths = ["lib"]
  29 +end

0 comments on commit 2b0ae53

Please sign in to comment.
Something went wrong with that request. Please try again.