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