justrelate_sdk 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ module JustRelate
2
+ # A JustRelate account is an organizational entity such as a company.
3
+ # @api public
4
+ class Account < Core::BasicResource
5
+ include Core::Mixins::Findable
6
+ include Core::Mixins::Modifiable
7
+ include Core::Mixins::ChangeLoggable
8
+ include Core::Mixins::MergeAndDeletable
9
+ include Core::Mixins::Searchable
10
+ include Core::Mixins::Inspectable
11
+ inspectable :id, :name
12
+
13
+ # @!parse extend Core::Mixins::Findable::ClassMethods
14
+ # @!parse extend Core::Mixins::Modifiable::ClassMethods
15
+ # @!parse extend Core::Mixins::Searchable::ClassMethods
16
+ end
17
+ end
@@ -0,0 +1,143 @@
1
+ module JustRelate
2
+ # A JustRelate activity is a record of an action or a sequence of actions,
3
+ # for example a support case.
4
+ # It can be associated with an {Account account} or a {Contact contact}.
5
+ #
6
+ # === Comments
7
+ # Comments can be read be means of {#comments}.
8
+ # In order to add a comment, set the following write-only attributes on {.create} or {#update}:
9
+ # * +comment_notes+ (String) - the comment text.
10
+ # * +comment_contact_id+ (String) - the contact ID of the comment author (optional).
11
+ # * +comment_published+ (Boolean) - whether the comment should be visible
12
+ # to the associated contact of this activity (+item.contact_id+).
13
+ # Default: +false+.
14
+ # * +comment_attachments+ (Array<String, #read>) - the comment attachments (optional).
15
+ # Every array element may either be an attachment ID or an object that implements +#read+
16
+ # (e.g. an open file). In the latter case, the content will be
17
+ # uploaded automatically. See {JustRelate::Core::AttachmentStore} for manually uploading attachments.
18
+ # @api public
19
+ class Activity < Core::BasicResource
20
+ include Core::Mixins::Findable
21
+ include Core::Mixins::Modifiable
22
+ include Core::Mixins::ChangeLoggable
23
+ include Core::Mixins::Searchable
24
+ include Core::Mixins::Inspectable
25
+ inspectable :id, :title, :type_id
26
+
27
+ # @!parse extend Core::Mixins::Findable::ClassMethods
28
+ # @!parse extend Core::Mixins::Modifiable::ClassMethods
29
+ # @!parse extend Core::Mixins::Searchable::ClassMethods
30
+
31
+ # Creates a new activity using the given +params+.
32
+ # See {Core::Mixins::Modifiable::ClassMethods#create Modifiable.create} for details.
33
+ # @return [self] the created activity.
34
+ # @api public
35
+ def self.create(attributes = {})
36
+ super(filter_attributes(attributes))
37
+ end
38
+
39
+ # Updates the attributes of this activity.
40
+ # See {Core::Mixins::Modifiable#update Modifiable#update} for details.
41
+ # @return [self] the updated activity.
42
+ # @api public
43
+ def update(attributes = {})
44
+ super(self.class.filter_attributes(attributes))
45
+ end
46
+
47
+ # +Comment+ represents a comment of an {Activity Activity},
48
+ # for example a single comment of a support case discussion.
49
+ # @api public
50
+ class Comment
51
+ include Core::Mixins::AttributeProvider
52
+
53
+ # +Attachment+ represents an attachment of an {Activity::Comment activity comment}.
54
+ # @api public
55
+ class Attachment
56
+ # Returns the ID of this attachment.
57
+ # @return [String]
58
+ # @api public
59
+ attr_reader :id
60
+
61
+ def initialize(id)
62
+ @id = id
63
+ end
64
+
65
+ # Generates a download URL for this attachment.
66
+ # Retrieve the attachment data by fetching this URL.
67
+ # This URL is only valid for a couple of minutes.
68
+ # Hence, it is recommended to have such URLs generated on demand.
69
+ # @return [String]
70
+ # @api public
71
+ def download_url
72
+ JustRelate::Core::AttachmentStore.generate_download_url(id)
73
+ end
74
+ end
75
+
76
+ def initialize(raw_comment)
77
+ comment = raw_comment.dup
78
+ comment['attachments'] = raw_comment['attachments'].map{ |attachment_id|
79
+ Attachment.new(attachment_id)
80
+ }
81
+ super(comment)
82
+ end
83
+
84
+ # @!attribute [r] attachments
85
+ # Returns the list of comment {Attachment attachments}.
86
+ # @return [Array<Attachment>]
87
+ # @api public
88
+
89
+ # @!attribute [r] updated_at
90
+ # Returns the timestamp of the comment.
91
+ # @return [Time]
92
+ # @api public
93
+
94
+ # @!attribute [r] updated_by
95
+ # Returns the login of the API user who created the comment.
96
+ # @return [String]
97
+ # @api public
98
+
99
+ # @!attribute [r] notes
100
+ # Returns the comment text.
101
+ # @return [String]
102
+ # @api public
103
+
104
+ # @!attribute [r] published?
105
+ # Returns whether the comment is published.
106
+ # @return [Boolean]
107
+ # @api public
108
+ def published?
109
+ published
110
+ end
111
+ end
112
+
113
+ # @!attribute [r] comments
114
+ # Returns the {Comment comments} of this activity.
115
+ # @return [Array<Comment>]
116
+ # @api public
117
+
118
+ def self.filter_attributes(attributes)
119
+ attachments = attributes.delete('comment_attachments') ||
120
+ attributes.delete(:comment_attachments)
121
+ if attachments
122
+ attributes['comment_attachments'] = attachments.map do |attachment|
123
+ if attachment.respond_to?(:read)
124
+ Core::AttachmentStore.upload(attachment)
125
+ else
126
+ attachment
127
+ end
128
+ end
129
+ end
130
+ attributes
131
+ end
132
+
133
+ protected
134
+
135
+ def load_attributes(raw_attributes)
136
+ attributes = raw_attributes.dup
137
+ attributes['comments'] = (attributes['comments'] || []).map do |comment_attributes|
138
+ Comment.new(comment_attributes)
139
+ end
140
+ super(attributes)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,41 @@
1
+ module JustRelate
2
+ # A JustRelate collection is a saved search. To execute such a saved search, call {#compute}.
3
+ # The results are persisted and can be accessed by means of {#output_items}.
4
+ # Output items can be {Account accounts}, {Contact contacts}, {Activity activities},
5
+ # and {Event events}.
6
+ # @api public
7
+ class Collection < Core::BasicResource
8
+ include Core::Mixins::Findable
9
+ include Core::Mixins::Modifiable
10
+ include Core::Mixins::ChangeLoggable
11
+ include Core::Mixins::Searchable
12
+ include Core::Mixins::Inspectable
13
+ inspectable :id, :title
14
+
15
+ # @!parse extend Core::Mixins::Findable::ClassMethods
16
+ # @!parse extend Core::Mixins::Modifiable::ClassMethods
17
+ # @!parse extend Core::Mixins::Searchable::ClassMethods
18
+
19
+ # Computes this collection.
20
+ # @return [self]
21
+ # @api public
22
+ def compute
23
+ load_attributes(Core::RestApi.instance.put("#{path}/compute", {}))
24
+ end
25
+
26
+ # Returns the IDs resulting from the computation.
27
+ # @return [Array<String>]
28
+ # @api public
29
+ def output_ids
30
+ Core::RestApi.instance.get("#{path}/output_ids")
31
+ end
32
+
33
+ # Returns an {Core::ItemEnumerator ItemEnumerator}
34
+ # that provides access to the items of {#output_ids}.
35
+ # @return [Core::ItemEnumerator]
36
+ # @api public
37
+ def output_items
38
+ Core::ItemEnumerator.new(output_ids)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,121 @@
1
+ module JustRelate
2
+ # A JustRelate contact represents contact information about a person.
3
+ # It can be associated with an {Account account}.
4
+ # @api public
5
+ class Contact < Core::BasicResource
6
+ include Core::Mixins::Findable
7
+ include Core::Mixins::Modifiable
8
+ include Core::Mixins::ChangeLoggable
9
+ include Core::Mixins::MergeAndDeletable
10
+ include Core::Mixins::Searchable
11
+ include Core::Mixins::Inspectable
12
+ inspectable :id, :last_name, :first_name, :email
13
+
14
+ # @!parse extend Core::Mixins::Findable::ClassMethods
15
+ # @!parse extend Core::Mixins::Modifiable::ClassMethods
16
+ # @!parse extend Core::Mixins::Searchable::ClassMethods
17
+
18
+ # @!group Authentication and password management
19
+
20
+ # Authenticates a contact using their +login+ and +password+.
21
+ # @example
22
+ # contact = JustRelate::Contact.authenticate!('jane@example.org', 'correct')
23
+ # # => JustRelate::Contact
24
+ #
25
+ # contact.login
26
+ # # => 'jane@example.org'
27
+ #
28
+ # JustRelate::Contact.authenticate!('jane@example.org', 'wrong')
29
+ # # => raises AuthenticationFailed
30
+ # @param login [String] the login of the contact.
31
+ # @param password [String] the password of the contact.
32
+ # @return [Contact] the authenticated contact.
33
+ # @raise [Errors::AuthenticationFailed] if the +login+/+password+ combination is wrong.
34
+ # @api public
35
+ def self.authenticate!(login, password)
36
+ new(Core::RestApi.instance.put("#{path}/authenticate",
37
+ {'login' => login, 'password' => password}))
38
+ end
39
+
40
+ # Authenticates a contact using their +login+ and +password+.
41
+ # @example
42
+ # contact = JustRelate::Contact.authenticate('jane@example.org', 'correct')
43
+ # # => JustRelate::Contact
44
+ #
45
+ # contact.login
46
+ # # => 'jane@example.org'
47
+ #
48
+ # JustRelate::Contact.authenticate('jane@example.org', 'wrong')
49
+ # # => nil
50
+ # @param login [String] the login of the contact.
51
+ # @param password [String] the password of the contact.
52
+ # @return [Contact, nil] the authenticated contact. +nil+ if authentication failed.
53
+ # @api public
54
+ def self.authenticate(login, password)
55
+ authenticate!(login, password)
56
+ rescue Errors::AuthenticationFailed
57
+ nil
58
+ end
59
+
60
+ # Sets the new password.
61
+ # @param new_password [String] the new password.
62
+ # @return [self] the updated contact.
63
+ # @api public
64
+ def set_password(new_password)
65
+ load_attributes(Core::RestApi.instance.put("#{path}/set_password",
66
+ {'password' => new_password}))
67
+ end
68
+
69
+ # Generates a password token.
70
+ #
71
+ # Use case: A project sends an e-mail to the contact. The e-mail contains a link to
72
+ # the project web app. The link contains the param +?token=...+.
73
+ # The web app retrieves and parses the token
74
+ # and passes it to {Contact.set_password_by_token}.
75
+ # @return [String] the generated token.
76
+ # @api public
77
+ def generate_password_token
78
+ Core::RestApi.instance.post("#{path}/generate_password_token", {})['token']
79
+ end
80
+
81
+ # Sets a contact's new password by means of the token. Generate a token by calling
82
+ # {#send_password_token_email} or {#generate_password_token}.
83
+ #
84
+ # Use case: A contact clicks a link (that includes a token) in an e-mail
85
+ # to get to a password change page.
86
+ # @param new_password [String] the new password.
87
+ # @param token [String] the given token.
88
+ # @return [Contact] the updated contact.
89
+ # @raise [Errors::ResourceNotFound] if +token+ is invalid.
90
+ # @api public
91
+ def self.set_password_by_token(new_password, token)
92
+ new(Core::RestApi.instance.put("#{path}/set_password_by_token", {
93
+ 'password' => new_password,
94
+ 'token' => token,
95
+ }))
96
+ end
97
+
98
+ # Clears the contact's password.
99
+ # @return [self] the updated contact.
100
+ # @api public
101
+ def clear_password
102
+ load_attributes(Core::RestApi.instance.put("#{path}/clear_password", {}))
103
+ end
104
+
105
+ # Sends a password token by e-mail to this contact.
106
+ #
107
+ # Put a link to the project web app into the +password_request_email_body+ template.
108
+ # The link should contain the +?token=...+ parameter, e.g.:
109
+ #
110
+ # <tt>https://example.com/user/set_password?token={{password_request_token}}</tt>
111
+ #
112
+ # The web app can then pass the token to {Contact.set_password_by_token}.
113
+ # @return [void]
114
+ # @api public
115
+ def send_password_token_email
116
+ Core::RestApi.instance.post("#{path}/send_password_token_email", {})
117
+ end
118
+
119
+ # @!endgroup
120
+ end
121
+ end
@@ -0,0 +1,122 @@
1
+ module JustRelate; module Core
2
+ # +AttachmentStore+ represents an attachment of an {Activity::Comment activity comment}.
3
+ #
4
+ # To upload a file as an attachment, add it to +comment_attachments+. The SDK will automatically
5
+ # upload the content of the file.
6
+ #
7
+ # Note that this method of uploading an attachment using a browser will upload the file twice.
8
+ # It is first uploaded to the ruby application (e.g. Rails) which then uploads it to AWS S3.
9
+ #
10
+ # To upload the attachment directly to AWS S3 (i.e. bypassing the ruby application),
11
+ # please proceed as follows:
12
+ #
13
+ # 1. Request an upload permission
14
+ # ({JustRelate::Core::AttachmentStore.generate_upload_permission AttachmentStore.generate_upload_permission}).
15
+ # The response grants the client permission to upload a file to a given key on AWS S3.
16
+ # This permission is valid for one hour.
17
+ # 2. Upload the file to the URL (+permission.url+ or
18
+ # +permission.uri+), together with the fields (+permission.fields+) as parameters.
19
+ # AWS S3 itself then verifies the signature of these parameters prior to accepting the upload.
20
+ # 3. Attach the upload to a new activity comment by setting its +comment_attachments+ attribute
21
+ # to an array of upload IDs. The client may append filenames to the upload IDs for producing
22
+ # download URLs with proper filenames later on.
23
+ #
24
+ # The format of +comment_attachments+ is
25
+ # <tt>["upload_id/filename.ext", ...]</tt>,
26
+ # e.g. <tt>["e13f0d960feeb2b2903bd/screenshot.jpg"]</tt>.
27
+ # JustRelate in turn translates these upload IDs to attachment IDs.
28
+ # Syntactically they look the same. Upload IDs, however, are only temporary,
29
+ # whereas attachment IDs are permanent. If the client appended a filename to the upload ID,
30
+ # the attachment ID will contain this filename, too. Otherwise, the attachment ID ends
31
+ # with <tt>"/file"</tt>. Please note that JustRelate replaces filename characters other
32
+ # than <tt>a-zA-Z0-9.+-</tt> with a dash. Multiple dashes will be joined into a single dash.
33
+ # 4. Later, when downloading the attachment, pass the attachment ID to
34
+ # {JustRelate::Core::AttachmentStore.generate_download_url}.
35
+ # JustRelate returns a signed AWS S3 URL that remains valid for 5 minutes.
36
+ # @api public
37
+ class AttachmentStore
38
+ # +Permission+ holds all the pieces of information required to upload an {AttachmentStore attachment}.
39
+ # Generate a permission by calling {AttachmentStore.generate_upload_permission}.
40
+ # @api public
41
+ class Permission
42
+ # Returns the {http://www.ruby-doc.org/stdlib/libdoc/uri/rdoc/URI.html URI}
43
+ # for uploading the new attachment data.
44
+ # @return [URI]
45
+ # @api public
46
+ attr_reader :uri
47
+
48
+ # Returns the URL for uploading the new attachment data.
49
+ # @return [String]
50
+ # @api public
51
+ attr_reader :url
52
+
53
+ # Returns a hash of additional request parameters to be sent to the {#url}.
54
+ # @return [Hash{String => String}]
55
+ # @api public
56
+ attr_reader :fields
57
+
58
+ # Returns a temporary ID associated with this upload.
59
+ # Use this ID when setting the +comment_attachments+ attribute of an activity.
60
+ # @return [String]
61
+ # @api public
62
+ attr_reader :upload_id
63
+
64
+ def initialize(uri, url, fields, upload_id)
65
+ @uri, @url, @fields, @upload_id = uri, url, fields, upload_id
66
+ end
67
+ end
68
+
69
+ class << self
70
+
71
+ # Obtains the permission to upload a file manually.
72
+ # The permission is valid for a couple of minutes.
73
+ # Hence, it is recommended to have such permissions generated on demand.
74
+ # @return [Permission]
75
+ # @api public
76
+ def generate_upload_permission
77
+ perm = Core::RestApi.instance.post("attachment_store/generate_upload_permission", {})
78
+ uri = resolve_uri(perm["url"])
79
+ Permission.new(uri, uri.to_s, perm["fields"], perm["upload_id"])
80
+ end
81
+
82
+ # Generates a download URL for the given attachment.
83
+ # The URL is valid for a couple of minutes.
84
+ # Hence, it is recommended to have such URLs generated on demand.
85
+ # @param attachment_id [String] the ID of an attachment.
86
+ # @return [String]
87
+ # @api public
88
+ def generate_download_url(attachment_id)
89
+ response = Core::RestApi.instance.post("attachment_store/generate_download_url",
90
+ {'attachment_id' => attachment_id})
91
+ resolve_uri(response["url"]).to_s
92
+ end
93
+
94
+ # Uploads a file to S3.
95
+ # @param file [File] the file to be uploaded.
96
+ # @return [String] the upload ID. Add this ID to the +comment_attachments+ attribute
97
+ # of an activity.
98
+ def upload(file)
99
+ permission = generate_upload_permission
100
+
101
+ file_name = File.basename(file.path)
102
+ upload_io = UploadIO.new(file, 'application/octet-stream', file_name)
103
+ params = permission.fields.merge(file: upload_io)
104
+ request = Net::HTTP::Post::Multipart.new(permission.uri, params)
105
+
106
+ response = Core::ConnectionManager.new(permission.uri).request(request)
107
+
108
+ if response.code.starts_with?('2')
109
+ [permission.upload_id, file_name].compact.join('/')
110
+ else
111
+ raise Errors::ServerError, "File upload failed with code #{response.code}"
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def resolve_uri(url)
118
+ Core::RestApi.instance.resolve_uri(url)
119
+ end
120
+ end
121
+ end
122
+ end; end
@@ -0,0 +1,68 @@
1
+ module JustRelate; module Core
2
+ # +BasicResource+ is the base class of all JustRelate resources.
3
+ # @api public
4
+ class BasicResource
5
+ include Mixins::AttributeProvider
6
+
7
+ def self.base_type
8
+ name.split(/::/).last
9
+ end
10
+
11
+ def self.resource_name
12
+ base_type.underscore
13
+ end
14
+
15
+ def self.path
16
+ resource_name.pluralize
17
+ end
18
+
19
+ # Returns the ID of this item.
20
+ # @return [String]
21
+ # @api public
22
+ def id
23
+ self['id']
24
+ end
25
+
26
+ def path
27
+ [self.class.path, id].compact.join('/')
28
+ end
29
+
30
+ # Returns the type object of this item.
31
+ # @return [JustRelate::Type]
32
+ # @api public
33
+ def type
34
+ ::JustRelate::Type.find(type_id)
35
+ end
36
+
37
+ # Reloads the attributes of this item from the remote web service.
38
+ # @example
39
+ # contact.locality
40
+ # # => 'Bergen'
41
+ #
42
+ # # Assume this contact has been modified concurrently.
43
+ #
44
+ # contact.reload
45
+ # # => JustRelate::Contact
46
+ #
47
+ # contact.locality
48
+ # # => 'Oslo'
49
+ # @return [self] the reloaded item.
50
+ # @api public
51
+ def reload
52
+ load_attributes(RestApi.instance.get(path))
53
+ end
54
+
55
+ def eql?(other)
56
+ other.equal?(self) || other.instance_of?(self.class) && other.id == id
57
+ end
58
+
59
+ alias_method :==, :eql?
60
+ delegate :hash, to: :id
61
+
62
+ private
63
+
64
+ def if_match_header
65
+ {'If-Match' => self['version']}
66
+ end
67
+ end
68
+ end; end
@@ -0,0 +1,65 @@
1
+ module JustRelate; module Core
2
+ # +Configuration+ is yielded by {JustRelate.configure}.
3
+ # It lets you set the credentials for accessing the API.
4
+ # The +tenant+, +login+, and +api_key+ attributes must be provided.
5
+ # @api public
6
+ class Configuration
7
+ attr_reader :api_key
8
+ attr_reader :login
9
+ attr_reader :tenant
10
+
11
+ attr_accessor :endpoint
12
+
13
+ # @param value [String]
14
+ # @return [void]
15
+ # @api public
16
+ attr_writer :api_key
17
+
18
+ # @param value [String]
19
+ # @return [void]
20
+ # @api public
21
+ attr_writer :login
22
+
23
+ # @param value [String]
24
+ # @return [void]
25
+ # @api public
26
+ attr_writer :tenant
27
+
28
+ def endpoint_uri
29
+ if endpoint.present?
30
+ url = endpoint
31
+ url = "https://#{url}" unless url.match(/^http/)
32
+ url += '/' unless url.end_with?('/')
33
+ URI.parse(url)
34
+ else
35
+ URI.parse("https://#{tenant}.crm.infopark.net/api2/")
36
+ end
37
+ end
38
+
39
+ def logger
40
+ JustRelate::Core::LogSubscriber.logger
41
+ end
42
+
43
+ # The {http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/Logger.html logger} of the
44
+ # JustRelate SDK. It logs request URLs according to the +:info+ level.
45
+ # Additionally, it logs request and response payloads according to the +:debug+ level.
46
+ # Password fields are filtered out.
47
+ # In a Rails environment, the logger defaults to +Rails.logger+. Otherwise, no logger is set.
48
+ # @param value [Logger]
49
+ # @return [void]
50
+ # @api public
51
+ # @!parse attr_writer :logger
52
+
53
+ def logger=(logger)
54
+ JustRelate::Core::LogSubscriber.logger = logger
55
+ end
56
+
57
+ def validate!
58
+ raise "Missing required configuration key: api_key" if api_key.blank?
59
+ raise "Missing required configuration key: login" if login.blank?
60
+ if tenant.blank? && endpoint.blank?
61
+ raise "Missing required configuration key: tenant"
62
+ end
63
+ end
64
+ end
65
+ end; end
@@ -0,0 +1,98 @@
1
+ module JustRelate; module Core
2
+ class ConnectionManager
3
+ SOCKET_ERRORS = [
4
+ EOFError,
5
+ Errno::ECONNABORTED,
6
+ Errno::ECONNREFUSED,
7
+ Errno::ECONNRESET,
8
+ Errno::EINVAL,
9
+ Errno::EPIPE,
10
+ Errno::ETIMEDOUT,
11
+ IOError,
12
+ SocketError,
13
+ Timeout::Error,
14
+ ].freeze
15
+
16
+ DEFAULT_TIMEOUT = 10.freeze
17
+
18
+ attr_reader :uri
19
+ attr_reader :ca_file
20
+ attr_reader :cert_store
21
+
22
+ def initialize(uri)
23
+ @uri = uri
24
+ @ca_file = File.expand_path('../../../../config/ca-bundle.crt', __FILE__)
25
+ @cert_store = OpenSSL::X509::Store.new.tap do |store|
26
+ store.set_default_paths
27
+ store.add_file(@ca_file)
28
+ end
29
+ @connection = nil
30
+ end
31
+
32
+ def request(request, timeout=DEFAULT_TIMEOUT)
33
+ request['User-Agent'] = user_agent
34
+ ensure_started(timeout)
35
+
36
+ begin
37
+ @connection.request(request)
38
+ rescue *SOCKET_ERRORS => e
39
+ ensure_finished
40
+ raise Errors::NetworkError.new(e.message, e)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def ensure_started(timeout)
47
+ if @connection && @connection.started?
48
+ configure_timeout(@connection, timeout)
49
+ else
50
+ conn = Net::HTTP.new(uri.host, uri.port)
51
+ if uri.scheme == 'https'
52
+ conn.use_ssl = true
53
+ conn.verify_mode = OpenSSL::SSL::VERIFY_PEER
54
+ conn.cert_store = @cert_store
55
+ end
56
+ configure_timeout(conn, timeout)
57
+ retry_twice_on_socket_error do |attempt|
58
+ ActiveSupport::Notifications.instrument("establish_connection.justrelate") do |msg|
59
+ msg[:attempt] = attempt
60
+ conn.start
61
+ end
62
+ end
63
+ @connection = conn
64
+ end
65
+ end
66
+
67
+ def ensure_finished
68
+ @connection.finish if @connection && @connection.started?
69
+ @connection = nil
70
+ end
71
+
72
+ def retry_twice_on_socket_error
73
+ attempt = 1
74
+ begin
75
+ yield attempt
76
+ rescue *SOCKET_ERRORS => e
77
+ raise Errors::NetworkError.new(e.message, e) if attempt > 2
78
+ attempt += 1
79
+ retry
80
+ end
81
+ end
82
+
83
+ def configure_timeout(connection, timeout)
84
+ connection.open_timeout = timeout
85
+ connection.read_timeout = timeout
86
+ connection.ssl_timeout = timeout
87
+ end
88
+
89
+ def user_agent
90
+ @user_agent ||= (
91
+ gem_info = Gem.loaded_specs["justrelate_sdk"]
92
+ if gem_info
93
+ "#{gem_info.name}-#{gem_info.version}"
94
+ end
95
+ )
96
+ end
97
+ end
98
+ end; end