contextio 0.5.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.document +4 -0
  2. data/.gitignore +8 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -0
  5. data/ChangeLog.md +5 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.md +20 -0
  8. data/README.md +62 -22
  9. data/Rakefile +46 -36
  10. data/contextio.gemspec +30 -0
  11. data/lib/contextio.rb +69 -583
  12. data/lib/contextio/account.rb +132 -0
  13. data/lib/contextio/account_collection.rb +57 -0
  14. data/lib/contextio/account_sync_data.rb +22 -0
  15. data/lib/contextio/api.rb +162 -0
  16. data/lib/contextio/api/association_helpers.rb +17 -0
  17. data/lib/contextio/api/resource.rb +230 -0
  18. data/lib/contextio/api/resource_collection.rb +174 -0
  19. data/lib/contextio/api/url_builder.rb +153 -0
  20. data/lib/contextio/body_part.rb +45 -0
  21. data/lib/contextio/body_part_collection.rb +13 -0
  22. data/lib/contextio/connect_token.rb +57 -0
  23. data/lib/contextio/connect_token_collection.rb +44 -0
  24. data/lib/contextio/contact.rb +43 -0
  25. data/lib/contextio/contact_collection.rb +21 -0
  26. data/lib/contextio/email_address.rb +53 -0
  27. data/lib/contextio/email_address_collection.rb +21 -0
  28. data/lib/contextio/email_settings.rb +146 -0
  29. data/lib/contextio/file.rb +92 -0
  30. data/lib/contextio/file_collection.rb +13 -0
  31. data/lib/contextio/folder.rb +56 -0
  32. data/lib/contextio/folder_collection.rb +18 -0
  33. data/lib/contextio/folder_sync_data.rb +32 -0
  34. data/lib/contextio/message.rb +96 -0
  35. data/lib/contextio/message_collection.rb +35 -0
  36. data/lib/contextio/oauth_provider.rb +29 -0
  37. data/lib/contextio/oauth_provider_collection.rb +46 -0
  38. data/lib/contextio/source.rb +55 -0
  39. data/lib/contextio/source_collection.rb +41 -0
  40. data/lib/contextio/source_sync_data.rb +23 -0
  41. data/lib/contextio/thread.rb +15 -0
  42. data/lib/contextio/thread_collection.rb +25 -0
  43. data/lib/contextio/version.rb +11 -0
  44. data/lib/contextio/webhook.rb +39 -0
  45. data/lib/contextio/webhook_collection.rb +26 -0
  46. data/spec/config.yml.example +3 -0
  47. data/spec/contextio/account_collection_spec.rb +78 -0
  48. data/spec/contextio/account_spec.rb +52 -0
  49. data/spec/contextio/api/association_helpers_spec.rb +28 -0
  50. data/spec/contextio/api/resource_collection_spec.rb +286 -0
  51. data/spec/contextio/api/resource_spec.rb +467 -0
  52. data/spec/contextio/api/url_builder_spec.rb +78 -0
  53. data/spec/contextio/api_spec.rb +123 -0
  54. data/spec/contextio/connect_token_collection_spec.rb +74 -0
  55. data/spec/contextio/connect_token_spec.rb +58 -0
  56. data/spec/contextio/email_settings_spec.rb +112 -0
  57. data/spec/contextio/oauth_provider_collection_spec.rb +36 -0
  58. data/spec/contextio/oauth_provider_spec.rb +120 -0
  59. data/spec/contextio/source_collection_spec.rb +57 -0
  60. data/spec/contextio/source_spec.rb +52 -0
  61. data/spec/contextio/version_spec.rb +10 -0
  62. data/spec/contextio_spec.rb +64 -0
  63. data/spec/spec_helper.rb +17 -0
  64. metadata +234 -12
  65. 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