halchemy 1.0.2
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 +7 -0
- data/.idea/.gitignore +8 -0
- data/.idea/halchemy.iml +69 -0
- data/.idea/icon.png +0 -0
- data/.idea/inspectionProfiles/Project_Default.xml +36 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +12 -0
- data/__tests__/common.rb +97 -0
- data/__tests__/configurable/base_url.rb +46 -0
- data/__tests__/configurable/error_handling.rb +75 -0
- data/__tests__/configurable/headers.rb +92 -0
- data/__tests__/make_http_requests/follow_link_relations.rb +86 -0
- data/__tests__/make_http_requests/http_response_details.rb +60 -0
- data/__tests__/make_http_requests/optimistic_concurrency.rb +38 -0
- data/__tests__/make_http_requests/with_headers.rb +32 -0
- data/__tests__/make_http_requests/with_parameters.rb +32 -0
- data/__tests__/make_http_requests/with_templates.rb +45 -0
- data/__tests__/use_resources/iterate_collections.rb +89 -0
- data/bdd +1 -0
- data/lib/halchemy/api.rb +150 -0
- data/lib/halchemy/error_handling.rb +13 -0
- data/lib/halchemy/follower.rb +20 -0
- data/lib/halchemy/http_model.rb +41 -0
- data/lib/halchemy/metadata.rb +20 -0
- data/lib/halchemy/requester.rb +181 -0
- data/lib/halchemy/resource.rb +68 -0
- data/lib/halchemy/status_codes.rb +60 -0
- data/lib/halchemy/version.rb +5 -0
- data/lib/halchemy.rb +8 -0
- data/sig/__tests__/base_url.rbs +1 -0
- data/sig/__tests__/halchemy.rbs +4 -0
- data/sig/__tests__/headers.rbs +1 -0
- data/sig/__tests__/remove_headers.rbs +1 -0
- data/sig/halchemy/api.rbs +54 -0
- data/sig/halchemy/base_requester.rbs +35 -0
- data/sig/halchemy/error_handling.rbs +6 -0
- data/sig/halchemy/follower.rbs +9 -0
- data/sig/halchemy/hal_resource.rbs +13 -0
- data/sig/halchemy/http_model/request.rbs +14 -0
- data/sig/halchemy/http_model/response.rbs +15 -0
- data/sig/halchemy/metadata.rbs +9 -0
- data/sig/halchemy/read_only_requester.rbs +9 -0
- data/sig/halchemy/requester.rbs +18 -0
- data/sig/halchemy/resource.rbs +5 -0
- data/sig/list_style_handlers.rbs +1 -0
- data/sig/matchers.rbs +1 -0
- data/sig/patterns.rbs +1 -0
- metadata +93 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
When(/^I make a request$/) do
|
4
|
+
@resource = {}
|
5
|
+
ALL_METHODS.each do |method|
|
6
|
+
@resource[method] = @api.follow(@root_resource).to("resource1").public_send(method)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
Then(/^the HTTP request and response details are available to me$/) do
|
11
|
+
ALL_METHODS.each do |method|
|
12
|
+
resource = @resource[method]
|
13
|
+
expect(resource).not_to be_nil
|
14
|
+
request = resource._halchemy.request
|
15
|
+
response = resource._halchemy.response
|
16
|
+
error = resource._halchemy.error
|
17
|
+
|
18
|
+
expect(request).not_to be_nil
|
19
|
+
expect(response).not_to be_nil
|
20
|
+
|
21
|
+
expect(request.method).to eq(method)
|
22
|
+
expect(request.url).to end_with("/path/to/resource1")
|
23
|
+
|
24
|
+
expect(response.status_code).to eq(200)
|
25
|
+
expect(response.reason).to eq("OK")
|
26
|
+
|
27
|
+
expect(error).to be_nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
When(/^the request I made fails: (.*)$/) do |failure|
|
32
|
+
@api.error_handling.raise_for_network_errors = false
|
33
|
+
|
34
|
+
server = stub_request(:any, %r{\A#{BASE_URL}/path(/.*)?\z})
|
35
|
+
if failure.start_with?("status_code:")
|
36
|
+
server.to_return(status: failure.sub("status_code:", "").to_i, body: { error: "error" }.to_json)
|
37
|
+
else
|
38
|
+
server.to_raise(failure)
|
39
|
+
end
|
40
|
+
@expected_failure = failure
|
41
|
+
|
42
|
+
@resource = {}
|
43
|
+
ALL_METHODS.each do |method|
|
44
|
+
@resource[method] = @api.follow(@root_resource).to("resource1").public_send(method)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
Then(/^I can access the error details$/) do
|
49
|
+
ALL_METHODS.each do |method|
|
50
|
+
response = @resource[method]._halchemy.response
|
51
|
+
error = @resource[method]._halchemy.error
|
52
|
+
expect(error).not_to be_nil
|
53
|
+
|
54
|
+
if @expected_failure.start_with?("status_code:")
|
55
|
+
expect(response.status_code).to eq(@expected_failure.sub("status_code:", "").to_i)
|
56
|
+
else
|
57
|
+
expect(error.message).to be @expected_failure
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Given(/^a modifiable HAL resource$/) do
|
4
|
+
stub_for_hal_resource_scenarios
|
5
|
+
@api = Halchemy::Api.new BASE_URL
|
6
|
+
root_resource = @api.root.get
|
7
|
+
@resource = @api.follow(root_resource).to("resource1").get
|
8
|
+
end
|
9
|
+
|
10
|
+
When(/^I request a change to the resource$/) do
|
11
|
+
@requests = make_requests(MODIFY_METHODS, @api.follow(@resource).to("self"))
|
12
|
+
end
|
13
|
+
|
14
|
+
Then(/^the If-Match header uses the resource's Etag header$/) do
|
15
|
+
MODIFY_METHODS.each do |method|
|
16
|
+
expect(@requests[method].headers["If-Match"]).to eq("from header")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
And(/^the response does not have an Etag header$/) do
|
21
|
+
@resource._halchemy&.response&.headers&.delete("Etag")
|
22
|
+
end
|
23
|
+
|
24
|
+
Then(/^the If-Match header uses the resource's _etag field$/) do
|
25
|
+
MODIFY_METHODS.each do |method|
|
26
|
+
expect(@requests[method].headers["If-Match"]).to eq("from field")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
And(/^the resource does not have an _etag field$/) do
|
31
|
+
@resource.delete("_etag")
|
32
|
+
end
|
33
|
+
|
34
|
+
Then(/^the request is made without an If-Match header$/) do
|
35
|
+
MODIFY_METHODS.each do |method|
|
36
|
+
expect(@requests[method].headers.keys).not_to include("If-Match")
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
When(/^I specify additional headers for a request$/) do
|
4
|
+
requester = @api.follow(@root_resource).to("resource1").with_headers({ "X-CustomHeader" => "custom value" })
|
5
|
+
@requests = make_requests(ALL_METHODS, requester)
|
6
|
+
end
|
7
|
+
|
8
|
+
Then(/^the request is made with those headers$/) do
|
9
|
+
ALL_METHODS.each do |method|
|
10
|
+
headers = @requests[method].headers
|
11
|
+
expect(headers.keys.map(&:downcase)).to include("x-customheader")
|
12
|
+
expect(headers["X-Customheader"]).to eq("custom value") # this case sensitivity is from WebMock, not halchemy
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Given(/^I have made a request with additional headers$/) do
|
17
|
+
stub_for_hal_resource_scenarios
|
18
|
+
@api = Halchemy::Api.new BASE_URL
|
19
|
+
@root_resource = @api.root.get
|
20
|
+
@api.follow(@root_resource).to("resource1").with_headers({ "X-CustomHeader" => "custom value" }).get
|
21
|
+
end
|
22
|
+
|
23
|
+
When(/^I make a new request without headers$/) do
|
24
|
+
@requests = make_requests(ALL_METHODS, @api.follow(@root_resource).to("resource1"))
|
25
|
+
end
|
26
|
+
|
27
|
+
Then(/^the previous request's headers are not included$/) do
|
28
|
+
ALL_METHODS.each do |method|
|
29
|
+
headers = @requests[method].headers
|
30
|
+
expect(headers.keys.map(&:downcase)).not_to include("x-customheader")
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
When(/^I supply (.*)$/) do |parameters|
|
4
|
+
requester = @api.follow(@root_resource).to("resource1").with_parameters(JSON.parse(parameters))
|
5
|
+
@requests = make_requests(ALL_METHODS, requester)
|
6
|
+
end
|
7
|
+
|
8
|
+
Then(/^the parameters are added to the URL as a RFC 3986 compliant (.*)$/) do |query_string|
|
9
|
+
ALL_METHODS.each do |method|
|
10
|
+
request_path = normalize_path(@requests[method].uri.to_s)
|
11
|
+
correct_query_string = normalize_path("/path?#{query_string}")[5..]
|
12
|
+
expect(request_path).to end_with(correct_query_string)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Given(/^an endpoint at (.*)$/) do |url|
|
17
|
+
@api = Halchemy::Api.new(BASE_URL)
|
18
|
+
@endpoint = @api.using_endpoint(url)
|
19
|
+
end
|
20
|
+
|
21
|
+
When(/^I provide (.*)$/) do |parameters|
|
22
|
+
parameters = JSON.parse(parameters)
|
23
|
+
@url = @endpoint.with_parameters(parameters).url
|
24
|
+
end
|
25
|
+
|
26
|
+
Then(/^the result is a (.*)$/) do |correct_url|
|
27
|
+
expect(@url).to end_with(correct_url)
|
28
|
+
end
|
29
|
+
|
30
|
+
And(/^I choose a parameters (.*)$/) do |list_style|
|
31
|
+
@api.parameters_list_style = list_style
|
32
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Given(/^a HAL resource with a link that is an RFC 6570 compliant (.*)$/) do |templated_href|
|
4
|
+
resource1_json = {
|
5
|
+
data: "some resource",
|
6
|
+
_links: {
|
7
|
+
self: { href: "/path/to/resource1" },
|
8
|
+
next: {
|
9
|
+
href: templated_href,
|
10
|
+
templated: true
|
11
|
+
}
|
12
|
+
}
|
13
|
+
}.to_json
|
14
|
+
stub_request(:any, /.*/).to_return(body: resource1_json, status: 200)
|
15
|
+
@api = Halchemy::Api.new BASE_URL
|
16
|
+
# @hal_resource = Halchemy::HalResource.new.merge! JSON.parse(resource1_json)
|
17
|
+
@hal_resource = @api.using_endpoint("/resource1").get
|
18
|
+
end
|
19
|
+
|
20
|
+
When(/^I follow that link and provide (.*)$/) do |template_values|
|
21
|
+
requester = @api.follow(@hal_resource).to("next").with_template_values(JSON.parse(template_values))
|
22
|
+
@requests = make_requests(ALL_METHODS, requester)
|
23
|
+
end
|
24
|
+
|
25
|
+
Then(/^the requested URL ends with the (.*)$/) do |correct_path|
|
26
|
+
ALL_METHODS.each do |method|
|
27
|
+
request_path = normalize_path(@requests[method].uri.to_s)
|
28
|
+
correct_path = normalize_path(correct_path)
|
29
|
+
expect(request_path).to eq(correct_path)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
When(/^the (.*) provided are missing one or more values$/) do |template_values|
|
34
|
+
requester = @api.follow(@hal_resource).to("next")
|
35
|
+
requester = requester.with_template_values(JSON.parse(template_values)) unless template_values == "-omit-"
|
36
|
+
@requests = make_requests(ALL_METHODS, requester)
|
37
|
+
end
|
38
|
+
|
39
|
+
Then(/^the constructed URL ends with the (.*)$/) do |correct_path|
|
40
|
+
ALL_METHODS.each do |method|
|
41
|
+
request_path = normalize_path(@requests[method].uri.to_s)
|
42
|
+
correct_path = normalize_path(correct_path)
|
43
|
+
expect(request_path).to eq(correct_path)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Given(/^a HAL resource that has a field which is a collection of objects in HAL format$/) do
|
4
|
+
@resource = Halchemy::HalResource.new.merge!(JSON.parse({
|
5
|
+
_items: [
|
6
|
+
{
|
7
|
+
field: "alpha",
|
8
|
+
_links: { self: { href: "/resource/alpha" } }
|
9
|
+
},
|
10
|
+
{
|
11
|
+
field: "beta",
|
12
|
+
_links: { self: { href: "/resource/beta" } }
|
13
|
+
},
|
14
|
+
{
|
15
|
+
field: "delta",
|
16
|
+
_links: { self: { href: "/resource/delta" } }
|
17
|
+
}
|
18
|
+
],
|
19
|
+
_links: { self: { href: "/resource" } }
|
20
|
+
}.to_json))
|
21
|
+
end
|
22
|
+
|
23
|
+
When(/^I iterate the items in that field's collection$/) do
|
24
|
+
@iterator = @resource.collection("_items")
|
25
|
+
@all_hal = @resource.collection("_items").reduce(false) do |all_hal, item|
|
26
|
+
all_hal || item.is_a?(Halchemy::HalResource)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Then(/^each item is a HAL resource$/) do
|
31
|
+
expect(@all_hal).to be true
|
32
|
+
end
|
33
|
+
|
34
|
+
When(/^I try to iterate as a collection a field in the resource that does not exist$/) do
|
35
|
+
@root_resource.collection("_items").reduce(false) { |all_hal, item| all_hal || item.is_a?(Halchemy::HalResource) }
|
36
|
+
rescue KeyError => e
|
37
|
+
@error = e
|
38
|
+
end
|
39
|
+
|
40
|
+
Then(/^it throws an exception telling me that the field does not exist$/) do
|
41
|
+
expect(@error).to be_a(KeyError)
|
42
|
+
expect(@error.message).to include("does not exist")
|
43
|
+
end
|
44
|
+
|
45
|
+
Given(/^a HAL resource with a non-collection field$/) do
|
46
|
+
@resource = Halchemy::HalResource.new.merge!(JSON.parse({
|
47
|
+
_id: "3a834-34f9f03-39b843",
|
48
|
+
_links: { self: { href: "/resource" } }
|
49
|
+
}.to_json))
|
50
|
+
end
|
51
|
+
|
52
|
+
When(/^I try to iterate as a collection a field that is not a collection$/) do
|
53
|
+
@resource.collection("_id").reduce(false) { |all_hal, item| all_hal || item.is_a?(Halchemy::HalResource) }
|
54
|
+
rescue TypeError => e
|
55
|
+
@error = e
|
56
|
+
end
|
57
|
+
|
58
|
+
Then(/^it throws an exception telling me that the field is not a collection$/) do
|
59
|
+
expect(@error).to be_a(TypeError)
|
60
|
+
expect(@error.message).to include("is not a collection")
|
61
|
+
end
|
62
|
+
|
63
|
+
Given(/^a HAL resource that has a field which is a collection, but not of HAL formatted objects$/) do
|
64
|
+
@resource = Halchemy::HalResource.new.merge!(JSON.parse({
|
65
|
+
_items: [
|
66
|
+
{
|
67
|
+
make: "Ford Mustang"
|
68
|
+
},
|
69
|
+
{
|
70
|
+
make: "Chrysler 300"
|
71
|
+
},
|
72
|
+
{
|
73
|
+
make: "RAM 1500"
|
74
|
+
}
|
75
|
+
],
|
76
|
+
_links: { self: { href: "/resource" } }
|
77
|
+
}.to_json))
|
78
|
+
end
|
79
|
+
|
80
|
+
When(/^I try to iterate the items in that field's collection$/) do
|
81
|
+
@resource.collection("_items").reduce(false) { |all_hal, item| all_hal || item.is_a?(Halchemy::HalResource) }
|
82
|
+
rescue TypeError => e
|
83
|
+
@error = e
|
84
|
+
end
|
85
|
+
|
86
|
+
Then(/^it throws an exception telling me collection contains non-HAL formatted objects$/) do
|
87
|
+
expect(@error).to be_a(TypeError)
|
88
|
+
expect(@error.message).to include("non-HAL")
|
89
|
+
end
|
data/bdd
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
cucumber ../../features/configurable/configure_base_url.feature:24 --publish-quiet
|
data/lib/halchemy/api.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cicphash"
|
4
|
+
require "httpx"
|
5
|
+
require "http_status_codes"
|
6
|
+
|
7
|
+
require_relative "requester"
|
8
|
+
require_relative "follower"
|
9
|
+
require_relative "error_handling"
|
10
|
+
require_relative "status_codes"
|
11
|
+
require_relative "resource"
|
12
|
+
require_relative "http_model"
|
13
|
+
require_relative "metadata"
|
14
|
+
|
15
|
+
module Halchemy
|
16
|
+
# This is the Halchemy::Api class, that is the main class for interacting with HAL-based APIs
|
17
|
+
class Api
|
18
|
+
attr_accessor :base_url, :headers, :error_handling, :parameters_list_style
|
19
|
+
|
20
|
+
def initialize(base_url = "http://localhost:2112", headers: {})
|
21
|
+
@base_url = base_url
|
22
|
+
configure
|
23
|
+
|
24
|
+
@headers.merge!(headers)
|
25
|
+
end
|
26
|
+
|
27
|
+
def root
|
28
|
+
using_endpoint("/", is_root: true)
|
29
|
+
end
|
30
|
+
|
31
|
+
def using_endpoint(target, is_root: false)
|
32
|
+
if is_root
|
33
|
+
ReadOnlyRequester.new(self, target)
|
34
|
+
else
|
35
|
+
Requester.new(self, target)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param [Hash[String, string]] headers
|
40
|
+
def add_headers(headers)
|
41
|
+
@headers.merge!(headers)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [Array[String] headers
|
45
|
+
def remove_headers(header_keys)
|
46
|
+
header_keys.map { |key| @headers.delete(key) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def request(method, target, headers = nil, data = nil)
|
50
|
+
url = build_url(target)
|
51
|
+
|
52
|
+
request_headers = headers.nil? ? @headers : @headers.merge(headers)
|
53
|
+
request = HttpModel::Request.new(method, url, data, request_headers)
|
54
|
+
|
55
|
+
http = HTTPX.with(headers: request_headers)
|
56
|
+
result = http.public_send(method, url, body: data)
|
57
|
+
|
58
|
+
raise_for_errors(result)
|
59
|
+
build_resource(request, result)
|
60
|
+
end
|
61
|
+
|
62
|
+
def follow(resource)
|
63
|
+
Follower.new(self, resource)
|
64
|
+
end
|
65
|
+
|
66
|
+
def optimistic_concurrency_header(resource)
|
67
|
+
etag = resource._halchemy.response.headers["Etag"] || resource[@etag_field]
|
68
|
+
etag.nil? ? {} : { "If-Match" => etag }
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def configure
|
74
|
+
@headers = CICPHash.new.merge!({
|
75
|
+
"Authorization" => "Basic cm9vdDpwYXNzd29yZA==",
|
76
|
+
"Content-type" => "application/json",
|
77
|
+
"Accept" => "application/hal+json, application/json;q=0.9, */*;q=0.8"
|
78
|
+
})
|
79
|
+
@error_handling = ErrorHandling.new
|
80
|
+
@parameters_list_style = "repeat_key"
|
81
|
+
@etag_field = "_etag"
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_url(target)
|
85
|
+
return target if target.start_with?("http")
|
86
|
+
|
87
|
+
[@base_url.chomp("/"), target.sub(%r{\A/+}, "")].join("/")
|
88
|
+
end
|
89
|
+
|
90
|
+
def raise_for_errors(result)
|
91
|
+
if @error_handling.raise_for_network_errors && result.error.instance_of?(StandardError)
|
92
|
+
raise HttpError, result.error.message
|
93
|
+
end
|
94
|
+
|
95
|
+
return unless result.respond_to?(:status)
|
96
|
+
return unless settings_include_status_code?(@error_handling.raise_for_status_codes, result.status)
|
97
|
+
|
98
|
+
raise HttpError, "Status code #{result.status} matches \"#{@error_handling.raise_for_status_codes}\""
|
99
|
+
end
|
100
|
+
|
101
|
+
# @param [HttpModel::Request] request
|
102
|
+
# @param [Object] result
|
103
|
+
# @return [Halchemy::Resource | Halchemy::HalResource]
|
104
|
+
def build_resource(request, result)
|
105
|
+
# result.request.url, result.request.headers, result.request.body
|
106
|
+
response = build_response(result)
|
107
|
+
json = parse_body(result)
|
108
|
+
|
109
|
+
resource = if json.nil?
|
110
|
+
Resource.new
|
111
|
+
elsif HalResource.hal?(json)
|
112
|
+
HalResource.new.merge! json
|
113
|
+
else
|
114
|
+
Resource.new.merge! json
|
115
|
+
end
|
116
|
+
|
117
|
+
resource.tap { |r| r._halchemy = Metadata.new(request, response, result.error) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_body(result)
|
121
|
+
body = extract_body(result)
|
122
|
+
return body if body.is_a?(Hash)
|
123
|
+
|
124
|
+
begin
|
125
|
+
json = JSON.parse(body.to_s) unless body.is_a?(Hash) || body.nil?
|
126
|
+
rescue JSON::ParserError
|
127
|
+
json = nil
|
128
|
+
end
|
129
|
+
json
|
130
|
+
end
|
131
|
+
|
132
|
+
def extract_body(result)
|
133
|
+
begin
|
134
|
+
body = result.json if result.respond_to?("json")
|
135
|
+
rescue HTTPX::Error
|
136
|
+
body = result.to_s # Return raw text if it fails
|
137
|
+
end
|
138
|
+
body
|
139
|
+
end
|
140
|
+
|
141
|
+
def build_response(result)
|
142
|
+
status = result.respond_to?(:status) ? result.status : 0
|
143
|
+
reason = status.positive? ? HTTPStatusCodes::MAP[status] : "Did not receive a response from the server"
|
144
|
+
headers = result.headers if result.respond_to?(:headers)
|
145
|
+
body = result.body if result.respond_to?(:body)
|
146
|
+
|
147
|
+
HttpModel::Response.new(status, reason, headers, body)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Halchemy
|
4
|
+
# A container for the settings which govern how errors are handled.
|
5
|
+
class ErrorHandling
|
6
|
+
attr_accessor :raise_for_network_errors, :raise_for_status_codes
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@raise_for_network_errors = true
|
10
|
+
@raise_for_status_codes = nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Halchemy
|
4
|
+
# Provides the way to navigate from a resource by following to a link relation
|
5
|
+
class Follower
|
6
|
+
def initialize(api, resource)
|
7
|
+
@api = api
|
8
|
+
@resource = resource
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param [String] rel
|
12
|
+
def to(rel)
|
13
|
+
unless @resource["_links"].key?(rel)
|
14
|
+
raise KeyError, "#{@resource.class} does not have a link relation named #{rel}"
|
15
|
+
end
|
16
|
+
|
17
|
+
Requester.new(@api, [@resource, rel])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Halchemy
|
4
|
+
module HttpModel
|
5
|
+
# frozen_string_literal: true
|
6
|
+
|
7
|
+
# Provides essential details about the request that was made which resulted in the Resource
|
8
|
+
# this is attached to
|
9
|
+
class Request
|
10
|
+
attr_accessor :method, :url, :headers, :body
|
11
|
+
|
12
|
+
def initialize(method, url, headers, body = nil)
|
13
|
+
@method = method
|
14
|
+
@url = url
|
15
|
+
@headers = headers
|
16
|
+
@body = body
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"#{@method.to_s.upcase} #{@url}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Provides HTTP metadata that came along with the response resulting in the Resource
|
25
|
+
# this is attached to
|
26
|
+
class Response
|
27
|
+
attr_accessor :status_code, :reason, :headers, :body, :error
|
28
|
+
|
29
|
+
def initialize(status_code, reason, headers, body = nil)
|
30
|
+
@status_code = status_code
|
31
|
+
@reason = reason
|
32
|
+
@headers = headers
|
33
|
+
@body = body
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_s
|
37
|
+
"#{@status_code} #{@reason}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Halchemy
|
4
|
+
# Allows a Resource to act first as a Hash, by keeping all HTTP related info in resource._halchemy
|
5
|
+
class Metadata
|
6
|
+
attr_reader :request, :response, :error
|
7
|
+
|
8
|
+
def initialize(request, response, error)
|
9
|
+
@request = request
|
10
|
+
@response = response
|
11
|
+
@error = error
|
12
|
+
end
|
13
|
+
|
14
|
+
def raise_for_status_codes(settings = ">399")
|
15
|
+
return unless settings_include_status_code?(settings, @response.status_code)
|
16
|
+
|
17
|
+
raise HttpError, "Status code #{@response.status_code} matches \"#{settings}\""
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|