wcc-api 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/wcc/api/json.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WCC::API
2
4
  module JSON
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WCC::API::JSON
2
4
  class Pagination
3
5
  attr_reader :query, :url_for
@@ -19,12 +21,12 @@ module WCC::API::JSON
19
21
  json.sort query.sort if query.respond_to?(:sort)
20
22
  json.filter query.filter
21
23
  json._links do
22
- json.self url_for.(base_url_params)
24
+ json.self url_for.call(base_url_params)
23
25
  if has_previous_page?
24
- json.previous url_for.(base_url_params.merge(offset: query.offset - query.limit))
26
+ json.previous url_for.call(base_url_params.merge(offset: query.offset - query.limit))
25
27
  end
26
28
  if has_next_page?
27
- json.next url_for.(base_url_params.merge(offset: query.offset + query.limit))
29
+ json.next url_for.call(base_url_params.merge(offset: query.offset + query.limit))
28
30
  end
29
31
  end
30
32
  end
@@ -41,17 +43,18 @@ module WCC::API::JSON
41
43
  end
42
44
 
43
45
  def base_url_params
44
- @base_url_params ||= {
45
- filter: query.filter,
46
- only_path: false
47
- }.tap do |params|
48
- if query.paging
49
- params[:limit] = query.limit
50
- params[:offset] = query.offset
46
+ @base_url_params ||=
47
+ {
48
+ filter: query.filter,
49
+ only_path: false
50
+ }.tap do |params|
51
+ if query.paging
52
+ params[:limit] = query.limit
53
+ params[:offset] = query.offset
54
+ end
55
+ params[:order_by] = query.order_by if query.respond_to?(:order_by)
56
+ params[:sort] = query.sort if query.respond_to?(:sort)
51
57
  end
52
- params[:order_by] = query.order_by if query.respond_to?(:order_by)
53
- params[:sort] = query.sort if query.respond_to?(:sort)
54
- end
55
58
  end
56
59
  end
57
60
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'wcc/api'
2
4
 
3
5
  module WCC
4
6
  module API
5
7
  class Railtie < Rails::Railtie
6
- initializer 'wcc.api railtie initializer', group: :all do |app|
8
+ initializer 'wcc.api railtie initializer', group: :all do |_app|
7
9
  ActiveSupport::Inflector.inflections(:en) do |inflect|
8
10
  inflect.acronym 'API'
9
11
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'rest_client/response'
4
+
5
+ module WCC::API
6
+ class RestClient
7
+ attr_reader :api_url
8
+
9
+ def initialize(api_url:, headers: nil, **options)
10
+ # normalizes a URL to have a slash on the end
11
+ @api_url = api_url.gsub(/\/+$/, '') + '/'
12
+
13
+ @adapter = RestClient.load_adapter(options[:adapter])
14
+
15
+ @options = options
16
+ @query_defaults = {}
17
+ @headers = {
18
+ 'Accept' => 'application/json'
19
+ }.merge(headers || {}).freeze
20
+ @response_class = options[:response_class] || DefaultResponse
21
+ end
22
+
23
+ # performs an HTTP GET request to the specified path within the configured
24
+ # space and environment. Query parameters are merged with the defaults and
25
+ # appended to the request.
26
+ def get(path, query = {})
27
+ url = URI.join(@api_url, path)
28
+
29
+ @response_class.new(self,
30
+ { url: url, query: query },
31
+ get_http(url, query))
32
+ end
33
+
34
+ ADAPTERS = {
35
+ http: ['http', '> 1.0', '< 3.0'],
36
+ typhoeus: ['typhoeus', '~> 1.0']
37
+ }.freeze
38
+
39
+ # This method is long due to the case statement,
40
+ # not really a better way to do it
41
+ def self.load_adapter(adapter)
42
+ case adapter
43
+ when nil
44
+ ADAPTERS.each do |a, spec|
45
+ begin
46
+ gem(*spec)
47
+ return load_adapter(a)
48
+ rescue Gem::LoadError
49
+ next
50
+ end
51
+ end
52
+ raise ArgumentError, 'Unable to load adapter! Please install one of '\
53
+ "#{ADAPTERS.values.map(&:join).join(',')}"
54
+ when :http
55
+ require_relative 'rest_client/http_adapter'
56
+ HttpAdapter.new
57
+ when :typhoeus
58
+ require_relative 'rest_client/typhoeus_adapter'
59
+ TyphoeusAdapter.new
60
+ else
61
+ unless adapter.respond_to?(:call)
62
+ raise ArgumentError, "Adapter #{adapter} is not invokeable! Please "\
63
+ "pass a proc or use one of #{ADAPTERS.keys}"
64
+ end
65
+ adapter
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def get_http(url, query, headers = {}, proxy = {})
72
+ headers = @headers.merge(headers || {})
73
+
74
+ q = @query_defaults.dup
75
+ q = q.merge(query) if query
76
+
77
+ resp = @adapter.call(url, q, headers, proxy)
78
+
79
+ resp = get_http(resp.headers['location'], nil, headers, proxy) if [301, 302, 307].include?(resp.code) && !@options[:no_follow_redirects]
80
+ resp
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WCC::API
4
+ class RestClient
5
+ class ApiError < StandardError
6
+ attr_reader :response
7
+
8
+ def self.[](code)
9
+ case code
10
+ when 404
11
+ NotFoundError
12
+ else
13
+ ApiError
14
+ end
15
+ end
16
+
17
+ def initialize(response)
18
+ @response = response
19
+ super(response.error_message)
20
+ end
21
+ end
22
+
23
+ class NotFoundError < ApiError
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem 'http'
4
+ require 'http'
5
+
6
+ module WCC::API
7
+ class RestClient
8
+ class HttpAdapter
9
+ def call(url, query, headers = {}, proxy = {})
10
+ if proxy[:host]
11
+ HTTP[headers].via(proxy[:host], proxy[:port], proxy[:username], proxy[:password])
12
+ .get(url, params: query)
13
+ else
14
+ HTTP[headers].get(url, params: query)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'api_error'
5
+
6
+ module WCC::API
7
+ class RestClient
8
+ class AbstractResponse
9
+ extend ::Forwardable
10
+
11
+ attr_reader :raw_response
12
+ attr_reader :raw_body
13
+ attr_reader :client
14
+ attr_reader :request
15
+
16
+ def_delegators :raw_response, :code, :headers
17
+
18
+ def body
19
+ @body ||= ::JSON.parse(raw_body)
20
+ end
21
+ alias_method :to_json, :body
22
+
23
+ def initialize(client, request, raw_response)
24
+ @client = client
25
+ @request = request
26
+ @raw_response = raw_response
27
+ @raw_body = raw_response.body.to_s
28
+ end
29
+
30
+ def skip
31
+ throw new NotImplementedError, 'Please implement "skip" parsing in response class'
32
+ end
33
+
34
+ def count
35
+ throw new NotImplementedError, 'Please implement "count" parsing in response class'
36
+ end
37
+
38
+ def collection_response?
39
+ page_items.nil? ? false : true
40
+ end
41
+
42
+ def page_items
43
+ throw new NotImplementedError, 'Please implement "page_items" parsing in response class'
44
+ end
45
+
46
+ def error_message
47
+ parsed_message =
48
+ begin
49
+ body.dig('error', 'message') || body.dig('message')
50
+ rescue ::JSON::ParserError
51
+ nil
52
+ end
53
+ parsed_message || "#{code}: #{raw_response.body}"
54
+ end
55
+
56
+ def next_page?
57
+ return false unless collection_response?
58
+
59
+ page_items.length + skip < count
60
+ end
61
+
62
+ def next_page
63
+ return unless next_page?
64
+
65
+ @next_page ||= @client.get(
66
+ @request[:url],
67
+ (@request[:query] || {}).merge(next_page_query)
68
+ )
69
+ @next_page.assert_ok!
70
+ end
71
+
72
+ def assert_ok!
73
+ return self if code >= 200 && code < 300
74
+
75
+ raise ApiError[code], self
76
+ end
77
+
78
+ # This method has a bit of complexity that is better kept in one location
79
+ def each_page(&block)
80
+ raise ArgumentError, 'Not a collection response' unless collection_response?
81
+
82
+ ret =
83
+ Enumerator.new do |y|
84
+ y << self
85
+
86
+ if next_page?
87
+ next_page.each_page.each do |page|
88
+ y << page
89
+ end
90
+ end
91
+ end
92
+
93
+ if block_given?
94
+ ret.map(&block)
95
+ else
96
+ ret.lazy
97
+ end
98
+ end
99
+
100
+ def items
101
+ return unless collection_response?
102
+
103
+ each_page.flat_map(&:page_items)
104
+ end
105
+
106
+ def first
107
+ raise ArgumentError, 'Not a collection response' unless collection_response?
108
+
109
+ page_items.first
110
+ end
111
+
112
+ def next_page_query
113
+ return unless collection_response?
114
+
115
+ {
116
+ skip: page_items.length + skip
117
+ }
118
+ end
119
+ end
120
+
121
+ class DefaultResponse < AbstractResponse
122
+ def skip
123
+ body['skip']
124
+ end
125
+
126
+ def count
127
+ body['total']
128
+ end
129
+
130
+ def page_items
131
+ body['items']
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ gem 'typhoeus'
5
+ require 'typhoeus'
6
+
7
+ module WCC::API
8
+ class RestClient
9
+ class TyphoeusAdapter
10
+ def call(url, query, headers = {}, proxy = {})
11
+ raise NotImplementedError, 'Proxying Not Yet Implemented' if proxy[:host]
12
+
13
+ TyphoeusAdapter::Response.new(
14
+ Typhoeus.get(
15
+ url,
16
+ params: query,
17
+ headers: headers
18
+ )
19
+ )
20
+ end
21
+
22
+ Response =
23
+ Struct.new(:raw) do
24
+ extend Forwardable
25
+
26
+ def_delegators :raw, :body, :to_s, :code, :headers
27
+
28
+ def status
29
+ raw.code
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/lib/wcc/api/rspec.rb CHANGED
@@ -1,4 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'wcc/api/rspec/cache_header_examples'
2
4
  require 'wcc/api/rspec/collection_matchers'
3
5
  require 'wcc/api/rspec/pagination_examples'
4
-
@@ -1,41 +1,43 @@
1
- RSpec.shared_examples_for "cached resource defaults" do
2
- it "sets etag" do
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for 'cached resource defaults' do
4
+ it 'sets etag' do
3
5
  expect(response.etag).to_not be_nil
4
6
  end
5
7
 
6
- it "sets public: true" do
8
+ it 'sets public: true' do
7
9
  expect(response.cache_control[:public]).to be_truthy
8
10
  end
9
11
  end
10
12
 
11
- RSpec.shared_examples_for "cached member resource" do
12
- include_examples "cached resource defaults"
13
+ RSpec.shared_examples_for 'cached member resource' do
14
+ include_examples 'cached resource defaults'
13
15
 
14
- it "sets last modified" do
16
+ it 'sets last modified' do
15
17
  expect(response.last_modified).to_not be_nil
16
18
  end
17
19
 
18
- it "does not set max age" do
20
+ it 'does not set max age' do
19
21
  expect(response.cache_control[:max_age]).to be_nil
20
22
  end
21
23
 
22
- it "does not set must_revalidate" do
24
+ it 'does not set must_revalidate' do
23
25
  expect(response.cache_control[:must_revalidate]).to be_nil
24
26
  end
25
27
  end
26
28
 
27
- RSpec.shared_examples_for "cached collection resource" do |max_age|
28
- include_examples "cached resource defaults"
29
+ RSpec.shared_examples_for 'cached collection resource' do |max_age|
30
+ include_examples 'cached resource defaults'
29
31
 
30
- it "sets max age" do
32
+ it 'sets max age' do
31
33
  expect(response.cache_control[:max_age]).to eq(max_age.to_s)
32
34
  end
33
35
 
34
- it "sets must_revalidate: true" do
36
+ it 'sets must_revalidate: true' do
35
37
  expect(response.cache_control[:must_revalidate]).to be_truthy
36
38
  end
37
39
 
38
- it "does not set last modified" do
40
+ it 'does not set last modified' do
39
41
  expect(response.last_modified).to be_nil
40
42
  end
41
43
  end
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module WCC::API::RSpec
2
4
  module CollectionMatchers
3
5
  def collection_match(coll1, coll2, matcher = :eq)
4
- coll1, coll2 = coll1.to_a, coll2.to_a
6
+ coll1 = coll1.to_a
7
+ coll2 = coll2.to_a
5
8
  expect(coll1.size).to eq(coll2.size)
6
9
 
7
10
  coll1.zip(coll2).each do |actual, expected|
@@ -10,4 +13,3 @@ module WCC::API::RSpec
10
13
  end
11
14
  end
12
15
  end
13
-