dottie 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a050415bffe078f5aa5be713416cfb686a9ec913
4
+ data.tar.gz: 6f9af95ef6a28fbd9a07ae1fdc3b519c9c4a77d9
5
+ SHA512:
6
+ metadata.gz: 46a17f46eb85e0757d222f1e1c2e81ab6e1cccc4657d5e6f723a091edf69013c6553b0b3cfe8e37409bd0c43a46eef739a8f85a7ce4c3f5a2241dea843e9b744
7
+ data.tar.gz: 06f2654c46e34627a7de943f59c9f2209413a5f3306733a4e34ebdfdbfc7322f89e780610a34ee890893770c23652cc4697cb5390f0be58536eb9f18025b8ff2
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nick Pearson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # Dottie
2
+
3
+ Dottie lets you access a Hash or Array (possibly containing other Hashes and Arrays) using a dot-delimited string as the key. The string is parsed into individual keys, and Dottie traverses the data structure to find the target value. If at any point along the way a node is not found, Dottie will return `nil` rather than raising an error.
4
+
5
+ A great place Dottie is useful is when accessing a data structure parsed from a JSON string, such as when consuming a JSON API. Since the only structural elements JSON can contain are objects and arrays, Dottie can easily access anything parsed from JSON.
6
+
7
+ Here's a simple example showing a data structure as well as why Dottie is a nice way of accessing the values, especially when certain parts of the data are not guaranteed to be present:
8
+
9
+ ```ruby
10
+ car = {
11
+ 'color' => 'black',
12
+ 'type' => {
13
+ 'make' => 'Tesla',
14
+ 'model' => 'Model S'
15
+ }
16
+ }
17
+
18
+ # normal Hash access
19
+ car['color'] # => "black"
20
+ car['type']['make'] # => "Tesla"
21
+ car['specs']['mileage'] # => # undefined method `[]' for nil:NilClass
22
+
23
+ # with Dottie
24
+ d = Dottie(car)
25
+ d['color'] # => "black"
26
+ d['type.make'] # => "Tesla"
27
+ d['specs.mileage'] # => nil
28
+ ```
29
+
30
+ Here's another example showing a hash containing an array:
31
+
32
+ ```ruby
33
+ family = {
34
+ 'mom' => 'Alice',
35
+ 'dad' => 'Bob',
36
+ 'kids' => [
37
+ { 'name' => 'Carol' },
38
+ { 'name' => 'Dan' }
39
+ ]
40
+ }
41
+
42
+ # normal Hash/Array access
43
+ family['kids'][0]['name'] # => "Carol"
44
+ family['kids'][2]['name'] # => # undefined method `[]' for nil:NilClass
45
+ family['pets'][0]['name'] # => # undefined method `[]' for nil:NilClass
46
+
47
+ # with Dottie
48
+ d = Dottie(family)
49
+ d['kids[0].name'] # => "Carol"
50
+ d['kids[2].name'] # => nil (array only has two elements)
51
+ d['pets[0].name'] # => nil ('pets' does not exist)
52
+ ```
53
+
54
+ ## Installation
55
+
56
+ Add this line to your application's Gemfile:
57
+
58
+ gem 'dottie'
59
+
60
+ And then execute:
61
+
62
+ $ bundle
63
+
64
+ Or install it yourself as:
65
+
66
+ $ gem install dottie
67
+
68
+ If you want the mixin behavior described below (where you can call `dottie` and `dottie!` on `Hash` and `Array` objects), require `'dottie/ext'` in your Gemfile:
69
+
70
+ gem 'dottie', require: 'dottie/ext'
71
+
72
+ ## Usage
73
+
74
+ First, let's start with some examples of Dottie's behaviors. After that, we'll see how we can access those behaviors.
75
+
76
+ ```ruby
77
+ # Since we have to start somewhere, here's one way of getting Dottie's
78
+ # behaviors. See the usage options below for different ways of doing
79
+ # this and to choose the best option for your app.
80
+ data = Dottie({
81
+ 'a' => { 'b' => 'c' }
82
+ })
83
+
84
+ # access data with normal keys or with Dottie-style keys
85
+ data['a'] # => {"b"=>"c"}
86
+ data['a.b'] # => "c"
87
+ data.fetch('a.b') # => "c"
88
+ data.has_key?('a.b') # => true
89
+
90
+ # store a value in a nested Hash, then check the Hash
91
+ data['a.b'] = 'd'
92
+ data['a.b'] # => "d"
93
+ data.hash # => {"a"=>{"b"=>"d"}}
94
+
95
+ # store a value at a new key
96
+ data['a.e'] = 'f'
97
+ data.hash # => {"a"=>{"b"=>"d", "e"=>"f"}}
98
+
99
+ # store a value deep in a Hash
100
+ # (Dottie fills in the structure when necessary)
101
+ h = Dottie({})
102
+ h['a.b.c'] = 'd'
103
+ h.hash # => {"a"=>{"b"=>{"c"=>"d"}}}
104
+
105
+ # Dottie can also work with nested Arrays
106
+ complex = Dottie({
107
+ 'a' => [{ 'b' => 'c' }, { 'd' => 'e' }]
108
+ })
109
+ complex['a[1].d'] # => "e"
110
+
111
+ # change the first array element value
112
+ complex['a[first].b'] = 'x'
113
+ complex.hash # => {"a"=>[{"b"=>"x"}, {"d"=>"e"}]}
114
+
115
+ # add another array element
116
+ complex['a[2]'] = 'y'
117
+ complex.hash # => {"a"=>[{"b"=>"x"}, {"d"=>"e"}, "y"]}
118
+ ```
119
+
120
+ ### Dottie Usage Options
121
+
122
+ There are three basic ways of using Dottie:
123
+
124
+ 1. By mixing Dottie's behavior into a `Hash`/`Array` (most versatile)
125
+ 2. As a wrapper around a Hash or Array (doesn't modify the `Hash`/`Array`)
126
+ 3. By calling Dottie's module methods directly (more verbose, good for one-off uses)
127
+
128
+ Here are examples of each of these usage options:
129
+
130
+ #### 1. Using Dottie as a Mixin
131
+
132
+ This is the simplest usage. You must `require 'dottie/ext'` in order for the `dottie!` method to be added to `Hash` and `Array`. This can be done in your Gemfile as shown above.
133
+
134
+ ```ruby
135
+ require 'dottie/ext' # not necessary if done in the Gemfile
136
+
137
+ # create a hash and add Dottie's behavior to it
138
+ hash = {
139
+ 'a' => 'b',
140
+ 'c' => {
141
+ 'd' => 'e'
142
+ }
143
+ }.dottie!
144
+
145
+ # the hash is still a Hash
146
+ hash.class # => Hash
147
+
148
+ # and it still does normal lookups
149
+ hash['a'] # => "b"
150
+
151
+ # but it can also look up Dottie-style keys now
152
+ hash['c.d'] # => "e"
153
+ ```
154
+
155
+ The `Hash` and `Array` extensions are a nice but optional way to get convenient access to Dottie's behavior on built-in classes. To add Dottie's behavior to a `Hash` or `Array` without the use of the class extensions, you can extend the object with Dottie's methods. This is what the `dottie!` extension methods do internally.
156
+
157
+ ```ruby
158
+ h = {
159
+ 'a' => { 'b' => 'c' }
160
+ }
161
+ h.extend(Dottie::Methods)
162
+ h['a.b'] # => "c"
163
+ ```
164
+
165
+ #### 2. Using Dottie as a Wrapper
166
+
167
+ This is the preferred method if you do not wish to modify the `Hash` or `Array` you're working with. In this case, your object will be wrapped in a `Dottie::Freckle`, which will more or less act like the original object. There are a few exceptions to this, such as when checking for equality. (For this, use `.hash` or `.array` to get access to the wrapped object.)
168
+
169
+ To wrap an object, use `Dottie()` or `Dottie[]` (which are equivalent), or manually create a `Dottie::Freckle` instance. Or, if you have required `'dottie/ext'`, you can call `dottie` (not `dottie!`) on your object.
170
+
171
+ ```ruby
172
+ # create a Hash
173
+ hash = {
174
+ 'a' => { 'b' => 'c' }
175
+ }
176
+
177
+ # then wrap the hash in a Dottie::Freckle with one of these (all equivalent)
178
+ d_hash = Dottie(hash)
179
+ d_hash = Dottie[Hash]
180
+ d_hash = hash.dottie # only available if 'dottie/ext' has been required
181
+
182
+ # regardless of how the Freckle was created, we can now access its data
183
+ d_hash['a'] # => {"b"=>"c"} (a standard Hash lookup)
184
+ d_hash['a.b'] # => "c" (a Dottie-style lookup)
185
+
186
+ # works the same way with Arrays
187
+ arr = ['a', { 'b' => 'c' }, 'd']
188
+ d_arr = Dottie(arr) # or Dottie[arr] or arr.dottie
189
+ d_arr['[1].b'] #=> "c"
190
+
191
+ # or do it in one line
192
+ d_hash = Dottie({ 'a' => 'b' }) # => <Dottie::Freckle {"a"=>"b"}>
193
+ d_arr = Dottie(['a', 'b', 'c']) # => <Dottie::Freckle ["a", "b", "c"]>
194
+
195
+ # or, use the class extensions (must require 'dottie/ext')
196
+ d_hash = { 'a' => 'b' }.dottie # => <Dottie::Freckle {"a"=>"b"}>
197
+ d_arr = ['a', 'b', 'c'].dottie # => <Dottie::Freckle ["a", "b", "c"]>
198
+ ```
199
+
200
+ #### 3. Using Dottie's Methods Directly
201
+
202
+ This is an easy way to use Dottie's behaviors without modifying your objects and without wrapping them. For this, the syntax is more verbose, but if this suits you, here it is:
203
+
204
+ ```ruby
205
+ # create a Hash
206
+ hash = {
207
+ 'a' => { 'b' => 'c' }
208
+ }
209
+
210
+ # perform a Dottie-style lookup
211
+ Dottie.get(hash, 'a.b') # => "c"
212
+
213
+ # perform a standard lookup to verify there's no Dottie behavior
214
+ hash['a.b'] # => nil (there is no literal "a.b" key)
215
+
216
+ # store a value, Dottie-style
217
+ Dottie.set(hash, 'a.b', 'x')
218
+ hash # => {"a"=>{"b"=>"x"}}
219
+
220
+ # store a value, Hash-style
221
+ hash['a.b'] = 'z'
222
+ hash # => {"a"=>{"b"=>"x"}, "a.b"=>"z"} # stored literally as "a.b"
223
+
224
+ # let's do another Dottie-style lookup on the modified hash
225
+ Dottie.get(hash, 'a.b') # => "x"
226
+
227
+ # and another standard lookup now that there's a literal "a.b" key
228
+ hash['a.b'] # => "z"
229
+ ```
230
+
231
+ ### Traversing Arrays
232
+
233
+ Array elements can be targeted with a bracketed index, which is a positive or negative integer or a `first` or `last` named index.
234
+
235
+ ```ruby
236
+ me = Dottie({
237
+ 'pets' => ['dog', 'cat', 'bird', 'fish']
238
+ })
239
+ me['pets[first]'] # => "dog"
240
+ me['pets[1]'] # => "cat"
241
+ me['pets[-2]'] # => "bird"
242
+ me['pets[last]'] # => "fish"
243
+ ```
244
+
245
+ ### Dottie Key Format
246
+
247
+ Dottie uses periods and brackets to delimit key parts. In general, hash keys are strings separated by periods, and array indexes are integers surrounded by brackets. For example, in the key `a.b[1]`, `a` and `b` are strings (hash keys) and `1` is a bracketed integer (an array index). Here's an example of how this key would be used:
248
+
249
+ ```ruby
250
+ d = Dottie({
251
+ 'a' => {
252
+ 'b' => ['x', 'y', 'z']
253
+ }
254
+ })
255
+ d['a.b[1]'] # => "y"
256
+ ```
257
+
258
+ For readability and convenience, `first` and `last`, when enclosed with brackets (such as `[first]`), are treated as named array indexes and are converted to `0` and `-1`, respectively.
259
+
260
+ Besides `first` and `last`, any other non-integer string is handled like a normal, dot-delimited string. For example, the key `a[b].c` is equivalent to `a.b.c`.
261
+
262
+ Brackets can also be used to quote key segments that contain periods. For example, `a[b.c].d` is interpreted internally as `['a', 'b.c', 'd']`. (If you need to see how a key will be interpreted, use `Dottie#key_parts` in an IRB session.)
263
+
264
+ Dottie is forgiving in its key parsing. Periods are optional around brackets. For example, the key `a[1]b[2]c` is the same as `a.[1].b.[2].c`. Ruby-like syntax is preferred, where a period follows each closing bracket but does not preceed any opening brackets. The preferred key in this case is `a[1].b[2].c`.
265
+
266
+ ### What Dottie Doesn't Do
267
+
268
+ When mixing Dottie into a `Hash` or `Array`, or when wrapping one in a `Dottie::Freckle` (which passes unrecognized method calls on to the wrapped object), Dottie provides `[]`, `[]=`, `fetch`, and `has_key?` methods, with the latter two assuming you're working with a `Hash`.
269
+
270
+ By design, Dottie does not provide an implementation for `keys`, `each`, or other built-in `Hash` and `Array` methods where the expected behavior might be ambiguous. Dottie is meant to be an easy way to store and retrieve data in a JSON-like data structure (hashes and/or arrays nested within each other) and is not meant as a replacement for any core classes.
271
+
272
+ ## FAQ
273
+
274
+ **Q:** Will Dottie make my life easier?
275
+ **A:** Probably.
276
+
277
+ **Q:** Will Dottie brush my teeth for me?
278
+ **A:** Probably not.
279
+
280
+ **Q:** Why is the wrapper class named `Freckle`?
281
+ **A:** Why not?
282
+
283
+ ## Contributing
284
+
285
+ 1. Fork it
286
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
287
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
288
+ 4. Push to the branch (`git push origin my-new-feature`)
289
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'bundler/gem_tasks'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |task|
5
+ task.rspec_opts = ['--color', '--format', 'nested']
6
+ end
7
+
8
+ task default: :spec
data/dottie.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dottie/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dottie"
8
+ spec.version = Dottie::VERSION
9
+ spec.authors = ["Nick Pearson"]
10
+ spec.email = ["nick@banyantheory.com"]
11
+ spec.summary = %q{Deep Hash and Array access with dotted keys}
12
+ spec.description = %q{Deeply access nested Hash/Array data structures
13
+ without checking for the existence of every node
14
+ along the way.}
15
+ spec.homepage = "https://github.com/nickpearson/dottie"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.5"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ end
data/lib/dottie.rb ADDED
@@ -0,0 +1,153 @@
1
+ require 'dottie/methods'
2
+ require 'dottie/freckle'
3
+ require 'dottie/helper'
4
+ require 'dottie/version'
5
+ require 'strscan'
6
+
7
+ module Dottie
8
+
9
+ ##
10
+ # Creates a new Dottie::Freckle from a standard Ruby Hash or Array.
11
+
12
+ def self.[](obj)
13
+ Dottie::Freckle.new(obj)
14
+ end
15
+
16
+ ##
17
+ # Gets a value from an object. Does not assume the object has been extended
18
+ # with Dottie methods.
19
+
20
+ def self.get(obj, key)
21
+ Dottie.key_parts(key).each do |k|
22
+ obj = case obj
23
+ when Hash, Array
24
+ obj[k]
25
+ else
26
+ nil
27
+ end
28
+ end
29
+ obj
30
+ end
31
+
32
+ ##
33
+ # Sets a value in an object, creating missing nodes (Hashes and Arrays) as
34
+ # needed. Does not assume the object has been extended with Dottie methods.
35
+
36
+ def self.set(obj, key, value)
37
+ key_parts = Dottie.key_parts(key)
38
+ key_parts.each_with_index do |k, i|
39
+ # set the value if this is the last key part
40
+ if i == key_parts.size - 1
41
+ case obj
42
+ when Hash, Array
43
+ obj[k] = value
44
+ else
45
+ raise TypeError.new("expected Hash or Array but got #{obj.class.name}")
46
+ end
47
+ # otherwise, walk down the tree, creating missing nodes along the way
48
+ else
49
+ obj = case obj
50
+ when Hash, Array
51
+ # look ahead at the next key to see if an array should be created
52
+ if key_parts[i + 1].is_a?(Integer)
53
+ obj[k] ||= []
54
+ else
55
+ obj[k] ||= {}
56
+ end
57
+ when nil
58
+ # look at the key to see if an array should be created
59
+ case k
60
+ when Integer
61
+ obj[k] = []
62
+ else
63
+ obj[k] = {}
64
+ end
65
+ else
66
+ raise TypeError.new("expected Hash, Array, or nil but got #{obj.class.name}")
67
+ end
68
+ end
69
+ end
70
+ # return the value that was set
71
+ value
72
+ end
73
+
74
+ ##
75
+ # Checks whether a Hash or Array contains the last part of a Dottie-style key.
76
+
77
+ def self.has_key?(obj, key)
78
+ key_parts = Dottie.key_parts(key)
79
+ key_parts.each_with_index do |k, i|
80
+ # look for the key if this is the last key part
81
+ if i == key_parts.size - 1
82
+ if obj.is_a?(Array) && k.is_a?(Integer)
83
+ return obj.size > k
84
+ elsif obj.is_a?(Hash)
85
+ return obj.has_key?(k)
86
+ else
87
+ return false
88
+ end
89
+ else
90
+ obj = case obj
91
+ when Hash, Array
92
+ obj[k]
93
+ else
94
+ return false
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ ##
101
+ # Mimics the behavior of Hash#fetch, raising an error if a key does not exist
102
+ # and no default value or block is provided.
103
+
104
+ def self.fetch(obj, key, default = :_fetch_default_)
105
+ if Dottie.has_key?(obj, key)
106
+ Dottie.get(obj, key)
107
+ elsif block_given?
108
+ yield(key)
109
+ elsif default != :_fetch_default_
110
+ default
111
+ else
112
+ raise KeyError.new(%{key not found: "#{key}"})
113
+ end
114
+ end
115
+
116
+ ##
117
+ # Checks whether a key looks like a key Dottie understands.
118
+
119
+ def self.dottie_key?(key)
120
+ !!(key.is_a?(String) && key =~ /[.\[]/) || key.is_a?(Array)
121
+ end
122
+
123
+ ##
124
+ # Parses a Dottie key into an Array of strings and integers.
125
+
126
+ def self.key_parts(key)
127
+ if key.is_a?(String)
128
+ parts = []
129
+ s = StringScanner.new(key)
130
+ loop do
131
+ if s.scan(/\./)
132
+ next
133
+ elsif (p = s.scan(/[^\[\].]+/))
134
+ parts << p
135
+ elsif (p = s.scan(/\[-?\d+\]/))
136
+ parts << p.scan(/-?\d+/).first.to_i
137
+ elsif (p = s.scan(/\[(first|last)\]/))
138
+ parts << (p[1..-2] == 'first' ? 0 : -1)
139
+ elsif (p = s.scan(/\[.+?\]/))
140
+ parts << p[1..-2] # remove '[' and ']'
141
+ else
142
+ break
143
+ end
144
+ end
145
+ parts
146
+ elsif key.is_a?(Array)
147
+ key
148
+ else
149
+ raise TypeError.new("expected String or Array but got #{key.class.name}")
150
+ end
151
+ end
152
+
153
+ end