hash19 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ff2d798a32b0fb67c383d53b88546b11acb8ce9d
4
- data.tar.gz: ad33dffbd106aa49d391743f63943011baf81b90
3
+ metadata.gz: 8d4abf9fa415283f0a656fd345f9bd33cc99bc59
4
+ data.tar.gz: 99a02257f4412c560e9efa21b01c1152366de9db
5
5
  SHA512:
6
- metadata.gz: 999a9eee4711d4d188155f2642a327c2dc02935e0e155529719b32749c1fcb48fa3e9997c9c08fd1584bd338f49d59ffcddaa0198cba68faf7b274f17a72ee1f
7
- data.tar.gz: c9a1b4b182208b75970e0bb4d40bcf2f0ded531549b9ae231f3071e7c4ce9f6a948dd48cf6fa7a28a99f8604134e82b2c19e2458c5276964b7bc5dd1360145e2
6
+ metadata.gz: f769a3aaaec08daeb22c90867935d858fd7fb31e3ad6d26cc81ca5be2acb90b1c39fc0ac7467a2a19cb9b0d23b54550d07928c26e0d096dff52d9cf6edf2dbad
7
+ data.tar.gz: f88e1b86c8e5309d79e8d591114fab773bcacf6a647d5c01d6b8060606ba121f0a7761b998bf2c792c21457cfe323dfc4930da2a9901a59fa53bc5576c63c665
data/README.md CHANGED
@@ -1,11 +1,21 @@
1
1
  # Hash19
2
-
3
2
  [![Build Status](https://travis-ci.org/rcdexta/hash19.svg)](https://travis-ci.org/rcdexta/hash19)
4
3
 
5
- TODO: Write a gem description
4
+ ![Hash-19](https://s3-us-west-1.amazonaws.com/rcdexta/hash-19-droid.png)
6
5
 
7
- ## Installation
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
- TODO: Write usage instructions here
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/[my-github-username]/hash19/fork )
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
+
@@ -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
- puts "warning: Association:<#{name}> is not present in #{self.class.name}. Possible specify a trigger" unless opts[:trigger]
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])) }) if opts[:trigger]
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
- if mod.const_defined?(class_name)
66
+ begin
64
67
  mod.const_get(class_name)
65
- else
68
+ rescue NameError
66
69
  raise("Class:<#{new_class}> not defined! Unable to resolve association:<#{assoc_name.downcase}>")
67
70
  end
68
71
  end
@@ -1,3 +1,3 @@
1
1
  module Hash19
2
- VERSION = '0.0.5'
2
+ VERSION = '0.0.6'
3
3
  end
@@ -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: 1)
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.5
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-14 00:00:00.000000000 Z
11
+ date: 2014-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler