contextio-lite 0.0.2

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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9a4c95db7436871ec9123eb36072dd02e24f5078
4
+ data.tar.gz: 55b65431a9f3f5b517992e5ed3cde2ac2aa4198a
5
+ SHA512:
6
+ metadata.gz: 56066f305c8e1341b3f911f18a25bce82297c99c1b6697569704d954fae1503f9c57179e16a8cc54917d4924b6c0d1b2e133da7738b1786e71fd137989d42641
7
+ data.tar.gz: 4d245707e67caeac959882263e47dd5b121f7464bf01251b707259e4b463d8c2992bc402f6c776af395da4d1d6a690b17c14256a6910464ed6c6853e495b86ab
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in contextio-lite.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 javi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,114 @@
1
+ # Contextio::Lite
2
+
3
+ This is API client for the [Context.IO Lite](https://context.io/) version based on [Context.IO Ruby](https://github.com/contextio/contextio-ruby) for the 2.0 version.
4
+ It works in the same way as the official, calling first a user object and then working with the collections...
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'contextio-lite'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install contextio-lite
21
+
22
+ ## Usage
23
+
24
+ ### Get a client
25
+
26
+ ```ruby
27
+ require 'contextio/lite'
28
+
29
+ client = ContextIO::Lite.new(API_KEY, API_SECRET)
30
+ ```
31
+
32
+ ### Dealing with users
33
+
34
+ List all the registered lite users IDs
35
+
36
+ ```ruby
37
+ p client.users.map(&:id)
38
+ ```
39
+
40
+ CRUD
41
+
42
+ ```ruby
43
+ user = client.users.create email: 'bob@gmail.com', first_name: 'Bob', server: 'imap.gmail.com' username: 'bob', 'use_sll':true, 'port': 993, 'type': 'IMAP
44
+
45
+ user_id = user.id
46
+
47
+ client.users[user_id].update :hash_attrs
48
+
49
+ client.users[user_id].delete
50
+ ```
51
+
52
+ ### Retrieving messages
53
+
54
+ To access the emails, first you need to select an email_account and the folder containing your email
55
+
56
+ So if you want to list the user email accounts or folders you can do like this
57
+
58
+ ```ruby
59
+ p client[user_id].email_accounts.map(&:label)
60
+ p client[user_id].email_accounts[label].folders.map(&:name) # can access email_accounts by number => email_accounts[0]
61
+ ```
62
+
63
+ Listing all the email subjects inside a folder (limited by context IO to 100)
64
+ ```ruby
65
+ client[user_id].email_accounts[0].folders['INBOX'].messages do
66
+ p messages.subject
67
+ end
68
+ ```
69
+
70
+ And if you want to filter the emails
71
+
72
+ ```ruby
73
+ client[user_id].email_accounts[0].folder['INBOX'].messages.where(limit: 3)
74
+ ```
75
+
76
+ You also may want to access the content of one message
77
+
78
+ ```ruby
79
+ client[user_id].email_accounts[0].folder['INBOX'].messages['<message_id>'].body_plain # or body_html
80
+
81
+ client[user_id].email_accounts[0].folder['INBOX'].messages['<message_id>'].with(include_body:true).body_plain
82
+ ```
83
+
84
+ The first one calls https://api.context.io/lite/users/id/email_accounts/label/folders/folder/messages/message_id/body
85
+ and the second calls https://api.context.io/lite/users/id/email_accounts/label/folders/folder/messages/message_id?include_body=1
86
+ but they return the same.
87
+
88
+ And the above should works also with 'flags' or 'headers'.
89
+
90
+ ### Webhooks
91
+
92
+ One of the main feature that this version has and 2.0 doesn't is the real-time webhooks.
93
+ So in order to work with them we can do
94
+
95
+ ```ruby
96
+ client[user_id].webhooks.map(&:webhook_id) # listing all the webhooks an user has
97
+
98
+ client[user_id].create callback_url, failure_url, filter_folder_added: 'INBOX', include_body: true
99
+ ```
100
+
101
+ So it will call the callback_url every time there is a new message on the user INBOX folder, posting the message info and body included.
102
+
103
+
104
+ ## Contributing
105
+
106
+ 1. Fork it ( https://github.com/javijuol/contextio-lite/fork )
107
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
108
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
109
+ 4. Push to the branch (`git push origin my-new-feature`)
110
+ 5. Create a new Pull Request
111
+
112
+ ## Copyright
113
+
114
+ This gem is distributed under the MIT License. See LICENSE.md for details.
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require File.expand_path('../lib/contextio', __FILE__)
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'contextio-lite'
8
+ spec.version = ContextIO.version
9
+ spec.authors = ['Javier Juan']
10
+ spec.email = ['javier@promivia.com']
11
+ spec.summary = %q{Provides interface to Context.IO Lite API}
12
+ spec.description = %q{Short implementation of a client rest API for the Context.IO Lite API}
13
+ spec.homepage = 'https://github.com/javijuol/contextio-lite'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'debase', '~> 0'
24
+ spec.add_development_dependency 'faraday', '~> 0.9.1'
25
+ spec.add_development_dependency 'faraday_middleware', '~> 0.9.1'
26
+
27
+ end
@@ -0,0 +1,14 @@
1
+ module ContextIO
2
+ VERSION = '0.0.2'
3
+
4
+ # @private
5
+ # Handle for the `API` instance. For internal use only.
6
+ attr_reader :api
7
+
8
+ def self.version
9
+ VERSION
10
+ end
11
+
12
+ end
13
+
14
+ require_relative 'contextio/lite'
@@ -0,0 +1,213 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+
6
+ module ContextIO
7
+ module API
8
+ class AbstractAPI
9
+
10
+ # @private
11
+ BASE_URL = 'https://api.context.io'
12
+
13
+ # @return [String] The version of the Context.IO API this version of the
14
+ # gem is intended for use with.
15
+ def self.version
16
+ raise NotDefinedError, 'VERSION is not defined in your API subclassed model.' if self::VERSION.nil?
17
+ self::VERSION
18
+ end
19
+
20
+ # @return [String] The base URL the API is served from.
21
+ def self.base_url
22
+ BASE_URL
23
+ end
24
+
25
+ def self.user_agent_string
26
+ raise NotDefinedError, 'user_agent_string undefined in your API subclassed model.'
27
+ end
28
+
29
+ def user_agent_string
30
+ self.class.user_agent_string
31
+ end
32
+
33
+ attr_accessor :base_url, :version
34
+
35
+ # @!attribute [r] key
36
+ # @return [String] The OAuth key for the user's Context.IO account.
37
+ # @!attribute [r] secret
38
+ # @return [String] The OAuth secret for the user's Context.IO account.
39
+ # @!attribute [r] opts
40
+ # @return [Hash] opts Optional options for OAuth connections.
41
+ attr_reader :key, :secret, :opts
42
+
43
+ # @param [String] key The user's OAuth key for their Context.IO account.
44
+ # @param [String] secret The user's OAuth secret for their Context.IO account.
45
+ # @param [Hash] opts Optional options for OAuth connections. ie. :timeout and :open_timeout are supported
46
+ def initialize(key, secret, opts={})
47
+ @key = key
48
+ @secret = secret
49
+ @opts = opts || {}
50
+ @base_url = self.class.base_url
51
+ @version = self.class.version
52
+ end
53
+
54
+ # Generates the path for a resource_path and params hash for use with the API.
55
+ #
56
+ # @param [String] resource_path The resource_path or full resource URL for
57
+ # the resource being acted on.
58
+ # @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
59
+ # A Hash of the query parameters for the action represented by this path.
60
+ def path(resource_path, params = {})
61
+ "/#{version}/#{strip_resource_path(resource_path)}#{self.class.hash_to_url_params(params)}"
62
+ end
63
+
64
+ # Makes a request against the Context.IO API.
65
+ #
66
+ # @param [String, Symbol] method The HTTP verb for the request (lower case).
67
+ # @param [String] resource_path The path to the resource in question.
68
+ # @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
69
+ # A Hash of the query parameters for the action represented by this
70
+ # request.
71
+ #
72
+ # @raise [API::Error] if the response code isn't in the 200 or 300 range.
73
+ def request(method, resource_path, params = {})
74
+ response = oauth_request(method, resource_path, params, { 'Accept' => 'application/json' })
75
+
76
+ with_error_handling(response) do |response|
77
+ parse_json(response.body)
78
+ end
79
+ end
80
+
81
+ def raw_request(method, resource_path, params={})
82
+ response = oauth_request(method, resource_path, params)
83
+
84
+ with_error_handling(response) do |response|
85
+ response.body
86
+ end
87
+ end
88
+
89
+ protected
90
+
91
+ # Makes a request signed for OAuth, encoding parameters correctly, etc.
92
+ #
93
+ # @param [String, Symbol] method The HTTP verb for the request (lower case).
94
+ # @param [String] resource_path The path to the resource in question.
95
+ # @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
96
+ # A Hash of the query parameters for the action represented by this
97
+ # request.
98
+ # @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] headers
99
+ # A Hash of headers to be merged with the default headers for making
100
+ # requests.
101
+ #
102
+ # @return [Faraday::Response] The response object from the request.
103
+ def oauth_request(method, resource_path, params, headers=nil)
104
+ normalized_params = params.inject({}) do |normalized_params, (key, value)|
105
+ normalized_params[key.to_sym] = value
106
+ normalized_params
107
+ end
108
+
109
+ connection.send(method, path(resource_path), normalized_params, headers) do |request|
110
+ if request.method == :put
111
+ request.params = normalized_params
112
+ request.body = {}
113
+ end
114
+ end
115
+ end
116
+
117
+ # So that we can accept full URLs, this strips the domain and version number
118
+ # out and returns just the resource path.
119
+ #
120
+ # @param [#to_s] resource_path The full URL or path for a resource.
121
+ #
122
+ # @return [String] The resource path.
123
+ def strip_resource_path(resource_path)
124
+ resource_path.to_s.gsub("#{base_url}/#{version}/", '')
125
+ end
126
+
127
+ # Context.IO's API expects query parameters that are arrays to be comma
128
+ # separated, rather than submitted more than once. This munges those arrays
129
+ # and then URL-encodes the whole thing into a query string.
130
+ #
131
+ # @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
132
+ # A Hash of the query parameters.
133
+ #
134
+ # @return [String] A URL-encoded version of the query parameters.
135
+ def self.hash_to_url_params(params = {})
136
+ return '' if params.empty?
137
+
138
+ params = params.inject({}) do |memo, (k, v)|
139
+ memo[k] = Array(v).join(',')
140
+
141
+ memo
142
+ end
143
+
144
+ "?#{URI.encode_www_form(params)}"
145
+ end
146
+
147
+ # @!attribute [r] connection
148
+ # @return [Faraday::Connection] A handle on the Faraday connection object.
149
+ def connection
150
+ @connection ||= Faraday::Connection.new(base_url) do |faraday|
151
+ faraday.headers['User-Agent'] = user_agent_string
152
+
153
+ faraday.request :oauth, consumer_key: key, consumer_secret: secret
154
+ faraday.request :url_encoded
155
+
156
+ faraday.adapter Faraday.default_adapter
157
+ end
158
+ end
159
+
160
+ # Errors can come in a few shapes and we want to detect them and extract the
161
+ # useful information. If no errors are found, it calls the provided block
162
+ # and passes the response through.
163
+ #
164
+ # @param [Faraday::Response] response A response object from making a request to the
165
+ # API with Faraday.
166
+ #
167
+ # @raise [API::Error] if the response code isn't in the 200 or 300 range.
168
+ def with_error_handling(response, &block)
169
+ return block.call(response) if response.success?
170
+
171
+ parsed_body = parse_json(response.body)
172
+ message = determine_best_error_message(parsed_body) || "HTTP #{response.status} Error"
173
+
174
+ raise ContextIO::API::Error, message
175
+ end
176
+
177
+ # Parses JSON if there's valid JSON passed in.
178
+ #
179
+ # @param [String] document A string you suspect may be a JSON document.
180
+ #
181
+ # @return [Hash, Array, Nil] Either a parsed version of the JSON document or
182
+ # nil, if the document wasn't valid JSON.
183
+ def parse_json(document)
184
+ return JSON.parse(document.to_s)
185
+ rescue JSON::ParserError => e
186
+ return nil
187
+ end
188
+
189
+
190
+ # Given a parsed JSON body from an error response, figures out if it can
191
+ # pull useful information therefrom.
192
+ #
193
+ # @param [Hash] parsed_body A Hash parsed from a JSON document that may
194
+ # describe an error condition.
195
+ #
196
+ # @return [String, Nil] If it can, it will return a human-readable
197
+ # error-describing String. Otherwise, nil.
198
+ def determine_best_error_message(parsed_body)
199
+ return unless parsed_body.respond_to?(:[])
200
+
201
+ if parsed_body['type'] == 'error'
202
+ return parsed_body['value']
203
+ elsif parsed_body.has_key?('success') && !parsed_body['success']
204
+ return [parsed_body['feedback_code'], parsed_body['connectionLog']].compact.join("\n")
205
+ end
206
+ end
207
+ end
208
+
209
+ class NotDefinedError < StandardError; end
210
+ class Error < StandardError; end
211
+
212
+ end
213
+ end
@@ -0,0 +1,17 @@
1
+ module ContextIO
2
+ module API
3
+ module AssociationHelpers
4
+ def self.class_for_association_name(association_name)
5
+ associations[association_name]
6
+ end
7
+
8
+ def self.register_resource(klass, association_name)
9
+ associations[association_name] = klass
10
+ end
11
+
12
+ def self.associations
13
+ @associations ||= {}
14
+ end
15
+ end
16
+ end
17
+ end