wcc-api 0.1.0 → 0.5.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +41 -0
- data/.env.example +0 -0
- data/.rspec +2 -0
- data/.rubocop.yml +190 -0
- data/.rubocop_todo.yml +63 -0
- data/Gemfile +2 -0
- data/Guardfile +32 -0
- data/Rakefile +2 -1
- data/bin/rspec +18 -0
- data/lib/wcc/api.rb +7 -8
- data/lib/wcc/api/active_record_shim.rb +69 -0
- data/lib/wcc/api/base_query.rb +10 -10
- data/lib/wcc/api/controller_helpers.rb +17 -0
- data/lib/wcc/api/json.rb +2 -0
- data/lib/wcc/api/json/pagination.rb +19 -14
- data/lib/wcc/api/railtie.rb +3 -1
- data/lib/wcc/api/rest_client.rb +238 -0
- data/lib/wcc/api/rest_client/api_error.rb +26 -0
- data/lib/wcc/api/rest_client/builder.rb +82 -0
- data/lib/wcc/api/rest_client/http_adapter.rb +19 -0
- data/lib/wcc/api/rest_client/response.rb +137 -0
- data/lib/wcc/api/rest_client/typhoeus_adapter.rb +63 -0
- data/lib/wcc/api/rspec.rb +4 -2
- data/lib/wcc/api/rspec/cache_header_examples.rb +43 -0
- data/lib/wcc/api/rspec/collection_matchers.rb +4 -2
- data/lib/wcc/api/rspec/pagination_examples.rb +8 -8
- data/lib/wcc/api/version.rb +3 -1
- data/lib/wcc/api/view_helpers.rb +3 -3
- data/spec/fixtures/contentful/entries.json +80 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/fixtures_helper.rb +8 -0
- data/spec/wcc/api/rest_client_spec.rb +296 -0
- data/wcc-api.gemspec +26 -13
- metadata +195 -14
@@ -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,137 @@
|
|
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, :status, :headers
|
17
|
+
alias_method :code, :status
|
18
|
+
|
19
|
+
def body
|
20
|
+
@body ||= ::JSON.parse(raw_body)
|
21
|
+
end
|
22
|
+
alias_method :to_json, :body
|
23
|
+
|
24
|
+
def initialize(client, request, raw_response)
|
25
|
+
@client = client
|
26
|
+
@request = request
|
27
|
+
@raw_response = raw_response
|
28
|
+
@raw_body = raw_response.body.to_s
|
29
|
+
end
|
30
|
+
|
31
|
+
def skip
|
32
|
+
throw new NotImplementedError, 'Please implement "skip" parsing in response class'
|
33
|
+
end
|
34
|
+
|
35
|
+
def count
|
36
|
+
throw new NotImplementedError, 'Please implement "count" parsing in response class'
|
37
|
+
end
|
38
|
+
|
39
|
+
def collection_response?
|
40
|
+
page_items.nil? ? false : true
|
41
|
+
end
|
42
|
+
|
43
|
+
def page_items
|
44
|
+
throw new NotImplementedError, 'Please implement "page_items" parsing in response class'
|
45
|
+
end
|
46
|
+
|
47
|
+
def error_message
|
48
|
+
parsed_message =
|
49
|
+
begin
|
50
|
+
body.dig('error', 'message') || body.dig('message')
|
51
|
+
rescue ::JSON::ParserError
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
parsed_message || "#{code}: #{raw_response.body}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def next_page?
|
58
|
+
return false unless collection_response?
|
59
|
+
return false if count.nil?
|
60
|
+
|
61
|
+
page_items.length + skip < count
|
62
|
+
end
|
63
|
+
|
64
|
+
def next_page
|
65
|
+
return unless next_page?
|
66
|
+
|
67
|
+
@next_page ||= @client.get(
|
68
|
+
@request[:url],
|
69
|
+
(@request[:query] || {}).merge(next_page_query)
|
70
|
+
)
|
71
|
+
@next_page.assert_ok!
|
72
|
+
end
|
73
|
+
|
74
|
+
def assert_ok!
|
75
|
+
return self if code >= 200 && code < 300
|
76
|
+
|
77
|
+
raise ApiError[code], self
|
78
|
+
end
|
79
|
+
|
80
|
+
# This method has a bit of complexity that is better kept in one location
|
81
|
+
def each_page(&block)
|
82
|
+
raise ArgumentError, 'Not a collection response' unless collection_response?
|
83
|
+
|
84
|
+
ret =
|
85
|
+
Enumerator.new do |y|
|
86
|
+
y << self
|
87
|
+
|
88
|
+
if next_page?
|
89
|
+
next_page.each_page.each do |page|
|
90
|
+
y << page
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if block_given?
|
96
|
+
ret.map(&block)
|
97
|
+
else
|
98
|
+
ret.lazy
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def items
|
103
|
+
return unless collection_response?
|
104
|
+
|
105
|
+
each_page.flat_map(&:page_items)
|
106
|
+
end
|
107
|
+
|
108
|
+
def first
|
109
|
+
raise ArgumentError, 'Not a collection response' unless collection_response?
|
110
|
+
|
111
|
+
page_items.first
|
112
|
+
end
|
113
|
+
|
114
|
+
def next_page_query
|
115
|
+
return unless collection_response?
|
116
|
+
|
117
|
+
{
|
118
|
+
skip: page_items.length + skip
|
119
|
+
}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class DefaultResponse < AbstractResponse
|
124
|
+
def skip
|
125
|
+
body['skip']
|
126
|
+
end
|
127
|
+
|
128
|
+
def count
|
129
|
+
body['total']
|
130
|
+
end
|
131
|
+
|
132
|
+
def page_items
|
133
|
+
body['items']
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
gem 'typhoeus'
|
5
|
+
require 'typhoeus'
|
6
|
+
|
7
|
+
class WCC::API::RestClient::TyphoeusAdapter
|
8
|
+
def get(url, params = {}, headers = {})
|
9
|
+
req = OpenStruct.new(params: params, headers: headers)
|
10
|
+
yield req if block_given?
|
11
|
+
Response.new(
|
12
|
+
Typhoeus.get(
|
13
|
+
url,
|
14
|
+
params: req.params,
|
15
|
+
headers: req.headers
|
16
|
+
)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def post(url, body, headers = {})
|
21
|
+
Response.new(
|
22
|
+
Typhoeus.post(
|
23
|
+
url,
|
24
|
+
body: body.is_a?(String) ? body : body.to_json,
|
25
|
+
headers: headers
|
26
|
+
)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def put(url, body, headers = {})
|
31
|
+
Response.new(
|
32
|
+
Typhoeus.put(
|
33
|
+
url,
|
34
|
+
body: body.is_a?(String) ? body : body.to_json,
|
35
|
+
headers: headers
|
36
|
+
)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete(url, query = {}, headers = {})
|
41
|
+
Response.new(
|
42
|
+
Typhoeus.delete(
|
43
|
+
url,
|
44
|
+
headers: headers
|
45
|
+
)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
class Response < SimpleDelegator
|
50
|
+
def raw
|
51
|
+
__getobj__
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
body&.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def status
|
59
|
+
code
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
data/lib/wcc/api/rspec.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.shared_examples_for 'cached resource defaults' do
|
4
|
+
it 'sets etag' do
|
5
|
+
expect(response.etag).to_not be_nil
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'sets public: true' do
|
9
|
+
expect(response.cache_control[:public]).to be_truthy
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
RSpec.shared_examples_for 'cached member resource' do
|
14
|
+
include_examples 'cached resource defaults'
|
15
|
+
|
16
|
+
it 'sets last modified' do
|
17
|
+
expect(response.last_modified).to_not be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not set max age' do
|
21
|
+
expect(response.cache_control[:max_age]).to be_nil
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'does not set must_revalidate' do
|
25
|
+
expect(response.cache_control[:must_revalidate]).to be_nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.shared_examples_for 'cached collection resource' do |max_age|
|
30
|
+
include_examples 'cached resource defaults'
|
31
|
+
|
32
|
+
it 'sets max age' do
|
33
|
+
expect(response.cache_control[:max_age]).to eq(max_age.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets must_revalidate: true' do
|
37
|
+
expect(response.cache_control[:must_revalidate]).to be_truthy
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'does not set last modified' do
|
41
|
+
expect(response.last_modified).to be_nil
|
42
|
+
end
|
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
|
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
|
-
|
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec.shared_examples_for :linked_pagination_object do |url_method, base_options, total|
|
2
|
-
it
|
4
|
+
it 'includes link to current page' do
|
3
5
|
url = public_send(
|
4
6
|
url_method,
|
5
7
|
base_options.merge(limit: 2, offset: 2)
|
@@ -9,20 +11,19 @@ RSpec.shared_examples_for :linked_pagination_object do |url_method, base_options
|
|
9
11
|
expect(subject['_links']['self']).to eq(url)
|
10
12
|
end
|
11
13
|
|
12
|
-
it
|
14
|
+
it 'includes link to next page when there is a next page' do
|
13
15
|
get public_send(url_method, limit: 2)
|
14
16
|
url = public_send(url_method,
|
15
|
-
base_options.merge(limit: 2, offset: 2)
|
16
|
-
)
|
17
|
+
base_options.merge(limit: 2, offset: 2))
|
17
18
|
expect(subject['_links']['next']).to eq(url)
|
18
19
|
end
|
19
20
|
|
20
|
-
it
|
21
|
+
it 'does not include link to next page when this is the last page' do
|
21
22
|
get public_send(url_method, limit: 2, offset: total - 1)
|
22
23
|
expect(subject['_links']['next']).to be_nil
|
23
24
|
end
|
24
25
|
|
25
|
-
it
|
26
|
+
it 'includes link to previous page when there is a previous page' do
|
26
27
|
get public_send(url_method, limit: 2, offset: 2)
|
27
28
|
url = public_send(
|
28
29
|
url_method,
|
@@ -31,9 +32,8 @@ RSpec.shared_examples_for :linked_pagination_object do |url_method, base_options
|
|
31
32
|
expect(subject['_links']['previous']).to eq(url)
|
32
33
|
end
|
33
34
|
|
34
|
-
it
|
35
|
+
it 'does not include link to next page when this is the last page' do
|
35
36
|
get public_send(url_method, limit: 2)
|
36
37
|
expect(subject['_links']['previous']).to be_nil
|
37
38
|
end
|
38
39
|
end
|
39
|
-
|
data/lib/wcc/api/version.rb
CHANGED
data/lib/wcc/api/view_helpers.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module WCC::API
|
2
4
|
module ViewHelpers
|
3
|
-
|
4
5
|
def api_pagination_for(query:)
|
5
6
|
WCC::API::JSON::Pagination.new(
|
6
7
|
query,
|
7
|
-
url_for: ->
|
8
|
+
url_for: ->(params) { url_for(params) }
|
8
9
|
).to_builder
|
9
10
|
end
|
10
|
-
|
11
11
|
end
|
12
12
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
{
|
2
|
+
"sys": {
|
3
|
+
"type": "Array"
|
4
|
+
},
|
5
|
+
"total": 63,
|
6
|
+
"skip": 0,
|
7
|
+
"limit": 2,
|
8
|
+
"items": [
|
9
|
+
{
|
10
|
+
"sys": {
|
11
|
+
"space": {
|
12
|
+
"sys": {
|
13
|
+
"type": "Link",
|
14
|
+
"linkType": "Space",
|
15
|
+
"id": "hw5pse7y1ojx"
|
16
|
+
}
|
17
|
+
},
|
18
|
+
"id": "6xJzDTX2HCo0u4QKIuGCOu",
|
19
|
+
"type": "Entry",
|
20
|
+
"createdAt": "2018-11-02T19:09:46.884Z",
|
21
|
+
"updatedAt": "2018-11-02T19:09:46.884Z",
|
22
|
+
"environment": {
|
23
|
+
"sys": {
|
24
|
+
"id": "gburgett",
|
25
|
+
"type": "Link",
|
26
|
+
"linkType": "Environment"
|
27
|
+
}
|
28
|
+
},
|
29
|
+
"revision": 1,
|
30
|
+
"contentType": {
|
31
|
+
"sys": {
|
32
|
+
"type": "Link",
|
33
|
+
"linkType": "ContentType",
|
34
|
+
"id": "menuButton"
|
35
|
+
}
|
36
|
+
},
|
37
|
+
"locale": "en-US"
|
38
|
+
},
|
39
|
+
"fields": {
|
40
|
+
"text": "What We Believe",
|
41
|
+
"externalLink": "http://www.watermark.org/fort-worth/about/beliefs"
|
42
|
+
}
|
43
|
+
},
|
44
|
+
{
|
45
|
+
"sys": {
|
46
|
+
"space": {
|
47
|
+
"sys": {
|
48
|
+
"type": "Link",
|
49
|
+
"linkType": "Space",
|
50
|
+
"id": "hw5pse7y1ojx"
|
51
|
+
}
|
52
|
+
},
|
53
|
+
"id": "5yozzvgItUSYu4eI8yQ0ee",
|
54
|
+
"type": "Entry",
|
55
|
+
"createdAt": "2018-11-02T19:09:51.627Z",
|
56
|
+
"updatedAt": "2018-11-02T19:09:51.627Z",
|
57
|
+
"environment": {
|
58
|
+
"sys": {
|
59
|
+
"id": "gburgett",
|
60
|
+
"type": "Link",
|
61
|
+
"linkType": "Environment"
|
62
|
+
}
|
63
|
+
},
|
64
|
+
"revision": 1,
|
65
|
+
"contentType": {
|
66
|
+
"sys": {
|
67
|
+
"type": "Link",
|
68
|
+
"linkType": "ContentType",
|
69
|
+
"id": "section-block-text"
|
70
|
+
}
|
71
|
+
},
|
72
|
+
"locale": "en-US"
|
73
|
+
},
|
74
|
+
"fields": {
|
75
|
+
"internalTitle": "Frisco - Life Stage: Kids' Ministry",
|
76
|
+
"body": "## Kids' Ministry\n\nKids' Ministry is availible for infants and children through 5th grade at both 9 AM and 11 AM services. "
|
77
|
+
}
|
78
|
+
}
|
79
|
+
]
|
80
|
+
}
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
SPEC_DIR = File.dirname(__FILE__)
|
4
|
+
FIXTURES_DIR = File.join(SPEC_DIR, 'fixtures')
|
5
|
+
|
6
|
+
$LOAD_PATH.unshift File.join(SPEC_DIR, '..', 'lib')
|
7
|
+
$LOAD_PATH.unshift SPEC_DIR
|
8
|
+
|
9
|
+
require 'dotenv'
|
10
|
+
Dotenv.load
|
11
|
+
|
12
|
+
require 'webmock/rspec'
|
13
|
+
Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
|
14
|
+
|
15
|
+
require 'wcc/api'
|
16
|
+
|
17
|
+
RSpec.configure do |config|
|
18
|
+
config.expect_with :rspec do |expectations|
|
19
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
20
|
+
end
|
21
|
+
|
22
|
+
config.mock_with :rspec do |mocks|
|
23
|
+
mocks.verify_partial_doubles = true
|
24
|
+
end
|
25
|
+
|
26
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
27
|
+
|
28
|
+
config.include FixturesHelper
|
29
|
+
end
|