poms 1.2.2 → 2.0.0.a
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +17 -0
- data/.rubocop.yml +24 -0
- data/.ruby-version +1 -0
- data/.todo.reek +50 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +4 -0
- data/README.md +54 -6
- data/bin/ci-run +57 -0
- data/bin/ci-setup +46 -0
- data/bin/reek +16 -0
- data/bin/rspec +16 -0
- data/bin/rubocop +16 -0
- data/examples/fetch.rb +16 -0
- data/examples/search.rb +17 -0
- data/lib/poms.rb +84 -156
- data/lib/poms/api/auth.rb +42 -22
- data/lib/poms/api/client.rb +58 -0
- data/lib/poms/api/drivers/net_http.rb +69 -0
- data/lib/poms/api/json_client.rb +35 -0
- data/lib/poms/api/pagination_client.rb +67 -0
- data/lib/poms/api/request.rb +29 -31
- data/lib/poms/api/response.rb +31 -0
- data/lib/poms/api/search.rb +38 -0
- data/lib/poms/api/uris.rb +10 -0
- data/lib/poms/api/uris/media.rb +41 -0
- data/lib/poms/api/uris/schedule.rb +28 -0
- data/lib/poms/configuration.rb +59 -0
- data/lib/poms/errors.rb +26 -0
- data/lib/poms/fields.rb +26 -3
- data/lib/poms/timestamp.rb +10 -3
- data/lib/poms/version.rb +1 -1
- data/poms.gemspec +10 -11
- metadata +59 -128
- data/Gemfile.lock +0 -120
- data/lib/poms/base.rb +0 -8
- data/lib/poms/broadcast.rb +0 -55
- data/lib/poms/builder.rb +0 -92
- data/lib/poms/builderless/broadcast.rb +0 -37
- data/lib/poms/builderless/clip.rb +0 -36
- data/lib/poms/connect.rb +0 -11
- data/lib/poms/has_ancestors.rb +0 -54
- data/lib/poms/has_base_attributes.rb +0 -29
- data/lib/poms/merged_series.rb +0 -28
- data/lib/poms/poms_error.rb +0 -5
- data/lib/poms/schedule_event.rb +0 -34
- data/lib/poms/season.rb +0 -13
- data/lib/poms/series.rb +0 -12
- data/lib/poms/views.rb +0 -52
- data/spec/fabricators/poms_fabricator.rb +0 -44
- data/spec/fixtures/poms_broadcast.json +0 -318
- data/spec/fixtures/poms_broadcast_multiple_schedule_events.json +0 -354
- data/spec/fixtures/poms_broadcast_pippi.json +0 -180
- data/spec/fixtures/poms_group.json +0 -1087
- data/spec/fixtures/poms_series.json +0 -49
- data/spec/fixtures/poms_single_broadcast_by_channel.json +0 -136
- data/spec/fixtures/poms_zapp.json +0 -47363
- data/spec/fixtures/vcr_cassettes/poms/builderless/broadcast_starts_at/returns_a_datetime.yml +0 -53
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_position.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_title.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_video_url.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_an_image_id.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_current_broadcast_fetches_the_current_broadcast.yml +0 -60
- data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_next_broadcast_and_key_fetches_the_current_broadcast.yml +0 -68
- data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_next_broadcast_and_key_returns_the_key.yml +0 -68
- data/spec/fixtures/vcr_cassettes/poms/merged_series/turns_the_json_into_a_hash.yml +0 -46
- data/spec/fixtures/vcr_cassettes/poms_fetch/a_broadcast_has_scheduled_events_with_last_with_starts_at.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms_fetch/a_clip_has_a_title.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms_fetch/an_aankeiler_has_images.yml +0 -43
- data/spec/fixtures/vcr_cassettes/poms_fetch_broadcasts_for_serie/returns_nil_when_a_broadcast_does_not_exist.yml +0 -42
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_a_mid.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_a_title.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_scheduled_events_with_last_with_starts_at.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast_and_key/has_a_broadcast_with_a_mid.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast_and_key/has_a_key.yml +0 -51
- data/spec/fixtures/vcr_cassettes/poms_fetch_descendant_mids/returns_a_list_of_mids.yml +0 -42
- data/spec/fixtures/vcr_cassettes/poms_fetch_descendants_for_serie/builds_a_list_of_broadcasts.yml +0 -102755
- data/spec/fixtures/vcr_cassettes/poms_fetch_group/has_a_child_with_a_media_type.yml +0 -448
- data/spec/fixtures/vcr_cassettes/poms_fetch_group/has_a_child_with_a_title.yml +0 -448
- data/spec/fixtures/vcr_cassettes/poms_fetch_playlist_clips/creates_an_array_of_clips.yml +0 -1051
- data/spec/integration/poms_spec.rb +0 -87
- data/spec/lib/poms/api/auth_spec.rb +0 -34
- data/spec/lib/poms/api/request_spec.rb +0 -30
- data/spec/lib/poms/broadcast_spec.rb +0 -49
- data/spec/lib/poms/builder_spec.rb +0 -53
- data/spec/lib/poms/builderless/broadcast_spec.rb +0 -11
- data/spec/lib/poms/builderless/clip_spec.rb +0 -20
- data/spec/lib/poms/fields_spec.rb +0 -63
- data/spec/lib/poms/merged_series_spec.rb +0 -26
- data/spec/lib/poms/schedule_event_spec.rb +0 -15
- data/spec/lib/poms/timestamp_spec.rb +0 -13
- data/spec/lib/poms/views_spec.rb +0 -38
- data/spec/lib/poms_spec.rb +0 -137
- data/spec/spec_helper.rb +0 -32
data/lib/poms/api/auth.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
1
3
|
module Poms
|
2
4
|
module Api
|
3
5
|
# This module can be used to create an authentication header for the Poms
|
@@ -5,36 +7,54 @@ module Poms
|
|
5
7
|
#
|
6
8
|
# see: http://wiki.publiekeomroep.nl/display/npoapi/Algemeen
|
7
9
|
module Auth
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
module_function
|
11
|
+
|
12
|
+
extend SingleForwardable
|
13
|
+
|
14
|
+
delegate %i(origin secret key) => :@credentials
|
15
|
+
|
16
|
+
# @param request The prepared request
|
17
|
+
# @param credentials The Poms API credentials
|
18
|
+
# @param clock Defaults to current time, but can be provided as Time
|
19
|
+
def sign(request, credentials, clock = Time.now)
|
20
|
+
@credentials = credentials
|
21
|
+
timestamp = clock.rfc822
|
22
|
+
message = generate_message(request.uri, timestamp)
|
23
|
+
|
24
|
+
request['Origin'] = origin
|
25
|
+
request['X-NPO-Date'] = timestamp
|
26
|
+
request['Authorization'] = "NPO #{key}:#{encrypt(message)}"
|
27
|
+
request
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a message for the Authorization header. This is an encrypted
|
31
|
+
# stringconsisting of a message that is hashed with a shared secret.
|
32
|
+
def encrypt(message)
|
15
33
|
sha256 = OpenSSL::Digest.new('sha256')
|
16
34
|
digest = OpenSSL::HMAC.digest(sha256, secret, message)
|
17
|
-
Base64.encode64(digest)
|
35
|
+
Base64.encode64(digest).strip
|
18
36
|
end
|
19
37
|
|
20
|
-
# Creates
|
21
|
-
#
|
22
|
-
#
|
23
|
-
# @param
|
24
|
-
|
25
|
-
# @param date The date as an RFC822 string
|
26
|
-
# @param params The url params as a ruby hash
|
27
|
-
def self.message(uri, origin, date, params = {})
|
28
|
-
params_string = params.sort.map do |key, value|
|
29
|
-
"#{key}:#{value}"
|
30
|
-
end.join(',') if params.present?
|
38
|
+
# Creates a message in the required format as specified by POMS
|
39
|
+
# documentation.
|
40
|
+
# @param uri The Addressable::URI
|
41
|
+
# @param timestamp An rfc822 formatted timestamp
|
42
|
+
def generate_message(uri, timestamp)
|
31
43
|
[
|
32
44
|
"origin:#{origin}",
|
33
|
-
"x-npo-date:#{
|
34
|
-
"uri:#{uri}",
|
35
|
-
params_string
|
45
|
+
"x-npo-date:#{timestamp}",
|
46
|
+
"uri:#{uri.path}",
|
47
|
+
params_string(uri.query_values)
|
36
48
|
].compact.join(',')
|
37
49
|
end
|
50
|
+
|
51
|
+
# Convert a hash of parameters to the format expected by the message
|
52
|
+
def params_string(params)
|
53
|
+
return unless params
|
54
|
+
params.map { |key, value| "#{key}:#{value}" }.sort.join(',')
|
55
|
+
end
|
56
|
+
|
57
|
+
private_class_method :generate_message, :encrypt, :params_string
|
38
58
|
end
|
39
59
|
end
|
40
60
|
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'poms/api/drivers/net_http'
|
2
|
+
require 'poms/api/request'
|
3
|
+
require 'poms/api/auth'
|
4
|
+
require 'poms/errors'
|
5
|
+
|
6
|
+
module Poms
|
7
|
+
module Api
|
8
|
+
# The Client module isolates all HTTP interactions, regardless of the driver
|
9
|
+
# module to implement the actual operations. Use the Client module to build
|
10
|
+
# signed requests and execute them.
|
11
|
+
#
|
12
|
+
# @see Poms::Api::Drivers::NetHttp
|
13
|
+
module Client
|
14
|
+
extend Drivers::NetHttp
|
15
|
+
|
16
|
+
module_function
|
17
|
+
|
18
|
+
def get(uri, credentials, headers = {})
|
19
|
+
handle_response(
|
20
|
+
execute(
|
21
|
+
Auth.sign(
|
22
|
+
prepare_get(uri, headers),
|
23
|
+
credentials
|
24
|
+
)
|
25
|
+
)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post(uri, body, credentials, headers = {})
|
30
|
+
handle_response(
|
31
|
+
execute(
|
32
|
+
Auth.sign(
|
33
|
+
prepare_post(uri, body, headers),
|
34
|
+
credentials
|
35
|
+
)
|
36
|
+
)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def handle_response(response)
|
41
|
+
case response.code
|
42
|
+
when 400..499 then raise Errors::HttpMissingError, response.code
|
43
|
+
when 500..599 then raise Errors::HttpServerError, response.code
|
44
|
+
else
|
45
|
+
response
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def prepare_get(uri, headers = {})
|
50
|
+
Request.get(uri, nil, headers)
|
51
|
+
end
|
52
|
+
|
53
|
+
def prepare_post(uri, body, headers = {})
|
54
|
+
Request.post(uri, body, headers)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'poms/api/response'
|
3
|
+
require 'poms/errors'
|
4
|
+
|
5
|
+
module Poms
|
6
|
+
module Api
|
7
|
+
module Drivers
|
8
|
+
# The NetHttp driver is a special module that can be used to implement the
|
9
|
+
# HTTP operations in the Client module. This is done by including a driver
|
10
|
+
# module into the client.
|
11
|
+
#
|
12
|
+
# This module isolates all knowledge of Net::HTTP.
|
13
|
+
#
|
14
|
+
# @see Poms::Api::Client
|
15
|
+
module NetHttp
|
16
|
+
NET_HTTP_ERRORS = [
|
17
|
+
Timeout::Error,
|
18
|
+
Errno::EINVAL,
|
19
|
+
Errno::ECONNRESET,
|
20
|
+
EOFError,
|
21
|
+
Net::HTTPBadResponse,
|
22
|
+
Net::HTTPHeaderSyntaxError,
|
23
|
+
Net::ProtocolError
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
def execute(request_description)
|
27
|
+
response = attempt_request(
|
28
|
+
request_description.uri,
|
29
|
+
prepare_request(request_description)
|
30
|
+
)
|
31
|
+
Response.new(response.code, response.body, response.to_hash)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def attempt_request(uri, request)
|
37
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
38
|
+
http.open_timeout = 5
|
39
|
+
http.read_timeout = 5
|
40
|
+
http.request(request)
|
41
|
+
end
|
42
|
+
rescue *NET_HTTP_ERRORS => e
|
43
|
+
raise Errors::HttpError,
|
44
|
+
"An error (#{e.class}) occured while processing your request."
|
45
|
+
end
|
46
|
+
|
47
|
+
def prepare_request(request_description)
|
48
|
+
request = request_to_net_http_request(request_description)
|
49
|
+
request.body = request_description.body.to_s
|
50
|
+
request_description.each_header do |key, value|
|
51
|
+
request[key] = value
|
52
|
+
end
|
53
|
+
request
|
54
|
+
end
|
55
|
+
|
56
|
+
def request_to_net_http_request(request_description)
|
57
|
+
uri = request_description.uri
|
58
|
+
if request_description.get?
|
59
|
+
Net::HTTP::Get.new(uri)
|
60
|
+
elsif request_description.post?
|
61
|
+
Net::HTTP::Post.new(uri)
|
62
|
+
else
|
63
|
+
raise ArgumentError, 'can only execute GET or POST requests'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'poms/api/client'
|
2
|
+
|
3
|
+
module Poms
|
4
|
+
module Api
|
5
|
+
# The JsonClient module is a wrapper around the regular Client module. It
|
6
|
+
# requests and responses to handle JSON-formatted bodies.
|
7
|
+
module JsonClient
|
8
|
+
DEFAULT_HEADERS = {
|
9
|
+
'Content-Type' => 'application/json',
|
10
|
+
'Accept' => 'application/json'
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
def get(uri, credentials, headers = {})
|
16
|
+
response = Client.get(
|
17
|
+
uri,
|
18
|
+
credentials,
|
19
|
+
DEFAULT_HEADERS.merge(headers)
|
20
|
+
)
|
21
|
+
JSON.parse(response.body)
|
22
|
+
end
|
23
|
+
|
24
|
+
def post(uri, body, credentials, headers = {})
|
25
|
+
response = Client.post(
|
26
|
+
uri,
|
27
|
+
body.to_json,
|
28
|
+
credentials,
|
29
|
+
DEFAULT_HEADERS.merge(headers)
|
30
|
+
)
|
31
|
+
JSON.parse(response.body)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'poms/api/json_client'
|
2
|
+
|
3
|
+
module Poms
|
4
|
+
module Api
|
5
|
+
# Creates lazy Enumerators to handle pagination of the Poms API and performs
|
6
|
+
# the request on demand.
|
7
|
+
module PaginationClient
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def get(uri, credentials)
|
11
|
+
execute(uri) do |page_uri|
|
12
|
+
Api::JsonClient.get(page_uri, credentials)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def post(uri, body, credentials)
|
17
|
+
execute(uri) do |page_uri|
|
18
|
+
Api::JsonClient.post(page_uri, body, credentials)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute(uri)
|
23
|
+
Enumerator.new do |yielder|
|
24
|
+
page = Page.new(uri)
|
25
|
+
loop do
|
26
|
+
page.execute { |page_uri| yield page_uri }
|
27
|
+
page.items.each { |item| yielder << item }
|
28
|
+
raise StopIteration if page.final?
|
29
|
+
page = page.next_page
|
30
|
+
end
|
31
|
+
end.lazy
|
32
|
+
end
|
33
|
+
|
34
|
+
# Keep track of number of items and how many have been retrieved
|
35
|
+
class Page
|
36
|
+
def initialize(uri, offset = 0)
|
37
|
+
uri.query_values = { offset: offset }
|
38
|
+
@uri = uri
|
39
|
+
end
|
40
|
+
|
41
|
+
def next_page
|
42
|
+
self.class.new(uri, next_index)
|
43
|
+
end
|
44
|
+
|
45
|
+
def final?
|
46
|
+
next_index >= response['total']
|
47
|
+
end
|
48
|
+
|
49
|
+
def items
|
50
|
+
response['items']
|
51
|
+
end
|
52
|
+
|
53
|
+
def execute
|
54
|
+
@response = yield uri
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :response, :uri
|
60
|
+
|
61
|
+
def next_index
|
62
|
+
response['offset'] + response['max']
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/poms/api/request.rb
CHANGED
@@ -1,43 +1,41 @@
|
|
1
|
-
require '
|
1
|
+
require 'forwardable'
|
2
2
|
|
3
3
|
module Poms
|
4
4
|
module Api
|
5
|
-
#
|
6
|
-
#
|
5
|
+
# The `Request` object is an implementation-agnostic description of an HTTP
|
6
|
+
# request, representing a combination of an HTTP method, URI, body and
|
7
|
+
# headers.
|
7
8
|
class Request
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
def initialize(uri, key, secret, origin)
|
17
|
-
@uri = uri
|
18
|
-
@path = URI(@uri).path
|
19
|
-
@key = key
|
20
|
-
@secret = secret
|
21
|
-
@origin = origin
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :@headers, :[], :[]=
|
11
|
+
def_delegator :@headers, :each, :each_header
|
12
|
+
|
13
|
+
attr_reader :uri, :body
|
14
|
+
|
15
|
+
def self.get(*args)
|
16
|
+
new(:get, *args)
|
22
17
|
end
|
23
18
|
|
24
|
-
|
25
|
-
|
26
|
-
open(@uri, headers)
|
19
|
+
def self.post(*args)
|
20
|
+
new(:post, *args)
|
27
21
|
end
|
28
22
|
|
29
|
-
|
23
|
+
def initialize(method, uri, body = nil, headers = {})
|
24
|
+
@method = method.to_sym
|
25
|
+
unless %i(get post).include?(@method)
|
26
|
+
raise ArgumentError, 'method should be :get or :post'
|
27
|
+
end
|
28
|
+
@uri = uri
|
29
|
+
@body = body.to_s
|
30
|
+
@headers = headers.to_h
|
31
|
+
end
|
32
|
+
|
33
|
+
def get?
|
34
|
+
@method == :get
|
35
|
+
end
|
30
36
|
|
31
|
-
def
|
32
|
-
|
33
|
-
origin = @origin
|
34
|
-
message = Auth.message(@path, @origin, date)
|
35
|
-
encoded_message = Auth.encode(@secret, message)
|
36
|
-
{
|
37
|
-
'Origin' => origin,
|
38
|
-
'X-NPO-Date' => date,
|
39
|
-
'Authorization' => "NPO #{@key}:#{encoded_message}"
|
40
|
-
}
|
37
|
+
def post?
|
38
|
+
@method == :post
|
41
39
|
end
|
42
40
|
end
|
43
41
|
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Poms
|
2
|
+
module Api
|
3
|
+
# Response is an implementation-agnostic representation of an HTTP-response,
|
4
|
+
# composing a HTTP status code, body and hash of headers.
|
5
|
+
class Response
|
6
|
+
# @return [Fixnum]
|
7
|
+
attr_reader :code
|
8
|
+
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :body
|
11
|
+
|
12
|
+
# @return [Hash]
|
13
|
+
attr_reader :headers
|
14
|
+
|
15
|
+
def initialize(code, body, headers)
|
16
|
+
@code = code.to_i
|
17
|
+
@body = body.to_s
|
18
|
+
@headers = headers.to_h
|
19
|
+
freeze
|
20
|
+
end
|
21
|
+
|
22
|
+
def eql?(other)
|
23
|
+
other.is_a?(self.class) &&
|
24
|
+
code == other.code &&
|
25
|
+
body == other.body &&
|
26
|
+
headers == other.headers
|
27
|
+
end
|
28
|
+
alias == eql?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'poms/timestamp'
|
2
|
+
|
3
|
+
module Poms
|
4
|
+
module Api
|
5
|
+
# Map search parameters to POMS specific format
|
6
|
+
module Search
|
7
|
+
TIME_PARAMS = {
|
8
|
+
starts_at: 'begin',
|
9
|
+
ends_at: 'end'
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
def self.build(options)
|
13
|
+
return {} if options.empty?
|
14
|
+
all = options.map do |key, value|
|
15
|
+
case key
|
16
|
+
when :starts_at, :ends_at
|
17
|
+
time_params(key, value)
|
18
|
+
when :type
|
19
|
+
{ 'facets' => { 'subsearch' => { 'types' => value } } }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
all.reduce(&:deep_merge)
|
23
|
+
end
|
24
|
+
|
25
|
+
private_class_method
|
26
|
+
|
27
|
+
def self.time_params(key, value)
|
28
|
+
{
|
29
|
+
'searches' => {
|
30
|
+
'sortDates' => {
|
31
|
+
TIME_PARAMS[key] => Timestamp.to_unix_ms(value)
|
32
|
+
}
|
33
|
+
}
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|