alula-ruby 0.50.1

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.
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