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/docker-compose.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Alula
|
|
2
|
+
class AlulaResponse
|
|
3
|
+
attr_accessor :data, :http_body, :http_headers, :http_status, :raw, :rate_limit
|
|
4
|
+
|
|
5
|
+
def ok?
|
|
6
|
+
(200..299).cover?(self.http_status)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.from_httparty_response(response)
|
|
10
|
+
resp = AlulaResponse.new
|
|
11
|
+
resp.data = response.parsed_response
|
|
12
|
+
resp.http_body = response.body
|
|
13
|
+
resp.http_headers = response.headers
|
|
14
|
+
resp.rate_limit = Alula::RateLimit.new(response.headers)
|
|
15
|
+
resp.http_status = response.code
|
|
16
|
+
resp.raw = response
|
|
17
|
+
resp
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Alula
|
|
2
|
+
module ApiOperations
|
|
3
|
+
module Delete
|
|
4
|
+
def self.extended(base)
|
|
5
|
+
base.include(InstanceMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module InstanceMethods
|
|
9
|
+
def delete
|
|
10
|
+
# payload = {
|
|
11
|
+
# data: {
|
|
12
|
+
# id: id,
|
|
13
|
+
# attributes: as_patchable_json
|
|
14
|
+
# }
|
|
15
|
+
# }
|
|
16
|
+
|
|
17
|
+
response = Alula::Client.request(:delete, resource_url)
|
|
18
|
+
|
|
19
|
+
return true if response.ok?
|
|
20
|
+
|
|
21
|
+
handle_errors(response)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def handle_errors(response)
|
|
27
|
+
# Should never see a 300-range response code
|
|
28
|
+
if (300..399).cover?(response.http_status)
|
|
29
|
+
raise Alula::UnknownError, "The Alula-Ruby gem encountered a response code of #{response.code}, that should not happen"
|
|
30
|
+
|
|
31
|
+
# Server errors, malformed crap, etc
|
|
32
|
+
elsif (500..599).cover?(response.http_status)
|
|
33
|
+
return AlulaError.for_response(response)
|
|
34
|
+
|
|
35
|
+
# Validation errors usually
|
|
36
|
+
elsif (400..499).cover?(response.http_status)
|
|
37
|
+
model_errors = Util.model_errors_from_response(response)
|
|
38
|
+
|
|
39
|
+
if model_errors
|
|
40
|
+
annotate_errors(model_errors)
|
|
41
|
+
return false
|
|
42
|
+
else
|
|
43
|
+
return AlulaError.for_response(response)
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
raise Alula::UnknownError, "Unknown HTTP response code, aborting. Code: #{response.http_status}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Alula
|
|
2
|
+
module ApiOperations
|
|
3
|
+
module List
|
|
4
|
+
def self.extended(base)
|
|
5
|
+
base.include(InstanceMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def method_missing(method, *args, &block)
|
|
9
|
+
QueryInterface.new(self).send(method, *args, &block)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def list(filters = {}, opts = {})
|
|
13
|
+
response = Alula::Client.request(:get, resource_url, filters, opts)
|
|
14
|
+
if response.ok?
|
|
15
|
+
list = ListObject.construct_from_response(self, response, opts)
|
|
16
|
+
list = build_and_merge_list_relationships(list, response.data['included']) if response.data['included']
|
|
17
|
+
list
|
|
18
|
+
else
|
|
19
|
+
error_class = AlulaError.for_response(response)
|
|
20
|
+
raise error_class
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build_and_merge_list_relationships(collection, relations)
|
|
25
|
+
relations.each do |relation|
|
|
26
|
+
model = Alula.class_from_resource_type(relation['type'])
|
|
27
|
+
model = model.new.construct_from(relation)
|
|
28
|
+
key = { type: model.get_type, id: model.id }.freeze
|
|
29
|
+
|
|
30
|
+
#
|
|
31
|
+
# TODO: Remove this line. This makes us ignore unknown relationships
|
|
32
|
+
unless get_relationship(model.get_type).nil?
|
|
33
|
+
collection.select{ |m| m.link_matchers.include?(key) }
|
|
34
|
+
.each { |m| m.add_model_to_relationship(model) }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
collection
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module InstanceMethods
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Alula
|
|
2
|
+
module ApiOperations
|
|
3
|
+
module Request
|
|
4
|
+
def self.extended(base)
|
|
5
|
+
base.include(InstanceMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Load a single model by ID
|
|
9
|
+
def retrieve(id, built_filters = {})
|
|
10
|
+
response = Alula::Client.request(:get, self.resource_url(id), built_filters, {})
|
|
11
|
+
if response.ok?
|
|
12
|
+
item = self.new.construct_from(response.data['data'])
|
|
13
|
+
item = build_and_merge_item_relationships(item, response.data['included']) if response.data['included']
|
|
14
|
+
item.rate_limit = response.rate_limit
|
|
15
|
+
item
|
|
16
|
+
else
|
|
17
|
+
error_class = AlulaError.for_response(response)
|
|
18
|
+
raise error_class
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_and_merge_item_relationships(item, relations)
|
|
23
|
+
relations.each do |relation|
|
|
24
|
+
model = Alula.class_from_resource_type(relation['type'])
|
|
25
|
+
model = model.new.construct_from(relation)
|
|
26
|
+
|
|
27
|
+
#
|
|
28
|
+
# TODO: Remove this line. This makes us ignore unknown relationships
|
|
29
|
+
unless get_relationship(model.get_type).nil?
|
|
30
|
+
item.add_model_to_relationship(model)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
item
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module InstanceMethods
|
|
38
|
+
def request(method, url, params, opts)
|
|
39
|
+
Alula::Client.request(method, url, params, opts)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Alula
|
|
2
|
+
module ApiOperations
|
|
3
|
+
module Save
|
|
4
|
+
def self.extended(base)
|
|
5
|
+
base.include(InstanceMethods)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module InstanceMethods
|
|
9
|
+
def save
|
|
10
|
+
payload = {
|
|
11
|
+
data: {
|
|
12
|
+
id: id,
|
|
13
|
+
attributes: as_patchable_json
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
response = Alula::Client.request(:patch, resource_url, payload, {})
|
|
18
|
+
|
|
19
|
+
if response.ok?
|
|
20
|
+
construct_from(response.data['data'])
|
|
21
|
+
return true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
handle_errors(response)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create
|
|
28
|
+
data = {
|
|
29
|
+
attributes: as_patchable_json
|
|
30
|
+
}
|
|
31
|
+
#
|
|
32
|
+
# Most creations _won't_ have an ID, but the Alula API utlizes a shared GUID strategy for a few resources
|
|
33
|
+
# We need to conditionally include this ID in the data array even though the record 'doesnt exist'
|
|
34
|
+
#
|
|
35
|
+
# - Dealer Branding shares the same primary ID as the Dealer record
|
|
36
|
+
# - Dealer Contact info shares the same primary ID as the Dealer record
|
|
37
|
+
data[:id] = id if id
|
|
38
|
+
|
|
39
|
+
payload = {
|
|
40
|
+
data: data
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
response = Alula::Client.request(:post, self.class.resource_url, payload, {})
|
|
44
|
+
|
|
45
|
+
if response.ok?
|
|
46
|
+
construct_from(response.data['data'])
|
|
47
|
+
return true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
handle_errors(response)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def handle_errors(response)
|
|
56
|
+
# Should never see a 300-range response code
|
|
57
|
+
if (300..399).cover?(response.http_status)
|
|
58
|
+
raise Alula::UnknownError, "The Alula-Ruby gem encountered a response code of #{response.code}, that should not happen"
|
|
59
|
+
|
|
60
|
+
# Server errors, malformed crap, etc
|
|
61
|
+
elsif (500..599).cover?(response.http_status)
|
|
62
|
+
return AlulaError.for_response(response)
|
|
63
|
+
|
|
64
|
+
# Validation errors usually
|
|
65
|
+
elsif (400..499).cover?(response.http_status)
|
|
66
|
+
model_errors = Util.model_errors_from_response(response)
|
|
67
|
+
|
|
68
|
+
if model_errors
|
|
69
|
+
annotate_errors(model_errors)
|
|
70
|
+
return false
|
|
71
|
+
else
|
|
72
|
+
return AlulaError.for_response(response)
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
raise Alula::UnknownError, "Unknown HTTP response code, aborting. Code: #{response.http_status}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
module Alula
|
|
2
|
+
# Parent class for all API objects
|
|
3
|
+
class ApiResource
|
|
4
|
+
attr_accessor :id, :raw_data, :values, :dirty_attributes, :errors, :links, :link_matchers, :rate_limit
|
|
5
|
+
|
|
6
|
+
def self.class_name
|
|
7
|
+
name.split("::")[-1]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(id = nil, attributes = {})
|
|
11
|
+
@raw_data = {}
|
|
12
|
+
@values = {}
|
|
13
|
+
@dirty_attributes = Set.new
|
|
14
|
+
@errors = ModelErrors.new(self.class)
|
|
15
|
+
|
|
16
|
+
construct_from(
|
|
17
|
+
'id' => id,
|
|
18
|
+
'attributes' => attributes
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Construct a new resource, ready to receive attributes, with
|
|
24
|
+
# empty values for all attrs.
|
|
25
|
+
# Useful for making New resources
|
|
26
|
+
# TODO: This will need testing and probably more work when we
|
|
27
|
+
# actually use it, at the moment we're only retrieving and
|
|
28
|
+
# modifying existing models.
|
|
29
|
+
def self.build
|
|
30
|
+
fields = self.get_fields.keys.map{ |k| Util.camelize(k) }
|
|
31
|
+
empty_shell = fields.each_with_object({}) { |f, obj| obj[f] = nil }
|
|
32
|
+
self.new(nil, empty_shell)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def construct_from(json_object)
|
|
36
|
+
@raw_data = json_object.dup
|
|
37
|
+
@values = json_object['attributes']
|
|
38
|
+
|
|
39
|
+
self.id = json_object['id'] unless [nil, ''].include?(json_object['id'])
|
|
40
|
+
|
|
41
|
+
@dirty_attributes = Set.new
|
|
42
|
+
@errors = ModelErrors.new(self.class)
|
|
43
|
+
|
|
44
|
+
@related_models = {}
|
|
45
|
+
cache_links(json_object['relationships'] || {})
|
|
46
|
+
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
# Take a hash of attributes and apply them to the model
|
|
52
|
+
def apply_attributes(attributes)
|
|
53
|
+
attributes.each do |key, value|
|
|
54
|
+
self.send("#{key}=", value)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reconstruct_from(json_object)
|
|
59
|
+
construct_from(json_object)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dirty?(attribute_name = nil)
|
|
63
|
+
return @dirty_attributes.any? if attribute_name.nil?
|
|
64
|
+
|
|
65
|
+
@dirty_attributes.include? attribute_name.to_sym
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def errors?
|
|
69
|
+
@errors.any?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def refresh
|
|
73
|
+
response = Alula::Client.request(:get, resource_url, {}, {})
|
|
74
|
+
if response.ok?
|
|
75
|
+
model = construct_from(response.data['data'])
|
|
76
|
+
model.rate_limit = response.rate_limit
|
|
77
|
+
model
|
|
78
|
+
else
|
|
79
|
+
error_class = AlulaError.for_response(response)
|
|
80
|
+
raise error_class
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# Fetch known attributes out of the object, collected into a Hash in camelCase format
|
|
86
|
+
# Intended for eventually making its way back up to the API
|
|
87
|
+
def as_json
|
|
88
|
+
self.field_names.each_with_object({}) do |ruby_key, obj|
|
|
89
|
+
key = Util.camelize(ruby_key)
|
|
90
|
+
val = self.send(ruby_key)
|
|
91
|
+
|
|
92
|
+
if self.date_fields.include?(ruby_key) && ![nil, ''].include?(val)
|
|
93
|
+
if val.respond_to? :strftime
|
|
94
|
+
obj[key] = val.strftime('%Y-%m-%dT%H:%M:%S.%L%z')
|
|
95
|
+
else
|
|
96
|
+
obj[key] = val.to_s
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
obj[key] = val
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
#
|
|
105
|
+
# Reduce as_json to a set that can be updated, removing any fields
|
|
106
|
+
# that are not patchable by the user
|
|
107
|
+
def as_patchable_json
|
|
108
|
+
values = as_json.each_pair.each_with_object({}) do |(key, value), collector|
|
|
109
|
+
ruby_key = Util.underscore(key).to_sym
|
|
110
|
+
collector[key] = value if !read_only_attributes.include?(ruby_key) && dirty?(ruby_key)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Remove blank values if creating a new record
|
|
114
|
+
values = values.select{ |k, v| !v.nil? } unless persisted?
|
|
115
|
+
values
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def annotate_errors(model_errors)
|
|
119
|
+
@errors = ModelErrors.new(self.class)
|
|
120
|
+
model_errors.each_pair do |field_name, error|
|
|
121
|
+
errors.add(field_name, error)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
#
|
|
126
|
+
# Links are hashes that identify any included models, they are used to
|
|
127
|
+
# distribute included models when also including relationships
|
|
128
|
+
# See list.rb#build_and_merge_relationships for details on usage
|
|
129
|
+
def cache_links(links)
|
|
130
|
+
@links = links
|
|
131
|
+
|
|
132
|
+
@link_matchers = links.each_pair.each_with_object([]) do |(type, link), collection|
|
|
133
|
+
data = link['data']
|
|
134
|
+
next if data.nil?
|
|
135
|
+
|
|
136
|
+
if data.is_a?(Array)
|
|
137
|
+
data.each do |nested_link|
|
|
138
|
+
collection << { type: nested_link['type'], id: nested_link['id'] }.freeze
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
collection << { type: data['type'], id: data['id'] }.freeze
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
#
|
|
147
|
+
# Return an instance of QueryEngine annotated with the correct model attributes
|
|
148
|
+
def filter_builder
|
|
149
|
+
Alula::FilterBuilder.new(self.class)
|
|
150
|
+
end
|
|
151
|
+
alias fb filter_builder
|
|
152
|
+
|
|
153
|
+
def model_name
|
|
154
|
+
self.class
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
class ModelErrors
|
|
160
|
+
include Enumerable
|
|
161
|
+
|
|
162
|
+
def initialize(model_class)
|
|
163
|
+
@model_class = model_class
|
|
164
|
+
@details = {}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def each
|
|
168
|
+
@details.each do |field, error|
|
|
169
|
+
yield({ field => error })
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def any?
|
|
174
|
+
@details.any?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def add(field_name, error_message)
|
|
178
|
+
@details[field_name] = error_message
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def [](field_name)
|
|
182
|
+
@details[field_name.to_sym]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def full_messages
|
|
186
|
+
@details.map { |field, error| "#{field}: #{error}" }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def full_messages_for(attribute_name)
|
|
190
|
+
return nil unless @details[attribute_name.to_sym].present?
|
|
191
|
+
|
|
192
|
+
"#{attribute_name}: #{@details[attribute_name.to_sym]}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
data/lib/alula/client.rb
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require 'httparty'
|
|
2
|
+
require 'alula/monkey_patches'
|
|
3
|
+
|
|
4
|
+
module Alula
|
|
5
|
+
class Client
|
|
6
|
+
include HTTParty
|
|
7
|
+
class << self
|
|
8
|
+
# Has pseudo accessors :api_key, :api_url, :user_agent, :debug
|
|
9
|
+
DEFAULT_CUSTOM_OPTIONS = {
|
|
10
|
+
omitRelationships: true
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
def request(http_method, resource_path, filters = {}, opts = {})
|
|
14
|
+
ensure_api_key_set
|
|
15
|
+
ensure_role_set if %i[patch post put].include? http_method
|
|
16
|
+
ensure_method_allowable(http_method)
|
|
17
|
+
|
|
18
|
+
raise Alula::NotConfiguredError, 'did you forget to set the Alula::Client.api_url config option?' unless api_url
|
|
19
|
+
|
|
20
|
+
request_opts = {
|
|
21
|
+
headers: {
|
|
22
|
+
'Authorization': "Bearer #{api_key}",
|
|
23
|
+
'User-Agent': "#{user_agent || 'No Agent Set'}/alula-ruby v#{Alula::VERSION}"
|
|
24
|
+
}
|
|
25
|
+
}.merge(opts)
|
|
26
|
+
|
|
27
|
+
request_opts[:headers]['Content-Type'] = 'application/json' unless opts[:multipart]
|
|
28
|
+
case http_method
|
|
29
|
+
when :patch,
|
|
30
|
+
:post
|
|
31
|
+
request_opts[:body] = opts[:multipart] ? filters : JSON.generate(filters)
|
|
32
|
+
when :get
|
|
33
|
+
if resource_path.match(%r{^/rest})
|
|
34
|
+
request_opts = request_opts.merge build_rest_options(filters)
|
|
35
|
+
elsif resource_path.match(%r{^/rpc})
|
|
36
|
+
request_opts = request_opts.merge build_rest_options(filters)
|
|
37
|
+
end
|
|
38
|
+
when :delete
|
|
39
|
+
# Nothing special
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
if debug == true
|
|
43
|
+
request_opts[:debug_output] = Alula.logger
|
|
44
|
+
logger Alula.logger, :info
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# TODO: Handle network failures
|
|
48
|
+
response = make_request(http_method, api_url + resource_path, request_opts)
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
resp = AlulaResponse.from_httparty_response(response)
|
|
52
|
+
rescue JSON::ParserError
|
|
53
|
+
# TODO: Should be able to send better info up with this
|
|
54
|
+
raise 'Unable to decode JSON'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
resp
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def make_request(http_method, full_url, request_opts)
|
|
61
|
+
send(http_method, full_url, request_opts)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# Using RequestStore so we're thread safe even with our non-thread-safe architecture :(
|
|
66
|
+
%i[api_key api_url user_agent debug].each do |prop|
|
|
67
|
+
define_method(prop) do
|
|
68
|
+
RequestStore.store["alula_#{prop}"]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
define_method("#{prop}=") do |value|
|
|
72
|
+
RequestStore.store["alula_#{prop}"] = value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
#
|
|
77
|
+
# Stash the user type as a role for us to infer from later
|
|
78
|
+
# You can set a symbolized role via role = :user, or provide the string userType
|
|
79
|
+
# We cast to a symbolized role for ease of use in consumers.
|
|
80
|
+
def role=(user_type)
|
|
81
|
+
unless user_type.is_a?(Symbol) || user_type.is_a?(String)
|
|
82
|
+
raise Alula::InvalidRoleError, 'Role must be symbol or string'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
RequestStore.store['alula_role'] = Util.underscore(user_type).to_sym
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def role
|
|
89
|
+
RequestStore.store['alula_role']
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ensure_api_key_set
|
|
93
|
+
if api_key.nil?
|
|
94
|
+
raise Alula::NotConfiguredError,
|
|
95
|
+
'Set your API access token before making requests with Alula::Client.api_key = {access_token}'
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ensure_method_allowable(http_method)
|
|
100
|
+
unless %i[get post put patch delete].include?(http_method)
|
|
101
|
+
raise "Unable to send a request with #{http_method} http method in #{self::OBJECT_NAME} class"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def ensure_role_set
|
|
106
|
+
if role.nil?
|
|
107
|
+
message = 'User role not configured! You must set '\
|
|
108
|
+
'Alula::Client.role '\
|
|
109
|
+
'before attempting to save any resources'
|
|
110
|
+
|
|
111
|
+
raise Alula::NotConfiguredError, message
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def build_rest_options(filters)
|
|
118
|
+
request_opts = {}
|
|
119
|
+
|
|
120
|
+
custom_options = filters.delete(:customOptions)
|
|
121
|
+
pagination = filters.delete(:page)
|
|
122
|
+
relationships = filters.delete(:include)
|
|
123
|
+
sort = filters.delete(:sort)
|
|
124
|
+
|
|
125
|
+
request_opts[:query] = {
|
|
126
|
+
filter: filters
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
request_opts[:query][:customOptions] = Util.deep_merge(DEFAULT_CUSTOM_OPTIONS, custom_options)
|
|
130
|
+
request_opts[:query][:page] = pagination if pagination
|
|
131
|
+
request_opts[:query][:include] = relationships if relationships
|
|
132
|
+
request_opts[:query][:sort] = sort if sort
|
|
133
|
+
|
|
134
|
+
request_opts
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def build_rpc_options(filters)
|
|
138
|
+
filters
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|