to_collection 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +314 -0
  4. data/lib/ext/object.rb +6 -0
  5. data/lib/to_collection.rb +44 -0
  6. metadata +119 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3c2310783afe2173cf5010b1aee2dcce714b525b
4
+ data.tar.gz: f398002762833fb9aaef65653c94843f88c15ded
5
+ SHA512:
6
+ metadata.gz: 428fdd1b1bf6b14316e4145e5dd8bdec6979d67c4797196190ad22f6c6a7409c1abf8557b62879321a3b12b5f88990fd0742af4e7871dfd622751035c1ea015a
7
+ data.tar.gz: 6d333a07fdc5191bcd23f1bf1c6687ab6642c93fb2a7e6b3fcc039467395910848c9f170a9943c39b50a9dd402d5fe7756799c76284ec92cb684c5ac368745b6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Andy Maleh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,314 @@
1
+ # `to_collection` v1.0.0
2
+ [![Gem Version](https://badge.fury.io/rb/to_collection.svg)](http://badge.fury.io/rb/to_collection)
3
+ [![Build Status](https://travis-ci.org/AndyObtiva/to_collection.svg?branch=master)](https://travis-ci.org/AndyObtiva/to_collection)
4
+ [![Coverage Status](https://coveralls.io/repos/github/AndyObtiva/to_collection/badge.svg?branch=master)](https://coveralls.io/github/AndyObtiva/to_collection?branch=master)
5
+
6
+ Treat an array of objects and a singular object uniformly as a collection of objects.
7
+ Especially useful in processing REST Web Service API JSON responses in a functional approach.
8
+
9
+ ## Introduction
10
+
11
+ Canonicalize data to treat uniformly whether it comes in as a single object or an array of objects, dropping `nils` out automatically.
12
+
13
+ API: `object.to_collection(compact)` where `compact` is a boolean for whether to compact collection or not. It is true by default.
14
+
15
+ Example:
16
+
17
+ ```ruby
18
+ city_counts = {}
19
+ people_http_request.to_collection.each do |person|
20
+ city_counts[person["city"]] ||= 0
21
+ city_counts[person["city"]] += 1
22
+ end
23
+ ```
24
+
25
+ Wanna keep `nil` values? No problem! Just pass `false` as an argument:
26
+
27
+ ```ruby
28
+ bad_people_count = 0
29
+ city_counts = {}
30
+ people_http_request.to_collection(false).each do |person|
31
+ if person.nil?
32
+ bad_people_count += 1
33
+ else
34
+ city_counts[person["city"]] ||= 0
35
+ city_counts[person["city"]] += 1
36
+ end
37
+ end
38
+ ```
39
+
40
+ ## Background
41
+
42
+ I'm sure you've encountered REST Web Service APIs that operate as follows:
43
+
44
+ HTTP Request: =>
45
+ ```
46
+ GET /people
47
+ ```
48
+
49
+ <= 1 person
50
+ JSON Response:
51
+ ```JSON
52
+ {"first_name":"karim","last_name":"akram","city":"Dubai"}
53
+ ```
54
+
55
+ HTTP Request: =>
56
+ ```
57
+ GET /people
58
+ ```
59
+
60
+ <= 3 people
61
+ JSON Response:
62
+
63
+ ```JSON
64
+ [{"first_name":"karim","last_name":"akram","city":"Dubai"}, {"first_name":"muhsen","last_name":"asaad","city":"Amman"}, {"first_name":"assaf","last_name":"munir","city":"Qatar"}]
65
+ ```
66
+
67
+ How do you work with the varied JSON responses in Ruby?
68
+
69
+ One approach for an app that needs to count people in cities:
70
+
71
+ ```ruby
72
+ city_counts = {}
73
+ json_response = people_http_request
74
+ if json_response.is_a?(Hash)
75
+ city_counts[json_response["city"]] ||= 0
76
+ city_counts[json_response["city"] += 1
77
+ elsif json_response.is_a?(Array)
78
+ json_response.each do |person|
79
+ city_counts[person["city"]] ||= 0
80
+ city_counts[person["city"]] += 1
81
+ end
82
+ end
83
+ ```
84
+
85
+ Not only is the code above repetitive (unDRY) and complicated, but it also breaks common Ruby and object oriented development standards by relying on explicit type checking instead of duck-typing, polymorphism, or design patterns.
86
+
87
+ A slightly better version relying on duck-typing would be:
88
+
89
+ ```ruby
90
+ city_counts = {}
91
+ json_response = people_http_request
92
+ if json_response.respond_to?(:each_pair)
93
+ city_counts[json_response["city"]] ||= 0
94
+ city_counts[json_response["city"] += 1
95
+ elsif json_response.respond_to?(:each_index)
96
+ json_response.each do |person|
97
+ city_counts[person["city"]] ||= 0
98
+ city_counts[person["city"]] += 1
99
+ end
100
+ end
101
+ ```
102
+
103
+ A slightly clearer version relying on design patterns (Strategy) and parametric polymorphism (functional) would be:
104
+
105
+ ```ruby
106
+ city_counts = {}
107
+ city_counting_strategies = {
108
+ Hash: -> { |json_response|
109
+ city_counts[json_response["city"]] ||= 0
110
+ city_counts[json_response["city"] += 1
111
+ },
112
+ Array: -> { |json_response|
113
+ json_response.each do |person|
114
+ city_counts[person["city"]] ||= 0
115
+ city_counts[person["city"]] += 1
116
+ end
117
+ }
118
+ }
119
+ json_response = people_http_request
120
+ city_counting_strategies[json_response.class].call(json_response)
121
+ ```
122
+
123
+ A more radical version relying on object-oriented polymorphism and Ruby open-classes would be:
124
+
125
+ ```ruby
126
+ Hash.class_eval do
127
+ def process_json_response(&processor)
128
+ processor.call(self)
129
+ end
130
+ end
131
+
132
+ Array.class_eval do
133
+ def process_json_response(&processor)
134
+ each(&processor)
135
+ end
136
+ end
137
+
138
+ city_counts = {}
139
+ json_response = people_http_request
140
+ json_response.process_json_response do |person|
141
+ city_counts[person["city"]] ||= 0
142
+ city_counts[person["city"]] += 1
143
+ end
144
+ ```
145
+
146
+ This version is quite elegant, clear, and Ruby idiomatic, but aren't we using a Nuclear device against a fly that sometimes comes as a swarm of flies? I'm sure we can have a much simpler solution, especially in a language like Ruby.
147
+
148
+ Well, how about this functional solution?
149
+
150
+ ```ruby
151
+ city_counts = {}
152
+ [people_http_request].flatten.each do |person|
153
+ city_counts[person["city"]] ||= 0
154
+ city_counts[person["city"]] += 1
155
+ end
156
+ ```
157
+
158
+ Yes, hybrid functional/object-oriented programming to the rescue.
159
+
160
+ One may wonder what to do if the response comes in as nil or includes nil values in an array. Well, this approach can scale to handle that too should ignoring nil be the requirement.
161
+
162
+ ```ruby
163
+ city_counts = {}
164
+ [people_http_request].flatten.compact.each do |person|
165
+ city_counts[person["city"]] ||= 0
166
+ city_counts[person["city"]] += 1
167
+ end
168
+ ```
169
+
170
+ Can we generalize this elegant solution beyond counting cities? After all, the key problem with the code on top is it gets quite expensive to maintain in a real-world production app containing many integrations with REST Web Service APIs.
171
+
172
+ This functional generalization should work by allowing you to switch json_response variable and process_json_response proc anyway you want:
173
+
174
+ ```ruby
175
+ [json_response].flatten.compact.each(&:process_json_response)
176
+ ```
177
+
178
+ Example:
179
+
180
+ ```ruby
181
+ [cities_json_response].flatten.compact.each(&:group_by_country)
182
+ ```
183
+
184
+ How about go one step further and bake this into all objects using our previous approach of object-oriented polymorphism and Ruby open-classes? That way, we don't just collapse the difference between dealing with arrays of hashes vs hashes but also arrays of objects vs singular objects by adding. Note the use of flatten(1) below to prevent arrays or arrays from collapsing more than one level.
185
+
186
+ ```ruby
187
+ Object.class_eval do
188
+ def to_collection
189
+ [self].flatten(1).compact
190
+ end
191
+ end
192
+ ```
193
+
194
+ Example usage (notice how more readable this is than the explicit version above by hiding flatten and compact):
195
+
196
+ ```ruby
197
+ city_counts = {}
198
+ people_http_request.to_collection.each do |person|
199
+ city_counts[person["city"]] ||= 0
200
+ city_counts[person["city"]] += 1
201
+ end
202
+ ```
203
+
204
+ A refactored version including optional compacting would be:
205
+
206
+ ```ruby
207
+ Object.class_eval do
208
+ def to_collection(compact=true)
209
+ collection = [self].flatten(1)
210
+ compact ? collection.compact : collection
211
+ end
212
+ end
213
+ ```
214
+
215
+ Example usage of `to_collection(compact)` to count bad person hashes coming as nil:
216
+
217
+ ```ruby
218
+ bad_people_count = 0
219
+ city_counts = {}
220
+ people_http_request.to_collection(false).each do |person|
221
+ if person.nil?
222
+ bad_people_count += 1
223
+ else
224
+ city_counts[person["city"]] ||= 0
225
+ city_counts[person["city"]] += 1
226
+ end
227
+ end
228
+ ```
229
+
230
+ You asked for "Elegant" didn't you? I hope that was what you were looking for.
231
+
232
+ ## How It Works
233
+
234
+ A [super_module](https://github.com/AndyObtiva/super_module) called `ToCollection`
235
+ contains the `#to_collection` method and is included (mixed) into `Object`, providing
236
+ `#to_collection` method to inheriting classes.
237
+
238
+ ## Options
239
+
240
+ ### `Object.to_collection_already_implemented_strategy`
241
+ Possible Values: `"raise_error"` (default), `"keep"`, `"overwrite"`
242
+
243
+ Setting this option allows developer to configure handling of the case when
244
+ `Object#to_collection` already exists before loading `to_collection` library.
245
+
246
+ #### `"raise_error"` (default)
247
+
248
+ For safety reasons, the library will raise AlreadyImplementedError by default to
249
+ alert the developer and provide information about the other options.
250
+ This prevents later surprises and puts control in the hand of the developer to
251
+ responsibly decide what option to pick next.
252
+
253
+ #### `"keep"`
254
+
255
+ This keeps existing `to_collection` untouched, disabling this library.
256
+
257
+ #### `"overwrite"`
258
+
259
+ This overwrites existing `to_collection` method, fully enabling this library.
260
+
261
+ ### `ENV['TO_COLLECTION_ALREADY_IMPLEMENTED_STRATEGY']`
262
+ Possible Values: `"raise_error"` (default), `"keep"`, `"overwrite"`
263
+
264
+ Same function as `Object.to_collection_already_implemented_strategy`.
265
+ Environment variable takes precedence over class accessor variable though.
266
+
267
+ ### `ENV['TO_COLLECTION_OBJECT_INCLUDE']`
268
+ Possible Values: `"true"` (default), `"false"`
269
+
270
+ Must be set before requiring/loading library. When using bundler, ensure `require` option is set to `false` or `nil`.
271
+
272
+ `ToCollection` [super_module](https://github.com/AndyObtiva/super_module) is automatically included in `Object` except when `ENV['TO_COLLECTION_OBJECT_INCLUDE']` is set to `"false"`, providing developer with the option to **manually** include (mix in) `ToCollection` [super_module](https://github.com/AndyObtiva/super_module) into classes that need it.
273
+
274
+ Example:
275
+
276
+ Bundler would have gem require option as false:
277
+
278
+ ```ruby
279
+ require 'to_collection', require: false
280
+ ```
281
+
282
+ Ruby code would then set that environment variable **manually** before requiring library:
283
+
284
+ ```ruby
285
+ ENV['TO_COLLECTION_OBJECT_INCLUDE'] = false
286
+ require 'to_collection'
287
+ Hash.instance_eval do
288
+ include ToCollection #enables to_collection method
289
+ end
290
+ Array.instance_eval do
291
+ include ToCollection #enables to_collection method
292
+ end
293
+ response_data = people_http_request #returns single hash or array of hashes
294
+ response_data.to_collection.each do |person_hash|
295
+ # do some work
296
+ end
297
+ ```
298
+
299
+ ## Contributing
300
+
301
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
302
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
303
+ * Fork the project.
304
+ * Start a feature/bugfix branch.
305
+ * `gem install bundler`
306
+ * `bundle`
307
+ * Commit and push until you are happy with your contribution.
308
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. Also, do not upgrade `jeweler`. It is intentionally at an old version that is compatible with running tests in Travis with older verison of Ruby as well as supporting Coveralls, Simplecov, and Code Climate.
309
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
310
+
311
+ ## Copyright
312
+
313
+ Copyright (c) 2017 Andy Maleh. See LICENSE.txt for
314
+ further details.
data/lib/ext/object.rb ADDED
@@ -0,0 +1,6 @@
1
+ ENV['TO_COLLECTION_OBJECT_INCLUDE'] ||= 'true'
2
+ if ENV['TO_COLLECTION_OBJECT_INCLUDE'].to_s.downcase == 'true'
3
+ Object.class_eval do
4
+ include ToCollection
5
+ end
6
+ end
@@ -0,0 +1,44 @@
1
+ require 'super_module'
2
+
3
+ super_module :ToCollection do
4
+ class AlreadyImplementedError < StandardError
5
+ end
6
+
7
+ def self.to_collection_already_implemented_strategy=(strategy)
8
+ @to_collection_already_implemented_strategy = strategy
9
+ end
10
+
11
+ def self.to_collection_already_implemented_strategy
12
+ (ENV['TO_COLLECTION_ALREADY_IMPLEMENTED_STRATEGY'] ||
13
+ @to_collection_already_implemented_strategy ||
14
+ :raise_error).to_sym
15
+ end
16
+
17
+ def __to_collection__(compact=true)
18
+ collection = [self].flatten(1)
19
+ compact ? collection.compact : collection
20
+ end
21
+
22
+ def self.define_to_collection_method
23
+ define_method(:to_collection, instance_method(:__to_collection__))
24
+ send(:remove_method, :__to_collection__)
25
+ end
26
+
27
+ case self.to_collection_already_implemented_strategy
28
+ when :raise_error
29
+ begin
30
+ instance_method(:to_collection)
31
+ message = "#to_collection is already implemented on Object. Please specify Object.to_collection_already_implemented_strategy or ENV['TO_COLLECTION_ALREADY_IMPLEMENTED_STRATEGY'] as :keep or :overwrite to handle this appropriately."
32
+ raise AlreadyImplementedError.new(message)
33
+ rescue NameError
34
+ self.define_to_collection_method
35
+ end
36
+ when :keep
37
+ # do nothing
38
+ when :overwrite
39
+ self.define_to_collection_method
40
+ end
41
+
42
+ end
43
+
44
+ require File.expand_path(File.join(__FILE__, '..', 'ext', 'object'))
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: to_collection
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Maleh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: super_module
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: jeweler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.3.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.3.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: coveralls
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.8.19
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.8.19
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.5.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.5.0
69
+ description: "\n Treat an array of objects and a singular object uniformly as a
70
+ collection of objects. Especially useful in processing REST Web Service API JSON
71
+ responses in a functional approach.\n\n Canonicalize data to treat uniformly
72
+ whether it comes in as a single object or an array of objects, dropping `nils` out
73
+ automatically.\n\n API: `object.to_collection(compact)` where `compact` is a
74
+ boolean for whether to compact collection or not. It is true by default.\n\n Example:\n\n
75
+ \ ```ruby\n city_counts = {}\n people_http_request.to_collection.each do
76
+ |person|\n city_counts[person[\"city\"]] ||= 0\n city_counts[person[\"city\"]]
77
+ += 1\n end\n ```\n\n Wanna keep `nil` values? No problem! Just pass `false`
78
+ as an argument:\n\n ```ruby\n bad_people_count = 0\n city_counts = {}\n
79
+ \ people_http_request.to_collection(false).each do |person|\n if person.nil?\n
80
+ \ bad_people_count += 1\n else\n city_counts[person[\"city\"]]
81
+ ||= 0\n city_counts[person[\"city\"]] += 1\n end\n end\n ```\n
82
+ \ "
83
+ email: andy.am@gmail.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files:
87
+ - LICENSE
88
+ - README.md
89
+ files:
90
+ - LICENSE
91
+ - README.md
92
+ - lib/ext/object.rb
93
+ - lib/to_collection.rb
94
+ homepage: http://github.com/AndyObtiva/to_collection
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.6.10
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Treat an array of objects and a singular object uniformly as a collection
118
+ of objects
119
+ test_files: []