connect_client 0.1.0
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/.gitignore +38 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +45 -0
- data/Rakefile +22 -0
- data/connect_client.gemspec +25 -0
- data/data/cacert.pem +3988 -0
- data/lib/connect_client.rb +41 -0
- data/lib/connect_client/client.rb +43 -0
- data/lib/connect_client/configuration.rb +12 -0
- data/lib/connect_client/event.rb +52 -0
- data/lib/connect_client/event_push_response.rb +49 -0
- data/lib/connect_client/http/event_endpoint.rb +137 -0
- data/lib/connect_client/version.rb +3 -0
- data/spec/connect_client/client_spec.rb +48 -0
- data/spec/connect_client/configuration_spec.rb +55 -0
- data/spec/connect_client/event_push_response_spec.rb +150 -0
- data/spec/connect_client/event_spec.rb +47 -0
- data/spec/connect_client/http/http_event_endpoint_spec.rb +121 -0
- data/spec/connect_client/http/synchrony/event_endpoint_spec.rb +60 -0
- data/spec/connect_client_spec.rb +25 -0
- metadata +155 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative 'connect_client/client'
|
2
|
+
require_relative 'connect_client/configuration'
|
3
|
+
|
4
|
+
module ConnectClient
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def configure
|
8
|
+
config = Configuration.new
|
9
|
+
yield(config)
|
10
|
+
|
11
|
+
@client = ConnectClient::Client.new config
|
12
|
+
end
|
13
|
+
|
14
|
+
def reset
|
15
|
+
@client = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(method, *args, &block)
|
19
|
+
return super unless client.respond_to?(method)
|
20
|
+
client.send(method, *args, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
def respond_to?(method)
|
24
|
+
return (!@client.nil? && @client.respond_to?(method)) || super
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def client
|
30
|
+
raise UnconfiguredError if @client.nil?
|
31
|
+
|
32
|
+
@client
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class UnconfiguredError < StandardError
|
37
|
+
def message
|
38
|
+
"Connect must configured before it can be used, please call Connect.configure"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative 'http/event_endpoint'
|
2
|
+
require_relative 'event'
|
3
|
+
|
4
|
+
module ConnectClient
|
5
|
+
class Client
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@end_point = ConnectClient::Http::EventEndpoint.new config
|
9
|
+
end
|
10
|
+
|
11
|
+
def push(collection_name_or_batches, event_or_events = nil)
|
12
|
+
has_multiple_events = event_or_events.is_a?(Array)
|
13
|
+
has_collection_name = collection_name_or_batches.is_a?(String)
|
14
|
+
is_batch = !has_collection_name || has_multiple_events
|
15
|
+
|
16
|
+
if is_batch
|
17
|
+
batch = create_batch(has_collection_name, collection_name_or_batches, event_or_events)
|
18
|
+
@end_point.push_batch batch
|
19
|
+
else
|
20
|
+
@end_point.push(collection_name_or_batches, ConnectClient::Event.new(event_or_events))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def create_batch(has_collection_name, collection_name_or_batches, event_or_events)
|
27
|
+
|
28
|
+
batches = has_collection_name ?
|
29
|
+
{ collection_name_or_batches.to_sym => event_or_events } :
|
30
|
+
collection_name_or_batches
|
31
|
+
|
32
|
+
create_event = Proc.new do |event_data|
|
33
|
+
ConnectClient::Event.new event_data
|
34
|
+
end
|
35
|
+
|
36
|
+
map_all_events = Proc.new do |col_name, events|
|
37
|
+
[col_name, events.map(&create_event)]
|
38
|
+
end
|
39
|
+
|
40
|
+
Hash[batches.map(&map_all_events)]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ConnectClient
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :base_url, :api_key, :project_id, :async
|
4
|
+
|
5
|
+
def initialize(api_key = '', project_id = '', async = false, base_url = 'https://api.getconnect.io')
|
6
|
+
@base_url = base_url
|
7
|
+
@api_key = api_key
|
8
|
+
@project_id = project_id
|
9
|
+
@async = async
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'json'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
module ConnectClient
|
6
|
+
class Event
|
7
|
+
|
8
|
+
@@RESERVED_PROPERTY_REGEX = /tp_.+/i
|
9
|
+
|
10
|
+
attr_reader :data
|
11
|
+
|
12
|
+
def initialize(data)
|
13
|
+
event_data_defaults = { id: SecureRandom.uuid, timestamp: Time.now.utc.iso8601 }
|
14
|
+
@data = event_data_defaults.merge(data)
|
15
|
+
|
16
|
+
if (@data[:timestamp].respond_to? :iso8601)
|
17
|
+
@data[:timestamp] = @data[:timestamp].iso8601
|
18
|
+
end
|
19
|
+
|
20
|
+
validate
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate
|
24
|
+
invalid_properties = @data.keys.grep(@@RESERVED_PROPERTY_REGEX)
|
25
|
+
|
26
|
+
raise EventDataValidationError.new(invalid_properties) if invalid_properties.any?
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_json(options = nil)
|
30
|
+
@data.to_json
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
"Event Data: #{@data}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class EventDataValidationError < StandardError
|
39
|
+
attr_reader :invalid_property_names
|
40
|
+
|
41
|
+
def initialize(invalid_property_names)
|
42
|
+
@invalid_property_names = invalid_property_names
|
43
|
+
end
|
44
|
+
|
45
|
+
def message
|
46
|
+
messages = ['The following properties use the reserved prefix tp_:'] + @invalid_property_names.map do |property_name|
|
47
|
+
"->#{property_name}"
|
48
|
+
end
|
49
|
+
messages.join "\n"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module ConnectClient
|
4
|
+
class EventPushResponse
|
5
|
+
attr_reader :data
|
6
|
+
attr_reader :http_status_code
|
7
|
+
|
8
|
+
def initialize(code, content_type, response_body, events_pushed)
|
9
|
+
@http_status_code = code.to_s
|
10
|
+
|
11
|
+
if content_type.include? 'application/json'
|
12
|
+
body = response_body
|
13
|
+
body = '{}' if response_body.to_s.empty?
|
14
|
+
parse_body(body, events_pushed)
|
15
|
+
else
|
16
|
+
@data = response_body
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def success?
|
21
|
+
@http_status_code.start_with? '2'
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
%{
|
26
|
+
Status: #{@http_status_code}
|
27
|
+
Successful: #{success?}
|
28
|
+
Data: #{data}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def parse_body(body, events_pushed)
|
35
|
+
@data = JSON.parse(body, :symbolize_names => true)
|
36
|
+
|
37
|
+
if (events_pushed.is_a?(Hash) && @data.is_a?(Hash))
|
38
|
+
@data.merge!(events_pushed) do |collection_name, responses, events|
|
39
|
+
responses.zip(events).map do |response, event|
|
40
|
+
response[:event] = event.data
|
41
|
+
response
|
42
|
+
end
|
43
|
+
end
|
44
|
+
else
|
45
|
+
@data[:event] = events_pushed.data
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative '../event_push_response'
|
3
|
+
|
4
|
+
module ConnectClient
|
5
|
+
module Http
|
6
|
+
class EventEndpoint
|
7
|
+
def initialize(config)
|
8
|
+
headers = {
|
9
|
+
"Content-Type" => "application/json",
|
10
|
+
"Accept" => "application/json",
|
11
|
+
"Accept-Encoding" => "identity",
|
12
|
+
"X-Api-Key" => config.api_key,
|
13
|
+
"X-Project-Id" => config.project_id
|
14
|
+
}
|
15
|
+
|
16
|
+
if config.async
|
17
|
+
@http = Async.new config.base_url, headers
|
18
|
+
else
|
19
|
+
@http = Sync.new config.base_url, headers
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def push(collection_name, event)
|
25
|
+
path_uri_part = "/events/#{collection_name}"
|
26
|
+
|
27
|
+
@http.send path_uri_part, event.data.to_json, event
|
28
|
+
end
|
29
|
+
|
30
|
+
def push_batch(events_by_collection)
|
31
|
+
path_uri_part = "/events"
|
32
|
+
|
33
|
+
@http.send path_uri_part, events_by_collection.to_json, events_by_collection
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
class Sync
|
40
|
+
def initialize(base_url, headers)
|
41
|
+
require 'uri'
|
42
|
+
require 'net/http'
|
43
|
+
require 'net/https'
|
44
|
+
|
45
|
+
@headers = headers
|
46
|
+
@connect_uri = URI.parse(base_url)
|
47
|
+
@http = Net::HTTP.new(@connect_uri.host, @connect_uri.port)
|
48
|
+
setup_ssl if @connect_uri.scheme == 'https'
|
49
|
+
end
|
50
|
+
|
51
|
+
def send(path, body, events)
|
52
|
+
response = @http.post(path, body, @headers)
|
53
|
+
ConnectClient::EventPushResponse.new response.code, response['Content-Type'], response.body, events
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def setup_ssl
|
59
|
+
@http.use_ssl = true
|
60
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
61
|
+
@http.verify_depth = 5
|
62
|
+
@http.ca_file = File.expand_path("../../../../data/cacert.pem", __FILE__)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class Async
|
67
|
+
|
68
|
+
def initialize(base_url, headers)
|
69
|
+
require 'em-http-request'
|
70
|
+
|
71
|
+
@headers = headers
|
72
|
+
@base_url = base_url.chomp('/')
|
73
|
+
end
|
74
|
+
|
75
|
+
def send(path, body, events)
|
76
|
+
raise AsyncHttpError unless defined?(EventMachine) && EventMachine.reactor_running?
|
77
|
+
|
78
|
+
use_syncrony = defined?(EM::Synchrony)
|
79
|
+
|
80
|
+
if use_syncrony
|
81
|
+
send_using_synchrony(path, body, events)
|
82
|
+
else
|
83
|
+
send_using_deferred(path, body, events)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def send_using_deferred(path, body, events)
|
88
|
+
deferred = DeferredHttpResponse.new
|
89
|
+
url_string = "#{@base_url}#{path}".chomp('/')
|
90
|
+
http = EventMachine::HttpRequest.new(url_string).post(:body => body, :head => @headers)
|
91
|
+
http_callback = Proc.new do
|
92
|
+
begin
|
93
|
+
response = create_response http, events
|
94
|
+
deferred.succeed response
|
95
|
+
rescue => error
|
96
|
+
deferred.fail error
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
http.callback &http_callback
|
101
|
+
http.errback &http_callback
|
102
|
+
|
103
|
+
deferred
|
104
|
+
end
|
105
|
+
|
106
|
+
def send_using_synchrony(path, body, events)
|
107
|
+
url_string = "#{@base_url}#{path}".chomp('/')
|
108
|
+
http = EventMachine::HttpRequest.new(url_string).
|
109
|
+
post(:body => body, :head => @headers)
|
110
|
+
|
111
|
+
create_response http, events
|
112
|
+
end
|
113
|
+
|
114
|
+
def create_response(http_reponse, events)
|
115
|
+
status = http_reponse.response_header.status
|
116
|
+
content_type = http_reponse.response_header['Content-Type']
|
117
|
+
if (http_reponse.error.to_s.empty?)
|
118
|
+
ConnectClient::EventPushResponse.new status, content_type, http_reponse.response, events
|
119
|
+
else
|
120
|
+
ConnectClient::EventPushResponse.new status, content_type, http_reponse.error, events
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class DeferredHttpResponse
|
126
|
+
include EventMachine::Deferrable
|
127
|
+
alias_method :response_received, :callback
|
128
|
+
alias_method :error_occured, :errback
|
129
|
+
end
|
130
|
+
|
131
|
+
class AsyncHttpError < StandardError
|
132
|
+
def message
|
133
|
+
"An EventMachine loop must be running to send an async http request via 'em-http-request'"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'minitest/spec'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'webmock/minitest'
|
4
|
+
require 'connect_client/client'
|
5
|
+
require 'connect_client/event_push_response'
|
6
|
+
require 'connect_client/configuration'
|
7
|
+
require 'securerandom'
|
8
|
+
|
9
|
+
describe ConnectClient::Client do
|
10
|
+
before do
|
11
|
+
@client = ConnectClient::Client.new (ConnectClient::Configuration.new)
|
12
|
+
@async_client = ConnectClient::Client.new (ConnectClient::Configuration.new '', '', true)
|
13
|
+
@sample_event = { id: SecureRandom.uuid, timestamp: Time.now.utc.iso8601, name: 'sample' }
|
14
|
+
@sample_events = [@sample_event]
|
15
|
+
@sample_events_reponse = '{"sample": [{"success": true}]}'
|
16
|
+
@sample_collection = 'sample'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should get a push response back when pushing a single event to a collection" do
|
20
|
+
stub_request(:post, "https://api.getconnect.io/events/#{@sample_collection}").
|
21
|
+
with(:body => @sample_event).
|
22
|
+
to_return(:status => 200, :body => "", :headers => { 'Content-Type'=>'application/json' })
|
23
|
+
|
24
|
+
response = @client.push @sample_collection, @sample_event
|
25
|
+
|
26
|
+
response.must_be_instance_of ConnectClient::EventPushResponse
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should get a push response back when pushing multiple events to a collection" do
|
30
|
+
stub_request(:post, "https://api.getconnect.io/events").
|
31
|
+
with(:body => { @sample_collection.to_sym => @sample_events }).
|
32
|
+
to_return(:status => 200, :body => @sample_events_reponse, :headers => { 'Content-Type'=>'application/json' })
|
33
|
+
|
34
|
+
response = @client.push @sample_collection, @sample_events
|
35
|
+
|
36
|
+
response.must_be_instance_of ConnectClient::EventPushResponse
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should get a push response back when pushing batches" do
|
40
|
+
batch = { @sample_collection.to_sym => @sample_events }
|
41
|
+
stub_request(:post, "https://api.getconnect.io/events").
|
42
|
+
with(:body => batch).
|
43
|
+
to_return(:status => 200, :body => @sample_events_reponse, :headers => { 'Content-Type'=>'application/json' })
|
44
|
+
|
45
|
+
response = @client.push batch
|
46
|
+
response.must_be_instance_of ConnectClient::EventPushResponse
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'minitest/spec'
|
2
|
+
require 'minitest/autorun'
|
3
|
+
require 'connect_client/configuration'
|
4
|
+
|
5
|
+
describe ConnectClient::Configuration do
|
6
|
+
|
7
|
+
it "should default the base_url to production" do
|
8
|
+
config = ConnectClient::Configuration.new
|
9
|
+
|
10
|
+
config.base_url.must_equal 'https://api.getconnect.io'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should default async to false" do
|
14
|
+
config = ConnectClient::Configuration.new
|
15
|
+
|
16
|
+
config.async.must_equal false
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should support setting the project id" do
|
20
|
+
config = ConnectClient::Configuration.new
|
21
|
+
id = 'id'
|
22
|
+
|
23
|
+
config.project_id = id
|
24
|
+
|
25
|
+
config.project_id.must_equal id
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should support setting the push key" do
|
29
|
+
config = ConnectClient::Configuration.new
|
30
|
+
key = 'key'
|
31
|
+
|
32
|
+
config.api_key = key
|
33
|
+
|
34
|
+
config.api_key.must_equal key
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should support setting whether requests are async" do
|
38
|
+
config = ConnectClient::Configuration.new
|
39
|
+
async = true
|
40
|
+
|
41
|
+
config.async = async
|
42
|
+
|
43
|
+
config.async.must_equal async
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should support setting the base url" do
|
47
|
+
config = ConnectClient::Configuration.new
|
48
|
+
url = 'url'
|
49
|
+
|
50
|
+
config.base_url = url
|
51
|
+
|
52
|
+
config.base_url.must_equal url
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|