hash19 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +175 -5
- data/lib/hash19/resolvers.rb +7 -4
- data/lib/hash19/version.rb +1 -1
- data/spec/hash19/core_spec.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d4abf9fa415283f0a656fd345f9bd33cc99bc59
|
4
|
+
data.tar.gz: 99a02257f4412c560e9efa21b01c1152366de9db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f769a3aaaec08daeb22c90867935d858fd7fb31e3ad6d26cc81ca5be2acb90b1c39fc0ac7467a2a19cb9b0d23b54550d07928c26e0d096dff52d9cf6edf2dbad
|
7
|
+
data.tar.gz: f88e1b86c8e5309d79e8d591114fab773bcacf6a647d5c01d6b8060606ba121f0a7761b998bf2c792c21457cfe323dfc4930da2a9901a59fa53bc5576c63c665
|
data/README.md
CHANGED
@@ -1,11 +1,21 @@
|
|
1
1
|
# Hash19
|
2
|
-
|
3
2
|
[](https://travis-ci.org/rcdexta/hash19)
|
4
3
|
|
5
|
-
|
4
|
+

|
6
5
|
|
7
|
-
|
6
|
+
>*Hash-19 is as an assassin droid in the Star Wars Universe. These are durasteel drones uploaded with only the most archaic kill programs* <sup>[Wookieepedia]</sup>
|
7
|
+
|
8
|
+
Ahem.. Ahem.. So about this gem itself.. When I was writing an aggregation API that had to talk to multiple services each with their own REST end-points and JSON schema, when mashing up multiple hashes and transforming it to a structure acceptable to the consumer, I ended up writing lot of boiler plate code. I could see patterns and there was clearly scope for optimisation.
|
8
9
|
|
10
|
+
Hash19 is an attempt at offering a DSL to tame the JSON manipulation and help in dealing with common use-cases. The features include
|
11
|
+
|
12
|
+
* whitelisting attributes
|
13
|
+
* attribute aliasing and keying
|
14
|
+
* `has_one` and `has_many` associations
|
15
|
+
* lazy loading associations via triggers
|
16
|
+
* mass injection of associations using bulk APIs
|
17
|
+
|
18
|
+
## Installation
|
9
19
|
Add this line to your application's Gemfile:
|
10
20
|
|
11
21
|
```ruby
|
@@ -20,14 +30,174 @@ Or install it yourself as:
|
|
20
30
|
|
21
31
|
$ gem install hash19
|
22
32
|
|
33
|
+
### One example for all
|
34
|
+
```ruby
|
35
|
+
class Jedi
|
36
|
+
include Hash19
|
37
|
+
attributes :name, :saber, :padawan_id
|
38
|
+
attribute :master, key: :trained_by
|
39
|
+
has_one :padawan, using: :padawan_id, trigger: ->(id) { Padawan.find id }
|
40
|
+
has_many :killings
|
41
|
+
end
|
42
|
+
|
43
|
+
class Padawan
|
44
|
+
include Hash19
|
45
|
+
attributes :id, :name
|
46
|
+
def find(id)..end #implementation hidden
|
47
|
+
def find_all(ids)..end #implementation hidden
|
48
|
+
end
|
49
|
+
|
50
|
+
json = '[{"name": "Anakin Skywalker", "saber": "Single Blade Blue", "trained_by": "Obi Wan",
|
51
|
+
"padawan": {"id": 201, "name": "Ahsoka Tano"}},
|
52
|
+
{"name": "Mace Windu", "saber": "Single Blade Violet", "padawan_id": 132, "trained_by": "Yoda"}]'
|
53
|
+
|
54
|
+
jedis = JSON.parse(json)
|
55
|
+
Jedi.new(jedis.first).to_h #{"name"=>"Anakin Skywalker", "saber"=>"Single Blade Blue", "master"=>"Obi Wan",
|
56
|
+
#"padawan"=>{"id"=>201, "name"=>"Ahsoka Tano"}}
|
57
|
+
|
58
|
+
Jedi.new(jedis.last).to_h #{"name"=>"Mace Windu", "saber"=>"Single Blade Violet", "master"=>"Yoda",
|
59
|
+
#"padawan"=>{"id"=>132, "name"=>"Depa Billaba["}}
|
60
|
+
|
61
|
+
```
|
62
|
+
All aspects of the code are explained with detailed examples below. This gives a quick snapshot of what the gem can do. Ergo...
|
63
|
+
* the attributes `name`, `saber` and `padawan_id` have been whitelisted. Any other attribute in the JSON will be ignored
|
64
|
+
* the attributes can have aliases in the actual JSON
|
65
|
+
* there can be an inline relationship within the JSON with another entity. For example, each `Jedi` entity can contain a `padawan` object. If the association already exists, it will be transformed. This is true for the first Jedi in the example
|
66
|
+
* If the association is not present in the JSON, it is lazy loaded. The first call to the attribute will call the trigger to fetch the association, if not present. In this case, for the second Jedi, a call `Padawan#find` will be triggered and the association fetched.
|
67
|
+
|
68
|
+
Now, this immediately raises the question about firing multiple calls to Padawan#find when there are many entries without the association populated. And that's where injection is recommended:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class Jedis
|
72
|
+
include Hash19
|
73
|
+
contains :jedi
|
74
|
+
inject at: '$', using: :padawan_id, reference: :id,
|
75
|
+
trigger: lambda { |ids| Padawan.find_all ids }, as: 'padawan'
|
76
|
+
end
|
77
|
+
```
|
78
|
+
This is like a wrapper class for the Jedi collection. It collects all `padawan_ids` from the complete JSON, calls `Padawan#find_all` once, the bulk-api equivalent of `find`, with a list of ids and injects the content back to the main collection at appropriate places as defined by the json_path in `at`
|
79
|
+
|
23
80
|
## Usage
|
24
81
|
|
25
|
-
|
82
|
+
To get started, include the Hash19 module in the target class and you are good.
|
83
|
+
|
84
|
+
A detailed documentation of all features can be found below:
|
85
|
+
|
86
|
+
###1. Whitelisting attributes
|
87
|
+
```ruby
|
88
|
+
class SuperHero
|
89
|
+
include Hash19
|
90
|
+
attributes :name, :strength
|
91
|
+
attribute :universe, key: :comic
|
92
|
+
end
|
93
|
+
```
|
94
|
+
Assume a JSON payload has many more attributes
|
95
|
+
```json
|
96
|
+
[{"name": "Flash", "strength": "Speed", "last_seen": "never", "comic": "DC"},
|
97
|
+
{"name": "Magneto", "strength": "Magnetism Control", "first_seen": 1963, "comic": "Marvel"},
|
98
|
+
{"name": "Hulk", "strength": "Super Strength", "weakness": "temper", "comic": "Marvel"}]
|
99
|
+
```
|
100
|
+
When this JSON is thrown at a Hash19 class...
|
101
|
+
```ruby
|
102
|
+
payload = JSON.parse(json)
|
103
|
+
results = payload.map { |hash| SuperHero.new(hash).to_h }
|
104
|
+
print results #[{"name"=>"Flash", "strength"=>"Speed", "universe"=>"DC"},
|
105
|
+
# {"name"=>"Magneto", "strength"=>"Magnetism Control", "universe"=>"Marvel"},
|
106
|
+
# {"name"=>"Hulk", "strength"=>"Super Strength", "universe"=>"Marvel"}]
|
107
|
+
```
|
108
|
+
Note that only the whitelisted attributes are accepted and keys can be aliased. The `to_h` method converts the native hash19 object into a ruby hash.
|
109
|
+
|
110
|
+
###2. Still a hash
|
111
|
+
The Hash19 object acts as a wrapper to Ruby Hash. All hash operations are supported by the wrapper. But, finally `to_h` should be called to retrieve the underlying hash.
|
112
|
+
``` ruby
|
113
|
+
hero = SuperHero.new(name: "Flash", strength: "Speed", comic: "DC")
|
114
|
+
hero[:name] #Flash
|
115
|
+
hero[:nick_name] = "Scarlet Speedster"
|
116
|
+
hero.keys #["name", "strength", "universe", "nick_name"]
|
117
|
+
hero.to_h #{"name"=>"Flash", "strength"=>"Speed", "universe"=>"DC", "nick_name"=>"Scarlet Speedster"}
|
118
|
+
```
|
119
|
+
###3. Associations
|
120
|
+
One-to-one and One-to-many relationships are supported. All associations are lazy loaded unless present directly in the root JSON.
|
121
|
+
```ruby
|
122
|
+
class Hashable
|
123
|
+
include Hash19
|
124
|
+
end
|
125
|
+
class SuperVillain < Hashable
|
126
|
+
attribute :name
|
127
|
+
has_many :minions
|
128
|
+
has_one :doctor
|
129
|
+
end
|
130
|
+
class Minion < Hashable
|
131
|
+
attributes :name, :sound
|
132
|
+
end
|
133
|
+
class Doctor < Hashable
|
134
|
+
attribute :name
|
135
|
+
end
|
136
|
+
```
|
137
|
+
Now, a JSON of the following structure
|
138
|
+
```json
|
139
|
+
{"name": "Gru", "doctor": {"name": "Nefario"},
|
140
|
+
"minions": [{"name": "Poppadom", "sound": "Weebaa"},{"name": "Gelato", "sound": "Ooojaa"}]
|
141
|
+
```
|
142
|
+
can be parsed with all associations loaded when calling `SuperVillain.new(json_as_hash)`
|
143
|
+
|
144
|
+
If the parent JSON does not contain the associations and they are powered by separate API calls, we can specify triggers to load them.
|
145
|
+
```ruby
|
146
|
+
class SuperVillain < Hashable
|
147
|
+
attribute :name, doctor_id
|
148
|
+
has_one :doctor, using: :doctor_id, trigger: ->(id) { Error.find id }
|
149
|
+
end
|
150
|
+
```
|
151
|
+
If you notice the trigger, the `using` parameter denotes the attribute to use to fetch the association and the lambda passed to `trigger` will be invoked to fetch the association. This is lazy loaded, in the sense when a call is made to `.doctor` or `.to_h`, the trigger is fired.
|
152
|
+
|
153
|
+
###4. Bulk Injections
|
154
|
+
|
155
|
+
Left to itself with associations, when the root JSON is a large collection with none of the associations populated in the first place, there will several triggers fired for each item in the collection. This is the HTTP equivalent of `N+1` in the ORM world. To avoid this, Hash19 supports association injections. Let's dive into an example:
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
class SuperHeroes < Hashable
|
159
|
+
contains :super_heroes
|
160
|
+
inject at: '$', using: :weapon_id, reference: :id, trigger: lambda { |ids| Weapon.find_all ids }
|
161
|
+
end
|
162
|
+
|
163
|
+
class SuperHero < Hashable
|
164
|
+
attributes :name, :power, :weapon_id
|
165
|
+
has_one :weapon, using: :weapon_id, trigger: lambda { |id| Weapon.find id }
|
166
|
+
end
|
167
|
+
|
168
|
+
class Weapon < Hashable
|
169
|
+
attributes :name, :id
|
170
|
+
def find_all(ids)..end #calls bulk API across wire. Implementation hidden
|
171
|
+
end
|
172
|
+
```
|
173
|
+
If you notice, `SuperHeroes` is a wrapper class around `SuperHero`. This is the object equivalent of a JSON collection. The `inject` method will extract `weapon_id` from all items in the collection and call the `trigger` and put back the resultant entities joining `superhero.weapon_id` and `weapon.id`
|
174
|
+
|
175
|
+
So, a json like below
|
176
|
+
```json
|
177
|
+
super_heroes = SuperHeroes.new([{name: 'iron man', power: 'none', weapon_id: 1},
|
178
|
+
{name: 'thor', power: 'class 100', weapon_id: 2},
|
179
|
+
{name: 'hulk', power: 'bulk', weapon_id: 3}])
|
180
|
+
```
|
181
|
+
|
182
|
+
will lead to one call to `Weapon#find_all` with params `[1,2,3]` to fetch all weapon details. And the final collection will be of the form:
|
183
|
+
```ruby
|
184
|
+
super_heroes.to_h #[{'name' => 'iron man', 'power' => 'none', 'weapon' => {'name' => 'jarvis', 'id' => 1}},
|
185
|
+
#{'name' => 'thor', 'power' => 'class 100', 'weapon' => {'name' => 'hammer', 'id' => 2}},
|
186
|
+
#{'name' => 'hulk', 'power' => 'bulk', 'weapon' => {'name' => 'hands', 'id' => 3}}
|
187
|
+
```
|
188
|
+
|
189
|
+
Note that `injection` always overrides the association trigger sinces the former is eager loaded and latter is lazy loaded thus avoiding the `N+1` calls.
|
190
|
+
|
191
|
+
Please refer to the [tests](https://github.com/rcdexta/hash19/tree/master/spec/hash19) for more examples and documentation.
|
26
192
|
|
27
193
|
## Contributing
|
28
194
|
|
29
|
-
1. Fork it ( https://github.com/
|
195
|
+
1. Fork it ( https://github.com/rcdexta/hash19/fork )
|
30
196
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
31
197
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
32
198
|
4. Push to the branch (`git push origin my-new-feature`)
|
33
199
|
5. Create a new Pull Request
|
200
|
+
|
201
|
+
|
202
|
+
|
203
|
+
|
data/lib/hash19/resolvers.rb
CHANGED
@@ -21,10 +21,13 @@ module Hash19
|
|
21
21
|
association.map { |hash| klass.send(:new, hash).to_h(lazy: true) }
|
22
22
|
end
|
23
23
|
else
|
24
|
-
|
24
|
+
unless opts[:trigger]
|
25
|
+
puts "warning: Association:<#{name}> is not present in #{self.class.name}. Possible specify a trigger"
|
26
|
+
next
|
27
|
+
end
|
25
28
|
puts "warning: Key:<#{opts[:using]}> not present in #{self.class.name}. Cannot map association:<#{name}>" unless @hash19.has_key? opts[:using]
|
26
29
|
if opts[:trigger] and @hash19.has_key? opts[:using]
|
27
|
-
@hash19[opts[:alias] || name] = LazyValue.new(-> { opts[:trigger].call(@hash19.delete(opts[:using])) })
|
30
|
+
@hash19[opts[:alias] || name] = LazyValue.new(-> { opts[:trigger].call(@hash19.delete(opts[:using])) })
|
28
31
|
end
|
29
32
|
end
|
30
33
|
end
|
@@ -60,9 +63,9 @@ module Hash19
|
|
60
63
|
full_class_name = self.class.name
|
61
64
|
new_class = full_class_name.gsub(full_class_name.demodulize, assoc_name)
|
62
65
|
new_class.split('::').inject(Object) do |mod, class_name|
|
63
|
-
|
66
|
+
begin
|
64
67
|
mod.const_get(class_name)
|
65
|
-
|
68
|
+
rescue NameError
|
66
69
|
raise("Class:<#{new_class}> not defined! Unable to resolve association:<#{assoc_name.downcase}>")
|
67
70
|
end
|
68
71
|
end
|
data/lib/hash19/version.rb
CHANGED
data/spec/hash19/core_spec.rb
CHANGED
@@ -43,8 +43,8 @@ describe Hash19::Core do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
it 'should be able to assign attributes based on alias' do
|
46
|
-
test = Test2.new(actual
|
47
|
-
expect(test.to_h).to eq('fake' => 1)
|
46
|
+
test = Test2.new("actual" => 1, "d" => 2)
|
47
|
+
expect(test.to_h).to eq('fake' => 1, "d" => 2)
|
48
48
|
end
|
49
49
|
|
50
50
|
it 'should be able to use both attribute and attributes constructs' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hash19
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- RC
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-12-
|
11
|
+
date: 2014-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|