lib/blobject.rb in blobject-0.3.3 vs lib/blobject.rb in blobject-0.3.7

- old
+ new

@@ -1,52 +1,55 @@ require 'json' require 'yaml' require_relative 'blobject/version' +# Wraps a hash to provide arbitrarily nested object-style attribute access class Blobject # filter :to_ary else Blobject#to_ary returns a # blobject which is not cool, especially if you are puts. ProhibitedNames = [:to_ary] - module Error; end - + # pass an optional hash of values to preload + # you can also pass a block, the new Blobject will be yield def initialize hash = {} - @hash = hash + @hash = Hash.new - @hash.keys.each do |key| - unless key.class <= Symbol - value = @hash.delete key - key = key.to_sym - @hash[key] = value - end + hash.each do |key, value| + key = key.to_sym unless key.is_a? Symbol + @hash[key] = value end - __visit_subtree__ do |name, node| - if node.class <= Hash - @hash[name] = Blobject.new node - end + @hash.each do |name, node| + @hash[name] = self.class.send(:__blobjectify__, node) end yield self if block_given? end + def empty? + @hash.empty? + end + + # delegates to the internal Hash def inspect @hash.inspect end + # access the internal hash. be careful, this is _not_ a copy def hash @hash end + # creates a recursive copy of the internal hash def to_hash h = hash.dup - __visit_subtree__ do |name, node| + @hash.each do |name, node| h[name] = node.to_hash if node.respond_to? :to_hash end h end @@ -58,16 +61,17 @@ case # assignment in conditionals is usually a bad smell, here it helps minimize regex matching when (name = method[/^\w+$/, 0]) && params.length == 0 # the call is an attribute reader - return nil if frozen? and not @hash.has_key?(method) - + + return self.class.new.freeze if frozen? and not @hash.has_key?(method) self.class.send :__define_attribute__, name return send(method) if @hash.has_key? method + # close the scope for storing call chain parent = self nested_blobject = self.class.new store_in_parent = lambda do parent.send "#{name}=", nested_blobject @@ -104,89 +108,81 @@ [/^(\w+)=$/, /^(\w+)\?$/, /^\w+$/].any? do |r| r.match(method) end end + # compares Blobjects to Blobjects or Hashes def == other return @hash == other.hash if other.class <= Blobject return @hash == other if other.class <= Hash super end + # hash-like access to the Blobject's attributes def [] name send name end + # hash-like attribtue setter def []= name, value send "#{name.to_s}=", value end + # freeze a Blobject to prevent it being modified def freeze - __visit_subtree__ { |name, node| node.freeze } @hash.freeze super end + def freeze_r + self.class.send(:__freeze_r__, self) + freeze + end + + + # returns a hash which can be serialized as json. + # this is for use in rails controllers: `render json: blobject` def as_json *args return hash.as_json(*args) if hash.respond_to? :as_json to_hash end + # serialize the Blobject as a json string def to_json *args as_json.to_json *args end - def as_yaml - - to_hash - end - + # serialize the Blobject as a yaml string def to_yaml as_yaml.to_yaml end + # get a Blobject from a json string + # if the yaml string describes an array, an array will be returned def self.from_json json - from_json!(json).freeze + __blobjectify__(JSON.parse(json)) end - def self.from_json! json - - __from_hash_or_array__(JSON.parse(json)) - end - + # get a Blobject from a yaml string + # if the yaml string describes an array, an array will be returned def self.from_yaml yaml - from_yaml!(yaml).freeze + __blobjectify__(YAML.load(yaml)) end - def self.from_yaml! yaml - - __from_hash_or_array__(YAML.load(yaml)) - end - private # to avoid naming collisions private method names are prefixed and suffix with double unerscores (__) - def __visit_subtree__ &block - - @hash.each do |name, node| - - if node.class <= Array - node.flatten.each do |node_node| - block.call(nil, node_node, &block) - end - end - - block.call name, node, &block - end - end - + # Used to tag and reraise errors from a Blobject + # Refer to "Tagging exceptions with modules" on p97 in Exceptional Ruby by Avdi Grimm # errors from this library can be handled with rescue Blobject::Error + module Error; end + def __tag_and_raise__ e raise e rescue e.extend Blobject::Error raise e @@ -194,23 +190,39 @@ class << self private - def __from_hash_or_array__ hash_or_array - - if hash_or_array.class <= Array - return hash_or_array.map do |e| - if e.class <= Hash - Blobject.new e - else - e - end + def __freeze_r__ object + + case object + when Array + return object.each do |e| + e.freeze + __freeze_r__(e) end + when Hash + return object.each do |k, v| + v.freeze + __freeze_r__(v) + end + when Blobject + object.freeze + __freeze_r__ object.hash + else + object.freeze end + end - Blobject.new hash_or_array + def __blobjectify__ object + + array = object if object.is_a? Array + hash = object if object.is_a? Hash + + return array.map{|a| __blobjectify__(a)} if array + return Blobject.new(hash) if hash + return object end def __define_attribute__ name __tag_and_raise__ NameError.new("invalid attribute name #{name}") unless name =~ /^\w+$/ @@ -220,11 +232,11 @@ setter_name = (name.to_s + '=').to_sym unless methods.include? setter_name self.send :define_method, setter_name do |value| begin - value = self.class.new(value) if value.class <= Hash + value = self.class.send(:__blobjectify__, value) if value.is_a?(Hash) or value.is_a?(Array) @hash[name] = value rescue ex __tag_and_raise__(ex) end @store_in_parent.call unless @store_in_parent.nil? @@ -234,12 +246,12 @@ unless methods.include? name self.send :define_method, name do value = @hash[name] - if value.nil? && !frozen? + if value.nil? value = self.class.new - @hash[name] = value + @hash[name] = value unless frozen? end value end end \ No newline at end of file