lib/subhash.rb in subhash-0.1.1 vs lib/subhash.rb in subhash-0.1.2

- old
+ new

@@ -15,840 +15,24 @@ # See the License for the specific language governing permissions and # limitations under the License. require 'rubygems' require 'yaml' +require 'rh' +require 'ihash' +require 'iarray' # Adding rh_clone at object level. This be able to use a generic rh_clone # redefined per object Hash and Array. class Object alias_method :rh_clone, :clone end -# Rh common module included in Hash and Array class. -module Rh - public - - def merge_cleanup! - _rh_remove_control(self) - end - - def merge_cleanup - _rh_remove_control(rh_clone) - end - - private - - # Function which will parse arrays in hierarchie and will remove any control - # element (index 0) - def _rh_remove_control(result) - return unless [Hash, Array].include?(result.class) - - if result.is_a?(Hash) - result.each { |elem| _rh_remove_control(elem) } - else - result.delete_at(0) if result[0].is_a?(Hash) && result[0].key?(:__control) - result.each_index { |index| _rh_remove_control(result[index]) } +if RUBY_VERSION.match(/1\.8/) + # Added missing <=> function in Symbol of ruby 1.8 + class Symbol + def <=>(other) + return to_s <=> other if other.is_a?(String) + to_s <=> other.to_s end - result end - - # Internal function to determine if result and data key contains both Hash or - # Array and if so, do the merge task on those sub Hash/Array - # - def _rh_merge_recursive(result, key, data) - return false unless [Array, Hash].include?(data.class) - - value = data[key] - return false unless [Array, Hash].include?(value.class) && - value.class == result[key].class - - if object_id == result.object_id - result[key].rh_merge!(value) - else - result[key] = result[key].rh_merge(value) - end - - true - end - - # Internal function to determine if changing from Hash/Array to anything else - # is authorized or not. - # - # The structure is changing if `result` or `value` move from Hash/Array to any - # other type. - # - # * *Args*: - # - result: Merged Hash or Array structure. - # - key : Key in result and data. - # - data : Hash or Array structure to merge. - # - # * *returns*: - # - +true+ : if :__struct_changing == true - # - +false+ : otherwise. - def _rh_struct_changing_ok?(result, key, data) - return true unless [Array, Hash].include?(data[key].class) || - [Array, Hash].include?(result[key].class) - - # result or value are structure (Hash or Array) - if result.is_a?(Hash) - control = result[:__struct_changing] - else - control = result[0][:__struct_changing] - key -= 1 - end - return true if control.is_a?(Array) && control.include?(key) - - false - end - - # Internal function to determine if a data merged can be updated by any - # other object like Array, String, etc... - # - # The decision is given by a :__unset setting. - # - # * *Args*: - # - Hash/Array data to replace. - # - key: string or symbol. - # - # * *returns*: - # - +false+ : if key is found in :__protected Array. - # - +true+ : otherwise. - def _rh_merge_ok?(result, key) - if result.is_a?(Hash) - control = result[:__protected] - else - control = result[0][:__protected] - key -= 1 - end - - return false if control.is_a?(Array) && control.include?(key) - - true - end - - def _rh_control_tags - [:__remove, :__remove_index, :__add, :__add_index, - :__protected, :__struct_changing, :__control] - end -end - -# Recursive Hash added to the Hash class -class Hash - # Recursive Hash deep level found counter - # This function will returns the count of deep level of recursive hash. - # * *Args* : - # - +p+ : Array of string or symbols. keys tree to follow and check - # existence in yVal. - # - # * *Returns* : - # - +integer+ : Represents how many deep level was found in the recursive - # hash - # - # * *Raises* : - # No exceptions - # - # Example: (implemented in spec) - # - # yVal = { :test => {:test2 => 'value1', :test3 => 'value2'}, - # :test4 => 'value3'} - # - # yVal can be represented like: - # - # yVal: - # test: - # test2 = 'value1' - # test3 = 'value2' - # test4 = 'value3' - # - # so: - # # test is found - # yVal.rh_lexist?(:test) => 1 - # - # # no test5 - # yVal.rh_lexist?(:test5) => 0 - # - # # :test/:test2 tree is found - # yVal.rh_lexist?(:test, :test2) => 2 - # - # # :test/:test2 is found (value = 2), but :test5 was not found in this tree - # yVal.rh_lexist?(:test, :test2, :test5) => 2 - # - # # :test was found. but :test/:test5 tree was not found. so level 1, ok. - # yVal.rh_lexist?(:test, :test5 ) => 1 - # - # # it is like searching for nothing... - # yVal.rh_lexist? => 0 - - def rh_lexist?(*p) - p = p.flatten - - return 0 if p.length == 0 - - if p.length == 1 - return 1 if self.key?(p[0]) - return 0 - end - return 0 unless self.key?(p[0]) - ret = 0 - ret = self[p[0]].rh_lexist?(p.drop(1)) if self[p[0]].is_a?(Hash) - 1 + ret - end - - # Recursive Hash deep level existence - # - # * *Args* : - # - +p+ : Array of string or symbols. keys tree to follow and check - # existence in yVal. - # - # * *Returns* : - # - +boolean+ : Returns True if the deep level of recursive hash is found. - # false otherwise - # - # * *Raises* : - # No exceptions - # - # Example:(implemented in spec) - # - # yVal = { :test => {:test2 => 'value1', :test3 => 'value2'}, - # :test4 => 'value3'} - # - # yVal can be represented like: - # - # yVal: - # test: - # test2 = 'value1' - # test3 = 'value2' - # test4 = 'value3' - # - # so: - # # test is found - # yVal.rh_exist?(:test) => True - # - # # no test5 - # yVal.rh_exist?(:test5) => False - # - # # :test/:test2 tree is found - # yVal.rh_exist?(:test, :test2) => True - # - # # :test/:test2 is found (value = 2), but :test5 was not found in this tree - # yVal.rh_exist?(:test, :test2, :test5) => False - # - # # :test was found. but :test/:test5 tree was not found. so level 1, ok. - # yVal.rh_exist?(:test, :test5 ) => False - # - # # it is like searching for nothing... - # yVal.rh_exist? => nil - def rh_exist?(*p) - p = p.flatten - - return nil if p.length == 0 - - count = p.length - (rh_lexist?(*p) == count) - end - - # Recursive Hash Get - # This function will returns the level of recursive hash was found. - # * *Args* : - # - +p+ : Array of string or symbols. keys tree to follow and check - # existence in yVal. - # - # * *Returns* : - # - +value+ : Represents the data found in the tree. Can be of any type. - # - # * *Raises* : - # No exceptions - # - # Example:(implemented in spec) - # - # yVal = { :test => {:test2 => 'value1', :test3 => 'value2'}, - # :test4 => 'value3'} - # - # yVal can be represented like: - # - # yVal: - # test: - # test2 = 'value1' - # test3 = 'value2' - # test4 = 'value3' - # - # so: - # yVal.rh_get(:test) => {:test2 => 'value1', :test3 => 'value2'} - # yVal.rh_get(:test5) => nil - # yVal.rh_get(:test, :test2) => 'value1' - # yVal.rh_get(:test, :test2, :test5) => nil - # yVal.rh_get(:test, :test5 ) => nil - # yVal.rh_get => { :test => {:test2 => 'value1', :test3 => 'value2'}, - # :test4 => 'value3'} - def rh_get(*p) - p = p.flatten - return self if p.length == 0 - - if p.length == 1 - return self[p[0]] if self.key?(p[0]) - return nil - end - return self[p[0]].rh_get(p.drop(1)) if self[p[0]].is_a?(Hash) - nil - end - - # Recursive Hash Set - # This function will build a recursive hash according to the '*p' key tree. - # if yVal is not nil, it will be updated. - # - # * *Args* : - # - +p+ : Array of string or symbols. keys tree to follow and check - # existence in yVal. - # - # * *Returns* : - # - +value+ : the value set. - # - # * *Raises* : - # No exceptions - # - # Example:(implemented in spec) - # - # yVal = {} - # - # yVal.rh_set(:test) => nil - # # yVal = {} - # - # yVal.rh_set(:test5) => nil - # # yVal = {} - # - # yVal.rh_set(:test, :test2) => :test - # # yVal = {:test2 => :test} - # - # yVal.rh_set(:test, :test2, :test5) => :test - # # yVal = {:test2 => {:test5 => :test} } - # - # yVal.rh_set(:test, :test5 ) => :test - # # yVal = {:test2 => {:test5 => :test}, :test5 => :test } - # - # yVal.rh_set('blabla', :test2, 'text') => :test - # # yVal = {:test2 => {:test5 => :test, 'text' => 'blabla'}, - # :test5 => :test } - def rh_set(value, *p) - p = p.flatten - return nil if p.length == 0 - - if p.length == 1 - self[p[0]] = value - return value - end - - self[p[0]] = {} unless self[p[0]].is_a?(Hash) - self[p[0]].rh_set(value, p.drop(1)) - end - - # Recursive Hash delete - # This function will remove the last key defined by the key tree - # - # * *Args* : - # - +p+ : Array of string or symbols. keys tree to follow and check - # existence in yVal. - # - # * *Returns* : - # - +value+ : The Hash updated. - # - # * *Raises* : - # No exceptions - # - # Example:(implemented in spec) - # - # yVal = {{:test2 => { :test5 => :test, - # 'text' => 'blabla' }, - # :test5 => :test}} - # - # - # yVal.rh_del(:test) => nil - # # yVal = no change - # - # yVal.rh_del(:test, :test2) => nil - # # yVal = no change - # - # yVal.rh_del(:test2, :test5) => {:test5 => :test} - # # yVal = {:test2 => {:test5 => :test} } - # - # yVal.rh_del(:test, :test2) - # # yVal = {:test2 => {:test5 => :test} } - # - # yVal.rh_del(:test, :test5) - # # yVal = {:test2 => {} } - # - def rh_del(*p) - p = p.flatten - - return nil if p.length == 0 - - return delete(p[0]) if p.length == 1 - - return nil if self[p[0]].nil? - self[p[0]].rh_del(p.drop(1)) - end - - # Move levels (default level 1) of tree keys to become symbol. - # - # * *Args* : - # - +levels+: level of key tree to update. - # * *Returns* : - # - a new hash of hashes updated. Original Hash is not updated anymore. - # - # examples: - # With hdata = { :test => { :test2 => { :test5 => :test, - # 'text' => 'blabla' }, - # 'test5' => 'test' }} - # - # hdata.rh_key_to_symbol(1) return no diff - # hdata.rh_key_to_symbol(2) return "test5" is replaced by :test5 - # # hdata = { :test => { :test2 => { :test5 => :test, - # # 'text' => 'blabla' }, - # # :test5 => 'test' }} - # rh_key_to_symbol(3) return "test5" replaced by :test5, and "text" to :text - # # hdata = { :test => { :test2 => { :test5 => :test, - # # :text => 'blabla' }, - # # :test5 => 'test' }} - # rh_key_to_symbol(4) same like rh_key_to_symbol(3) - - def rh_key_to_symbol(levels = 1) - result = {} - each do |key, value| - new_key = key - new_key = key.to_sym if key.is_a?(String) - if value.is_a?(Hash) && levels > 1 - value = value.rh_key_to_symbol(levels - 1) - end - result[new_key] = value - end - result - end - - # Check if levels of tree keys are all symbols. - # - # * *Args* : - # - +levels+: level of key tree to update. - # * *Returns* : - # - true : one key path is not symbol. - # - false : all key path are symbols. - # * *Raises* : - # Nothing - # - # examples: - # With hdata = { :test => { :test2 => { :test5 => :test, - # 'text' => 'blabla' }, - # 'test5' => 'test' }} - # - # hdata.rh_key_to_symbol?(1) return false - # hdata.rh_key_to_symbol?(2) return true - # hdata.rh_key_to_symbol?(3) return true - # hdata.rh_key_to_symbol?(4) return true - def rh_key_to_symbol?(levels = 1) - each do |key, value| - return true if key.is_a?(String) - - res = false - if levels > 1 && value.is_a?(Hash) - res = value.rh_key_to_symbol?(levels - 1) - end - return true if res - end - false - end - - # return an exact clone of the recursive Array and Hash contents. - # - # * *Args* : - # - # * *Returns* : - # - Recursive Array/Hash cloned. Other kind of objects are kept referenced. - # * *Raises* : - # Nothing - # - # examples: - # hdata = { :test => { :test2 => { :test5 => :test, - # 'text' => 'blabla' }, - # 'test5' => 'test' }, - # :array => [{ :test => :value1 }, 2, { :test => :value3 }]} - # - # hclone = hdata.rh_clone - # hclone[:test] = "test" - # hdata[:test] == { :test2 => { :test5 => :test,'text' => 'blabla' } - # # => true - # hclone[:array].pop - # hdata[:array].length != hclone[:array].length - # # => true - # hclone[:array][0][:test] = "value2" - # hdata[:array][0][:test] != hclone[:array][0][:test] - # # => true - def rh_clone - result = {} - each do |key, value| - if [Array, Hash].include?(value.class) - result[key] = value.rh_clone - else - result[key] = value - end - end - result - end - - # Merge the current Hash object (self) cloned with a Hash/Array tree contents - # (data). - # - # 'self' is used as original data to merge to. - # 'data' is used as data to merged to clone of 'self'. If you want to update - # 'self', use rh_merge! - # - # if 'self' or 'data' contains a Hash tree, the merge will be executed - # recursively. - # - # The current function will execute the merge of the 'self' keys with the top - # keys in 'data' - # - # The merge can be controlled by an additionnal Hash key '__*' in each - # 'self' key. - # If both a <key> exist in 'self' and 'data', the following decision is made: - # - if both 'self' and 'data' key contains an Hash or and Array, a recursive - # merge if Hash or update if Array, is started. - # - # - if 'self' <key> contains an Hash or an Array, but not 'data' <key>, then - # 'self' <key> will be set to the 'data' <key> except if 'self' <Key> has - # :__struct_changing: true - # data <key> value can set :unset value - # - # - if 'self' <key> is :unset and 'data' <key> is any value - # 'self' <key> value is set with 'data' <key> value. - # 'data' <key> value can contains a Hash with :__no_unset: true to - # protect this key against the next merge. (next config layer merge) - # - # - if 'data' <key> exist but not in 'self', 'data' <key> is just added. - # - # - if 'data' & 'self' <key> exist, 'self'<key> is updated except if key is in - # :__protected array list. - # - # * *Args* : - # - hash : Hash data to merge. - # - # * *Returns* : - # - Recursive Array/Hash merged. - # - # * *Raises* : - # Nothing - # - # examples: - # - def rh_merge(data) - _rh_merge(clone, data) - end - - # Merge the current Hash object (self) with a Hash/Array tree contents (data). - # - # For details on this functions, see #rh_merge - # - def rh_merge!(data) - _rh_merge(self, data) - end -end - -# Recursive Hash added to the Hash class -class Hash - private - - # Internal function which do the real merge task by #rh_merge and #rh_merge! - # - # See #rh_merge for details - # - def _rh_merge(result, data) - return _rh_merge_choose_data(result, data) unless data.is_a?(Hash) - - data.each do |key, _value| - next if [:__struct_changing, :__protected].include?(key) - - _do_rh_merge(result, key, data) - end - [:__struct_changing, :__protected].each do |key| - # Refuse merge by default if key data type are different. - # This assume that the first layer merge has set - # :__unset as a Hash, and :__protected as an Array. - - _do_rh_merge(result, key, data, true) if data.key?(key) - - # Remove all control element in arrays - _rh_remove_control(result[key]) if result.key?(key) - end - - result - end - - def _rh_merge_choose_data(result, data) - # return result as first one impose the type between Hash/Array. - return result if [Hash, Array].include?(result.class) || - [Hash, Array].include?(data.class) - - data - end - # Internal function to execute the merge on one key provided by #_rh_merge - # - # if refuse_discordance is true, then result[key] can't be updated if - # stricly not of same type. - def _do_rh_merge(result, key, data, refuse_discordance = false) - value = data[key] - - return if _rh_merge_do_add_key(result, key, value) - - return if _rh_merge_recursive(result, key, data) - - return if refuse_discordance - - return unless _rh_struct_changing_ok?(result, key, data) - - return unless _rh_merge_ok?(result, key) - - _rh_merge_do_upd_key(result, key, value) - end - - def _rh_merge_do_add_key(result, key, value) - unless result.key?(key) || value == :unset - result[key] = value # New key added - return true - end - false - end - - def _rh_merge_do_upd_key(result, key, value) - if value == :unset - result.delete(key) if result.key?(key) - return - end - - result[key] = value # Key updated - end - - include Rh -end - -# Defines rh_clone for Array -class Array - # return an exact clone of the recursive Array and Hash contents. - # - # * *Args* : - # - # * *Returns* : - # - Recursive Array/Hash cloned. - # * *Raises* : - # Nothing - # - # examples: - # hdata = { :test => { :test2 => { :test5 => :test, - # 'text' => 'blabla' }, - # 'test5' => 'test' }, - # :array => [{ :test => :value1 }, 2, { :test => :value3 }]} - # - # hclone = hdata.rh_clone - # hclone[:test] = "test" - # hdata[:test] == { :test2 => { :test5 => :test,'text' => 'blabla' } - # # => true - # hclone[:array].pop - # hdata[:array].length != hclone[:array].length - # # => true - # hclone[:array][0][:test] = "value2" - # hdata[:array][0][:test] != hclone[:array][0][:test] - # # => true - def rh_clone - result = [] - each do |value| - begin - result << value.rh_clone - rescue - result << value - end - end - result - end - - # This function is part of the rh_merge functionnality adapted for Array. - # - # To provide Array recursivity, we uses the element index. - # - # **Warning!** If the Array order has changed (sort/random) the index changed - # and can generate unwanted result. - # - # To implement recursivity, and some specific Array management (add/remove) - # you have to create an Hash and insert it at position 0 in the 'self' Array. - # - # **Warning!** If you create an Array, where index 0 contains a Hash, this - # Hash will be considered as the Array control element. - # If the first index of your Array is not a Hash, an empty Hash will be - # inserted at position 0. - # - # 'data' has the same restriction then 'self' about the first element. - # 'data' can influence the rh_merge Array behavior, by updating the first - # element. - # - # The first Hash element has the following attributes: - # - # - :__struct_changing: Array of index which accepts to move from a structured - # data (Hash/Array) to another structure or type. - # - # Ex: Hash => Array, Array => Integer - # - # - :__protected: Array of index which protects against update from 'data' - # - # - :__remove: Array of elements to remove. each element are remove with - # Array.delete function. See Array delete function for details. - # - # - :__remove_index: Array of indexes to remove. - # Each element are removed with Array.delete_at function. - # It starts from the highest index until the lowest. - # See Array delete function for details. - # - # **NOTE**: __remove and __remove_index cannot be used together. - # if both are set, __remove is choosen - # - # **NOTE** : __remove* is executed before __add* - # - # - :__add: Array of elements to add. Those elements are systematically added - # at the end of the Array. See Array.<< for details. - # - # - :__add_index: Hash of index(key) + Array of data(value) to add. - # The index listed refer to merged 'self' Array. several elements with same - # index are grouply inserted in the index. - # ex: - # [:data3].rh_merge({:__add_index => [0 => [:data1, :data2]]}) - # => [{}, :data1, :data2, :data3] - # - # **NOTE**: __add and __add_index cannot be used together. - # if both are set, __add is choosen - # - # How merge is executed: - # - # Starting at index 0, each index of 'data' and 'self' are used to compare - # indexed data. - # - If 'data' index 0 has not an Hash, the 'self' index 0 is just skipped. - # - If 'data' index 0 has the 'control' Hash, the array will be updated - # according to :__add and :__remove arrays. - # when done, those attributes are removed - # - # - For all next index (1 => 'data'.length), data are compared - # - # - If the 'data' length is > than 'self' length - # addtionnal indexed data are added to 'self' - # - # - If index element exist in both 'data' and 'self', - # 'self' indexed data is updated/merged according to control. - # 'data' indexed data can use :unset to remove the data at this index - # nil is also supported. But the index won't be removed. data will just - # be set to nil - # - # when all Arrays elements are merged, rh_merge will: - # - remove 'self' elements containing ':unset' - # - # - merge 'self' data at index 0 with 'data' found index 0 - # - def rh_merge(data) - _rh_merge(clone, data) - end - - def rh_merge!(data) - _rh_merge(self, data) - end - - private - - def _rh_merge(result, data) - data = data.clone - data_control = _rh_merge_control(data) - result_control = _rh_merge_control(result) - - _rh_do_control_merge(result_control, result, data_control, data) - - (1..(data.length - 1)).each do |index| - _rh_do_array_merge(result, index, data) - end - - (-(result.length - 1)..-1).each do |index| - result.delete_at(index.abs) if result[index.abs] == :unset - end - - _rh_do_array_merge(result, 0, [data_control]) - # Remove all control elements in tree of arrays - _rh_remove_control(result[0]) - - result - end - - def _rh_do_array_merge(result, index, data) - return if _rh_merge_recursive(result, index, data) - - return unless _rh_struct_changing_ok?(result, index, data) - - return unless _rh_merge_ok?(result, index) - - result[index] = data[index] unless data[index] == :kept - end - - # Get the control element. or create it if missing. - def _rh_merge_control(array) - unless array[0].is_a?(Hash) - array.insert(0, :__control => true) - return array[0] - end - - _rh_control_tags.each do |prop| - if array[0].key?(prop) - array[0][:__control] = true - return array[0] - end - end - - array.insert(0, :__control => true) - - array[0] - end - - # Do the merge according to :__add and :__remove - def _rh_do_control_merge(_result_control, result, data_control, _data) - if data_control[:__remove].is_a?(Array) - _rh_do_control_remove(result, data_control[:__remove]) - elsif data_control[:__remove_index].is_a?(Array) - index_to_remove = data_control[:__remove_index].uniq.sort.reverse - _rh_do_control_remove_index(result, index_to_remove) - end - - data_control.delete(:__remove) - data_control.delete(:__remove_index) - - if data_control[:__add].is_a?(Array) - data_control[:__add].each { |element| result << element } - elsif data_control[:__add_index].is_a?(Hash) - _rh_do_control_add_index(result, data_control[:__add_index].sort) - end - - data_control.delete(:__add) - data_control.delete(:__add_index) - end - - def _rh_do_control_add_index(result, add_index) - add_index.reverse_each do |elements_to_insert| - next unless elements_to_insert.is_a?(Array) && - elements_to_insert[0].is_a?(Fixnum) && - elements_to_insert[1].is_a?(Array) - - index = elements_to_insert[0] + 1 - elements = elements_to_insert[1] - - elements.reverse_each { |element| result.insert(index, element) } - end - end - - # do the element removal. - def _rh_do_control_remove(result, remove) - remove.each { |element| result.delete(element) } - end - - def _rh_do_control_remove_index(result, index_to_remove) - index_to_remove.each { |index| result.delete_at(index + 1) } - end - - include Rh end