Module: Dottie

Defined in:
lib/dottie.rb,
lib/dottie/freckle.rb,
lib/dottie/methods.rb,
lib/dottie/version.rb

Defined Under Namespace

Modules: Methods Classes: Freckle

Constant Summary collapse

VERSION =
'0.0.4'

Class Method Summary collapse

Class Method Details

.[](obj) ⇒ Object

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



14
15
16
17
18
19
20
# File 'lib/dottie.rb', line 14

def self.[](obj)
  if obj.is_a?(Dottie::Freckle)
    obj
  else
    Dottie::Freckle.new(obj)
  end
end

.build_key(parts) ⇒ Object

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



239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/dottie.rb', line 239

def self.build_key(parts)
  key = String.new('')
  parts.each_with_index do |part, i|
    case part
    when String
      key << '.' unless i == 0
      key << part
    when Integer
      key << "[#{part}]"
    end
  end
  key
end

.delete(obj, key) ⇒ Object

Deletes the value at the specified key and returns it.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/dottie.rb', line 137

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?(Integer)
      obj.delete_at(key_parts.last)
    else
      obj.delete(key_parts.last)
    end
  else
    nil
  end
end

.dottie_key?(key) ⇒ Boolean

Checks whether a key looks like a key Dottie understands.

Returns:

  • (Boolean)


202
203
204
# File 'lib/dottie.rb', line 202

def self.dottie_key?(key)
  !!(key.is_a?(String) && key =~ /[.\[]/) || key.is_a?(Array)
end

.fetch(obj, key, default = :_fetch_default_) ⇒ Object

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



122
123
124
125
126
127
128
129
130
131
132
# File 'lib/dottie.rb', line 122

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) ⇒ Object

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



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/dottie.rb', line 157

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) ⇒ Object

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



26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/dottie.rb', line 26

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) ⇒ Boolean

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

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/dottie.rb', line 95

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) ⇒ Object

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



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/dottie.rb', line 209

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 = {}) ⇒ Object

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



195
196
197
# File 'lib/dottie.rb', line 195

def self.keys(obj, options = {})
  Dottie.flatten(obj, { keys_only: true }.merge(options)).keys
end

.set(obj, key, value) ⇒ Object

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.



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/dottie.rb', line 44

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