trailer_vote-api 3.1.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rubocop.yml +29 -0
  4. data/CHANGELOG.md +56 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +77 -0
  7. data/README.md +188 -0
  8. data/Rakefile +12 -0
  9. data/bin/console +15 -0
  10. data/bin/setup +8 -0
  11. data/lib/trailer_vote/api.rb +67 -0
  12. data/lib/trailer_vote/api/autoload.rb +28 -0
  13. data/lib/trailer_vote/api/composable/common.rb +91 -0
  14. data/lib/trailer_vote/api/composable/get.rb +66 -0
  15. data/lib/trailer_vote/api/configuration.rb +69 -0
  16. data/lib/trailer_vote/api/errors.rb +113 -0
  17. data/lib/trailer_vote/api/fallback_content_types.rb +50 -0
  18. data/lib/trailer_vote/api/issue.rb +29 -0
  19. data/lib/trailer_vote/api/issue/create.rb +72 -0
  20. data/lib/trailer_vote/api/issue/find.rb +42 -0
  21. data/lib/trailer_vote/api/links.rb +54 -0
  22. data/lib/trailer_vote/api/place.rb +22 -0
  23. data/lib/trailer_vote/api/place/children.rb +33 -0
  24. data/lib/trailer_vote/api/place/children/urls.rb +67 -0
  25. data/lib/trailer_vote/api/place/create.rb +83 -0
  26. data/lib/trailer_vote/api/place/find.rb +60 -0
  27. data/lib/trailer_vote/api/place/lookup.rb +72 -0
  28. data/lib/trailer_vote/api/product.rb +31 -0
  29. data/lib/trailer_vote/api/product/create.rb +63 -0
  30. data/lib/trailer_vote/api/product/find.rb +55 -0
  31. data/lib/trailer_vote/api/product/image.rb +36 -0
  32. data/lib/trailer_vote/api/product/image/create.rb +83 -0
  33. data/lib/trailer_vote/api/product/image/find.rb +54 -0
  34. data/lib/trailer_vote/api/product/image/urls.rb +66 -0
  35. data/lib/trailer_vote/api/product/lookup.rb +95 -0
  36. data/lib/trailer_vote/api/product/place.rb +38 -0
  37. data/lib/trailer_vote/api/product/place/link.rb +75 -0
  38. data/lib/trailer_vote/api/product/update.rb +80 -0
  39. data/lib/trailer_vote/api/product/video.rb +36 -0
  40. data/lib/trailer_vote/api/product/video/create.rb +80 -0
  41. data/lib/trailer_vote/api/product/video/find.rb +56 -0
  42. data/lib/trailer_vote/api/product/video/urls.rb +66 -0
  43. data/lib/trailer_vote/api/push_recipe_android.rb +37 -0
  44. data/lib/trailer_vote/api/push_recipe_ios.rb +37 -0
  45. data/lib/trailer_vote/api/type_registry.rb +95 -0
  46. data/lib/trailer_vote/api/version.rb +7 -0
  47. data/lib/trailer_vote/api/vista_config.rb +37 -0
  48. data/trailer_vote-api.gemspec +45 -0
  49. metadata +261 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailer_vote/api'
4
+
5
+ require 'trailer_vote/api/issue'
6
+ require 'trailer_vote/api/issue/create'
7
+ require 'trailer_vote/api/issue/find'
8
+ require 'trailer_vote/api/place'
9
+ require 'trailer_vote/api/place/children'
10
+ require 'trailer_vote/api/place/children/urls'
11
+ require 'trailer_vote/api/place/create'
12
+ require 'trailer_vote/api/place/find'
13
+ require 'trailer_vote/api/place/lookup'
14
+ require 'trailer_vote/api/product'
15
+ require 'trailer_vote/api/product/create'
16
+ require 'trailer_vote/api/product/find'
17
+ require 'trailer_vote/api/product/lookup'
18
+ require 'trailer_vote/api/product/image'
19
+ require 'trailer_vote/api/product/image/create'
20
+ require 'trailer_vote/api/product/image/find'
21
+ require 'trailer_vote/api/product/place'
22
+ require 'trailer_vote/api/product/place/link'
23
+ require 'trailer_vote/api/product/video'
24
+ require 'trailer_vote/api/product/video/create'
25
+ require 'trailer_vote/api/product/video/find'
26
+ require 'trailer_vote/api/push_recipe_ios'
27
+ require 'trailer_vote/api/push_recipe_android'
28
+ require 'trailer_vote/api/vista_config'
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrailerVote
4
+ module Api
5
+ module Composable
6
+ module Common
7
+
8
+ def self.included(base)
9
+ base.class_eval do
10
+ private
11
+
12
+ attr_accessor :configuration
13
+ end
14
+ end
15
+
16
+ # Execute the call (and all dependent calls)
17
+ #
18
+ # @see #forward_klazz
19
+ # @see #redirect_klazz
20
+ def call(**_opts)
21
+ raise format('Missing implementation of #args in %<name>s', self.class.name)
22
+ end
23
+
24
+ private
25
+
26
+ def resolve_client
27
+ configuration.client
28
+ end
29
+
30
+ # Class used to branch forward to in case of a HTTP 307 or 308.
31
+ #
32
+ # @see #branch
33
+ # @see #forward
34
+ #
35
+ # @private
36
+ # @return [Class]
37
+ def forward_klazz
38
+ self.class
39
+ end
40
+
41
+ # Class used to branch redirect to in case of a HTTP 200, 201, 204, 301, 302, 303 or 304.
42
+ #
43
+ # When the status does not allow for redirecting, instead sends the retrieved result to the redirected class.
44
+ #
45
+ # @see #branch
46
+ # @see #redirect
47
+ #
48
+ # @private
49
+ # @return [Class]
50
+ def redirect_klazz
51
+ self.class
52
+ end
53
+
54
+ def guard_network_errors
55
+ yield
56
+ rescue HTTP::ConnectionError => err
57
+ raise ConnectionError, err
58
+ rescue HTTP::TimeoutError => err
59
+ raise TimeoutError, err
60
+ rescue HTTP::Error => err
61
+ raise NetworkError, err
62
+ end
63
+
64
+ def branch(result, data: nil)
65
+ raise_on_error(result)
66
+ forward(result, data: data) || redirect(result)
67
+ end
68
+
69
+ def raise_on_error(result)
70
+ return unless result.status.client_error? || result.status.server_error?
71
+ TrailerVote::Api.raise_error result
72
+ end
73
+
74
+ def forward(result, data:)
75
+ return unless [307, 308].include?(result.status)
76
+ forward_klazz.new(configuration: configuration)
77
+ .call(data: data, url: redirected_url(result))
78
+ end
79
+
80
+ def redirect(result)
81
+ redirect_klazz.new(configuration: configuration, result: result)
82
+ .call(url: redirected_url(result))
83
+ end
84
+
85
+ def redirected_url(result)
86
+ result['Location'] || result['Content-Location']
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailer_vote/api/composable/common'
4
+ require 'trailer_vote/api/links'
5
+
6
+ module TrailerVote
7
+ module Api
8
+ module Composable
9
+ module Get
10
+ def self.included(base)
11
+ base.include Common
12
+ base.class_eval do
13
+ attr_accessor :result
14
+ end
15
+ end
16
+
17
+ # Return the {Links} for the result
18
+ # @return [Links] the links
19
+ def links
20
+ # TODO: or headers
21
+ @links ||= Links.new(data[:_links])
22
+ end
23
+
24
+ # Decode the result via {TypeRegistry}. This also takes care of MediaTypes validation.
25
+ #
26
+ # @see #decode
27
+ # @return [Hash, Array] the decoded response
28
+ def to_h
29
+ @to_h ||= TrailerVote::Api.decode(call.result)
30
+ end
31
+
32
+ # Return the HTTP status
33
+ # @return [Numeric] the HTTP status
34
+ def to_i
35
+ call.result.status
36
+ end
37
+
38
+ # Return the ETag value
39
+ # @return [String, NilClass] the etag or nil
40
+ def etag
41
+ call.result[Headers::ETAG]
42
+ end
43
+
44
+ # Return the decoded result inner object
45
+ # @return [Object] the inner object
46
+ def data
47
+ raise format('Missing implementation of #data in %<name>s', self.class.name)
48
+ end
49
+
50
+ private
51
+
52
+ def ok?
53
+ result&.status == 200
54
+ end
55
+
56
+ def redirecting?
57
+ [301, 302, 303, 307, 308].include?(result&.status)
58
+ end
59
+
60
+ def redirect_to
61
+ self.class
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailer_vote/media_types'
4
+ require 'trailer_vote/api/composable/get'
5
+
6
+ module TrailerVote
7
+ module Api
8
+
9
+ module_function
10
+
11
+ def configure(url:, key:, secret:)
12
+ client = Api.default_client(key, secret)
13
+ Configuration.new(client: client, url: url)
14
+ end
15
+
16
+ class Configuration
17
+ include Composable::Get
18
+
19
+ SUCCESS = MediaTypes::Configuration.to_constructable.version(5)
20
+ FAILURE = MediaTypes::Errors.to_constructable.version(1)
21
+
22
+ ACCEPT = [SUCCESS.to_s, FAILURE.to_s(0.1)].join(', ').freeze
23
+
24
+ attr_accessor :client
25
+
26
+ def initialize(client:, url: nil, result: nil)
27
+ self.client = client
28
+ self.result = result
29
+ self.url = url
30
+ end
31
+
32
+ def data
33
+ to_h[:configuration]
34
+ end
35
+
36
+ def call(url: resolve_url)
37
+ return self if ok? || !url
38
+ guard_network_errors do
39
+ merge(resolve_client.headers(Headers::ACCEPT => ACCEPT).get(url))
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ attr_accessor :url
46
+
47
+ def ok?
48
+ result&.status == 200
49
+ end
50
+
51
+ def redirecting?(result)
52
+ [301, 302, 303, 307, 308].include?(result.status)
53
+ end
54
+
55
+ alias resolve_url url
56
+ alias resolve_client client
57
+
58
+ def merge(result)
59
+ raise_on_error(result)
60
+ self.result = redirecting?(result) ? redirect(result).result : result
61
+ self
62
+ end
63
+
64
+ def redirect(result)
65
+ call(url: result['Location'] || result['Content-Location'])
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailer_vote/media_types'
4
+
5
+ module TrailerVote
6
+ module Api
7
+
8
+ class Error < RuntimeError; end
9
+ class UnknownMediaType < Error; end
10
+ class NetworkError < Error; end
11
+ class ConnectionError < NetworkError; end
12
+ class TimeoutError < NetworkError; end
13
+
14
+ class EncodeError < Error
15
+ def initialize(media_type:, source:)
16
+ super format(
17
+ 'Failed to encode data for %<media_type>s. Reason: %<reason>s',
18
+ media_type: media_type,
19
+ reason: source.message
20
+ )
21
+
22
+ self.source = source
23
+ end
24
+
25
+ attr_accessor :source
26
+ end
27
+
28
+ class DecodeError < Error
29
+ def initialize(media_type:, source:)
30
+ super format(
31
+ 'Failed to decode data for %<media_type>s. Reason: %<reason>s',
32
+ media_type: media_type,
33
+ reason: source.message
34
+ )
35
+
36
+ self.source = source
37
+ end
38
+
39
+ attr_accessor :source
40
+ end
41
+
42
+ class ErrorsResponse < Error
43
+ attr_accessor :result
44
+
45
+ def initialize(result)
46
+ self.result = result
47
+ super messages
48
+ end
49
+
50
+ def messages
51
+ Array(formatted_data[:errors]).map { |error| error[:message] }.join(', ')
52
+ end
53
+
54
+ def data
55
+ @data ||= TrailerVote::Api.decode(result)
56
+ rescue DecodeError, UnknownMediaType
57
+ # noinspection RubyStringKeysInHashInspection
58
+ @data = { errors: [{ message: result.status.reason }] }
59
+ end
60
+
61
+ def status
62
+ result.status.to_i
63
+ end
64
+
65
+ def inspect
66
+ format('[%<status>s] %<klazz>s' + "\n" + '%<messages>s', status: status, klazz: self.class.name, messages: messages)
67
+ end
68
+
69
+ alias to_i status
70
+ alias to_s inspect
71
+
72
+ private
73
+
74
+ def formatted_data
75
+ return data if data.is_a?(::Hash)
76
+
77
+ { errors: [{ message: String(data) }] }
78
+ end
79
+ end
80
+
81
+ class ClientError < ErrorsResponse; end
82
+ class BadRequest < ClientError; end
83
+ class Unauthorized < ClientError; end
84
+ class Forbidden < ClientError; end
85
+ class NotFound < ClientError; end
86
+ class Conflict < ClientError; end
87
+ class Gone < ClientError; end
88
+ class PreconditionFailed < ClientError; end
89
+ class UnprocessableEntity < ClientError; end
90
+ class TooManyRequests < ClientError; end
91
+
92
+ class ServerError < ErrorsResponse; end
93
+
94
+ ERROR_MAPPING = Hash.new { |_, key| key < 500 ? ClientError : ServerError }.merge(
95
+ 400 => BadRequest,
96
+ 401 => Unauthorized,
97
+ 403 => Forbidden,
98
+ 404 => NotFound,
99
+ 409 => Conflict,
100
+ 410 => Gone,
101
+ 412 => PreconditionFailed,
102
+ 422 => UnprocessableEntity,
103
+ 429 => TooManyRequests
104
+ ).freeze
105
+
106
+ module_function
107
+
108
+ def raise_error(result)
109
+ raise ERROR_MAPPING[result.status], result
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+ require 'trailer_vote/api/type_registry'
5
+
6
+ module TrailerVote
7
+ module Api
8
+ module JsonTypeAdapter
9
+ module_function
10
+
11
+ def encode(obj)
12
+ Oj.dump(obj, mode: :compat)
13
+ rescue Oj::Error => err
14
+ raise EncodeError.new(media_type: 'application/json', source: err)
15
+ end
16
+
17
+ def decode(obj)
18
+ Oj.load(obj, mode: :strict, symbol_keys: true)
19
+ rescue Oj::Error => err
20
+ raise DecodeError.new(media_type: 'application/json', source: err)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ TrailerVote::Api::TypeRegistry['application/json'] = TrailerVote::Api::JsonTypeAdapter
27
+ TrailerVote::Api::TypeRegistry.shortcut('application/json', :json)
28
+ TrailerVote::Api::TypeRegistry.shortcut('application/json', 'text/json')
29
+
30
+ module TrailerVote
31
+ module Api
32
+ module HtmlTypeAdapter
33
+ module_function
34
+
35
+ def encode(obj)
36
+ return obj if obj.is_a?(String)
37
+ raise EncodeError.new(
38
+ media_type: 'text/html',
39
+ source: ArgumentError.new('HTML must be passed in as a HTML string')
40
+ )
41
+ end
42
+
43
+ def decode(str)
44
+ str
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ TrailerVote::Api::TypeRegistry['text/html'] = TrailerVote::Api::HtmlTypeAdapter
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trailer_vote/media_types'
4
+ require 'trailer_vote/api/configuration'
5
+
6
+ module TrailerVote
7
+ module Api
8
+ class Configuration
9
+ # @return [TrailerVote::Api::Issue::Find] issues attached to the credentials
10
+ def issue
11
+ Issue.new(configuration: self)
12
+ end
13
+ end
14
+
15
+ class Issue
16
+ def initialize(configuration:)
17
+ self.configuration = configuration
18
+ end
19
+
20
+ def back
21
+ configuration
22
+ end
23
+
24
+ private
25
+
26
+ attr_accessor :configuration
27
+ end
28
+ end
29
+ end