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