alula-ruby 0.50.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +14 -0
- data/.env.example +8 -0
- data/.github/workflows/gem-push.yml +45 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Dockerfile +6 -0
- data/Gemfile +12 -0
- data/Guardfile +42 -0
- data/README.md +423 -0
- data/Rakefile +6 -0
- data/VERSION.md +84 -0
- data/alula-docker-compose.yml +80 -0
- data/alula.gemspec +38 -0
- data/bin/console +15 -0
- data/bin/docparse +36 -0
- data/bin/genresource +79 -0
- data/bin/setup +8 -0
- data/bin/testauth +24 -0
- data/bin/testprep +9 -0
- data/data/docs/Alula_API_Documentation_2021-04-06.html +16240 -0
- data/docker-compose.yml +11 -0
- data/lib/alula/alula_response.rb +20 -0
- data/lib/alula/api_operations/delete.rb +52 -0
- data/lib/alula/api_operations/list.rb +45 -0
- data/lib/alula/api_operations/request.rb +44 -0
- data/lib/alula/api_operations/save.rb +81 -0
- data/lib/alula/api_resource.rb +196 -0
- data/lib/alula/client.rb +142 -0
- data/lib/alula/errors.rb +169 -0
- data/lib/alula/filter_builder.rb +271 -0
- data/lib/alula/helpers/device_attribute_translations.rb +68 -0
- data/lib/alula/list_object.rb +64 -0
- data/lib/alula/meta.rb +16 -0
- data/lib/alula/monkey_patches.rb +24 -0
- data/lib/alula/oauth.rb +118 -0
- data/lib/alula/pagination.rb +25 -0
- data/lib/alula/procedures/dealer_device_stats_proc.rb +16 -0
- data/lib/alula/procedures/dealer_restore_proc.rb +25 -0
- data/lib/alula/procedures/dealer_suspend_proc.rb +25 -0
- data/lib/alula/procedures/device_assign_proc.rb +23 -0
- data/lib/alula/procedures/device_cellular_history_proc.rb +33 -0
- data/lib/alula/procedures/device_rateplan_get_proc.rb +21 -0
- data/lib/alula/procedures/device_register_proc.rb +31 -0
- data/lib/alula/procedures/device_signal_add_proc.rb +42 -0
- data/lib/alula/procedures/device_signal_delivered_proc.rb +31 -0
- data/lib/alula/procedures/device_signal_update_proc.rb +32 -0
- data/lib/alula/procedures/device_unassign_proc.rb +16 -0
- data/lib/alula/procedures/device_unregister_proc.rb +21 -0
- data/lib/alula/procedures/upload_touchpad_branding_proc.rb +24 -0
- data/lib/alula/procedures/user_plansvideo_price_get.rb +21 -0
- data/lib/alula/procedures/user_transfer_accept.rb +19 -0
- data/lib/alula/procedures/user_transfer_authorize.rb +18 -0
- data/lib/alula/procedures/user_transfer_cancel.rb +18 -0
- data/lib/alula/procedures/user_transfer_deny.rb +19 -0
- data/lib/alula/procedures/user_transfer_reject.rb +19 -0
- data/lib/alula/procedures/user_transfer_request.rb +19 -0
- data/lib/alula/query_interface.rb +142 -0
- data/lib/alula/rate_limit.rb +11 -0
- data/lib/alula/relationship_attributes.rb +107 -0
- data/lib/alula/resource_attributes.rb +206 -0
- data/lib/alula/resources/admin_user.rb +207 -0
- data/lib/alula/resources/billing_program.rb +41 -0
- data/lib/alula/resources/dealer.rb +218 -0
- data/lib/alula/resources/dealer_account_transfer.rb +172 -0
- data/lib/alula/resources/dealer_address.rb +89 -0
- data/lib/alula/resources/dealer_branding.rb +226 -0
- data/lib/alula/resources/dealer_program.rb +75 -0
- data/lib/alula/resources/dealer_suspension_log.rb +49 -0
- data/lib/alula/resources/device.rb +716 -0
- data/lib/alula/resources/device_cellular_status.rb +134 -0
- data/lib/alula/resources/device_charge.rb +70 -0
- data/lib/alula/resources/device_event_log.rb +167 -0
- data/lib/alula/resources/device_program.rb +54 -0
- data/lib/alula/resources/event_trigger.rb +75 -0
- data/lib/alula/resources/event_webhook.rb +47 -0
- data/lib/alula/resources/feature_bysubject.rb +74 -0
- data/lib/alula/resources/feature_plan.rb +57 -0
- data/lib/alula/resources/feature_planvideo.rb +54 -0
- data/lib/alula/resources/feature_price.rb +46 -0
- data/lib/alula/resources/receiver_connection.rb +95 -0
- data/lib/alula/resources/receiver_group.rb +74 -0
- data/lib/alula/resources/revision.rb +91 -0
- data/lib/alula/resources/self.rb +61 -0
- data/lib/alula/resources/station.rb +130 -0
- data/lib/alula/resources/token_exchange.rb +34 -0
- data/lib/alula/resources/user.rb +229 -0
- data/lib/alula/resources/user_address.rb +121 -0
- data/lib/alula/resources/user_phone.rb +116 -0
- data/lib/alula/resources/user_preferences.rb +57 -0
- data/lib/alula/resources/user_pushtoken.rb +75 -0
- data/lib/alula/resources/user_videoprofile.rb +38 -0
- data/lib/alula/rest_resource.rb +17 -0
- data/lib/alula/rpc_resource.rb +40 -0
- data/lib/alula/rpc_response.rb +14 -0
- data/lib/alula/singleton_rest_resource.rb +26 -0
- data/lib/alula/util.rb +107 -0
- data/lib/alula/version.rb +5 -0
- data/lib/alula.rb +135 -0
- data/lib/parser.rb +199 -0
- metadata +282 -0
data/lib/alula/errors.rb
ADDED
@@ -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
|