GutenYe/optimism

View on GitHub
lib/optimism.rb

Summary

Maintainability
D
2 days
Test Coverage
require "optimism/util"
require "optimism/parser"
require "optimism/semantics"
require "optimism/require"

#
# <#Optimism> is a node, it has _data, _parent and _root attribute.
#
#  Rc = Optimism do
#    a.b 1
#    a.c do
#      d 2
#    end
#  end
#
#  p Rc
#  #=> <#Optimism
#     :a => <#Optimism
#       :b => 1
#       :c => <#Optimism
#         :d => 2>>>
#
#  Rc.a #=> <#Optimism>
#  Rc.a._data #=> {:b => 1, :c => <#Optimism>}
#  Rc.a._parent #=> is Rc
#  Rc.a._root #=> is Rc
#
#  Rc._parent #=>  nil
#  Rc._root #=> is Rc
#
# internal, string-key is converted into symbol-key
#
#  Rc = Optimism.new
#  Rc[:a] = 1
#  Rc["a"] = 2
#  p Rc[:a] #=> 2
#
# if you want disable it. with :symbolize_key => ture in constructor function.
class Optimism
  autoload :VERSION, "optimism/version"

  Error         = Class.new Exception 
  EMissingFile   = Class.new Error
  EPath         = Class.new Error
  EParse        = Class.new Error

  BUILTIN_METHODS = [:p, :sleep, :rand, :srand, :exit, :require, :at_exit, :autoload, :open, :send] # not :raise
  UNDEF_METHODS = [:to_ary, :&, :_root=, :_=, :_replace]

  @@extension = {}
  class << self
    public *BUILTIN_METHODS 
    public :undef_method

    # Return all extensions for require.
    def extension
      @@extension
    end

    # Add an extension for require.
    #
    # @example
    #
    #   add_extension(".rb", Optimism::Parser::Default)
    #   Optimism.require("a.rb")   # will use Default Parser to parse the content.
    #
    # @param [String, Array]
    # @param [Class] parser
    def add_extension(extension_s, parser)
      extensions = Util.wrap_array(extension_s)

      extensions.each { |ext|
        @@extension[ext] = parser
      }
    end
  end

  include Require

  undef_method *BUILTIN_METHODS
  include Semantics

  # the node name, a Symbol
  attr_accessor :_name

  attr_accessor :_data 
  alias _d _data
  alias _children _data

  # initialize
  # 
  # @example
  #
  #   # with :default option
  #   rc = Optimism.new(nil, default: 1)
  #   rc.i.donot.exists #=> 1
  #
  #   # with :namespace option
  #   rc = Optimism.new("foo=1", :namespace => "a.b")
  #   rc               -> <#Optimism foo: 1>
  #   rc._root         -> <#Optimism a: <#Optimsm b: <#Optimism foo: 1>>>
  #
  #   # root node
  #   Optimism.new()         # is {name: "", parent: nil}
  #
  #   # sub node
  #   Optimism.new(nil, name: "b", parent: o) 
  #
  #   # link sub node
  #   a = Optimism.new()
  #   b = Optimism.new()
  #   a[:foo] = b            # set b._name and b._parent
  #
  # @param [String,Hash,Optimism] content
  # @param [Hash] options
  # @option options [Object] :default (nil) default value for Hash
  # @option options [Boolean] :symbolize_key (true)
  # @option options [String] :namespace (nil)
  # @option options [String] :name ("") node name
  # @option options [String] :filename (nil) filename passed to eval
  # @option options [Optimism] :parent (nil) parent node
  # @option options [Symbol,Class,Proc] :parser (:default) parser to parse content and block. 
  def initialize(content=nil, options={}, &blk)
    @options = {symbolize_key: true}.merge(options)

    @_parser = case (p=options[:parser])
               when Symbol
                Parser.parsers[p].method(:parse)
               when Class
                 p.method(:parse)
               when Proc
                 p
               else
                 Parser::Default.method(:parse)
               end

    @_name = @options[:name] || ""
    @_parent = @options[:parent]

    case content
    when Hash
      @_data = _convert_hash(content, @options)._d
    when Optimism
      @_data = content._d
    else
      @_data = Hash.new(@options[:default])
      _parse! content, &blk if content or blk
    end

    _walk_up(@options[:namespace], :build => true, :reverse => true) if @options[:namespace]
  end

  # Returns true if equal without compare node name.
  #
  def ==(other)
    case other 
    when Optimism
      _data == other._d
    else
      false
    end
  end

  # deep merge new data IN PLACE
  #
  # @params [Hash,Optimism,String] other
  # @return [self]
  def _merge!(other)
    other = Optimism.new(other, Util.slice(@options, :default, :symbolize_key, :parser))
    
    other._each { |k, v|
      if Optimism === self[k] and Optimism === other[k] 
        self[k]._merge!(other[k])
      else
        self[k] = other[k]
      end
    }

    self
  end

  alias << _merge!

  # deep merge new data
  #
  # @params [Hash,Optimism] obj
  # @return [Optimism] new <#Optimism>
  def _merge(other)
    self._dup._merge!(other)
  end

  alias + _merge

  # shadow Duplicate
  #
  # @exmaple
  # 
  #  a = Optimism({a: {b: {c: 1}}})
  #  b = a._walk_down("a")
  #
  #  b2 = b._dup
  #  b2._parent = parent_node
  #
  #  b  -> {c: 1}
  #  b2 -> {c: 1}
  #
  # @return [Optimism] new <#Optimism>
  def _dup
    o = Optimism.new(nil, @options)
    o._data = _d.dup
    o._data.each {|k,v| v.instance_variable_set(:@_parent, o) if Optimism === v}

    o
  end

  ## path
  ##

  # parent node
  attr_accessor :_parent 
  def _parent=(parent)
    @_parent = parent

    if parent
      parent._d[_name.to_sym] = self
    end
  end

  # root node
  attr_reader :_root
  def _root
    node = self

    while node._parent
      node = node._parent
    end

    node
  end
  alias _r _root

  alias _children _data

  # current node
  attr_reader :_
  def _
    self
  end

  # walk along the path.
  #
  # @example 
  #
  #  _walk("_")    ->   self
  #
  #  o = Optimism(a: {b: {c: {d: 1}}})
  #  o._walk("a.b.c")               -> <#Optimism:c ..>
  #  o._walk("-b.a")                -> <#Optimism:a ..>
  #
  # @param [String] path
  # @param [Hash] opts
  # @option opts [Boolean] :build (nil) build the path if path doesn't exists.
  # @option opts [Boolean] :reverse (nil) reverse the path
  # @return [Optimism,nil] the result node
  def _walk(path, opts={})
    return self if %w[_ -_].include?(path)

    path =~ /^-/ ? _walk_up(path[1..-1], opts) : _walk_down(path, opts)
  end

  # @see _walk
  def _walk_down(path, opts={})
    node = self
    nodes = path.split(".")
    nodes.reverse! if opts[:reverse]

    nodes.each { |name|
      name = name.to_sym
      if node._has_key?(name) and Optimism === node[name]
        node = node[name]
      elsif !node._has_key?(name) and opts[:build]
        node = node._create_child_node(name)
      else
        return nil
      end
    }

    node
  end

  # @see _walk
  #
  def _walk_up(path, opts={})
    node = self
    nodes = path.split(".")
    nodes.reverse! if opts[:reverse]

    nodes.each { |name|
      if node._parent and node._parent._name == name
          node = node._parent
      elsif !node._parent and opts[:build]
        node = node._create_parent_node(name)
      else
        return nil
      end
    }
    node
  end

  # support path
  #
  # @overload _has_key?(key)
  #   @param [String,Symbol] key
  # @overload _has_key?(path)
  #   @param [String] path
  #
  # @see Hash#has_key?
  def _has_key?(path)
    case path
    when Symbol
      base, key = "_", path
    else
      base, key = _split_path(path.to_s)
    end

    node = _walk(base)

    if node then
      return node._d.has_key?(_convert_key(key))
    else
      return false
    end
  end

  def [](key)
    _data[_convert_key(key)]
  end

  # set data
  #
  def []=(key, value)
    # link node if value is <#Optimism>
    if Optimism === value
      value.instance_variable_set(:@_parent, self)
      value._name = key.to_sym
    end

    _data[_convert_key(key)] = value
  end

  # fetch with path support.
  #
  # @overload _fetch(key_s, [default])
  #   @param [Array, String, Symbol] key_s
  # @overload _fetch(path, [default])
  #   @param [String] path
  #
  # @example
  #
  #   o = Optimism do |c|
  #     c.a = 1
  #     c.b.c = 2
  #   end
  #
  #   o._fetch("not_exitts")      -> raise KeyError
  #   o._fetch("not_exitts", nil) -> nil
  #   o._fetch("b.c")             -> 2
  #   o._fetch("c.d", nil)        -> nil. path doesn't exist.
  #   o._fetch("a.b", nil)        -> nil. path is wrong
  #
  #   Default value and individual values.
  #
  #   o = Optimism do
  #     _.username = "foo"
  #     _.password = "pass"
  #     google do
  #       _.username = "bar"
  #     end
  #   end
  #
  #   o._fetch(["google.username", "username"], nil)     -> "bar"
  #   o._fetch(["google.password", "password"], nil)     -> "pass"
  #
  # @param [String] key
  # @return [Object] value
  # @see Hash#fetch
  def _fetch(*args)
    if args.length == 1 then
      path_s = args[0]
      raise_error = true
    else
      path_s, default = args
    end
    paths = Util.wrap_array(path_s)

    paths.each {|path|
      case path
      when Symbol
        base, key = "_", path
      else
        base, key = _split_path(path.to_s)
      end

      node = _walk(base)

      if node && node._has_key?(key) then
        return node[key]
      else
        next
      end
    }

    if raise_error then
      raise KeyError, "key not found -- #{path.inspect}"
    else
      return default
    end
  end

  # store with path support.
  #
  # @overload _store(key, value)
  #   @param [Symbol, String] key
  # @overload _store(path, value)
  #   @param [String] path
  #
  # @exampe
  #
  #  o = Optimism.new
  #  o._store("a.b", 1)      -> 1
  #
  # @param [Hash] o
  # @return [Object] value
  def _store(path, value)
    case path
    when Symbol
      base, key = "_", path
    else
      base, key = _split_path(path.to_s)
    end

    node = _walk(base, :build => true)
    node[key] = value

    value
  end

  # Delete an item.
  #
  # @overload _delete(key)
  #   @param [String, Symbol] key
  # @overload _delete(path)
  #   @param [String] path
  #
  def _delete(path, &blk)
    case path
    when Symbol
      base, key = "_", path
    else
      base, key = _split_path(path.to_s)
    end

    node = _walk(base)

    if node
      node._d.delete(_convert_key(key), &blk)
    else
      blk ? blk.call : nil
    end
  end

  def _parse!(content=nil, &blk)
    @_parser.call(self, content, {filename: @options[:filename]}, &blk)
  end

  # pretty print
  # 
  #   <#Optimism 
  #     :b => 1
  #     :c => 2
  #     :d => <#Optimism
  #       :c => 2>> 
  def inspect(indent="  ")
    rst = ""
    rst << "<#Optimism:#{_name}\n"
    _d.each { |k,v|
      rst << "#{indent}#{k.inspect} => "
      rst << (Optimism === v ? "#{v.inspect(indent+"  ")}\n" : "#{v.inspect}\n")
    }
    rst.rstrip! << ">"

    rst
  end

  alias to_s inspect
  alias to_str to_s

  def to_hash
    _data
  end

  # everything goes here.
  #
  #   .name? 
  #   .name= value 
  #   .name value 
  #   ._name
  #
  #   .c 
  #   .a.b.c
  #
  def method_missing(name, *args, &blk)
    return super if UNDEF_METHODS.include?(name)

    # relative path: __
    if name =~ /^__+$/
      num = name.to_s.count("_") - 1
      node = self
      num.times {
        return unless node
        node = node._parent
      }
      return node

    # .name=
    elsif name =~ /(.*)=$/
      return _data[$1.to_sym] = args[0]

    # ._name
    elsif name =~ /^_(.*)/
      name = $1.to_sym
      args.map!{|arg| Optimism === arg ? arg._d : arg} 
      return _data.__send__(name, *args, &blk)

    # .name?
    elsif name =~ /(.*)\?$/
      return !! _data[$1.to_sym]

    ##
    ## a.c  # return data if has :c
    ## a.c  # create new <#Optimism> if no :c 
    ##

    # p Rc.a             #=> 1
    # p Rc.compute_attr  #=> lambda{}.call  
    #
    # p Rc.node    
    # node do
    #  _.a = lambda{ Time.now }
    #  _.b = 2
    # end
    #
    elsif _data.has_key?(name)
      value = _data[name]
      if Proc === value && value.lambda? 
        value.call(*args)
      elsif Optimism === value && (blk || !args.empty?)
        value._parse!(*args, &blk)
      else
        value
      end

    # p Rc.a.b.c #=> create new <#Optimism>
    #
    # a.b do |c|
    #   c.a = 2
    # end
    #
    # a.b <<EOF
    #   a = 2
    # EOF
    else
      return _create_child_node(name, args[0], &blk)
    end
  end

  def respond_to_missing?(name, include_private=false)
    return super if UNDEF_METHODS.include?(name)
    true
  end

  # Create a new child node, link it and return it.
  # @protected
  #
  def _create_child_node(name, content=nil, opts={}, &blk)
    options = Util.slice(@options, :default, :symbolize_key, :parser).merge(opts).merge({name: name.to_s, parent: self})
    next_node = Optimism.new(content, options, &blk)
    _data[name.to_sym] = next_node

    next_node
  end

  # Create a new parent node, link it  and return it.
  # @protected
  def _create_parent_node(child_name, content=nil, opts={}, &blk)
    options = Util.slice(@options, :default, :symbolize_key, :parser).merge(opts)
    prev_node = Optimism.new(content, options, &blk)
    self._name = child_name.to_s
    self._parent = prev_node
    prev_node._data[child_name.to_sym] = self

    prev_node
  end

protected

  # deep convert Hash to optimism. 
  # I'm rescursive.
  # @protected
  # 
  # @overload convert_hash(hash) 
  #
  # @example
  #
  #   convert_hash({a: {b: 1})     -> {:a => <#Optimism :b => 1>}
  #
  # @param [Hash] hash
  # @option opts [Hash] :symbolize_key (nil)
  # @return [Hash]
  def _convert_hash(hash, opts={})
    o = Optimism.new(nil, opts)

    hash.each { |k, v|
      v = _convert_hash(v, opts.merge(name: k.to_s, parent: o)) if Hash === v
      k = (k.to_sym rescue k) || k if opts[:symbolize_key]

      o._d[k] = v
    }

    o
  end

  # Deep destructively convert all keys to symbols, as long as they respond
  # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
  # I'm recursive.
  def _symbolize_keys!(hash)
    hash.keys.each do |key, value|
      value = hash.delete(key)
      _symbolize_keys!(value) if Hash === value

      hash[(key.to_sym rescue key) || key] = value
    end

    hash
  end

  def _convert_key(key)
    if @options[:symbolize_key] and String === key
      key.to_sym 
    else
      key
    end
  end

  # split a path into path and key.
  #
  # "foo.bar.baz" => ["foo.bar", "baz"]
  # "foo" => [ "_", "foo"]
  #
  # @return [Array<string>] [base_path, key]
  def _split_path(path, opts={})
    paths = path.split('.')

    if paths.size == 1
      ["_", paths[0]]
    else
      [paths[0..-2].join('.'), paths[-1]]
    end
  end
end

module Kernel
  # a short-cut to Optimism.new
  def Optimism(*args, &blk)
    Optimism.new(*args, &blk)
  end
end

# extensions
require "optimism/parser/default"
require "optimism/parser/yaml"
require "optimism/parser/json"