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