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