contextio 0.5.0 → 1.0.0
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.
- data/.document +4 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +5 -0
- data/Gemfile +3 -0
- data/LICENSE.md +20 -0
- data/README.md +62 -22
- data/Rakefile +46 -36
- data/contextio.gemspec +30 -0
- data/lib/contextio.rb +69 -583
- data/lib/contextio/account.rb +132 -0
- data/lib/contextio/account_collection.rb +57 -0
- data/lib/contextio/account_sync_data.rb +22 -0
- data/lib/contextio/api.rb +162 -0
- data/lib/contextio/api/association_helpers.rb +17 -0
- data/lib/contextio/api/resource.rb +230 -0
- data/lib/contextio/api/resource_collection.rb +174 -0
- data/lib/contextio/api/url_builder.rb +153 -0
- data/lib/contextio/body_part.rb +45 -0
- data/lib/contextio/body_part_collection.rb +13 -0
- data/lib/contextio/connect_token.rb +57 -0
- data/lib/contextio/connect_token_collection.rb +44 -0
- data/lib/contextio/contact.rb +43 -0
- data/lib/contextio/contact_collection.rb +21 -0
- data/lib/contextio/email_address.rb +53 -0
- data/lib/contextio/email_address_collection.rb +21 -0
- data/lib/contextio/email_settings.rb +146 -0
- data/lib/contextio/file.rb +92 -0
- data/lib/contextio/file_collection.rb +13 -0
- data/lib/contextio/folder.rb +56 -0
- data/lib/contextio/folder_collection.rb +18 -0
- data/lib/contextio/folder_sync_data.rb +32 -0
- data/lib/contextio/message.rb +96 -0
- data/lib/contextio/message_collection.rb +35 -0
- data/lib/contextio/oauth_provider.rb +29 -0
- data/lib/contextio/oauth_provider_collection.rb +46 -0
- data/lib/contextio/source.rb +55 -0
- data/lib/contextio/source_collection.rb +41 -0
- data/lib/contextio/source_sync_data.rb +23 -0
- data/lib/contextio/thread.rb +15 -0
- data/lib/contextio/thread_collection.rb +25 -0
- data/lib/contextio/version.rb +11 -0
- data/lib/contextio/webhook.rb +39 -0
- data/lib/contextio/webhook_collection.rb +26 -0
- data/spec/config.yml.example +3 -0
- data/spec/contextio/account_collection_spec.rb +78 -0
- data/spec/contextio/account_spec.rb +52 -0
- data/spec/contextio/api/association_helpers_spec.rb +28 -0
- data/spec/contextio/api/resource_collection_spec.rb +286 -0
- data/spec/contextio/api/resource_spec.rb +467 -0
- data/spec/contextio/api/url_builder_spec.rb +78 -0
- data/spec/contextio/api_spec.rb +123 -0
- data/spec/contextio/connect_token_collection_spec.rb +74 -0
- data/spec/contextio/connect_token_spec.rb +58 -0
- data/spec/contextio/email_settings_spec.rb +112 -0
- data/spec/contextio/oauth_provider_collection_spec.rb +36 -0
- data/spec/contextio/oauth_provider_spec.rb +120 -0
- data/spec/contextio/source_collection_spec.rb +57 -0
- data/spec/contextio/source_spec.rb +52 -0
- data/spec/contextio/version_spec.rb +10 -0
- data/spec/contextio_spec.rb +64 -0
- data/spec/spec_helper.rb +17 -0
- metadata +234 -12
- data/README.textile +0 -29
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'contextio/api/resource'
|
2
|
+
require 'contextio/api/association_helpers'
|
3
|
+
require 'contextio/account_sync_data'
|
4
|
+
|
5
|
+
class ContextIO
|
6
|
+
class Account
|
7
|
+
include ContextIO::API::Resource
|
8
|
+
|
9
|
+
self.primary_key = :id
|
10
|
+
self.association_name = :account
|
11
|
+
|
12
|
+
has_many :sources
|
13
|
+
has_many :connect_tokens
|
14
|
+
has_many :messages
|
15
|
+
has_many :threads
|
16
|
+
has_many :webhooks
|
17
|
+
has_many :contacts
|
18
|
+
has_many :files
|
19
|
+
|
20
|
+
# @!attribute [r] id
|
21
|
+
# @return [String] The id assigned to this account by Context.IO.
|
22
|
+
# @!attribute [r] username
|
23
|
+
# @return [String] The username assigned to this account by Context.IO.
|
24
|
+
# @!attribute [r] first_name
|
25
|
+
# @return [String] The account holder's first name.
|
26
|
+
# @!attribute [r] last_name
|
27
|
+
# @return [String] The account holder's last name.
|
28
|
+
lazy_attributes :id, :username, :created, :suspended, :first_name,
|
29
|
+
:last_name, :password_expired, :nb_messages, :nb_files
|
30
|
+
private :created, :suspended, :password_expired
|
31
|
+
|
32
|
+
def email_addresses
|
33
|
+
# It would be nice if the data returned from the API were formatted like
|
34
|
+
# other resources, but it isn't. So hacks.
|
35
|
+
@email_addresses = nil if @email_addresses.is_a?(Array)
|
36
|
+
|
37
|
+
return @email_addresses if @email_addresses
|
38
|
+
|
39
|
+
association_class = ContextIO::API::AssociationHelpers.class_for_association_name(:email_addresses)
|
40
|
+
|
41
|
+
reconstructed_email_hashes = api_attributes['email_addresses'].collect do |addy|
|
42
|
+
{'email' => addy}
|
43
|
+
end
|
44
|
+
|
45
|
+
@email_addresses = association_class.new(
|
46
|
+
api,
|
47
|
+
self.class.association_name => self,
|
48
|
+
attribute_hashes: reconstructed_email_hashes
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
# @!attribute [r] created_at
|
53
|
+
# @return [Time] The time this account was created (with Context.IO).
|
54
|
+
def created_at
|
55
|
+
@created_at ||= Time.at(created)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @!attribute [r] suspended_at
|
59
|
+
# @return [Time] The time this account was suspended.
|
60
|
+
def suspended_at
|
61
|
+
return @suspended_at if instance_variable_defined?(:@suspended_at)
|
62
|
+
|
63
|
+
@suspended_at = suspended == 0 ? nil : Time.at(suspended)
|
64
|
+
|
65
|
+
@suspended_at
|
66
|
+
end
|
67
|
+
|
68
|
+
# @!attribute [r] suspended?
|
69
|
+
# @return [Boolean] Whether this account is currently suspended.
|
70
|
+
def suspended?
|
71
|
+
!!suspended_at
|
72
|
+
end
|
73
|
+
|
74
|
+
# @!attribute [r] password_expired_at
|
75
|
+
# @return [Time] The time this account's password expired.
|
76
|
+
def password_expired_at
|
77
|
+
return @password_expired_at if instance_variable_defined?(:@password_expired_at)
|
78
|
+
|
79
|
+
@password_expired_at = password_expired == 0 ? nil : Time.at(password_expired)
|
80
|
+
|
81
|
+
@password_expired_at
|
82
|
+
end
|
83
|
+
|
84
|
+
# @!attribute [r] password_expired?
|
85
|
+
# @return [Boolean] Whether this account's password is expired.
|
86
|
+
def password_expired?
|
87
|
+
!!password_expired_at
|
88
|
+
end
|
89
|
+
|
90
|
+
# Updates the account.
|
91
|
+
#
|
92
|
+
# @param [Hash{String, Symbol => String}] options You can update first_name
|
93
|
+
# or last_name (or both).
|
94
|
+
def update(options={})
|
95
|
+
first_name = options[:first_name] || options['first_name']
|
96
|
+
last_name = options[:last_name] || options['last_name']
|
97
|
+
|
98
|
+
attrs = {}
|
99
|
+
attrs[:first_name] = first_name if first_name
|
100
|
+
attrs[:last_name] = last_name if last_name
|
101
|
+
|
102
|
+
return nil if attrs.empty?
|
103
|
+
|
104
|
+
it_worked = api.request(:post, resource_url, attrs)['success']
|
105
|
+
|
106
|
+
if it_worked
|
107
|
+
@first_name = first_name || @first_name
|
108
|
+
@last_name = last_name || @last_name
|
109
|
+
end
|
110
|
+
|
111
|
+
it_worked
|
112
|
+
end
|
113
|
+
|
114
|
+
def sync_data
|
115
|
+
return @sync_data if @sync_data
|
116
|
+
|
117
|
+
sync_hashes = api.request(:get, "#{resource_url}/sync")
|
118
|
+
|
119
|
+
@sync_data = ContextIO::AccountSyncData.new(sync_hashes)
|
120
|
+
|
121
|
+
return @sync_data
|
122
|
+
end
|
123
|
+
|
124
|
+
def sync!
|
125
|
+
api.request(:post, "#{resource_url}/sync")['success']
|
126
|
+
end
|
127
|
+
|
128
|
+
def delete
|
129
|
+
api.request(:delete, resource_url)['success']
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative 'api/resource_collection'
|
2
|
+
require_relative 'account'
|
3
|
+
|
4
|
+
class ContextIO
|
5
|
+
# Represents a collection of email accounts for your Context.IO account. You
|
6
|
+
# can use this to add a new one to your account, iterate over them, or fetch a
|
7
|
+
# specific one.
|
8
|
+
#
|
9
|
+
# You can also limit which accounts belongin the collection using the `where`
|
10
|
+
# method. Valid keys are: email, status, status_ok, limit and offset. See
|
11
|
+
# [the Context.IO documentation](http://context.io/docs/2.0/accounts#get) for
|
12
|
+
# more explanation of what each key means.
|
13
|
+
#
|
14
|
+
# @example You can iterate over them with `each`:
|
15
|
+
# contextio.accounts.each do |accounts|
|
16
|
+
# puts account.email_addresses
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @example You can lazily access a specific one with square brackets:
|
20
|
+
# account = contextio.accounts['some id']
|
21
|
+
#
|
22
|
+
# @example Lazily limit based on a hash of criteria with `where`:
|
23
|
+
# disabled_accounts = contextio.accounts.where(status: 'DISABLED')
|
24
|
+
class AccountCollection
|
25
|
+
include ContextIO::API::ResourceCollection
|
26
|
+
|
27
|
+
self.resource_class = ContextIO::Account
|
28
|
+
self.association_name = :accounts
|
29
|
+
|
30
|
+
# Creates a new email account for your Context.IO account.
|
31
|
+
#
|
32
|
+
# @param [Hash{String, Symbol => String}] options Information you can
|
33
|
+
# provide at creation: email, first_name and/or last_name. If the
|
34
|
+
# collection isn't already limited by email, then you must provide it.
|
35
|
+
#
|
36
|
+
# @return [Account] A new email account instance based on the data you
|
37
|
+
# input.
|
38
|
+
def create(options={})
|
39
|
+
email = options.delete(:email) || options.delete('email') ||
|
40
|
+
where_constraints[:email] || where_constraints['email']
|
41
|
+
|
42
|
+
if email.nil?
|
43
|
+
raise ArgumentError, "You must provide an email for new Accounts."
|
44
|
+
end
|
45
|
+
|
46
|
+
result_hash = api.request(
|
47
|
+
:post,
|
48
|
+
resource_url,
|
49
|
+
options.merge(email: email)
|
50
|
+
)
|
51
|
+
|
52
|
+
result_hash.delete('success')
|
53
|
+
|
54
|
+
resource_class.new(api, result_hash)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'source_sync_data'
|
2
|
+
|
3
|
+
class ContextIO
|
4
|
+
class AccountSyncData
|
5
|
+
attr_reader :source_labels, :sources
|
6
|
+
|
7
|
+
def initialize(source_hash)
|
8
|
+
@source_hash = source_hash
|
9
|
+
@source_labels = source_hash.keys
|
10
|
+
|
11
|
+
@sources = source_hash.collect do |source_label, folder_hash|
|
12
|
+
ContextIO::SourceSyncData.new(source_label, folder_hash)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def source_hash
|
20
|
+
@source_hash
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'oauth'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require 'contextio/api/url_builder'
|
6
|
+
|
7
|
+
class ContextIO
|
8
|
+
# **For internal use only.** Users of this gem should not be using this
|
9
|
+
# directly. Represents the handle on the Context.IO API. It handles the
|
10
|
+
# user's OAuth credentials with Context.IO and signing requests, etc.
|
11
|
+
class API
|
12
|
+
# For differentiating API errors from other errors that might happen during
|
13
|
+
# requests.
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
# @private
|
17
|
+
VERSION = '2.0'
|
18
|
+
|
19
|
+
# @return [String] The version of the Context.IO API this version of the
|
20
|
+
# gem is intended for use with.
|
21
|
+
def self.version
|
22
|
+
VERSION
|
23
|
+
end
|
24
|
+
|
25
|
+
# @private
|
26
|
+
BASE_URL = 'https://api.context.io'
|
27
|
+
|
28
|
+
# @return [String] The base URL the API is served from.
|
29
|
+
def self.base_url
|
30
|
+
BASE_URL
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param [Object] resource The resource you want the URL for.
|
34
|
+
#
|
35
|
+
# @return [String] The URL for the resource in the API.
|
36
|
+
def self.url_for(resource)
|
37
|
+
ContextIO::API::URLBuilder.url_for(resource)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param [Object] resource The resource you want the URL for.
|
41
|
+
#
|
42
|
+
# @return [String] The URL for the resource in the API.
|
43
|
+
def url_for(resource)
|
44
|
+
ContextIO::API.url_for(resource)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.user_agent_string
|
48
|
+
"contextio-ruby-#{ContextIO.version}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def user_agent_string
|
52
|
+
self.class.user_agent_string
|
53
|
+
end
|
54
|
+
|
55
|
+
# @!attribute [r] key
|
56
|
+
# @return [String] The OAuth key for the user's Context.IO account.
|
57
|
+
# @!attribute [r] secret
|
58
|
+
# @return [String] The OAuth secret for the user's Context.IO account.
|
59
|
+
attr_reader :key, :secret
|
60
|
+
|
61
|
+
# @param [String] key The user's OAuth key for their Context.IO account.
|
62
|
+
# @param [String] secret The user's OAuth secret for their Context.IO account.
|
63
|
+
def initialize(key, secret)
|
64
|
+
@key = key
|
65
|
+
@secret = secret
|
66
|
+
end
|
67
|
+
|
68
|
+
# Generates the path for a resource_path and params hash for use with the API.
|
69
|
+
#
|
70
|
+
# @param [String] resource_path The resource_path or full resource URL for
|
71
|
+
# the resource being acted on.
|
72
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
|
73
|
+
# A Hash of the query parameters for the action represented by this path.
|
74
|
+
def path(resource_path, params = {})
|
75
|
+
"/#{API.version}/#{API.strip_resource_path(resource_path)}#{API.hash_to_url_params(params)}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Makes a request against the Context.IO API.
|
79
|
+
#
|
80
|
+
# @param [String, Symbol] method The HTTP verb for the request (lower case).
|
81
|
+
# @param [String] resource_path The path to the resource in question.
|
82
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
|
83
|
+
# A Hash of the query parameters for the action represented by this
|
84
|
+
# request.
|
85
|
+
#
|
86
|
+
# @raise [API::Error] if the response code isn't in the 200 or 300 range.
|
87
|
+
def request(method, resource_path, params = {})
|
88
|
+
response = token.send(method, path(resource_path, params), 'Accept' => 'application/json', 'User-Agent' => user_agent_string)
|
89
|
+
body = response.body
|
90
|
+
|
91
|
+
results = JSON.parse(body) unless response.body.empty?
|
92
|
+
|
93
|
+
if response.code =~ /[45]\d\d/
|
94
|
+
if results.is_a?(Hash) && results['type'] == 'error'
|
95
|
+
message = results['value']
|
96
|
+
else
|
97
|
+
message = response.message
|
98
|
+
end
|
99
|
+
|
100
|
+
raise API::Error, message
|
101
|
+
end
|
102
|
+
|
103
|
+
results
|
104
|
+
end
|
105
|
+
|
106
|
+
def raw_request(method, resource_path, params={})
|
107
|
+
response = token.send(method, path(resource_path, params), 'User-Agent' => user_agent_string)
|
108
|
+
|
109
|
+
if response.code =~ /[45]\d\d/
|
110
|
+
raise API::Error, response.message
|
111
|
+
end
|
112
|
+
|
113
|
+
response.body
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# So that we can accept full URLs, this strips the domain and version number
|
119
|
+
# out and returns just the resource path.
|
120
|
+
#
|
121
|
+
# @param [#to_s] resource_path The full URL or path for a resource.
|
122
|
+
#
|
123
|
+
# @return [String] The resource path.
|
124
|
+
def self.strip_resource_path(resource_path)
|
125
|
+
resource_path.to_s.gsub("#{base_url}/#{version}/", '')
|
126
|
+
end
|
127
|
+
|
128
|
+
# Context.IO's API expects query parameters that are arrays to be comma
|
129
|
+
# separated, rather than submitted more than once. This munges those arrays
|
130
|
+
# and then URL-encodes the whole thing into a query string.
|
131
|
+
#
|
132
|
+
# @param [{String, Symbol => String, Symbol, Array<String, Symbol>}] params
|
133
|
+
# A Hash of the query parameters.
|
134
|
+
#
|
135
|
+
# @return [String] A URL-encoded version of the query parameters.
|
136
|
+
def self.hash_to_url_params(params = {})
|
137
|
+
return '' if params.empty?
|
138
|
+
|
139
|
+
params = params.inject({}) do |memo, (k, v)|
|
140
|
+
memo[k] = Array(v).join(',')
|
141
|
+
|
142
|
+
memo
|
143
|
+
end
|
144
|
+
|
145
|
+
"?#{URI.encode_www_form(params)}"
|
146
|
+
end
|
147
|
+
|
148
|
+
# @!attribute [r] consumer
|
149
|
+
# @return [OAuth::Consumer] An Oauth consumer object for credentials
|
150
|
+
# purposes.
|
151
|
+
def consumer
|
152
|
+
@consumer ||= OAuth::Consumer.new(key, secret, site: API.base_url)
|
153
|
+
end
|
154
|
+
|
155
|
+
# @!attribute [r] token
|
156
|
+
# @return [Oauth::AccessToken] An Oauth token object for credentials
|
157
|
+
# purposes.
|
158
|
+
def token
|
159
|
+
@token ||= OAuth::AccessToken.new(consumer)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class ContextIO
|
2
|
+
class 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
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require_relative 'association_helpers'
|
2
|
+
|
3
|
+
class ContextIO
|
4
|
+
class API
|
5
|
+
# When `include`d into a class, this module provides some helper methods for
|
6
|
+
# various things a singular resource will need or find useful.
|
7
|
+
module Resource
|
8
|
+
# (see ContextIO#api)
|
9
|
+
attr_reader :api
|
10
|
+
|
11
|
+
# @private
|
12
|
+
#
|
13
|
+
# For internal use only. Users of this gem shouldn't be calling this
|
14
|
+
# directly.
|
15
|
+
#
|
16
|
+
# @param [API] api A handle on the Context.IO API.
|
17
|
+
# @param [Hash{String, Symbol => String, Numeric, Boolean}] options A Hash
|
18
|
+
# of attributes describing the resource.
|
19
|
+
def initialize(api, options = {})
|
20
|
+
validate_options(options)
|
21
|
+
|
22
|
+
@api = api
|
23
|
+
|
24
|
+
options.each do |key, value|
|
25
|
+
key = key.to_s.gsub('-', '_')
|
26
|
+
|
27
|
+
if self.class.associations.include?(key.to_sym) && value.is_a?(Array)
|
28
|
+
association_class = ContextIO::API::AssociationHelpers.class_for_association_name(key.to_sym)
|
29
|
+
|
30
|
+
value = association_class.new(api, self.class.association_name => self, attribute_hashes: value)
|
31
|
+
end
|
32
|
+
|
33
|
+
instance_variable_set("@#{key}", value)
|
34
|
+
|
35
|
+
unless self.respond_to?(key)
|
36
|
+
define_singleton_method(key) do
|
37
|
+
instance_variable_get("@#{key}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @!attribute [r] resource_url
|
44
|
+
# @return [String] The URL that will fetch attributes from the API.
|
45
|
+
def resource_url
|
46
|
+
@resource_url ||= api.url_for(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Deletes the resource.
|
50
|
+
#
|
51
|
+
# @return [Boolean] Whether the deletion worked or not.
|
52
|
+
def delete
|
53
|
+
api.request(:delete, resource_url)['success']
|
54
|
+
end
|
55
|
+
|
56
|
+
# @!attribute [r] api_attributes
|
57
|
+
# @return [{String => Numeric, String, Hash, Array, Boolean}] The
|
58
|
+
# attributes returned from the API as a Hash. If it hasn't been
|
59
|
+
# populated, it will ask the API and populate it.
|
60
|
+
def api_attributes
|
61
|
+
@api_attributes ||= fetch_attributes
|
62
|
+
end
|
63
|
+
|
64
|
+
# @!attribute [r] primary_key
|
65
|
+
# @return [String, Symbol] The name of the key used to build the resource
|
66
|
+
# URL.
|
67
|
+
def primary_key
|
68
|
+
self.class.primary_key
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Make sure a Resource has the declarative syntax handy.
|
74
|
+
def self.included(other_mod)
|
75
|
+
other_mod.extend(DeclarativeClassSyntax)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Raises ArgumentError unless the primary key or the resource URL is
|
79
|
+
# supplied. Use this to ensure that the initializer has or can build the
|
80
|
+
# right URL to fetch its self.
|
81
|
+
#
|
82
|
+
# @param [Hash] options_hash The hash of options to validate.
|
83
|
+
def validate_options(options_hash)
|
84
|
+
required_keys = ['resource_url', :resource_url]
|
85
|
+
|
86
|
+
unless self.primary_key.nil?
|
87
|
+
required_keys << primary_key.to_s
|
88
|
+
required_keys << primary_key.to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
if (options_hash.keys & required_keys).empty?
|
92
|
+
raise ArgumentError, "Required option missing. Make sure you have either resource_url or #{primary_key}."
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Fetches attributes from the API for the resource. Relies on having a
|
97
|
+
# handle on a `ContextIO::API` via an `api` method and on a `resource_url`
|
98
|
+
# method that returns the path for the resource.
|
99
|
+
#
|
100
|
+
# Defines getter methods for any attributes that come back and don't
|
101
|
+
# already have them. This way, if the API expands, the gem will still let
|
102
|
+
# users get attributes we didn't explicitly declare as lazy.
|
103
|
+
#
|
104
|
+
# @return [{String => Numeric, String, Hash, Array, Boolean}] The
|
105
|
+
# attributes returned from the API as a Hash. If it hasn't been
|
106
|
+
# populated, it will ask the API and populate it.
|
107
|
+
def fetch_attributes
|
108
|
+
api.request(:get, resource_url).inject({}) do |memo, (key, value)|
|
109
|
+
key = key.to_s.gsub('-', '_')
|
110
|
+
|
111
|
+
unless respond_to?(key)
|
112
|
+
self.define_singleton_method(key) do
|
113
|
+
value
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
memo[key] = value
|
118
|
+
|
119
|
+
memo
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# This module contains helper methods for `API::Resource`s' class
|
124
|
+
# definitions. It gets `extend`ed into a class when `API::Resource` is
|
125
|
+
# `include`d.
|
126
|
+
module DeclarativeClassSyntax
|
127
|
+
def primary_key
|
128
|
+
@primary_key
|
129
|
+
end
|
130
|
+
|
131
|
+
# @!attribute [r] association_name
|
132
|
+
# @return [Symbol] The association name registered for this resource.
|
133
|
+
def association_name
|
134
|
+
@association_name
|
135
|
+
end
|
136
|
+
|
137
|
+
# @!attribute [r] associations
|
138
|
+
# @return [Array<String] An array of the belong_to associations for
|
139
|
+
# the collection
|
140
|
+
def associations
|
141
|
+
@associations ||= []
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# Declares the primary key used to build the resource URL. Consumed by
|
147
|
+
# `Resource#validate_options`.
|
148
|
+
#
|
149
|
+
# @param [String, Symbol] key Primary key name.
|
150
|
+
def primary_key=(key)
|
151
|
+
@primary_key = key
|
152
|
+
end
|
153
|
+
|
154
|
+
# Declares the association name for the resource.
|
155
|
+
#
|
156
|
+
# @param [String, Symbol] association_name The name.
|
157
|
+
def association_name=(association_name)
|
158
|
+
@association_name = association_name.to_sym
|
159
|
+
ContextIO::API::AssociationHelpers.register_resource(self, @association_name)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Declares a list of attributes to be lazily loaded from the API. Getter
|
163
|
+
# methods are written for each attribute. If the user asks for one and
|
164
|
+
# the object in question doesn't have it already, then it will look for
|
165
|
+
# it in the api_attributes Hash.
|
166
|
+
#
|
167
|
+
# @example an example of the generated methods
|
168
|
+
# def some_attribute
|
169
|
+
# return @some_attribute if instance_variable_defined?(@some_attribute)
|
170
|
+
# api_attributes["some_attribute"]
|
171
|
+
# end
|
172
|
+
#
|
173
|
+
# @param [Array<String, Symbol>] attributes Attribute names.
|
174
|
+
def lazy_attributes(*attributes)
|
175
|
+
attributes.each do |attribute_name|
|
176
|
+
define_method(attribute_name) do
|
177
|
+
return instance_variable_get("@#{attribute_name}") if instance_variable_defined?("@#{attribute_name}")
|
178
|
+
api_attributes[attribute_name.to_s]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Declares that this resource is related to a single instance of another
|
184
|
+
# resource. This related resource will be lazily created as it can be,
|
185
|
+
# but in some cases may cause an API call.
|
186
|
+
#
|
187
|
+
# @param [Symbol] association_name The name of the association for the
|
188
|
+
# class in question. Singular classes will have singular names
|
189
|
+
# registered. For instance, :message should reger to the Message
|
190
|
+
# resource.
|
191
|
+
def belongs_to(association_name)
|
192
|
+
define_method(association_name) do
|
193
|
+
if instance_variable_get("@#{association_name}")
|
194
|
+
instance_variable_get("@#{association_name}")
|
195
|
+
else
|
196
|
+
association_attrs = api_attributes[association_name.to_s]
|
197
|
+
association_class = ContextIO::API::AssociationHelpers.class_for_association_name(association_name)
|
198
|
+
|
199
|
+
if association_attrs && !association_attrs.empty?
|
200
|
+
instance_variable_set("@#{association_name}", association_class.new(api, association_attrs))
|
201
|
+
else
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
associations << association_name.to_sym
|
208
|
+
end
|
209
|
+
|
210
|
+
# Declares that this resource is related to a collection of another
|
211
|
+
# resource. These related resources will be lazily created as they can
|
212
|
+
# be, but in some cases may cause an API call.
|
213
|
+
#
|
214
|
+
# @param [Symbol] association_name The name of the association for the
|
215
|
+
# class in question. Collection classes will have plural names
|
216
|
+
# registered. For instance, :messages should reger to the
|
217
|
+
# MessageCollection resource.
|
218
|
+
def has_many(association_name)
|
219
|
+
define_method(association_name) do
|
220
|
+
association_class = ContextIO::API::AssociationHelpers.class_for_association_name(association_name)
|
221
|
+
|
222
|
+
instance_variable_get("@#{association_name}") || instance_variable_set("@#{association_name}", association_class.new(api, self.class.association_name => self, attribute_hashes: api_attributes[association_name.to_s]))
|
223
|
+
end
|
224
|
+
|
225
|
+
associations << association_name.to_sym
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|