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)