episodic-platform 0.9

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 (36) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +35 -0
  3. data/PostInstall.txt +7 -0
  4. data/README.rdoc +86 -0
  5. data/Rakefile +45 -0
  6. data/lib/episodic/platform.rb +82 -0
  7. data/lib/episodic/platform/analytics_methods.rb +190 -0
  8. data/lib/episodic/platform/base.rb +64 -0
  9. data/lib/episodic/platform/collection_response.rb +45 -0
  10. data/lib/episodic/platform/connection.rb +305 -0
  11. data/lib/episodic/platform/exceptions.rb +105 -0
  12. data/lib/episodic/platform/query_methods.rb +721 -0
  13. data/lib/episodic/platform/response.rb +94 -0
  14. data/lib/episodic/platform/write_methods.rb +446 -0
  15. data/script/console +10 -0
  16. data/script/destroy +14 -0
  17. data/script/generate +14 -0
  18. data/test/fixtures/1-0.mp4 +0 -0
  19. data/test/fixtures/create-episode-response-s3.xml +15 -0
  20. data/test/fixtures/create-episode-response.xml +3 -0
  21. data/test/fixtures/episodes-response.xml +408 -0
  22. data/test/fixtures/episodes-summary-report-response.xml +4 -0
  23. data/test/fixtures/invalid-param-response-multiple.xml +8 -0
  24. data/test/fixtures/invalid-param-response-single.xml +9 -0
  25. data/test/fixtures/playlists-response.xml +611 -0
  26. data/test/fixtures/shows-response.xml +26 -0
  27. data/test/test_analytics_requests.rb +42 -0
  28. data/test/test_analytics_responses.rb +14 -0
  29. data/test/test_connection.rb +31 -0
  30. data/test/test_error_responses.rb +29 -0
  31. data/test/test_helper.rb +8 -0
  32. data/test/test_query_requests.rb +62 -0
  33. data/test/test_query_responses.rb +165 -0
  34. data/test/test_write_requests.rb +56 -0
  35. data/test/test_write_responses.rb +24 -0
  36. metadata +135 -0
@@ -0,0 +1,64 @@
1
+ module Episodic #:nodoc:
2
+
3
+ #
4
+ # Episodic::Platform is a Ruby library for Episodic's Platform REST API (http://app.episodic.com/help/server_api)
5
+ #
6
+ # == Getting started
7
+ #
8
+ # To get started you need to require 'episodic/platform':
9
+ #
10
+ # require 'episodic/platform'
11
+ #
12
+ # Before you can use any of the object methods, you need to create a connection using <tt>Base.establish_connection!</tt>. The
13
+ # <tt>Base.establish_connection!</tt> method requires that you pass your Episodic API Key and Episodic Secret Key.
14
+ #
15
+ # Episodic::Platform::Base.establish_connection!('my_api_key', 'my_secret_key')
16
+ #
17
+ # == Handling errors
18
+ #
19
+ # Any errors returned from the Episodic Platform API are converted to exceptions and raised from the called method. For example,
20
+ # the following response would cause <tt>Episodic::Platform::InvalidAPIKey</tt> to be raised.
21
+ #
22
+ # <?xml version="1.0" encoding="UTF-8"?>
23
+ # <error>
24
+ # <code>1</code>
25
+ # <message>Invalid API Key</message>
26
+ # </error>
27
+ #
28
+ module Platform
29
+ API_HOST = 'app.episodic.com'
30
+ API_VERSION = 'v2'
31
+
32
+ #
33
+ # Episodic::Platform::Base is the abstract super class of all classes who make requests against the Episodic Platform REST API.
34
+ #
35
+ # Establishing a connection with the Base class is the entry point to using the library:
36
+ #
37
+ # Episodic::Platform::Base.establish_connection!('my_api_key', 'my_secret_key')
38
+ #
39
+ class Base
40
+
41
+ class << self
42
+
43
+ #
44
+ # Helper method to construct an Episodic Platform API request URL.
45
+ #
46
+ # ==== Parameters
47
+ #
48
+ # api_name<String>:: Specifies the API you are calling. Examples are "write", query" and "analytics"
49
+ # method_name<String>:: The method being invoked.
50
+ #
51
+ # ==== Returns
52
+ #
53
+ # URI:: The constructed URL.
54
+ #
55
+ def construct_url api_name, method_name
56
+ return URI.parse("http://#{connection.connection_options[:api_host] || API_HOST}/api/#{API_VERSION}/#{api_name}/#{method_name}")
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,45 @@
1
+ module Episodic
2
+
3
+ module Platform
4
+
5
+ #
6
+ # Base class for responses that return an collection of objects.
7
+ #
8
+ class CollectionResponse < Response
9
+
10
+ COLLECTION_RESPONSE_ATTRIBUTES = [:page, :pages, :total, :per_page]
11
+
12
+ #
13
+ # Constructor
14
+ #
15
+ # ==== Parameters
16
+ #
17
+ # response<Episodic::Platform::HttpResponse>:: The response object returned from an Episodic Platform API request.
18
+ # xml_options<Hash>:: A set of options used by XmlSimple when parsing the response body
19
+ #
20
+ def initialize response, xml_options = {}
21
+ super(response, xml_options)
22
+ end
23
+
24
+ #
25
+ # Override to look up attributes by name
26
+ #
27
+ def method_missing(method_sym, *arguments, &block)
28
+ method_name = method_sym.to_s
29
+ if (COLLECTION_RESPONSE_ATTRIBUTES.include?(method_sym))
30
+ return @parsed_body[method_name].to_i
31
+ end
32
+
33
+ return super
34
+ end
35
+
36
+ #
37
+ # Attributes can be accessed as methods.
38
+ #
39
+ def respond_to?(symbol, include_private = false)
40
+ return COLLECTION_RESPONSE_ATTRIBUTES.include?(symbol)
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,305 @@
1
+ module Episodic
2
+ module Platform
3
+
4
+ #
5
+ # Class used to make the actual requests to the Episodic Platform API.
6
+ #
7
+ class Connection
8
+
9
+ attr_reader :episodic_api_key, :episodic_secret_key, :connection_options
10
+
11
+ #
12
+ # Constructor
13
+ #
14
+ # episodic_api_key<String>:: The caller's Episodic API Key
15
+ # episodic_secret_key<String>:: The caller's Episodic Secret Key
16
+ # options<Hash>:: Used mostly for testing by allowing the caller to override some constants such
17
+ # the API host.
18
+ #
19
+ def initialize(episodic_api_key, episodic_secret_key, options = {})
20
+ @episodic_api_key = episodic_api_key
21
+ @episodic_secret_key = episodic_secret_key
22
+ @connection_options = options
23
+ end
24
+
25
+ #
26
+ # Perform the POST request to the specified URL. This method takes the params passed in and
27
+ # generates the signature parameter using the Episodic Secret Key for this connection. The
28
+ # signature, the Episodic API Key for this connection and the passed in params are then used to
29
+ # generate the form post.
30
+ #
31
+ # If there are any filenames passed then these are also included in the form post.
32
+ #
33
+ # ==== Parameters
34
+ #
35
+ # url<URI>:: The URL to the Episodic Platform API endpoint
36
+ # params<Hash>:: A hash of parameters to include include in the post request.
37
+ # file_params<Hash>:: A hash of file parameters. The name is the parameter name and value is the path to the file.
38
+ #
39
+ # ==== Returns
40
+ #
41
+ # Episodic::Platform::HTTPResponse:: The full response object.
42
+ #
43
+ def do_post url, params, file_params = nil
44
+
45
+ # Convert all the params to strings
46
+ request_params = convert_params_for_request(params)
47
+
48
+ # Add in the common params
49
+ append_common_params(request_params)
50
+
51
+ c = Curl::Easy.new(url.to_s)
52
+ c.multipart_form_post = true
53
+
54
+ fields = []
55
+ request_params.each_pair do |name, value|
56
+ fields << Curl::PostField.content(name, value)
57
+ end
58
+ file_params.each do |name, value|
59
+ fields << Curl::PostField.file(name, value)
60
+ end unless file_params.nil?
61
+
62
+ # Make the request
63
+ c.http_post(*fields)
64
+
65
+ return Episodic::Platform::HTTPResponse.new(c.response_code, c.body_str)
66
+ end
67
+
68
+ #
69
+ # Perform the GET request to the specified URL. This method takes the params passed in and
70
+ # generates the signature parameter using the Episodic Secret Key for this connection. The
71
+ # signature, the Episodic API Key for this connection and the passed in params are then used to
72
+ # generate the query string.
73
+ #
74
+ # ==== Parameters
75
+ #
76
+ # url<URI>:: The URL to the Episodic Platform API endpoint
77
+ # params<Hash>:: A hash of parameters to include include in the query string.
78
+ #
79
+ # ==== Returns
80
+ #
81
+ # Episodic::Platform::HTTPResponse:: The full response object.
82
+ #
83
+ def do_get url, params
84
+
85
+ # Convert all the params to strings
86
+ request_params = convert_params_for_request(params)
87
+
88
+ # Add in the common params
89
+ append_common_params(request_params)
90
+
91
+ queryString = ""
92
+ request_params.keys.each_with_index do |key, index|
93
+ queryString << "#{index == 0 ? '?' : '&'}#{key}=#{::URI.escape(request_params[key])}"
94
+ end
95
+
96
+ # Create the request
97
+ http = Net::HTTP.new(url.host, url.port)
98
+ response = http.start() {|req| req.get(url.path + queryString)}
99
+
100
+ return Episodic::Platform::HTTPResponse.new(response.code, response.body)
101
+ end
102
+
103
+ protected
104
+
105
+ #
106
+ # Helper method to generate the request signature
107
+ #
108
+ # ==== Parameters
109
+ #
110
+ # params<Hash>:: The set of params that will be passed either in the form post or in the
111
+ # query string.
112
+ #
113
+ def generate_signature_from_params params
114
+ sorted_keys = params.keys.sort {|x,y| x.to_s <=> y.to_s }
115
+ string_to_sign = @episodic_secret_key
116
+ sorted_keys.each do |key|
117
+ string_to_sign += "#{key.to_s}=#{params[key]}"
118
+ end
119
+
120
+ return Digest::SHA256.hexdigest(string_to_sign)
121
+ end
122
+
123
+ #
124
+ # Apply the common Episodic params such as expires, signature and key
125
+ #
126
+ # ==== Parameters
127
+ #
128
+ # params<Hash>:: The params to update.
129
+ #
130
+ def append_common_params params
131
+
132
+ # Add an expires value if it has not been added already
133
+ params["expires"] ||= (Time.now.to_i + 30).to_s
134
+
135
+ # Sign the request
136
+ params["signature"] = self.generate_signature_from_params(params)
137
+
138
+ # Add our key
139
+ params["key"] = @episodic_api_key
140
+ end
141
+
142
+ #
143
+ # Converts all parameters to a form for a request. This includes converting arrays to comma delimited strings,
144
+ # Times to integers and Hashes to a form depending on its level in the passed in params.
145
+ #
146
+ # ==== Parameters
147
+ #
148
+ # params<Hash>:: The params to convert.
149
+ #
150
+ # ==== Returns
151
+ #
152
+ # Hash:: A single level hash where all keys and values are strings.
153
+ #
154
+ def convert_params_for_request params
155
+ result = {}
156
+
157
+ params.each_pair do |key, value|
158
+
159
+ # We don't want to deal with nils
160
+ value = "" if value.nil?
161
+
162
+ if value.is_a?(Array)
163
+ # Convert to a comma delimited string
164
+ result[key.to_s] = value.join(",")
165
+ elsif value.is_a?(Time)
166
+ #Convert the time to an integer (then string)
167
+ result[key.to_s] = value.to_i.to_s
168
+ elsif value.is_a?(Hash)
169
+ # Used for custom fields
170
+ value.each_pair do |sub_key, sub_value|
171
+ sub_value = "" if sub_value.nil?
172
+ if (sub_value.is_a?(Time))
173
+ result["#{key.to_s}[#{sub_key.to_s}]"] = sub_value.to_i.to_s
174
+ elsif (sub_value.is_a?(Hash))
175
+ # Put the hash in the external select field form
176
+ val = ""
177
+ sub_value.each_pair do |k, v|
178
+ val << "#{k}|#{v};"
179
+ end
180
+ result["#{key.to_s}[#{sub_key.to_s}]"] = val
181
+ else
182
+ result["#{key.to_s}[#{sub_key.to_s}]"] = sub_value.to_s
183
+ end
184
+ end
185
+ else
186
+ result[key.to_s] = value.to_s
187
+ end
188
+ end
189
+
190
+ return result
191
+ end
192
+
193
+ module Management #:nodoc:
194
+ def self.included(base)
195
+ base.cattr_accessor :connections
196
+ base.connections = {}
197
+ base.extend ClassMethods
198
+ end
199
+
200
+ #
201
+ # Manage the creation and destruction of connections for Episodic::Platform::Base and its subclasses. Connections are
202
+ # created with establish_connection!.
203
+ #
204
+ module ClassMethods
205
+
206
+ #
207
+ # Creates a new connection with which to make requests to the Episodic Platform for the
208
+ # calling class.
209
+ #
210
+ # Episodic::Platform::Base.establish_connection!(episodic_api_key, episodic_secret_key)
211
+ #
212
+ # You can set connections for every subclass of Episodic::Platform::Base. Once the initial
213
+ # connection is made on Base, all subsequent connections will inherit whatever values you
214
+ # don't specify explictly.
215
+ #
216
+ # ==== Parameters
217
+ #
218
+ # episodic_api_key<String>:: This is your Episodic API Key
219
+ # episodic_secret_key<String>:: This is your Episodic Secret Key
220
+ #
221
+ def establish_connection!(episodic_api_key, episodic_secret_key, options = {})
222
+ connections[connection_name] = Connection.new(episodic_api_key, episodic_secret_key, options)
223
+ end
224
+
225
+ #
226
+ # Returns the connection for the current class, or Base's default connection if the current class does not
227
+ # have its own connection.
228
+ #
229
+ # If not connection has been established yet, NoConnectionEstablished will be raised.
230
+ #
231
+ # ==== Returns
232
+ #
233
+ # Episodic::Platform::Connection:: The connection for the current class or the default connection
234
+ #
235
+ def connection
236
+ if connected?
237
+ connections[connection_name] || default_connection
238
+ else
239
+ raise NoConnectionEstablished
240
+ end
241
+ end
242
+
243
+ #
244
+ # Returns true if a connection has been made yet.
245
+ #
246
+ # ==== Returns
247
+ #
248
+ # Boolean:: <tt>true</tt> if there is at least one connection
249
+ #
250
+ def connected?
251
+ !connections.empty?
252
+ end
253
+
254
+ #
255
+ # Removes the connection for the current class. If there is no connection for the current class, the default
256
+ # connection will be removed.
257
+ #
258
+ # ==== Parameters
259
+ #
260
+ # name<String>:: The name of the connection. This defaults to the default connection name.
261
+ #
262
+ def disconnect(name = connection_name)
263
+ name = default_connection unless connections.has_key?(name)
264
+ connections.delete(name)
265
+ end
266
+
267
+ #
268
+ # Removes all connections
269
+ #
270
+ def disconnect!
271
+ connections.each_key {|connection| disconnect(connection)}
272
+ end
273
+
274
+ private
275
+
276
+ #
277
+ # Get the name of this connection
278
+ #
279
+ # ==== Returns
280
+ #
281
+ # String:: The connection name
282
+ #
283
+ def connection_name
284
+ name
285
+ end
286
+
287
+ #
288
+ # Hardcoded default connection name
289
+ #
290
+ def default_connection_name
291
+ 'Episodic::Platform::Base'
292
+ end
293
+
294
+ #
295
+ # Shortcut to get the default connection
296
+ #
297
+ def default_connection
298
+ connections[default_connection_name]
299
+ end
300
+ end
301
+ end
302
+
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,105 @@
1
+ module Episodic
2
+ module Platform
3
+
4
+ #
5
+ # Abstract super class of all Episodic::Platform exceptions
6
+ #
7
+ class EpisodicPlatformException < StandardError
8
+ end
9
+
10
+ class FileUploadFailed < EpisodicPlatformException
11
+ end
12
+
13
+ #
14
+ # An execption that is raised as a result of the response content.
15
+ #
16
+ class ResponseError < EpisodicPlatformException
17
+
18
+ attr_reader :response
19
+
20
+ #
21
+ # Constructor
22
+ #
23
+ # ==== Parameters
24
+ #
25
+ # message<String>:: The message to include in the exception
26
+ # response<Episodic::Platform::HTTPResponse>:: The response object.
27
+ #
28
+ def initialize(message, response)
29
+ @response = response
30
+ super(message)
31
+ end
32
+ end
33
+
34
+ #
35
+ # There was an unexpected error on the server. .
36
+ #
37
+ class InternalError < ResponseError
38
+ end
39
+
40
+ #
41
+ # The API Key wasn't provided or is invalid or the signature is invalid.
42
+ #
43
+ class InvalidAPIKey < ResponseError
44
+ end
45
+
46
+ #
47
+ # The requested report could not be found. Either the report token is invalid or the report has expired and is no longer available.
48
+ #
49
+ class ReportNotFound < ResponseError
50
+ end
51
+
52
+ #
53
+ # The request failed to specifiy one or more of the required parameters to an API method.
54
+ #
55
+ class MissingRequiredParameter < ResponseError
56
+ end
57
+
58
+ #
59
+ # The request is no longer valid because the expires parameter specifies a time that has passed.
60
+ #
61
+ class RequestExpired < ResponseError
62
+ end
63
+
64
+ #
65
+ # The specified object (i.e. show, episode, etc.) could not be found.
66
+ #
67
+ class NotFound < ResponseError
68
+ end
69
+
70
+ #
71
+ # API access for the user is disabled.
72
+ #
73
+ class APIAccessDisabled < ResponseError
74
+ end
75
+
76
+ #
77
+ # The value specified for a parameter is not valid.
78
+ #
79
+ class InvalidParameters < ResponseError
80
+
81
+ #
82
+ # Constructor. Override to include inforation about the invalid parameter(s).
83
+ #
84
+ # ==== Parameters
85
+ #
86
+ # message<String>:: The message to include in the exception
87
+ # response<Episodic::Platform::HTTPResponse>:: The response object.
88
+ # invalid_parameters<Object>:: This is either a Hash or an Array of hashes if there is more than one invalid parameter.
89
+ #
90
+ def initialize(message, response, invalid_parameters)
91
+
92
+ if (invalid_parameters)
93
+ invalid_parameters["invalid_parameter"] = [invalid_parameters["invalid_parameter"]] if invalid_parameters["invalid_parameter"].is_a?(Hash)
94
+
95
+ # Append to the message
96
+ invalid_parameters["invalid_parameter"].each do |ip|
97
+ message << "\n#{ip['name']}: #{ip['content']}"
98
+ end
99
+ end
100
+
101
+ super(message, response)
102
+ end
103
+ end
104
+ end
105
+ end