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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -0
  3. data/.rubocop.yml +24 -0
  4. data/.ruby-version +1 -0
  5. data/.todo.reek +50 -0
  6. data/CHANGELOG.md +1 -0
  7. data/Gemfile +4 -0
  8. data/README.md +54 -6
  9. data/bin/ci-run +57 -0
  10. data/bin/ci-setup +46 -0
  11. data/bin/reek +16 -0
  12. data/bin/rspec +16 -0
  13. data/bin/rubocop +16 -0
  14. data/examples/fetch.rb +16 -0
  15. data/examples/search.rb +17 -0
  16. data/lib/poms.rb +84 -156
  17. data/lib/poms/api/auth.rb +42 -22
  18. data/lib/poms/api/client.rb +58 -0
  19. data/lib/poms/api/drivers/net_http.rb +69 -0
  20. data/lib/poms/api/json_client.rb +35 -0
  21. data/lib/poms/api/pagination_client.rb +67 -0
  22. data/lib/poms/api/request.rb +29 -31
  23. data/lib/poms/api/response.rb +31 -0
  24. data/lib/poms/api/search.rb +38 -0
  25. data/lib/poms/api/uris.rb +10 -0
  26. data/lib/poms/api/uris/media.rb +41 -0
  27. data/lib/poms/api/uris/schedule.rb +28 -0
  28. data/lib/poms/configuration.rb +59 -0
  29. data/lib/poms/errors.rb +26 -0
  30. data/lib/poms/fields.rb +26 -3
  31. data/lib/poms/timestamp.rb +10 -3
  32. data/lib/poms/version.rb +1 -1
  33. data/poms.gemspec +10 -11
  34. metadata +59 -128
  35. data/Gemfile.lock +0 -120
  36. data/lib/poms/base.rb +0 -8
  37. data/lib/poms/broadcast.rb +0 -55
  38. data/lib/poms/builder.rb +0 -92
  39. data/lib/poms/builderless/broadcast.rb +0 -37
  40. data/lib/poms/builderless/clip.rb +0 -36
  41. data/lib/poms/connect.rb +0 -11
  42. data/lib/poms/has_ancestors.rb +0 -54
  43. data/lib/poms/has_base_attributes.rb +0 -29
  44. data/lib/poms/merged_series.rb +0 -28
  45. data/lib/poms/poms_error.rb +0 -5
  46. data/lib/poms/schedule_event.rb +0 -34
  47. data/lib/poms/season.rb +0 -13
  48. data/lib/poms/series.rb +0 -12
  49. data/lib/poms/views.rb +0 -52
  50. data/spec/fabricators/poms_fabricator.rb +0 -44
  51. data/spec/fixtures/poms_broadcast.json +0 -318
  52. data/spec/fixtures/poms_broadcast_multiple_schedule_events.json +0 -354
  53. data/spec/fixtures/poms_broadcast_pippi.json +0 -180
  54. data/spec/fixtures/poms_group.json +0 -1087
  55. data/spec/fixtures/poms_series.json +0 -49
  56. data/spec/fixtures/poms_single_broadcast_by_channel.json +0 -136
  57. data/spec/fixtures/poms_zapp.json +0 -47363
  58. data/spec/fixtures/vcr_cassettes/poms/builderless/broadcast_starts_at/returns_a_datetime.yml +0 -53
  59. data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_position.yml +0 -43
  60. data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_title.yml +0 -43
  61. data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_a_video_url.yml +0 -43
  62. data/spec/fixtures/vcr_cassettes/poms/builderless/clip/has_an_image_id.yml +0 -43
  63. data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_current_broadcast_fetches_the_current_broadcast.yml +0 -60
  64. data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_next_broadcast_and_key_fetches_the_current_broadcast.yml +0 -68
  65. data/spec/fixtures/vcr_cassettes/poms/fetch_broadcasts_fetch_next_broadcast_and_key_returns_the_key.yml +0 -68
  66. data/spec/fixtures/vcr_cassettes/poms/merged_series/turns_the_json_into_a_hash.yml +0 -46
  67. data/spec/fixtures/vcr_cassettes/poms_fetch/a_broadcast_has_scheduled_events_with_last_with_starts_at.yml +0 -43
  68. data/spec/fixtures/vcr_cassettes/poms_fetch/a_clip_has_a_title.yml +0 -43
  69. data/spec/fixtures/vcr_cassettes/poms_fetch/an_aankeiler_has_images.yml +0 -43
  70. data/spec/fixtures/vcr_cassettes/poms_fetch_broadcasts_for_serie/returns_nil_when_a_broadcast_does_not_exist.yml +0 -42
  71. data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_a_mid.yml +0 -51
  72. data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_a_title.yml +0 -51
  73. data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast/has_scheduled_events_with_last_with_starts_at.yml +0 -51
  74. data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast_and_key/has_a_broadcast_with_a_mid.yml +0 -51
  75. data/spec/fixtures/vcr_cassettes/poms_fetch_current_broadcast_and_key/has_a_key.yml +0 -51
  76. data/spec/fixtures/vcr_cassettes/poms_fetch_descendant_mids/returns_a_list_of_mids.yml +0 -42
  77. data/spec/fixtures/vcr_cassettes/poms_fetch_descendants_for_serie/builds_a_list_of_broadcasts.yml +0 -102755
  78. data/spec/fixtures/vcr_cassettes/poms_fetch_group/has_a_child_with_a_media_type.yml +0 -448
  79. data/spec/fixtures/vcr_cassettes/poms_fetch_group/has_a_child_with_a_title.yml +0 -448
  80. data/spec/fixtures/vcr_cassettes/poms_fetch_playlist_clips/creates_an_array_of_clips.yml +0 -1051
  81. data/spec/integration/poms_spec.rb +0 -87
  82. data/spec/lib/poms/api/auth_spec.rb +0 -34
  83. data/spec/lib/poms/api/request_spec.rb +0 -30
  84. data/spec/lib/poms/broadcast_spec.rb +0 -49
  85. data/spec/lib/poms/builder_spec.rb +0 -53
  86. data/spec/lib/poms/builderless/broadcast_spec.rb +0 -11
  87. data/spec/lib/poms/builderless/clip_spec.rb +0 -20
  88. data/spec/lib/poms/fields_spec.rb +0 -63
  89. data/spec/lib/poms/merged_series_spec.rb +0 -26
  90. data/spec/lib/poms/schedule_event_spec.rb +0 -15
  91. data/spec/lib/poms/timestamp_spec.rb +0 -13
  92. data/spec/lib/poms/views_spec.rb +0 -38
  93. data/spec/lib/poms_spec.rb +0 -137
  94. 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
- # Create an auth header for the Poms API. This is a codified string
9
- # consisting of a message that is hashed with a secret.
10
- #
11
- # @see message
12
- # @param secret The Poms API secret key
13
- # @param message The message that needs to be hashed.
14
- def self.encode(secret, message)
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 the header that is used for authenticating a request to the Poms
21
- # API.
22
- #
23
- # @param uri The uri that is being requested
24
- # @param origin The origin header
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:#{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
@@ -1,43 +1,41 @@
1
- require 'poms/api/auth'
1
+ require 'forwardable'
2
2
 
3
3
  module Poms
4
4
  module Api
5
- # A request to the Poms API. Does the authentication and allows you to
6
- # execute the request.
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
- # Create a new request to the Poms API. The request is initialized with a
9
- # URI to be called and the key, secret and origin that are needed for
10
- # authentication.
11
- #
12
- # @param uri The full URI to call on the Poms API
13
- # @param key The api key
14
- # @param secret The secret that goes with the api key
15
- # @param origin The whitelisted origin for this api key
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
- # Executes the request.
25
- def call
26
- open(@uri, headers)
19
+ def self.post(*args)
20
+ new(:post, *args)
27
21
  end
28
22
 
29
- private
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 headers
32
- date = Time.now.rfc822
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