lib/blobject.rb in blobject-0.2.3 vs lib/blobject.rb in blobject-0.3.2

- old
+ new

@@ -1,209 +1,259 @@ -require 'blobject/version' require 'json' +require 'yaml' +require_relative 'blobject/version' -def blobject *parameters, &block - Blobject.new *parameters, &block -end - class Blobject - def initialize hash = {}, &block - @hash = {} - merge hash + # 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 + + def initialize hash = {} + + @hash = hash + + @hash.keys.each do |key| + unless key.class <= Symbol + value = @hash.delete key + key = key.to_sym + @hash[key] = value + end + end + + __visit_subtree__ do |name, node| + if node.class <= Hash + @hash[name] = Blobject.new node + end + end + + yield self if block_given? + end + + def inspect - @modifying = false - self.modify &block if block_given? + @hash.inspect end - - def modify &block + + def hash - __r_modify_set__ true + @hash + end + + def to_hash - exception = nil - - begin - self.instance_eval &block - rescue Exception => e - exception = e + h = hash.dup + __visit_subtree__ do |name, node| + h[name] = node.to_hash if node.respond_to? :to_hash end - - __r_modify_set__ false - - raise exception unless exception.nil? - return self + h end - - def method_missing sym, *params, &block - - if match = /^has_(.+)\?/.match(sym) - return @hash.has_key? match[1].to_sym + + # method_missing is only called the first time an attribute is used. successive calls use + # memoized getters, setters and checkers + def method_missing method, *params, &block + + __tag_and_raise__ NoMethodError.new(method) if ProhibitedNames.include?(method) + + 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) + + self.class.send :__define_attribute__, name + + return send(method) if @hash.has_key? method + + parent = self + nested_blobject = self.class.new + + store_in_parent = lambda do + parent.send "#{name}=", nested_blobject + nested_blobject.send :remove_instance_variable, :@store_in_parent + end + + nested_blobject.instance_variable_set :@store_in_parent, store_in_parent + + return nested_blobject + + when (name = method[/^(\w+)=$/, 1]) && params.length == 1 + # the call is an attribute writer + + self.class.send :__define_attribute__, name + return send method, params.first + + when (name = method[/^(\w+)\?$/, 1]) && params.length == 0 + # the call is an attribute checker + + self.class.send :__define_attribute__, name + return send method end + + super + end + + def respond_to? method - if @modifying - - case params.length - when 0 # get - value = @hash[sym] - - if value - value.modify(&block) if block_given? and value.instance_of?(Blobject) - return value - end - - child = Blobject.new - parent = self - - child.__r_modify_set__ true - - store_in_parent = lambda { - - parent_hash = parent.instance_variable_get '@hash' - parent_hash[sym] = child + return true if self.methods.include?(method) + return false if ProhibitedNames.include?(method) - parent_store_in_parent = parent.instance_variable_get :@__store_in_parent__ - parent_store_in_parent.call unless parent_store_in_parent.nil? - - child.method(:remove_instance_variable).call(:@__store_in_parent__) - } - - child.instance_variable_set :@__store_in_parent__, store_in_parent - - return block_given? ? child.modify(&block) : child - when 1 # set - @hash[sym] = params[0] - - store_in_parent = @__store_in_parent__ - store_in_parent.call unless store_in_parent.nil? - - return self - end - else - return @hash[sym] if @hash.has_key? sym + method = method.to_s + + [/^(\w+)=$/, /^(\w+)\?$/, /^\w+$/].any? do |r| + r.match(method) end - + end + + def == other + return @hash == other.hash if other.class <= Blobject + return @hash == other if other.class <= Hash super end - - def merge hash + + def [] name - hash.each do |key, value| - @hash[key.to_s.to_sym] = self.class.__blobjectify__ value - end + send name + end + + def []= name, value - self + send "#{name.to_s}=", value end - def empty? - @hash.empty? + def freeze + __visit_subtree__ { |name, node| node.freeze } + @hash.freeze + super end - - def [] key - @hash[key.to_s.to_sym] + + def as_json + + to_hash end - - def keys - @hash.keys + + def to_json + + as_json.to_json end - - def values - @hash.values + + def as_yaml + + to_hash end - - def each &block - return @hash.each &block + + def to_yaml + + as_yaml.to_yaml end - - def []= key, value - send key, value + + def self.from_json json + + from_json!(json).freeze end - - def dup - Marshal.load(Marshal.dump(self)) - end - - def to_hash - hash = @hash + + def self.from_json! json - hash.each do |key, value| - hash[key] = value.to_hash if (value.instance_of? Blobject) - - if value.instance_of? Array - hash[key] = value.map do |v| - v.instance_of?(Blobject) ? v.to_hash : v - end - end - end - - hash + __from_hash_or_array__(JSON.parse(json)) end - - def from_hash hash - Blobject.new hash - end - - def to_yaml *params - to_hash.to_yaml *params - end - + def self.from_yaml yaml - __blobjectify__ YAML.load(yaml) + + from_yaml!(yaml).freeze end - - def to_json *params - @hash.to_json *params + + def self.from_yaml! yaml + + __from_hash_or_array__(YAML.load(yaml)) end - - def self.from_json json - __blobjectify__ JSON.load(json) - end - - def self.read path - case File.extname(path).downcase - when /\.y(a)?ml$/ - from_yaml File.read(path) - when /\.js(on)?$/ - from_json File.read(path) - else - raise "Cannot handle file format of #{path}" + +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 - - def dup - Blobject.new to_hash + + # errors from this library can be handled with rescue Blobject::Error + def __tag_and_raise__ e + raise e + rescue + e.extend Blobject::Error + raise e end - - def inspect - @hash.inspect - end -protected - - def self.__blobjectify__ obj - - if obj.instance_of?(Hash) - - obj.each do |key, value| - obj[key] = __blobjectify__ value + 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 + end end - - return self.new obj + + Blobject.new hash_or_array end - - if obj.instance_of?(Array) - return obj.map do |e| - __blobjectify__ e + + def __define_attribute__ name + + __tag_and_raise__ NameError.new("invalid attribute name #{name}") unless name =~ /^\w+$/ + name = name.to_sym + + methods = self.instance_methods + + 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 + @hash[name] = value + rescue ex + __tag_and_raise__(ex) + end + @store_in_parent.call unless @store_in_parent.nil? + end end - end - - obj - end - - def __r_modify_set__ v - @modifying = v - @hash.values.each do |child| - if child.class <= Blobject - child.__r_modify_set__ v + + unless methods.include? name + self.send :define_method, name do + + value = @hash[name] + + if value.nil? && !frozen? + value = self.class.new + @hash[name] = value + end + + value + end end + + checker_name = (name.to_s + '?').to_sym + unless methods.include? checker_name + self.send :define_method, checker_name do + @hash.key?(name) + end + end + + name end end end \ No newline at end of file