nylas 3.2.0 → 4.0.0.rc2
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 +4 -4
- data/lib/nylas.rb +33 -380
- data/lib/nylas/api.rb +43 -0
- data/lib/nylas/collection.rb +110 -0
- data/lib/nylas/constraints.rb +46 -0
- data/lib/nylas/contact.rb +30 -0
- data/lib/nylas/current_account.rb +23 -0
- data/lib/nylas/email_address.rb +14 -0
- data/lib/nylas/errors.rb +36 -0
- data/lib/nylas/http_client.rb +135 -0
- data/lib/nylas/im_address.rb +14 -0
- data/lib/nylas/logging.rb +41 -0
- data/lib/nylas/model.rb +92 -0
- data/lib/nylas/model/attributable.rb +57 -0
- data/lib/nylas/model/attribute_definition.rb +19 -0
- data/lib/nylas/model/attributes.rb +47 -0
- data/lib/nylas/model/list_attribute_definition.rb +28 -0
- data/lib/nylas/nylas_date.rb +20 -0
- data/lib/nylas/phone_number.rb +14 -0
- data/lib/nylas/physical_address.rb +19 -0
- data/lib/nylas/registry.rb +37 -0
- data/lib/nylas/types.rb +83 -0
- data/lib/nylas/version.rb +3 -0
- data/lib/nylas/web_page.rb +14 -0
- metadata +144 -98
- data/lib/account.rb +0 -32
- data/lib/api_account.rb +0 -22
- data/lib/api_thread.rb +0 -85
- data/lib/calendar.rb +0 -16
- data/lib/contact.rb +0 -10
- data/lib/draft.rb +0 -50
- data/lib/event.rb +0 -47
- data/lib/expanded_message.rb +0 -20
- data/lib/file.rb +0 -40
- data/lib/folder.rb +0 -12
- data/lib/label.rb +0 -4
- data/lib/message.rb +0 -102
- data/lib/mixins.rb +0 -26
- data/lib/parameters.rb +0 -26
- data/lib/restful_model.rb +0 -82
- data/lib/restful_model_collection.rb +0 -160
- data/lib/time_attr_accessor.rb +0 -12
- data/lib/version.rb +0 -3
@@ -0,0 +1,110 @@
|
|
1
|
+
module Nylas
|
2
|
+
# An enumerable for working with index and search endpoints
|
3
|
+
class Collection
|
4
|
+
attr_accessor :model, :api, :constraints
|
5
|
+
def initialize(model:, api:, constraints: nil)
|
6
|
+
self.constraints = Constraints.from_constraints(constraints)
|
7
|
+
self.model = model
|
8
|
+
self.api = api
|
9
|
+
end
|
10
|
+
|
11
|
+
def new(**attributes)
|
12
|
+
model.from_hash(attributes, api: api)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create(**attributes)
|
16
|
+
model.raise_if_read_only
|
17
|
+
instance = model.from_hash(attributes, api: api)
|
18
|
+
instance.save
|
19
|
+
instance
|
20
|
+
end
|
21
|
+
|
22
|
+
def where(filters)
|
23
|
+
raise NotImplementedError, "#{model} does not support search" unless model.searchable?
|
24
|
+
self.class.new(model: model, api: api, constraints: constraints.merge(where: filters))
|
25
|
+
end
|
26
|
+
|
27
|
+
def count
|
28
|
+
self.class.new(model: model, api: api, constraints: constraints.merge(view: "count")).execute[:count]
|
29
|
+
end
|
30
|
+
|
31
|
+
def first
|
32
|
+
model.from_hash(execute.first, api: api)
|
33
|
+
end
|
34
|
+
|
35
|
+
def[](index)
|
36
|
+
model.from_hash(execute(index), api: api)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Iterates over a single page of results based upon current pagination settings
|
40
|
+
def each
|
41
|
+
return enum_for(:each) unless block_given?
|
42
|
+
execute.each do |result|
|
43
|
+
yield(model.from_hash(result, api: api))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_a
|
48
|
+
each.to_a
|
49
|
+
end
|
50
|
+
|
51
|
+
def map(&block)
|
52
|
+
each.map(&block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def limit(quantity)
|
56
|
+
self.class.new(model: model, api: api, constraints: constraints.merge(limit: quantity))
|
57
|
+
end
|
58
|
+
|
59
|
+
def offset(start)
|
60
|
+
self.class.new(model: model, api: api, constraints: constraints.merge(offset: start))
|
61
|
+
end
|
62
|
+
|
63
|
+
# Iterates over every result that meets the filters, retrieving a page at a time
|
64
|
+
def find_each
|
65
|
+
return enum_for(:find_each) unless block_given?
|
66
|
+
query = self
|
67
|
+
accumulated = 0
|
68
|
+
|
69
|
+
while query
|
70
|
+
results = query.each do |instance|
|
71
|
+
yield(instance)
|
72
|
+
end
|
73
|
+
|
74
|
+
accumulated += results.length
|
75
|
+
query = query.next_page(accumulated: accumulated, current_page: results)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def next_page(accumulated: nil, current_page: nil)
|
80
|
+
return nil unless more_pages?(accumulated, current_page)
|
81
|
+
self.class.new(model: model, api: api, constraints: constraints.next_page)
|
82
|
+
end
|
83
|
+
|
84
|
+
def more_pages?(accumulated, current_page)
|
85
|
+
return false if current_page.empty?
|
86
|
+
return false if constraints.limit && accumulated >= constraints.limit
|
87
|
+
return false if constraints.per_page && current_page.length < constraints.per_page
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Retrieves a record. Nylas doesn't support where filters on GET so this will not take into
|
92
|
+
# consideration other query constraints, such as where clauses.
|
93
|
+
def find(id)
|
94
|
+
instance = model.from_hash({ id: id }, api: api)
|
95
|
+
instance.reload
|
96
|
+
instance
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Hash] Specification for request to be passed to {API#execute}
|
100
|
+
def to_be_executed
|
101
|
+
{ method: :get, path: model.resources_path, query: constraints.to_query }
|
102
|
+
end
|
103
|
+
|
104
|
+
# Retrieves the data from the API for the particular constraints
|
105
|
+
# @return [Hash,Array]
|
106
|
+
def execute
|
107
|
+
api.execute(to_be_executed)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Nylas
|
2
|
+
# The constraints a particular GET request will include in their query params
|
3
|
+
class Constraints
|
4
|
+
attr_accessor :where, :limit, :offset, :view, :per_page
|
5
|
+
def initialize(where: {}, limit: nil, offset: 0, view: nil, per_page: 100)
|
6
|
+
self.where = where
|
7
|
+
self.limit = limit
|
8
|
+
self.offset = offset
|
9
|
+
self.view = view
|
10
|
+
self.per_page = per_page
|
11
|
+
end
|
12
|
+
|
13
|
+
def merge(where: {}, limit: nil, offset: nil, view: nil, per_page: nil)
|
14
|
+
Constraints.new(where: where.merge(where),
|
15
|
+
limit: limit || self.limit,
|
16
|
+
per_page: per_page || self.per_page,
|
17
|
+
offset: offset || self.offset,
|
18
|
+
view: view || self.view)
|
19
|
+
end
|
20
|
+
|
21
|
+
def next_page
|
22
|
+
merge(offset: offset + per_page)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_query
|
26
|
+
query = where.each_with_object({}) do |(name, value), result|
|
27
|
+
result[name] = value
|
28
|
+
end
|
29
|
+
query[:limit] = limit_for_query
|
30
|
+
query[:offset] = offset unless offset.nil?
|
31
|
+
query[:view] = view unless view.nil?
|
32
|
+
query
|
33
|
+
end
|
34
|
+
|
35
|
+
def limit_for_query
|
36
|
+
!limit.nil? && limit < per_page ? limit : per_page
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.from_constraints(constraints = Constraints.new)
|
40
|
+
return constraints if constraints.is_a?(Constraints)
|
41
|
+
return new(**constraints) if constraints.respond_to?(:key?)
|
42
|
+
return new if constraints.nil?
|
43
|
+
raise TypeError, "passed in constraints #{constraints} cannot be cast to a set of constraints"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Nylas
|
2
|
+
# ActiveModel compliant interface for interacting with the Contacts API
|
3
|
+
# @see https://docs.nylas.com/reference#contacts
|
4
|
+
class Contact
|
5
|
+
include Model
|
6
|
+
self.resources_path = "/contacts"
|
7
|
+
|
8
|
+
attribute :id, :string, exclude_when: %i(creating updating)
|
9
|
+
attribute :object, :string, default: "contact"
|
10
|
+
attribute :account_id, :string, exclude_when: %i(creating updating)
|
11
|
+
attribute :given_name, :string
|
12
|
+
attribute :middle_name, :string
|
13
|
+
attribute :surname, :string
|
14
|
+
attribute :birthday, :nylas_date
|
15
|
+
attribute :suffix, :string
|
16
|
+
attribute :nickname, :string
|
17
|
+
attribute :company_name, :string
|
18
|
+
attribute :job_title, :string
|
19
|
+
attribute :manager_name, :string
|
20
|
+
attribute :office_location, :string
|
21
|
+
attribute :notes, :string
|
22
|
+
attribute :web_page, :web_page
|
23
|
+
|
24
|
+
has_n_of_attribute :email_addresses, :email_address
|
25
|
+
has_n_of_attribute :im_addresses, :im_address
|
26
|
+
has_n_of_attribute :physical_addresses, :physical_address
|
27
|
+
has_n_of_attribute :phone_numbers, :phone_number
|
28
|
+
has_n_of_attribute :web_pages, :web_page
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Nylas
|
2
|
+
# Ruby representation of the Nylas /account API
|
3
|
+
# @see https://docs.nylas.com/reference#account
|
4
|
+
class CurrentAccount
|
5
|
+
include Model
|
6
|
+
|
7
|
+
self.read_only = true
|
8
|
+
self.searchable = false
|
9
|
+
self.collectionable = false
|
10
|
+
|
11
|
+
self.resources_path = "/account"
|
12
|
+
|
13
|
+
attribute :id, :string
|
14
|
+
attribute :object, :string, default: "account"
|
15
|
+
|
16
|
+
attribute :account_id, :string
|
17
|
+
attribute :email_address, :string
|
18
|
+
attribute :name, :string
|
19
|
+
attribute :organization_unit, :string
|
20
|
+
attribute :provider, :string
|
21
|
+
attribute :sync_state, :string
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Nylas
|
2
|
+
# Structure to represent the Email Address Schema
|
3
|
+
# @see https://docs.nylas.com/reference#contactsid
|
4
|
+
class EmailAddress
|
5
|
+
include Model::Attributable
|
6
|
+
attribute :type, :string
|
7
|
+
attribute :email, :string
|
8
|
+
end
|
9
|
+
|
10
|
+
# Serializes, Deserializes between {EmailAddress} objects and a {Hash}
|
11
|
+
class EmailAddressType < Types::HashType
|
12
|
+
casts_to EmailAddress
|
13
|
+
end
|
14
|
+
end
|
data/lib/nylas/errors.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Nylas
|
2
|
+
Error = Class.new(::StandardError)
|
3
|
+
|
4
|
+
# Indicates that a given method needs an access token to work.
|
5
|
+
class NoAuthToken < Error
|
6
|
+
def initialize(method_name)
|
7
|
+
super "No access token was provided and the #{method_name} method requires one"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
UnexpectedAccountAction = Class.new(Error)
|
12
|
+
UnexpectedResponse = Class.new(Error)
|
13
|
+
|
14
|
+
# Base class to inflate the standard errors returned from the Nylas API
|
15
|
+
class APIError < Error
|
16
|
+
attr_accessor :type
|
17
|
+
attr_accessor :message
|
18
|
+
attr_accessor :server_error
|
19
|
+
|
20
|
+
def initialize(type, message, server_error = nil)
|
21
|
+
super(message)
|
22
|
+
self.type = type
|
23
|
+
self.message = message
|
24
|
+
self.server_error = server_error
|
25
|
+
end
|
26
|
+
end
|
27
|
+
AccessDenied = Class.new(APIError)
|
28
|
+
ResourceNotFound = Class.new(APIError)
|
29
|
+
InvalidRequest = Class.new(APIError)
|
30
|
+
MessageRejected = Class.new(APIError)
|
31
|
+
SendingQuotaExceeded = Class.new(APIError)
|
32
|
+
ServiceUnavailable = Class.new(APIError)
|
33
|
+
BadGateway = Class.new(APIError)
|
34
|
+
InternalError = Class.new(APIError)
|
35
|
+
MailProviderError = Class.new(APIError)
|
36
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Nylas
|
2
|
+
# Plain HTTP client that can be used to interact with the Nylas API sans any type casting.
|
3
|
+
class HttpClient
|
4
|
+
HTTP_CODE_TO_EXCEPTIONS = {
|
5
|
+
400 => InvalidRequest,
|
6
|
+
402 => MessageRejected,
|
7
|
+
403 => AccessDenied,
|
8
|
+
404 => ResourceNotFound,
|
9
|
+
422 => MailProviderError,
|
10
|
+
429 => SendingQuotaExceeded,
|
11
|
+
500 => InternalError,
|
12
|
+
502 => BadGateway,
|
13
|
+
503 => ServiceUnavailable
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
include Logging
|
17
|
+
attr_accessor :api_server, :default_headers
|
18
|
+
attr_reader :access_token
|
19
|
+
attr_reader :app_id
|
20
|
+
attr_reader :app_secret
|
21
|
+
|
22
|
+
# @param app_id [String] Your application id from the Nylas Dashboard
|
23
|
+
# @param app_secret [String] Your application secret from the Nylas Dashboard
|
24
|
+
# @param access_token [String] (Optional) Your users access token.
|
25
|
+
# @param api_server [String] (Optional) Which Nylas API Server to connect to. Only change this if
|
26
|
+
# you're using a self-hosted Nylas instance.
|
27
|
+
# @param service_domain [String] (Optional) Host you are authenticating OAuth against.
|
28
|
+
# @return [Nylas::API]
|
29
|
+
def initialize(app_id:, app_secret:, access_token: nil, api_server: "https://api.nylas.com",
|
30
|
+
service_domain: "api.nylas.com")
|
31
|
+
unless api_server.include?("://")
|
32
|
+
raise "When overriding the Nylas API server address, you must include https://"
|
33
|
+
end
|
34
|
+
@api_server = api_server
|
35
|
+
@access_token = access_token
|
36
|
+
@app_secret = app_secret
|
37
|
+
@app_id = app_id
|
38
|
+
@service_domain = service_domain
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sends a request to the Nylas API and rai
|
42
|
+
# @param method [Symbol] HTTP method for the API call. Either :get, :post, :delete, or :patch
|
43
|
+
# @param url [String] (Optional, defaults to nil) - Full URL to access. Deprecated and will be removed in
|
44
|
+
# 5.0.
|
45
|
+
# @param path [String] (Optional, defaults to nil) - Relative path from the API Base. Preferred way to
|
46
|
+
# execute arbitrary or-not-yet-SDK-ified API commands.
|
47
|
+
# @param headers [Hash] (Optional, defaults to {}) - Additional HTTP headers to include in the payload.
|
48
|
+
# @param query [Hash] (Optional, defaults to {}) - Hash of names and values to include in the query
|
49
|
+
# section of the URI fragment
|
50
|
+
# @param payload [String,Hash] (Optional, defaults to nil) - Body to send with the request.
|
51
|
+
# @return [Array Hash Stringn]
|
52
|
+
# rubocop:disable Metrics/ParameterLists
|
53
|
+
def execute(method:, url: nil, path: nil, headers: {}, query: {}, payload: nil)
|
54
|
+
headers[:params] = query
|
55
|
+
url ||= url_for_path(path)
|
56
|
+
resulting_headers = default_headers.merge(headers)
|
57
|
+
rest_client_execute(method: method, url: url, payload: payload,
|
58
|
+
headers: resulting_headers) do |response, _request, result|
|
59
|
+
|
60
|
+
response = parse_response(response)
|
61
|
+
handle_failed_response(result: result, response: response)
|
62
|
+
response
|
63
|
+
end
|
64
|
+
end
|
65
|
+
# rubocop:enable Metrics/ParameterLists
|
66
|
+
inform_on :execute, level: :debug,
|
67
|
+
also_log: { result: true, values: %i(method url path headers query payload) }
|
68
|
+
|
69
|
+
# Syntactical sugar for making GET requests via the API.
|
70
|
+
# @see #execute
|
71
|
+
def get(path: nil, url: nil, headers: {}, query: {})
|
72
|
+
execute(method: :get, path: path, query: query, url: url, headers: headers)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Syntactical sugar for making POST requests via the API.
|
76
|
+
# @see #execute
|
77
|
+
def post(path: nil, url: nil, payload: nil, headers: {}, query: {})
|
78
|
+
execute(method: :post, path: path, url: url, headers: headers, query: query, payload: payload)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Syntactical sugar for making PUT requests via the API.
|
82
|
+
# @see #execute
|
83
|
+
def put(path: nil, url: nil, payload:, headers: {}, query: {})
|
84
|
+
execute(method: :put, path: path, url: url, headers: headers, query: query, payload: payload)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Syntactical sugar for making DELETE requests via the API.
|
88
|
+
# @see #execute
|
89
|
+
def delete(path: nil, url: nil, payload: nil, headers: {}, query: {})
|
90
|
+
execute(method: :delete, path: path, url: url, headers: headers, query: query, payload: payload)
|
91
|
+
end
|
92
|
+
# rubocop:enable Metrics/ParameterList
|
93
|
+
|
94
|
+
private def rest_client_execute(method:, url:, headers:, payload:, &block)
|
95
|
+
::RestClient::Request.execute(method: method, url: url, payload: payload,
|
96
|
+
headers: headers, &block)
|
97
|
+
end
|
98
|
+
inform_on :rest_client_execute, level: :debug,
|
99
|
+
also_log: { result: true, values: %i(method url headers payload) }
|
100
|
+
|
101
|
+
def default_headers
|
102
|
+
@default_headers ||= {
|
103
|
+
"X-Nylas-API-Wrapper" => "ruby",
|
104
|
+
"User-Agent" => "Nylas Ruby SDK #{Nylas::VERSION} - #{RUBY_VERSION}",
|
105
|
+
"Content-types" => "application/json"
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
private def parse_response(response)
|
110
|
+
response.is_a?(Enumerable) ? response : JSON.parse(response, symbolize_names: true)
|
111
|
+
rescue JSON::ParserError
|
112
|
+
response
|
113
|
+
end
|
114
|
+
|
115
|
+
private def url_for_path(path)
|
116
|
+
raise NoAuthToken if @access_token.nil? && (!@app_secret.nil? || !@app_id.nil?)
|
117
|
+
protocol, domain = @api_server.split("//")
|
118
|
+
"#{protocol}//#{@access_token}:@#{domain}#{path}"
|
119
|
+
end
|
120
|
+
|
121
|
+
private def handle_failed_response(result:, response:)
|
122
|
+
http_code = result.code.to_i
|
123
|
+
|
124
|
+
handle_anticipated_failure_mode(http_code: http_code, response: response)
|
125
|
+
raise UnexpectedResponse, result.msg if result.is_a?(Net::HTTPClientError)
|
126
|
+
end
|
127
|
+
|
128
|
+
private def handle_anticipated_failure_mode(http_code:, response:)
|
129
|
+
return if http_code == 200
|
130
|
+
return unless response.is_a?(Hash)
|
131
|
+
exception = HTTP_CODE_TO_EXCEPTIONS.fetch(http_code, APIError)
|
132
|
+
raise exception.new(response["type"], response["message"], response.fetch("server_error", nil))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Nylas
|
2
|
+
# Structure to represent the IM Address Schema
|
3
|
+
# @see https://docs.nylas.com/reference#contactsid
|
4
|
+
class IMAddress
|
5
|
+
include Model::Attributable
|
6
|
+
attribute :type, :string
|
7
|
+
attribute :im_address, :string
|
8
|
+
end
|
9
|
+
|
10
|
+
# Serializes, Deserializes between {IMAddress} objects and their JSON representation
|
11
|
+
class IMAddressType < Types::HashType
|
12
|
+
casts_to IMAddress
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# We are explicitely choosing to allow clients to use or not use informed at their discretion
|
2
|
+
# rubocop:disable Lint/HandleExceptions
|
3
|
+
begin
|
4
|
+
require "informed"
|
5
|
+
rescue LoadError
|
6
|
+
end
|
7
|
+
# rubocop:enable Lint/HandleExceptions
|
8
|
+
|
9
|
+
module Nylas
|
10
|
+
# Exposes a shared logger for debugging purposes
|
11
|
+
module Logging
|
12
|
+
def self.included(object)
|
13
|
+
if const_defined? :Informed
|
14
|
+
object.include Informed
|
15
|
+
Informed.logger = logger
|
16
|
+
else
|
17
|
+
object.extend NoOpInformOn
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.logger
|
22
|
+
return @logger if @logger
|
23
|
+
@logger = Logger.new(STDOUT)
|
24
|
+
@logger.level = level
|
25
|
+
@logger
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.level
|
29
|
+
Logger.const_get(ENV["NYLAS_LOG_LEVEL"] || :WARN)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.logger=(logger)
|
33
|
+
@logger = logger
|
34
|
+
end
|
35
|
+
|
36
|
+
# No op for inform_on if user does not have the informed gem installed.
|
37
|
+
module NoOpInformOn
|
38
|
+
def inform_on(method, level: :debug, also_log: {}); end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|