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.2'

Class Method Summary collapse

Class Method Details

.[](obj) ⇒ Object

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



12
13
14
15
16
17
18
# 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) ⇒ Object

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



237
238
239
240
241
242
243
244
245
246
247
248
249
# 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) ⇒ Object

Deletes the value at the specified key and returns it.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# 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) ⇒ Boolean

Checks whether a key looks like a key Dottie understands.

Returns:

  • (Boolean)


200
201
202
# 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_) ⇒ Object

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



120
121
122
123
124
125
126
127
128
129
130
# 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) ⇒ Object

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



155
156
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
# 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) ⇒ Object

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



24
25
26
27
28
29
30
31
32
33
34
35
36
# 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) ⇒ Boolean

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

Returns:

  • (Boolean)


93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# 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) ⇒ Object

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



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# 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 = {}) ⇒ Object

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



193
194
195
# 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) ⇒ 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.



42
43
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
# 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