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