lib/xmlsimple.rb in xml-simple-1.0.16 vs lib/xmlsimple.rb in xml-simple-1.1.0
- old
+ new
@@ -9,11 +9,11 @@
# Easy API to maintain XML (especially configuration files).
class XmlSimple
include REXML
- @@VERSION = '1.0.15'
+ @@VERSION = '1.1.0'
# A simple cache for XML documents that were already transformed
# by xml_in.
class Cache
# Creates and initializes a new Cache object.
@@ -21,11 +21,11 @@
@mem_share_cache = {}
@mem_copy_cache = {}
end
# Saves a data structure into a file.
- #
+ #
# data::
# Data structure to be saved.
# filename::
# Name of the file belonging to the data structure.
def save_storable(data, filename)
@@ -120,11 +120,11 @@
# Create a "global" cache.
@@cache = Cache.new
# Creates and intializes a new XmlSimple object.
- #
+ #
# defaults::
# Default values for options.
def initialize(defaults = nil)
unless defaults.nil? || defaults.instance_of?(Hash)
raise ArgumentError, "Options have to be a Hash."
@@ -141,11 +141,11 @@
#
# - nil: Tries to load and parse '<scriptname>.xml'.
# - filename: Tries to load and parse filename.
# - IO object: Reads from object until EOF is detected and parses result.
# - XML string: Parses string.
- #
+ #
# options::
# Options to be used.
def xml_in(string = nil, options = nil)
handle_options('in', options)
@@ -180,11 +180,11 @@
raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
end
return content if content
}
end
-
+
@doc = load_xml_file(filename)
end
elsif string.kind_of?(IO) || string.kind_of?(StringIO) || string.kind_of?(Zlib::GzipReader)
@doc = parse(string.read)
else
@@ -200,11 +200,11 @@
# This is the functional version of the instance method xml_in.
def XmlSimple.xml_in(string = nil, options = nil)
xml_simple = XmlSimple.new
xml_simple.xml_in(string, options)
end
-
+
# Converts a data structure into an XML document.
#
# ref::
# Reference to data structure to be converted into XML.
# options::
@@ -256,20 +256,20 @@
# This is the functional version of the instance method xml_out.
def XmlSimple.xml_out(hash, options = nil)
xml_simple = XmlSimple.new
xml_simple.xml_out(hash, options)
end
-
+
private
# Declare options that are valid for xml_in and xml_out.
KNOWN_OPTIONS = {
'in' => %w(
keyattr keeproot forcecontent contentkey noattr
searchpath forcearray suppressempty anonymoustag
cache grouptags normalisespace normalizespace
- variables varattr keytosymbol attrprefix
+ variables varattr keytosymbol attrprefix conversions
),
'out' => %w(
keyattr keeproot contentkey noattr rootname
xmldeclaration outputfile noescape suppressempty
anonymoustag indent grouptags noindent attrprefix
@@ -283,11 +283,11 @@
DEF_XML_DECLARATION = "<?xml version='1.0' standalone='yes'?>"
DEF_ANONYMOUS_TAG = 'anon'
DEF_FORCE_ARRAY = true
DEF_INDENTATION = ' '
DEF_KEY_TO_SYMBOL = false
-
+
# Normalizes option names in a hash, i.e., turns all
# characters to lower case and removes all underscores.
# Additionally, this method checks, if an unknown option
# was used and raises an according exception.
#
@@ -305,13 +305,13 @@
end
result[lkey] = value
}
result
end
-
+
# Merges a set of options with the default options.
- #
+ #
# direction::
# 'in': If options should be handled for xml_in.
# 'out': If options should be handled for xml_out.
# options::
# Options to be merged with the default options.
@@ -493,13 +493,13 @@
if @options['grouptags'][key] == child_key
result[key] = child_value
end
}
end
-
+
# Fold Hashes containing a single anonymous Array up into just the Array.
- if count == 1
+ if count == 1
anonymoustag = @options['anonymoustag']
if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array)
return result[anonymoustag]
end
end
@@ -535,11 +535,11 @@
end
end
end
# Folds all arrays in a Hash.
- #
+ #
# hash::
# Hash to be folded.
def fold_arrays(hash)
fold_amount = 0
keyattr = @options['keyattr']
@@ -583,11 +583,11 @@
return array unless key_matched
}
hash = collapse_content(hash) if @options['collapseagain']
hash
end
-
+
# Folds an Array to a Hash, if possible. Folding happens
# according to the content of keyattr, which has to be
# a Hash.
#
# name::
@@ -626,11 +626,11 @@
return hash unless value.instance_of?(Hash) && value.size == 1 && value.has_key?(content_key)
hash.each_key { |key| hash[key] = hash[key][content_key] }
}
hash
end
-
+
# Adds a new key/value pair to an existing Hash. If the key to be added
# does already exist and the existing value associated with key is not
# an Array, it will be converted into an Array. Then the new value is
# appended to that Array.
#
@@ -642,31 +642,35 @@
# Value to be associated with key.
def merge(hash, key, value)
if value.instance_of?(String)
value = normalise_space(value) if @options['normalisespace'] == 2
+ if conv = @options['conversions'] and conv = conv.find {|c,_| c.match(key)} and conv = conv.at(1)
+ value = conv.call(value)
+ end
+
# do variable substitutions
unless @_var_values.nil? || @_var_values.empty?
value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) }
end
-
+
# look for variable definitions
if @options.has_key?('varattr')
varattr = @options['varattr']
if hash.has_key?(varattr)
set_var(hash[varattr], value)
end
end
end
-
+
#patch for converting keys to symbols
if @options.has_key?('keytosymbol')
if @options['keytosymbol'] == true
key = key.to_s.downcase.to_sym
end
end
-
+
if hash.has_key?(key)
if hash[key].instance_of?(Array)
hash[key] << value
else
hash[key] = [ hash[key], value ]
@@ -680,25 +684,25 @@
hash[key] = value
end
end
hash
end
-
+
# Checks, if the 'forcearray' option has to be used for
# a certain key.
def force_array?(key)
return false if key == @options['contentkey']
return true if @options['forcearray'] == true
forcearray = @options['forcearray']
if forcearray.instance_of?(Hash)
- return true if forcearray.has_key?(key)
+ return true if forcearray.has_key?(key)
return false unless forcearray.has_key?('_regex')
forcearray['_regex'].each { |x| return true if key =~ x }
end
return false
end
-
+
# Converts the attributes array of a document node into a Hash.
# Returns an empty Hash, if node has no attributes.
#
# node::
# Document node to extract attributes from.
@@ -709,22 +713,22 @@
else
node.attributes.each { |n,v| attributes[n] = v }
end
attributes
end
-
+
# Determines, if a document element has mixed content.
#
# element::
# Document element to be checked.
def has_mixed_content?(element)
if element.has_text? && element.has_elements?
return true if element.texts.join('') !~ /^\s*$/
end
false
end
-
+
# Called when a variable definition is encountered in the XML.
# A variable definition looks like
# <element attrname="name">value</element>
# where attrname matches the varattr setting.
def set_var(name, value)
@@ -738,11 +742,11 @@
return @_var_values[name]
else
return "${#{name}}"
end
end
-
+
# Recurses through a data structure building up and returning an
# XML representation of that structure as a string.
#
# ref::
# Reference to the data structure to be encoded.
@@ -780,11 +784,11 @@
if @options['grouptags'].has_key?(key)
ref[key] = { @options['grouptags'][key] => value }
end
}
end
-
+
nested = []
text_content = nil
if named
result << indent << '<' << name
end
@@ -798,11 +802,11 @@
end
value = {}
end
# Check for the '@' attribute prefix to allow separation of attributes and elements
-
+
if (@options['noattr'] ||
(@options['attrprefix'] && !(key =~ /^@(.*)/)) ||
!scalar(value)
) &&
key != @options['contentkey']
@@ -859,27 +863,27 @@
raise ArgumentError, "Can't encode a value of type: #{ref.type}."
end
@ancestors.pop if !scalar(ref)
result.join('')
end
-
+
# Checks, if a certain value is a "scalar" value. Whatever
# that will be in Ruby ... ;-)
- #
+ #
# value::
# Value to be checked.
def scalar(value)
return false if value.instance_of?(Hash) || value.instance_of?(Array)
return true
end
# Attempts to unfold a hash of hashes into an array of hashes. Returns
# a reference to th array on success or the original hash, if unfolding
# is not possible.
- #
+ #
# parent::
- #
+ #
# hashref::
# Reference to the hash to be unfolded.
def hash_to_array(parent, hashref)
arrayref = []
hashref.each { |key, value|
@@ -892,19 +896,19 @@
arrayref << { @options['keyattr'][0] => key }.update(value)
end
}
arrayref
end
-
+
# Replaces XML markup characters by their external entities.
#
# data::
# The string to be escaped.
def escape_value(data)
Text::normalize(data)
end
-
+
# Removes leading and trailing whitespace and sequences of
# whitespaces from a string.
#
# text::
# String to be normalised.
@@ -925,21 +929,21 @@
return value !~ /\S/m
else
return value.nil?
end
end
-
+
# Converts a document node into a String.
# If the node could not be converted into a String
# for any reason, default will be returned.
#
# node::
# Document node to be converted.
# default::
# Value to be returned, if node could not be converted.
def node_to_text(node, default = nil)
- if node.instance_of?(REXML::Element)
+ if node.instance_of?(REXML::Element)
node.texts.map { |t| t.value }.join('')
elsif node.instance_of?(REXML::Attribute)
node.value.nil? ? default : node.value.strip
elsif node.instance_of?(REXML::Text)
node.value.strip
@@ -958,11 +962,11 @@
# REXML::ParseException::
# If the specified file is not wellformed.
def parse(xml_string)
Document.new(xml_string)
end
-
+
# Searches in a list of paths for a certain file. Returns
# the full path to the file, if it could be found. Otherwise,
# an exception will be raised.
#
# filename::
@@ -985,17 +989,17 @@
return file if File::file?(file)
raise ArgumentError, "File does not exist: #{file}."
end
raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>"
end
-
+
# Loads and parses an XML configuration file.
#
# filename::
# Name of the configuration file to be loaded.
#
# The following exceptions may be raised:
- #
+ #
# Errno::ENOENT::
# If the specified file does not exist.
# REXML::ParseException::
# If the specified file is not wellformed.
def load_xml_file(filename)