contextio 0.5.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|