wcc-api 0.2.0 → 0.3.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.
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
-