spark_api 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +14 -0
- data/README.md +42 -233
- data/VERSION +1 -1
- data/lib/spark_api.rb +1 -0
- data/lib/spark_api/authentication/oauth2.rb +39 -9
- data/lib/spark_api/authentication/oauth2_impl/cli_provider.rb +96 -0
- data/lib/spark_api/authentication/oauth2_impl/faraday_middleware.rb +28 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_base.rb +7 -2
- data/lib/spark_api/authentication/oauth2_impl/single_session_provider.rb +27 -0
- data/lib/spark_api/cli.rb +29 -10
- data/lib/spark_api/cli/api_auth.rb +1 -0
- data/lib/spark_api/cli/oauth2.rb +23 -8
- data/lib/spark_api/cli/setup.rb +31 -0
- data/lib/spark_api/configuration.rb +10 -2
- data/lib/spark_api/configuration/yaml.rb +6 -1
- data/lib/spark_api/connection.rb +1 -1
- data/lib/spark_api/errors.rb +48 -0
- data/lib/spark_api/models.rb +3 -0
- data/lib/spark_api/models/account.rb +9 -1
- data/lib/spark_api/models/base.rb +24 -19
- data/lib/spark_api/models/concerns.rb +7 -0
- data/lib/spark_api/models/concerns/destroyable.rb +32 -0
- data/lib/spark_api/models/concerns/savable.rb +66 -0
- data/lib/spark_api/models/contact.rb +6 -25
- data/lib/spark_api/models/dirty.rb +57 -0
- data/lib/spark_api/models/finders.rb +0 -4
- data/lib/spark_api/models/saved_search.rb +10 -0
- data/lib/spark_api/models/subresource.rb +5 -1
- data/lib/spark_api/models/subscription.rb +52 -0
- data/lib/spark_api/request.rb +17 -4
- data/lib/spark_api/response.rb +0 -37
- data/script/combined_flow_example.rb +3 -3
- data/script/oauth2_example.rb +3 -3
- data/spec/fixtures/base.json +3 -1
- data/spec/fixtures/contacts/new.json +2 -3
- data/spec/fixtures/contacts/new_empty.json +2 -3
- data/spec/fixtures/contacts/new_notify.json +1 -1
- data/spec/fixtures/{listings/saved_search.json → saved_searches/get.json} +1 -1
- data/spec/fixtures/saved_searches/new.json +8 -0
- data/spec/fixtures/saved_searches/post.json +12 -0
- data/spec/fixtures/saved_searches/update.json +6 -0
- data/spec/fixtures/subscriptions/get.json +19 -0
- data/spec/fixtures/subscriptions/new.json +13 -0
- data/spec/fixtures/subscriptions/post.json +10 -0
- data/spec/fixtures/subscriptions/put.json +12 -0
- data/spec/fixtures/subscriptions/subscribe.json +5 -0
- data/spec/fixtures/subscriptions/update.json +6 -0
- data/spec/mock_helper.rb +14 -6
- data/spec/oauth2_helper.rb +2 -0
- data/spec/spec_helper.rb +4 -7
- data/spec/unit/spark_api/authentication/api_auth_spec.rb +0 -1
- data/spec/unit/spark_api/authentication/oauth2_impl/faraday_middleware_spec.rb +32 -0
- data/spec/unit/spark_api/authentication/oauth2_impl/single_session_provider_spec.rb +9 -0
- data/spec/unit/spark_api/authentication/oauth2_spec.rb +29 -3
- data/spec/unit/spark_api/authentication_spec.rb +4 -10
- data/spec/unit/spark_api/configuration/yaml_spec.rb +4 -3
- data/spec/unit/spark_api/configuration_spec.rb +22 -8
- data/spec/unit/spark_api/models/account_spec.rb +5 -0
- data/spec/unit/spark_api/models/base_spec.rb +27 -0
- data/spec/unit/spark_api/models/concerns/destroyable_spec.rb +28 -0
- data/spec/unit/spark_api/models/concerns/savable_spec.rb +61 -0
- data/spec/unit/spark_api/models/contact_spec.rb +5 -5
- data/spec/unit/spark_api/models/dirty_spec.rb +46 -0
- data/spec/unit/spark_api/models/finders_spec.rb +0 -7
- data/spec/unit/spark_api/models/saved_search_spec.rb +34 -3
- data/spec/unit/spark_api/models/shared_listing_spec.rb +1 -1
- data/spec/unit/spark_api/models/subscription_spec.rb +106 -0
- data/spec/unit/spark_api/multi_client_spec.rb +14 -4
- data/spec/unit/spark_api/paginate_spec.rb +0 -1
- data/spec/unit/spark_api/request_spec.rb +10 -0
- data/spec/unit/spark_api_spec.rb +0 -3
- metadata +127 -45
- data/lib/spark_api/authentication/oauth2_impl/password_provider.rb +0 -24
@@ -0,0 +1,32 @@
|
|
1
|
+
module SparkApi
|
2
|
+
module Models
|
3
|
+
module Concerns
|
4
|
+
|
5
|
+
module Destroyable
|
6
|
+
|
7
|
+
def destroy(arguments = {})
|
8
|
+
self.errors = []
|
9
|
+
begin
|
10
|
+
return destroy!(arguments)
|
11
|
+
rescue BadResourceRequest => e
|
12
|
+
self.errors << {:code => e.code, :message => e.message}
|
13
|
+
SparkApi.logger.error("Failed to destroy resource #{self}: #{e.message}")
|
14
|
+
rescue NotFound => e
|
15
|
+
SparkApi.logger.error("Failed to destroy resource #{self}: #{e.message}")
|
16
|
+
end
|
17
|
+
false
|
18
|
+
end
|
19
|
+
def destroy!(arguments = {})
|
20
|
+
connection.delete("#{self.class.path}/#{self.Id}", arguments)
|
21
|
+
@destroyed = true
|
22
|
+
true
|
23
|
+
end
|
24
|
+
alias_method :delete, :destroy # backwards compatibility
|
25
|
+
|
26
|
+
def destroyed?; @destroyed ? @destroyed : false end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module SparkApi
|
2
|
+
module Models
|
3
|
+
module Concerns
|
4
|
+
|
5
|
+
module Savable
|
6
|
+
|
7
|
+
def save(arguments = {})
|
8
|
+
self.errors = [] # clear the errors hash
|
9
|
+
begin
|
10
|
+
return save!(arguments)
|
11
|
+
rescue BadResourceRequest => e
|
12
|
+
self.errors << {:code => e.code, :message => e.message}
|
13
|
+
SparkApi.logger.error("Failed to save resource #{self}: #{e.message}")
|
14
|
+
rescue NotFound => e
|
15
|
+
SparkApi.logger.error("Failed to save resource #{self}: #{e.message}")
|
16
|
+
end
|
17
|
+
false
|
18
|
+
end
|
19
|
+
def save!(arguments = {})
|
20
|
+
persisted? ? update!(arguments) : create!(arguments)
|
21
|
+
end
|
22
|
+
|
23
|
+
def create!(arguments = {})
|
24
|
+
results = connection.post self.class.path, {
|
25
|
+
resource_pluralized => [ attributes ]
|
26
|
+
}.merge(params_for_save), arguments
|
27
|
+
|
28
|
+
update_resource_identifiers(results.first)
|
29
|
+
reset_dirty
|
30
|
+
params_for_save.clear
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def update!(arguments = {})
|
35
|
+
return true unless changed?
|
36
|
+
connection.put "#{self.class.path}/#{self.Id}", dirty_attributes, arguments
|
37
|
+
reset_dirty
|
38
|
+
params_for_save.clear
|
39
|
+
true
|
40
|
+
end
|
41
|
+
|
42
|
+
def params_for_save
|
43
|
+
@params_for_save ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def update_resource_identifiers(result)
|
49
|
+
attributes['ResourceUri'] = result['ResourceUri']
|
50
|
+
attributes['Id'] = result['Id'] ? result['Id'] : parse_id(result['ResourceUri'])
|
51
|
+
end
|
52
|
+
|
53
|
+
# can be overridden
|
54
|
+
def resource_pluralized
|
55
|
+
resource = self.class.name.split('::').last
|
56
|
+
unless resource.split('').last == "s"
|
57
|
+
resource = resource + "s"
|
58
|
+
end
|
59
|
+
resource
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -2,28 +2,11 @@ module SparkApi
|
|
2
2
|
module Models
|
3
3
|
class Contact < Base
|
4
4
|
extend Finders
|
5
|
+
include Concerns::Savable,
|
6
|
+
Concerns::Destroyable
|
7
|
+
|
5
8
|
self.element_name="contacts"
|
6
9
|
|
7
|
-
def save(arguments={})
|
8
|
-
self.errors = [] # clear the errors hash
|
9
|
-
begin
|
10
|
-
return save!(arguments)
|
11
|
-
rescue BadResourceRequest => e
|
12
|
-
self.errors << {:code => e.code, :message => e.message}
|
13
|
-
SparkApi.logger.error("Failed to save resource #{self}: #{e.message}")
|
14
|
-
rescue NotFound => e
|
15
|
-
SparkApi.logger.error("Failed to save resource #{self}: #{e.message}")
|
16
|
-
end
|
17
|
-
false
|
18
|
-
end
|
19
|
-
def save!(arguments={})
|
20
|
-
results = connection.post self.class.path, {"Contacts" => [ attributes ], "Notify" => notify? }, arguments
|
21
|
-
result = results.first
|
22
|
-
attributes['ResourceUri'] = result['ResourceUri']
|
23
|
-
attributes['Id'] = parse_id(result['ResourceUri'])
|
24
|
-
true
|
25
|
-
end
|
26
|
-
|
27
10
|
def self.by_tag(tag_name, arguments={})
|
28
11
|
collect(connection.get("#{path}/tags/#{tag_name}", arguments))
|
29
12
|
end
|
@@ -37,11 +20,9 @@ module SparkApi
|
|
37
20
|
end
|
38
21
|
|
39
22
|
# Notify the agent of contact creation via a Spark notification.
|
40
|
-
def notify
|
41
|
-
|
42
|
-
|
43
|
-
def notify=(notify_me=true)
|
44
|
-
@notify = notify_me
|
23
|
+
def notify?; params_for_save[:Notify] == true end
|
24
|
+
def notify=(notify_me)
|
25
|
+
params_for_save[:Notify] = notify_me
|
45
26
|
end
|
46
27
|
|
47
28
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module SparkApi
|
2
|
+
module Models
|
3
|
+
module Dirty
|
4
|
+
|
5
|
+
def changed?
|
6
|
+
changed.any?
|
7
|
+
end
|
8
|
+
|
9
|
+
def changed
|
10
|
+
changed_attributes.keys
|
11
|
+
end
|
12
|
+
|
13
|
+
def changes
|
14
|
+
Hash[changed.map { |attr| [attr, attribute_change(attr)] }]
|
15
|
+
end
|
16
|
+
|
17
|
+
def previous_changes
|
18
|
+
@previously_changed
|
19
|
+
end
|
20
|
+
|
21
|
+
# hash with changed attributes and their original values
|
22
|
+
def changed_attributes
|
23
|
+
@changed_attributes ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# hash with changed attributes and their new values
|
27
|
+
def dirty_attributes
|
28
|
+
changed.inject({}) { |h, k| h[k] = attributes[k]; h }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def reset_dirty
|
34
|
+
@previously_changed = changed_attributes
|
35
|
+
@changed_attributes.clear
|
36
|
+
end
|
37
|
+
|
38
|
+
def attribute_changed?(attr)
|
39
|
+
changed.include?(attr)
|
40
|
+
end
|
41
|
+
|
42
|
+
def attribute_change(attr)
|
43
|
+
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
44
|
+
end
|
45
|
+
|
46
|
+
def attribute_will_change!(attr)
|
47
|
+
begin
|
48
|
+
value = __send__(attr)
|
49
|
+
value = value.duplicable? ? value.clone : value
|
50
|
+
rescue TypeError, NoMethodError; end
|
51
|
+
|
52
|
+
changed_attributes[attr] = value unless changed.include?(attr)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,7 +1,11 @@
|
|
1
1
|
module SparkApi
|
2
2
|
module Models
|
3
|
+
|
3
4
|
class SavedSearch < Base
|
4
5
|
extend Finders
|
6
|
+
include Concerns::Savable,
|
7
|
+
Concerns::Destroyable
|
8
|
+
|
5
9
|
self.element_name="savedsearches"
|
6
10
|
|
7
11
|
def self.provided()
|
@@ -11,6 +15,12 @@ module SparkApi
|
|
11
15
|
SparkApi.logger.info("#{self.name}.path: #{provided.path}")
|
12
16
|
end
|
13
17
|
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def resource_pluralized; "SavedSearches" end
|
22
|
+
|
14
23
|
end
|
24
|
+
|
15
25
|
end
|
16
26
|
end
|
@@ -25,6 +25,7 @@ module SparkApi
|
|
25
25
|
|
26
26
|
begin
|
27
27
|
datetime = DateTime.strptime(formatted_date, '%m/%d/%YT%l:%M %P')
|
28
|
+
dst_offset = 0
|
28
29
|
rescue => ex
|
29
30
|
; # Do nothing; doesn't matter
|
30
31
|
end
|
@@ -35,6 +36,7 @@ module SparkApi
|
|
35
36
|
begin
|
36
37
|
datetime = DateTime.strptime(formatted_date, format)
|
37
38
|
datetime = datetime.new_offset DateTime.now.offset
|
39
|
+
dst_offset = Time.now.dst? ? 0 : 1
|
38
40
|
break
|
39
41
|
rescue => ex
|
40
42
|
next
|
@@ -46,9 +48,11 @@ module SparkApi
|
|
46
48
|
unless datetime
|
47
49
|
raise ArgumentError.new('invalid date')
|
48
50
|
end
|
51
|
+
|
52
|
+
|
49
53
|
|
50
54
|
attributes[time] = Time.local(datetime.year, datetime.month, datetime.day,
|
51
|
-
datetime.hour, datetime.min, datetime.sec)
|
55
|
+
datetime.hour + dst_offset, datetime.min, datetime.sec)
|
52
56
|
end
|
53
57
|
attributes['Date'] = date
|
54
58
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module SparkApi
|
2
|
+
module Models
|
3
|
+
|
4
|
+
class Subscription < Base
|
5
|
+
extend Finders
|
6
|
+
include Concerns::Savable,
|
7
|
+
Concerns::Destroyable
|
8
|
+
|
9
|
+
self.element_name = "subscriptions"
|
10
|
+
|
11
|
+
# list subscribers (private role)
|
12
|
+
def subscribers
|
13
|
+
return {} unless persisted?
|
14
|
+
results = connection.get("#{self.class.path}/#{@attributes["Id"]}/subscribers")
|
15
|
+
@attributes['RecipientIds'] = results.first['RecipientIds']
|
16
|
+
results
|
17
|
+
end
|
18
|
+
|
19
|
+
# subscribe/unsubscribe contact (private role)
|
20
|
+
[:subscribe, :unsubscribe].each do |action|
|
21
|
+
method = (action == :subscribe ? :put : :delete)
|
22
|
+
define_method(action) do |contact|
|
23
|
+
return false unless persisted?
|
24
|
+
self.errors = []
|
25
|
+
contact_id = contact.is_a?(Contact) ? contact.Id : contact
|
26
|
+
begin
|
27
|
+
connection.send(method, "#{self.class.path}/#{@attributes["Id"]}/subscribers/#{contact_id}")
|
28
|
+
rescue BadResourceRequest, NotFound => e
|
29
|
+
self.errors << { :code => e.code, :message => e.message }
|
30
|
+
SparkApi.logger.error("Failed to #{action} contact #{contact}: #{e.message}")
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
update_recipients(action, contact_id)
|
34
|
+
true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def update_recipients(method, contact_id)
|
41
|
+
@attributes['RecipientIds'] = [] if @attributes['RecipientIds'].nil?
|
42
|
+
if method == :subscribe
|
43
|
+
@attributes['RecipientIds'] << contact_id
|
44
|
+
else
|
45
|
+
@attributes['RecipientIds'].delete contact_id
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/spark_api/request.rb
CHANGED
@@ -24,7 +24,7 @@ module SparkApi
|
|
24
24
|
# Hash of the json results as documented in the api.
|
25
25
|
# :raises:
|
26
26
|
# SparkApi::ClientError or subclass if the request failed.
|
27
|
-
def post(path, body=
|
27
|
+
def post(path, body = nil, options={})
|
28
28
|
request(:post, path, body, options)
|
29
29
|
end
|
30
30
|
|
@@ -37,7 +37,7 @@ module SparkApi
|
|
37
37
|
# Hash of the json results as documented in the api.
|
38
38
|
# :raises:
|
39
39
|
# SparkApi::ClientError or subclass if the request failed.
|
40
|
-
def put(path, body=
|
40
|
+
def put(path, body = nil, options={})
|
41
41
|
request(:put, path, body, options)
|
42
42
|
end
|
43
43
|
|
@@ -64,13 +64,13 @@ module SparkApi
|
|
64
64
|
begin
|
65
65
|
request_opts = {}
|
66
66
|
request_opts.merge!(options)
|
67
|
-
post_data = body.nil? ? nil : {"D" => body }.to_json
|
68
67
|
request_path = "/#{version}#{path}"
|
69
68
|
start_time = Time.now
|
70
69
|
SparkApi.logger.debug("#{method.to_s.upcase} Request: #{request_path}")
|
71
|
-
if
|
70
|
+
if [:get, :delete, :head].include?(method.to_sym)
|
72
71
|
response = authenticator.request(method, request_path, nil, request_opts)
|
73
72
|
else
|
73
|
+
post_data = process_request_body(body)
|
74
74
|
SparkApi.logger.debug("#{method.to_s.upcase} Data: #{post_data}")
|
75
75
|
response = authenticator.request(method, request_path, post_data, request_opts)
|
76
76
|
end
|
@@ -89,6 +89,19 @@ module SparkApi
|
|
89
89
|
raise
|
90
90
|
end
|
91
91
|
response.body
|
92
|
+
rescue Faraday::Error::ConnectionFailed => e
|
93
|
+
if self.ssl_verify && e.message =~ /certificate verify failed/
|
94
|
+
SparkApi.logger.error(SparkApi::Errors.ssl_verification_error)
|
95
|
+
end
|
96
|
+
raise e
|
97
|
+
end
|
98
|
+
|
99
|
+
def process_request_body(body)
|
100
|
+
if body.is_a?(Hash)
|
101
|
+
body.empty? ? "{}" : {"D" => body }.to_json
|
102
|
+
else
|
103
|
+
body
|
104
|
+
end
|
92
105
|
end
|
93
106
|
|
94
107
|
end
|
data/lib/spark_api/response.rb
CHANGED
@@ -8,43 +8,6 @@ module SparkApi
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
# All known response codes listed in the API
|
12
|
-
module ResponseCodes
|
13
|
-
NOT_FOUND = 404
|
14
|
-
METHOD_NOT_ALLOWED = 405
|
15
|
-
INVALID_KEY = 1000
|
16
|
-
DISABLED_KEY = 1010
|
17
|
-
API_USER_REQUIRED = 1015
|
18
|
-
SESSION_TOKEN_EXPIRED = 1020
|
19
|
-
SSL_REQUIRED = 1030
|
20
|
-
INVALID_JSON = 1035
|
21
|
-
INVALID_FIELD = 1040
|
22
|
-
MISSING_PARAMETER = 1050
|
23
|
-
INVALID_PARAMETER = 1053
|
24
|
-
CONFLICTING_DATA = 1055
|
25
|
-
NOT_AVAILABLE= 1500
|
26
|
-
RATE_LIMIT_EXCEEDED = 1550
|
27
|
-
end
|
28
|
-
|
29
|
-
# Errors built from API responses
|
30
|
-
class InvalidResponse < StandardError; end
|
31
|
-
class ClientError < StandardError
|
32
|
-
attr_reader :code, :status, :details
|
33
|
-
def initialize (options = {})
|
34
|
-
# Support the standard initializer for errors
|
35
|
-
opts = options.is_a?(Hash) ? options : {:message => options.to_s}
|
36
|
-
@code = opts[:code]
|
37
|
-
@status = opts[:status]
|
38
|
-
@details = opts[:details]
|
39
|
-
super(opts[:message])
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
class NotFound < ClientError; end
|
44
|
-
class PermissionDenied < ClientError; end
|
45
|
-
class NotAllowed < ClientError; end
|
46
|
-
class BadResourceRequest < ClientError; end
|
47
|
-
|
48
11
|
# Nice and handy class wrapper for the api response hash
|
49
12
|
class ApiResponse < ::Array
|
50
13
|
include SparkApi::Response
|
@@ -14,8 +14,8 @@ SparkApi.configure do |config|
|
|
14
14
|
config.api_key = "YOUR_CLIENT_ID"
|
15
15
|
config.api_secret = "YOUR_CLIENT_SECRET"
|
16
16
|
config.callback = "YOUR_REDIRECT_URI"
|
17
|
-
config.auth_endpoint = "https://
|
18
|
-
config.endpoint = 'https://
|
17
|
+
config.auth_endpoint = "https://sparkplatform.com/openid"
|
18
|
+
config.endpoint = 'https://sparkapi.com'
|
19
19
|
end
|
20
20
|
|
21
21
|
client = SparkApi.client
|
@@ -24,7 +24,7 @@ client = SparkApi.client
|
|
24
24
|
# Step 1:
|
25
25
|
# To get your code to post to /v1/oauth2/grant, send the end user to this URI, replacing the all-capped strings with
|
26
26
|
# the CGI-escaped credentials for your key:
|
27
|
-
# https://
|
27
|
+
# https://sparkplatform.com/oauth2?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI
|
28
28
|
# When the user has finished, they will land at:
|
29
29
|
# YOUR_REDIRECT_URI?code=CODE.
|
30
30
|
puts "Go here and log in to get your code: #{client.authenticator.authorization_url}"
|