module Dottie

Constants

VERSION

Public Class Methods

[](obj) click to toggle source

Creates a new Dottie::Freckle from a standard Ruby Hash or Array.

# File lib/dottie.rb, line 12
def self.[](obj)
  if obj.is_a?(Dottie::Freckle)
    obj
  else
    Dottie::Freckle.new(obj)
  end
end
build_key(parts) click to toggle source

Builds a Dottie key from an Array of strings and integers.

# File lib/dottie.rb, line 237
def self.build_key(parts)
  key = ''
  parts.each_with_index do |part, i|
    case part
    when String
      key << '.' unless i == 0
      key << part
    when Fixnum
      key << "[#{part}]"
    end
  end
  key
end
delete(obj, key) click to toggle source

Deletes the value at the specified key and returns it.

# File lib/dottie.rb, line 135
def self.delete(obj, key)
  if Dottie.has_key?(obj, key)
    key_parts = Dottie.key_parts(key)
    if key_parts.size > 1
      key = Dottie.build_key(key_parts[0..-2])
      obj = Dottie.get(obj, key)
    end
    if obj.is_a?(Array) && key_parts.last.is_a?(Fixnum)
      obj.delete_at(key_parts.last)
    else
      obj.delete(key_parts.last)
    end
  else
    nil
  end
end
dottie_key?(key) click to toggle source

Checks whether a key looks like a key Dottie understands.

# File lib/dottie.rb, line 200
def self.dottie_key?(key)
  !!(key.is_a?(String) && key =~ /[.\[]/) || key.is_a?(Array)
end
fetch(obj, key, default = :_fetch_default_) { |key| ... } click to toggle source

Mimics the behavior of Hash#fetch, raising an error if a key does not exist and no default value or block is provided.

# File lib/dottie.rb, line 120
def self.fetch(obj, key, default = :_fetch_default_)
  if Dottie.has_key?(obj, key)
    Dottie.get(obj, key)
  elsif block_given?
    yield(key)
  elsif default != :_fetch_default_
    default
  else
    raise KeyError.new(%{key not found: "#{key}"})
  end
end
flatten(obj, options = {}, path = nil, flat = nil) click to toggle source

Flattens a Hash or Array to a single-depth Hash with Dottie-style keys.

# File lib/dottie.rb, line 155
def self.flatten(obj, options = {}, path = nil, flat = nil)
  path ||= []
  flat ||= {}
  case obj
  when Hash
    obj.each do |k, v|
      this_path = path + [k]
      case v
      when Hash, Array
        flat[this_path.join('.')] = options[:keys_only] ? nil : v if options[:intermediate]
        Dottie.flatten(v, options, this_path, flat)
      else
        flat[this_path.join('.')] = options[:keys_only] ? nil : v
      end
    end
  when Array
    obj.each_with_index do |v, i|
      this_path = path.dup
      if this_path.any?
        this_path[-1] = this_path[-1].to_s + "[#{i}]"
      else
        this_path = ["[#{i}]"]
      end
      case v
      when Hash, Array
        flat[this_path.join('.')] = options[:keys_only] ? nil : v if options[:intermediate]
        Dottie.flatten(v, options, this_path, flat)
      else
        flat[this_path.join('.')] = options[:keys_only] ? nil : v
      end
    end
  end
  flat
end
get(obj, key) click to toggle source

Gets a value from an object. Does not assume the object has been extended with Dottie methods.

# File lib/dottie.rb, line 24
def self.get(obj, key)
  Dottie.key_parts(key).each do |k|
    obj = case obj
    when Hash, Array
      # use an array index if it appears that's what was intended
      k = k.to_i if obj.is_a?(Array) && k.to_i.to_s == k
      obj[k]
    else
      nil
    end
  end
  obj
end
has_key?(obj, key) click to toggle source

Checks whether a Hash or Array contains the last part of a Dottie-style key.

# File lib/dottie.rb, line 93
def self.has_key?(obj, key)
  key_parts = Dottie.key_parts(key)
  key_parts.each_with_index do |k, i|
    # look for the key if this is the last key part
    if i == key_parts.size - 1
      if obj.is_a?(Array) && k.is_a?(Integer)
        return obj.size > k
      elsif obj.is_a?(Hash)
        return obj.has_key?(k)
      else
        return false
      end
    else
      obj = case obj
      when Hash, Array
        obj[k]
      else
        return false
      end
    end
  end
end
key_parts(key) click to toggle source

Parses a Dottie key into an Array of strings and integers.

# File lib/dottie.rb, line 207
def self.key_parts(key)
  if key.is_a?(String)
    parts = []
    s = StringScanner.new(key)
    loop do
      if s.scan(/\./)
        next
      elsif (p = s.scan(/[^\[\].]+/))
        parts << p
      elsif (p = s.scan(/\[-?\d+\]/))
        parts << p.scan(/-?\d+/).first.to_i
      elsif (p = s.scan(/\[(first|last)\]/))
        parts << (p[1..-2] == 'first' ? 0 : -1)
      elsif (p = s.scan(/\[.+?\]/))
        parts << p[1..-2] # remove '[' and ']'
      else
        break
      end
    end
    parts
  elsif key.is_a?(Array)
    key
  else
    raise TypeError.new("expected String or Array but got #{key.class.name}")
  end
end
keys(obj, options = {}) click to toggle source

Gets an array of the Dottie-style keys that exist in a Hash or Array.

# File lib/dottie.rb, line 193
def self.keys(obj, options = {})
  Dottie.flatten(obj, { keys_only: true }.merge(options)).keys
end
set(obj, key, value) click to toggle source

Sets a value in an object, creating missing nodes (Hashes and Arrays) as needed. Does not assume the object has been extended with Dottie methods.

# File lib/dottie.rb, line 42
def self.set(obj, key, value)
  key_parts = Dottie.key_parts(key)
  key_parts.each_with_index do |k, i|
    # set the value if this is the last key part
    if i == key_parts.size - 1
      case obj
      when Hash
        obj[k] = value
      when Array
        case k
        when '-', 'prepend', '>>'
          obj.unshift(value)
        when '+', 'append', '<<'
          obj << value
        else
          obj[k] = value
        end
      else
        raise TypeError.new("expected Hash or Array but got #{obj.class.name}")
      end
    # otherwise, walk down the tree, creating missing nodes along the way
    else
      obj = case obj
      when Hash, Array
        # look ahead at the next key to see if an array should be created
        if key_parts[i + 1].is_a?(Integer) ||
           key_parts[i + 1] =~ /\A(-|\+|prepend|append|>>|<<)\z/
          obj[k] ||= []
        else
          obj[k] ||= {}
        end
      when nil
        # look at the key to see if an array should be created
        case k
        when Integer, '-', '+', 'prepend', 'append', '>>', '<<'
          obj[k] = []
        else
          obj[k] = {}
        end
      else
        raise TypeError.new("expected Hash, Array, or nil but got #{obj.class.name}")
      end
    end
  end
  # return the value that was set
  value
end