connect_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module ConnectClient
2
+ VERSION = "0.1.0"
3
+ 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