episodic-platform 0.9

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