alula-ruby 0.50.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +14 -0
  3. data/.env.example +8 -0
  4. data/.github/workflows/gem-push.yml +45 -0
  5. data/.gitignore +23 -0
  6. data/.rspec +3 -0
  7. data/.travis.yml +7 -0
  8. data/Dockerfile +6 -0
  9. data/Gemfile +12 -0
  10. data/Guardfile +42 -0
  11. data/README.md +423 -0
  12. data/Rakefile +6 -0
  13. data/VERSION.md +84 -0
  14. data/alula-docker-compose.yml +80 -0
  15. data/alula.gemspec +38 -0
  16. data/bin/console +15 -0
  17. data/bin/docparse +36 -0
  18. data/bin/genresource +79 -0
  19. data/bin/setup +8 -0
  20. data/bin/testauth +24 -0
  21. data/bin/testprep +9 -0
  22. data/data/docs/Alula_API_Documentation_2021-04-06.html +16240 -0
  23. data/docker-compose.yml +11 -0
  24. data/lib/alula/alula_response.rb +20 -0
  25. data/lib/alula/api_operations/delete.rb +52 -0
  26. data/lib/alula/api_operations/list.rb +45 -0
  27. data/lib/alula/api_operations/request.rb +44 -0
  28. data/lib/alula/api_operations/save.rb +81 -0
  29. data/lib/alula/api_resource.rb +196 -0
  30. data/lib/alula/client.rb +142 -0
  31. data/lib/alula/errors.rb +169 -0
  32. data/lib/alula/filter_builder.rb +271 -0
  33. data/lib/alula/helpers/device_attribute_translations.rb +68 -0
  34. data/lib/alula/list_object.rb +64 -0
  35. data/lib/alula/meta.rb +16 -0
  36. data/lib/alula/monkey_patches.rb +24 -0
  37. data/lib/alula/oauth.rb +118 -0
  38. data/lib/alula/pagination.rb +25 -0
  39. data/lib/alula/procedures/dealer_device_stats_proc.rb +16 -0
  40. data/lib/alula/procedures/dealer_restore_proc.rb +25 -0
  41. data/lib/alula/procedures/dealer_suspend_proc.rb +25 -0
  42. data/lib/alula/procedures/device_assign_proc.rb +23 -0
  43. data/lib/alula/procedures/device_cellular_history_proc.rb +33 -0
  44. data/lib/alula/procedures/device_rateplan_get_proc.rb +21 -0
  45. data/lib/alula/procedures/device_register_proc.rb +31 -0
  46. data/lib/alula/procedures/device_signal_add_proc.rb +42 -0
  47. data/lib/alula/procedures/device_signal_delivered_proc.rb +31 -0
  48. data/lib/alula/procedures/device_signal_update_proc.rb +32 -0
  49. data/lib/alula/procedures/device_unassign_proc.rb +16 -0
  50. data/lib/alula/procedures/device_unregister_proc.rb +21 -0
  51. data/lib/alula/procedures/upload_touchpad_branding_proc.rb +24 -0
  52. data/lib/alula/procedures/user_plansvideo_price_get.rb +21 -0
  53. data/lib/alula/procedures/user_transfer_accept.rb +19 -0
  54. data/lib/alula/procedures/user_transfer_authorize.rb +18 -0
  55. data/lib/alula/procedures/user_transfer_cancel.rb +18 -0
  56. data/lib/alula/procedures/user_transfer_deny.rb +19 -0
  57. data/lib/alula/procedures/user_transfer_reject.rb +19 -0
  58. data/lib/alula/procedures/user_transfer_request.rb +19 -0
  59. data/lib/alula/query_interface.rb +142 -0
  60. data/lib/alula/rate_limit.rb +11 -0
  61. data/lib/alula/relationship_attributes.rb +107 -0
  62. data/lib/alula/resource_attributes.rb +206 -0
  63. data/lib/alula/resources/admin_user.rb +207 -0
  64. data/lib/alula/resources/billing_program.rb +41 -0
  65. data/lib/alula/resources/dealer.rb +218 -0
  66. data/lib/alula/resources/dealer_account_transfer.rb +172 -0
  67. data/lib/alula/resources/dealer_address.rb +89 -0
  68. data/lib/alula/resources/dealer_branding.rb +226 -0
  69. data/lib/alula/resources/dealer_program.rb +75 -0
  70. data/lib/alula/resources/dealer_suspension_log.rb +49 -0
  71. data/lib/alula/resources/device.rb +716 -0
  72. data/lib/alula/resources/device_cellular_status.rb +134 -0
  73. data/lib/alula/resources/device_charge.rb +70 -0
  74. data/lib/alula/resources/device_event_log.rb +167 -0
  75. data/lib/alula/resources/device_program.rb +54 -0
  76. data/lib/alula/resources/event_trigger.rb +75 -0
  77. data/lib/alula/resources/event_webhook.rb +47 -0
  78. data/lib/alula/resources/feature_bysubject.rb +74 -0
  79. data/lib/alula/resources/feature_plan.rb +57 -0
  80. data/lib/alula/resources/feature_planvideo.rb +54 -0
  81. data/lib/alula/resources/feature_price.rb +46 -0
  82. data/lib/alula/resources/receiver_connection.rb +95 -0
  83. data/lib/alula/resources/receiver_group.rb +74 -0
  84. data/lib/alula/resources/revision.rb +91 -0
  85. data/lib/alula/resources/self.rb +61 -0
  86. data/lib/alula/resources/station.rb +130 -0
  87. data/lib/alula/resources/token_exchange.rb +34 -0
  88. data/lib/alula/resources/user.rb +229 -0
  89. data/lib/alula/resources/user_address.rb +121 -0
  90. data/lib/alula/resources/user_phone.rb +116 -0
  91. data/lib/alula/resources/user_preferences.rb +57 -0
  92. data/lib/alula/resources/user_pushtoken.rb +75 -0
  93. data/lib/alula/resources/user_videoprofile.rb +38 -0
  94. data/lib/alula/rest_resource.rb +17 -0
  95. data/lib/alula/rpc_resource.rb +40 -0
  96. data/lib/alula/rpc_response.rb +14 -0
  97. data/lib/alula/singleton_rest_resource.rb +26 -0
  98. data/lib/alula/util.rb +107 -0
  99. data/lib/alula/version.rb +5 -0
  100. data/lib/alula.rb +135 -0
  101. data/lib/parser.rb +199 -0
  102. metadata +282 -0
@@ -0,0 +1,169 @@
1
+ module Alula
2
+ class AlulaError < StandardError
3
+ attr_reader :http_status, :raw_response, :error, :message
4
+ def initialize(error)
5
+ if error.class == String
6
+ @message = error
7
+ return
8
+ end
9
+
10
+ @http_status = error.http_status
11
+ @raw_response = error
12
+ @error = error.data['error']
13
+ @message = error.data['error_description'] || error.data['message']
14
+ end
15
+
16
+ def ok?
17
+ false
18
+ end
19
+
20
+ def self.for_response(response)
21
+ if !response.data['error'].nil? && !response.data['error'].empty?
22
+ self.error_for_response(response)
23
+
24
+ elsif !response.data['errors'].nil? && !response.data['errors'].empty?
25
+ self.errors_for_response(response)
26
+
27
+ elsif response.data.match(/^<!DOCTYPE html>/)
28
+ self.critical_error_for_response(response.data.scan(/<pre>(.*)<\/pre>/))
29
+ else
30
+ message = "Unable to derive error from response: #{response.inspect}"
31
+ Alula.logger.error message
32
+ raise UnknownApiError.new(message)
33
+ end
34
+
35
+ rescue NoMethodError
36
+ message = "Unable to derive error from response: #{response.inspect}"
37
+ Alula.logger.error message
38
+ raise UnknownApiError.new(message)
39
+ end
40
+
41
+ private
42
+
43
+ # Handle HTML-based errors from the API
44
+ def self.critical_error_for_response(error_text)
45
+ InvalidRequestError.new(error_text.first.first)
46
+ end
47
+
48
+ # Figure out what error should be raised
49
+ def self.error_for_response(response)
50
+ case response.data['error']
51
+ when 'RateLimit'
52
+ Alula.logger.error response
53
+ RateLimitError.new(response)
54
+ when 'invalid_token'
55
+ InvalidTokenError.new(response)
56
+ when 'insufficient_scope'
57
+ InsufficientScopeError.new(response)
58
+ when 'server_error'
59
+ Alula.logger.error response
60
+ ServerError.new(response)
61
+ else
62
+ #
63
+ # RPC errors are identified by jsonrpc in the body.
64
+ return ProcedureError.new(response) if response.data['jsonrpc']
65
+
66
+
67
+ Alula.logger.error response
68
+ raise NotImplementedError.new("Unable to derive error for #{response.data['error']}")
69
+ end
70
+ end
71
+
72
+ # Some responses have an error array. We won't be able to raise on all errors but we can raise on the first one
73
+ def self.errors_for_response(response)
74
+ error = response.data['errors'].first
75
+ case error['title']
76
+ when 'RateLimit'
77
+ RateLimitError.new(response)
78
+ when 'Forbidden'
79
+ ForbiddenError.new(error['detail'])
80
+ when 'Not Found'
81
+ NotFoundError.new(error['detail'])
82
+ when 'Bad Request'
83
+ Alula.logger.error response
84
+ BadRequestError.new(error['detail'])
85
+ when 'API Error Unknown'
86
+ Alula.logger.error response
87
+ UnknownError.new(error['detail'])
88
+ when 'Insufficient Scope'
89
+ InsufficientScopeError.new(error['detail'])
90
+ else
91
+ #
92
+ # RPC errors are identified by jsonrpc in the body.
93
+ return ProcedureError.new(response) if response.data['jsonrpc']
94
+
95
+ Alula.logger.error response
96
+ raise NotImplementedError.new("Unable to derive error for #{response.data['errors']}")
97
+ end
98
+ end
99
+ end
100
+
101
+ class UnknownApiError < AlulaError
102
+ end
103
+
104
+ class RateLimitError < AlulaError
105
+ end
106
+
107
+ class NotFoundError < AlulaError
108
+ end
109
+
110
+ class ForbiddenError < AlulaError
111
+ end
112
+
113
+ class InvalidRequestError < AlulaError
114
+ end
115
+
116
+ class InvalidTokenError < AlulaError
117
+ end
118
+
119
+ class InsufficientScopeError < AlulaError
120
+ end
121
+
122
+ class BadRequestError < AlulaError
123
+ end
124
+
125
+ class NotConfiguredError < AlulaError
126
+ end
127
+
128
+ class InvalidFilterFieldError < AlulaError
129
+ end
130
+
131
+ class InvalidSortFieldError < AlulaError
132
+ end
133
+
134
+ class InvalidRelationshipError < AlulaError
135
+ end
136
+
137
+ class InvalidRoleError < AlulaError
138
+ end
139
+
140
+ class ServerError < AlulaError
141
+ end
142
+
143
+ class UnknownError < AlulaError
144
+ end
145
+
146
+ class ProcedureError < AlulaError
147
+ attr_reader :code, :full_messages
148
+
149
+ def initialize(response)
150
+ # Take 1 error from the request
151
+ # TODO: Multiple errors are possible, we probably want to shlep that up
152
+ error = response.data['error'] || response.data['errors'].first
153
+
154
+ @http_status = response.http_status
155
+ @raw_response = response
156
+ @error = error['message']
157
+ @full_messages = error.dig('data', 'message')&.split(', ') ||
158
+ [error['message']]
159
+ @message = error['message']
160
+ @code = error['code']
161
+ end
162
+
163
+ #
164
+ # Provides interface mirroring to success responses
165
+ def ok?
166
+ false
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,271 @@
1
+ require 'json'
2
+
3
+ module Alula
4
+ class FilterBuilder
5
+
6
+ attr_accessor :model_class, :query_interface, :builder
7
+
8
+ def initialize(model_class, query_interface, **args)
9
+ self.query_interface = query_interface
10
+ self.model_class = model_class
11
+ @cache = Hash.new
12
+ end
13
+
14
+ #
15
+ # Pseudo-delegator. Merges the current filter into the QueryInterface
16
+ # then delegates the used method forward
17
+ instance_eval do
18
+ %i(list list_all filter_builder limit offset custom_options includes).each do |method|
19
+ define_method(method) do |*args|
20
+ query_interface.filter(as_json)
21
+ query_interface.send(method, *args)
22
+ end
23
+ end
24
+ end
25
+
26
+ #
27
+ # Takes a hash, one for each key
28
+ def sort(**args)
29
+ cleaned_sorts = args.each_pair.each_with_object({}) do |(field, value), collector|
30
+ field_name = validate_field_sortability! field
31
+ field_value = validate_sort_value! value
32
+
33
+ collector[field_name] = field_value
34
+ end
35
+
36
+ query_interface.filter(as_json)
37
+ query_interface.query_sort(cleaned_sorts)
38
+ end
39
+
40
+ #
41
+ # Takes a hash, for each key => value pair returns a strict
42
+ # hash response for a $where query
43
+ def where(fields)
44
+ cleaned = fields.each_pair.each_with_object({}) do |(key, value), collector|
45
+ field_name = validate_field_filterability! key
46
+ field_value = simple_value! value
47
+
48
+ collector[field_name] = field_value
49
+ end
50
+
51
+ update_and_return(cleaned)
52
+ end
53
+ #
54
+ # Takes a hash, for each key => value pair returns a strict
55
+ # hash response for a $and $where query
56
+ def and(fields)
57
+ cleaned = fields.each_pair.each_with_object({}) do |(key, value), collector|
58
+ field_name = validate_field_filterability! key
59
+ field_value = simple_value! value
60
+
61
+ collector[field_name] = field_value
62
+ end
63
+
64
+ update_and_return({ '$and' => cleaned })
65
+ end
66
+
67
+ #
68
+ # Define simple comparison operator methods for these query names
69
+ # Give each method a hash containing fieldName => fieldValue,
70
+ # Returns a query object in the form of:
71
+ # {
72
+ # "fieldName" => {
73
+ # "operator_symbol" => "fieldValue"
74
+ # }
75
+ # }
76
+ %i(not ne gt gte lt lte like not_like).each do |simple_operator|
77
+ define_method(simple_operator) do |fields = {}|
78
+ cleaned = fields.each_pair.each_with_object({}) do |(key, value), collector|
79
+ field_name = validate_field_filterability! key
80
+ field_value = simple_value! value
81
+
82
+ collector[field_name] = { "$#{Util.camelize(simple_operator)}" => field_value }
83
+ end
84
+
85
+ update_and_return cleaned
86
+ end
87
+ end
88
+
89
+ #
90
+ # Dual operation
91
+ # If given a block it will yield a fresh instance of FilterBuilder, the contents
92
+ # of which will be stuffed directly into the $or operator.
93
+ # If given an array, the 1st value is
94
+ def or(*args, &block)
95
+ if block_given?
96
+ new_builder = yield self.class.new(model_class, query_interface)
97
+ return update_and_return({ '$or' => new_builder.as_json })
98
+ end
99
+
100
+ value = args[0]
101
+ fields = args[1..-1]
102
+
103
+ new_values = fields.each_with_object('$or'=>{}) do |key, collector|
104
+ field_name = validate_field_filterability! key
105
+ collector['$or'][field_name] = value
106
+ end
107
+
108
+ update_and_return(new_values)
109
+ end
110
+
111
+ def or_like(**args)
112
+ new_values = args.each_pair.each_with_object({}) do |(key, value), collector|
113
+ field_name = validate_field_filterability! key
114
+ field_value = simple_value! value
115
+
116
+ collector[field_name] = { '$like' => field_value }
117
+ end
118
+
119
+ update_and_return('$or' => new_values)
120
+ end
121
+
122
+ #
123
+ # Generate a $between query for hash, the values of the hash must be
124
+ # an array with 2 elements that are JSON-encodable
125
+ def between(**args)
126
+ new_values = args.each_pair.each_with_object({}) do |(key, range), collector|
127
+ field_name = validate_field_filterability! key
128
+ range_start, range_end = validate_range_value! range, field_name
129
+ collector[field_name] = { '$between' => [range_start, range_end] }
130
+ end
131
+
132
+ update_and_return(new_values)
133
+ end
134
+
135
+ #
136
+ # Generate a $not_between query for hash, the values of the hash must be
137
+ # an array with 2 elements that are JSON-encodable
138
+ def not_between(**args)
139
+ new_values = args.each_pair.each_with_object({}) do |(key, range), collector|
140
+ field_name = validate_field_filterability! key
141
+ range_start, range_end = validate_range_value! range, field_name
142
+ collector[field_name] = { '$notBetween' => [range_start, range_end] }
143
+ end
144
+
145
+ update_and_return(new_values)
146
+ end
147
+
148
+ #
149
+ # Generate an $in query for hash, the values of the hash must be
150
+ # an array that is JSON-encodable
151
+ def in(**args)
152
+ new_values = args.each_pair.each_with_object({}) do |(key, range), collector|
153
+ field_name = validate_field_filterability! key
154
+ values = simple_values! range
155
+ collector[field_name] = { '$in' => values }
156
+ end
157
+
158
+ update_and_return(new_values)
159
+ end
160
+ #
161
+ # Generate an $in query for hash, the values of the hash must be
162
+ # an array that is JSON-encodable
163
+ def not_in(**args)
164
+ new_values = args.each_pair.each_with_object({}) do |(key, range), collector|
165
+ field_name = validate_field_filterability! key
166
+ values = simple_values! range
167
+ collector[field_name] = { '$notIn' => values }
168
+ end
169
+
170
+ update_and_return(new_values)
171
+ end
172
+
173
+ def as_json
174
+ @cache
175
+ end
176
+
177
+ private
178
+
179
+ #
180
+ # Check the field name against known field names.
181
+ # Ensure that the field is exists, is filterable
182
+ # Transform the field name into an API-valid lowerCamelCase
183
+ # format and return it.
184
+ def validate_field_filterability!(field_name)
185
+ underscored = Util.underscore(field_name).to_sym
186
+ camelized = Util.camelize(underscored).to_s
187
+
188
+ unless model_class.get_fields.include?(underscored)
189
+ error = "Field `#{underscored}` does not exist resource `#{model_class}`"
190
+ raise Alula::InvalidFilterFieldError.new(error)
191
+ end
192
+
193
+ unless model_class.filterable_fields.include?(underscored)
194
+ error = "Field `#{underscored}` is not filterable on resource `#{model_class}`"
195
+ raise Alula::InvalidFilterFieldError.new(error)
196
+ end
197
+
198
+ camelized
199
+ end
200
+
201
+ #
202
+ # Check that a field name is known, and that it is sortable.
203
+ # Transform the field name into an API-validated lowerCamelCase
204
+ # TODO: This doesn't belong here, should be on its own include I think
205
+ def validate_field_sortability!(field_name)
206
+ underscored = Util.underscore(field_name).to_sym
207
+ camelized = Util.camelize(underscored).to_s
208
+
209
+ unless model_class.get_fields.include?(underscored)
210
+ error = "Field `#{underscored}` does not exist resource `#{model_class}`"
211
+ raise Alula::InvalidFilterFieldError.new(error)
212
+ end
213
+
214
+ unless model_class.sortable_fields.include?(underscored)
215
+ error = "Field `#{underscored}` is not sortable on resource `#{model_class}`"
216
+ raise Alula::InvalidSortFieldError.new(error)
217
+ end
218
+
219
+ camelized
220
+ end
221
+
222
+ def validate_range_value!(range, field_name)
223
+ unless range.length == 2
224
+ error = "Provide an array with 2 values to field `#{field_name}` "\
225
+ "perform a range-based query"
226
+ raise Alula::InvalidFilterFieldError.new(error)
227
+ end
228
+
229
+ range[0] = range[0].iso8601 if range[0].respond_to?(:iso8601)
230
+ range[1] = range[1].iso8601 if range[1].respond_to?(:iso8601)
231
+
232
+ [simple_value!(range[0]), simple_value!(range[1])]
233
+ end
234
+
235
+ # Ensure a value will be a JSON-encodable value. We don't really care what it is,
236
+ # but what comes out of JSON serialization is what we'll use.
237
+ def simple_value!(value)
238
+ JSON.parse(JSON.fast_generate({ purify: value }))['purify']
239
+ end
240
+
241
+ def simple_values!(values)
242
+ values.map{ |v| simple_value!(v) }
243
+ end
244
+
245
+ def update_cache(new_values)
246
+ @cache = Util.deep_merge(@cache, new_values)
247
+ self
248
+ end
249
+
250
+ def update_and_return(new_values)
251
+ return new_values if @functional
252
+ update_cache(new_values)
253
+ end
254
+
255
+ def validate_sort_value!(value)
256
+ val = value.to_s.downcase.to_sym
257
+
258
+ unless %i(asc desc).include? val
259
+ error = "Cannot sort on direction #{val}, please use ASC or DESC only"
260
+ raise Alula::InvalidSortFieldError.new(error)
261
+ end
262
+
263
+ val
264
+ end
265
+
266
+ def update_and_return(new_values)
267
+ @cache = Util.deep_merge(@cache, new_values)
268
+ self
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,68 @@
1
+ module Alula
2
+
3
+ #
4
+ # Relies on the implementing class having reader properties of `program_id`
5
+ # and `auto_cfg`. Translates those values into usable strings
6
+ module DeviceAttributeTranslations
7
+ PROGRAM_ID_MAP = {
8
+ 32 => 'CONNECT+',
9
+ 35 => 'BAT-FIRE',
10
+ 34 => 'BAT-CONNECT',
11
+ 211 => 'CLARE-D3',
12
+ 4096 => 'CAMERA',
13
+ 783 => 'BAT-LTE',
14
+ 0 => 'IPD-BAT',
15
+ 1 => 'IPD-BAT',
16
+ 2 => 'IPD-BAT',
17
+ 3 => 'IPD-BAT-ZKI',
18
+ 4 => 'IPD-BAT',
19
+ 8 => 'IPD-BAT',
20
+ 6 => 'IPD-ZGW',
21
+ 9 => 'IPD-BAT-U',
22
+ 11 => 'IPD-ZGW',
23
+ 15 => 'IPD-BAT-CDMA',
24
+ 16 => 'IPD-BAT-WIFI',
25
+ 17 => 'IPD-BAT-CDMA-L',
26
+ 18 => 'IPD-MPWC',
27
+ 19 => 'IPD-CAT-CDMA',
28
+ 20 => 'IPD-BAT-CDMA-WIFI',
29
+ 22 => 'IPD-CAT-XT',
30
+ 33 => 'IGM',
31
+ 271 => 'IPD-BAT-CDMA1'
32
+ }.freeze
33
+
34
+ PANEL_NAME_MAP = {
35
+ 0 => 'Honeywell/Vista',
36
+ 1 => 'Honeywell/Vista+Passive',
37
+ 2 => 'Honeywell/Lynx',
38
+ 3 => 'Honeywell/Lynx+Passive',
39
+ 4 => 'DSC/Depricated',
40
+ 5 => 'GE-Interlogix/Caddx',
41
+ 6 => 'DSC/Alexor',
42
+ 7 => 'DSC/PowerSeries',
43
+ 8 => 'Dialer Capture or Keyswitch',
44
+ 9 => 'GE-Interlogix/Concord',
45
+ 10 => '',
46
+ 11 => 'Gateway',
47
+ 12 => 'GE Simon XT/XTi',
48
+ 13 => 'Cinch',
49
+ 14 => 'Helix',
50
+ 15 => 'Clare D3',
51
+ 19 => 'No Panel',
52
+ }.freeze
53
+
54
+ def device_name
55
+ self.class::PROGRAM_ID_MAP[self.program_id] || "Unknown (#{self.program_id})"
56
+ end
57
+
58
+ #
59
+ # autoCfg contains multiple pieces of information depending on how you examine it.
60
+ # Doing a bitwise operation with 0xff takes the last 8 bits,
61
+ # which is the connected panel ID.
62
+ # https://ipdatatel.atlassian.net/wiki/spaces/SYS/pages/16482314/auto+cfg
63
+ def panel_name
64
+ panel_id = auto_cfg.to_i & 0xff
65
+ self.class::PANEL_NAME_MAP[panel_id] || "Unknown (#{panel_id})"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ module Alula
2
+ class ListObject
3
+ include Enumerable
4
+
5
+ attr_reader :items, :type, :meta
6
+ attr_accessor :rate_limit
7
+
8
+ def initialize list_type
9
+ @type = list_type
10
+ @items = []
11
+ end
12
+
13
+ def length
14
+ items.length
15
+ end
16
+
17
+ def each
18
+ items.each { |i| yield i }
19
+ end
20
+
21
+ def[] index
22
+ @items[index]
23
+ end
24
+
25
+ def first
26
+ @items.first
27
+ end
28
+
29
+ def last
30
+ @items.last
31
+ end
32
+
33
+ def << item
34
+ @items << item
35
+ end
36
+
37
+ def set_meta(meta)
38
+ @meta = meta
39
+ end
40
+
41
+ def pagination
42
+ meta.page
43
+ end
44
+
45
+ def self.construct_from_response klass, response, opts
46
+ list = ListObject.new(klass)
47
+ response.data['data'].each do |item|
48
+ list << klass.new(item['id']).construct_from(item)
49
+ end
50
+ list.set_meta Meta.new(response.data['meta'])
51
+ list.rate_limit = response.rate_limit
52
+
53
+ list
54
+ end
55
+
56
+ def self.construct_from_array klass, collection
57
+ list = ListObject.new(klass)
58
+ collection.each do |item|
59
+ list << klass.new(item['id']).construct_from(item)
60
+ end
61
+ list
62
+ end
63
+ end
64
+ end
data/lib/alula/meta.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Alula
2
+ class Meta
3
+ attr_reader :page, :total, :number, :size
4
+
5
+ def initialize(meta)
6
+ return unless meta['page']
7
+
8
+ @page = Alula::Pagination.new(meta['page'])
9
+
10
+ # TODO: TJ Deprecate, the meta can hold a lot more than the page
11
+ @total = @page.total
12
+ @number = @page.number
13
+ @size = @page.size
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ module HTTParty
2
+ module HashConversions
3
+ # @param key<Object> The key for the param.
4
+ # @param value<Object> The value for the param.
5
+ #
6
+ # @return <String> This key value pair as a param
7
+ #
8
+ # @example normalize_param(:name, "Bob Jones") #=> "name=Bob%20Jones&"
9
+ def self.normalize_param(key, value)
10
+ normalized_keys = normalize_keys(key, value)
11
+
12
+ normalized_keys.flatten.each_slice(2).inject('') do |string, (k, v)|
13
+ # TODO: TJ - Our API needs nil params to be blank, aka '&someParam&'
14
+ # and not '&someParam=', otherwise we take that as an empty string.
15
+ # Monkey patch to get around this problem.
16
+ if v.nil?
17
+ string + "#{ERB::Util.url_encode(k)}&"
18
+ else
19
+ string + "#{ERB::Util.url_encode(k)}=#{ERB::Util.url_encode(v.to_s)}&"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end