justrelate_sdk 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +840 -0
- data/README.md +114 -0
- data/UPGRADE.md +509 -0
- data/config/ca-bundle.crt +4484 -0
- data/lib/justrelate/account.rb +17 -0
- data/lib/justrelate/activity.rb +143 -0
- data/lib/justrelate/collection.rb +41 -0
- data/lib/justrelate/contact.rb +121 -0
- data/lib/justrelate/core/attachment_store.rb +122 -0
- data/lib/justrelate/core/basic_resource.rb +68 -0
- data/lib/justrelate/core/configuration.rb +65 -0
- data/lib/justrelate/core/connection_manager.rb +98 -0
- data/lib/justrelate/core/item_enumerator.rb +61 -0
- data/lib/justrelate/core/log_subscriber.rb +41 -0
- data/lib/justrelate/core/mixins/attribute_provider.rb +135 -0
- data/lib/justrelate/core/mixins/change_loggable.rb +98 -0
- data/lib/justrelate/core/mixins/findable.rb +24 -0
- data/lib/justrelate/core/mixins/inspectable.rb +27 -0
- data/lib/justrelate/core/mixins/merge_and_deletable.rb +17 -0
- data/lib/justrelate/core/mixins/modifiable.rb +102 -0
- data/lib/justrelate/core/mixins/searchable.rb +88 -0
- data/lib/justrelate/core/mixins.rb +6 -0
- data/lib/justrelate/core/rest_api.rb +148 -0
- data/lib/justrelate/core/search_configurator.rb +207 -0
- data/lib/justrelate/core.rb +6 -0
- data/lib/justrelate/errors.rb +169 -0
- data/lib/justrelate/event.rb +17 -0
- data/lib/justrelate/event_contact.rb +16 -0
- data/lib/justrelate/mailing.rb +111 -0
- data/lib/justrelate/template_set.rb +81 -0
- data/lib/justrelate/type.rb +78 -0
- data/lib/justrelate.rb +149 -0
- metadata +147 -0
@@ -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
|