Skip to content

Commit

Permalink
feat: add hal client
Browse files Browse the repository at this point in the history
  • Loading branch information
bethesque committed Jun 4, 2018
1 parent 6e1d247 commit 4d93bf9
Show file tree
Hide file tree
Showing 6 changed files with 519 additions and 0 deletions.
78 changes: 78 additions & 0 deletions lib/pact_broker/client/hal/entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
require 'uri'
require 'delegate'
require 'pact_broker/client/hal/link'

module PactBroker
module Client
module Hal
class Entity
def initialize(data, http_client, response = nil)
@data = data
@links = (@data || {}).fetch("_links", {})
@client = http_client
@response = response
end

def get(key, *args)
_link(key).get(*args)
end

def post(key, *args)
_link(key).post(*args)
end

def put(key, *args)
_link(key).put(*args)
end

def can?(key)
@links.key? key.to_s
end

def follow(key, http_method, *args)
Link.new(@links[key].merge(method: http_method), @client).run(*args)
end

def _link(key)
if @links[key]
Link.new(@links[key], @client)
else
nil
end
end

def success?
true
end

def response
@response
end

def fetch(key)
@links[key]
end

def method_missing(method_name, *args, &block)
if @data.key?(method_name.to_s)
@data[method_name.to_s]
elsif @links.key?(method_name)
Link.new(@links[method_name], @client).run(*args)
else
super
end
end

def respond_to_missing?(method_name, include_private = false)
@data.key?(method_name) || @links.key?(method_name)
end
end

class ErrorEntity < Entity
def success?
false
end
end
end
end
end
71 changes: 71 additions & 0 deletions lib/pact_broker/client/hal/http_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require 'pact_broker/client/retry'

module PactBroker
module Client
module Hal
class HttpClient
attr_accessor :username, :password

def initialize options
@username = options[:username]
@password = options[:password]
end

def get href, params = {}, headers = {}
query = params.collect{ |(key, value)| "#{CGI::escape(key)}=#{CGI::escape(value)}" }.join("&")
uri = URI(href)
uri.query = query
perform_request(create_request(uri, 'Get', nil, headers), uri)
end

def put href, body = nil, headers = {}
uri = URI(href)
perform_request(create_request(uri, 'Put', body, headers), uri)
end

def post href, body = nil, headers = {}
uri = URI(href)
perform_request(create_request(uri, 'Post', body, headers), uri)
end

def create_request uri, http_method, body = nil, headers = {}
request = Net::HTTP.const_get(http_method).new(uri.to_s)
request['Content-Type'] = "application/json" if ['Post', 'Put', 'Patch'].include?(http_method)
request['Accept'] = "application/hal+json"
headers.each do | key, value |
request[key] = value
end

request.body = body if body
request.basic_auth username, password if username
request
end

def perform_request request, uri
options = {:use_ssl => uri.scheme == 'https'}
response = Retry.until_true do
Net::HTTP.start(uri.host, uri.port, :ENV, options) do |http|
http.request request
end
end
Response.new(response)
end

class Response < SimpleDelegator
def body
bod = __getobj__().body
if bod && bod != ''
JSON.parse(bod)
else
nil
end
end

def success?
__getobj__().code.start_with?("2")
end
end
end
end
end
end
67 changes: 67 additions & 0 deletions lib/pact_broker/client/hal/link.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require 'uri'
require 'delegate'

module PactBroker
module Client
module Hal
class Link
attr_reader :request_method, :href

def initialize(attrs, http_client)
@attrs = attrs
@request_method = attrs.fetch(:method, :get).to_sym
@href = attrs.fetch('href')
@http_client = http_client
end

def run(payload = nil)
response = case request_method
when :get
get(payload)
when :put
put(payload)
when :post
post(payload)
end
end

def get(payload = {}, headers = {})
wrap_response(@http_client.get(href, payload, headers))
end

def put(payload = nil, headers = {})
wrap_response(@http_client.put(href, payload ? JSON.dump(payload) : nil, headers))
end

def post(payload = nil, headers = {})
wrap_response(@http_client.post(href, payload ? JSON.dump(payload) : nil, headers))
end

def expand(params)
expanded_url = expand_url(params, href)
new_attrs = @attrs.merge('href' => expanded_url)
Link.new(new_attrs, @http_client)
end

private

def wrap_response(http_response)
require 'pact_broker/client/hal/entity' # avoid circular reference
if http_response.success?
Entity.new(http_response.body, @http_client, http_response)
else
ErrorEntity.new(http_response.body, @http_client, http_response)
end
end

def expand_url(params, url)
new_url = url
params.each do | key, value |
new_url = new_url.gsub('{' + key.to_s + '}', value)
end
new_url
end
end
end
end
end
93 changes: 93 additions & 0 deletions spec/lib/pact_broker/client/hal/entity_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
require 'pact_broker/client/hal/entity'
require 'pact_broker/client/hal/http_client'

module PactBroker::Client
module Hal
describe Entity do
let(:http_client) do
instance_double('Pact::Hal::HttpClient', post: provider_response)
end

let(:provider_response) do
double('response', body: provider_hash, success?: true)
end

let(:provider_hash) do
{
"name" => "Provider"
}
end
let(:pact_hash) do
{
"name" => "a name",

"_links" => {
"pb:provider" => {
"href" => "http://provider"
},
"pb:version-tag" => {
"href" => "http://provider/version/{version}/tag/{tag}"
}
}
}
end

subject(:entity) { Entity.new(pact_hash, http_client) }

it "delegates to the properties in the data" do
expect(subject.name).to eq "a name"
end

describe "post" do
let(:post_provider) { entity.post("pb:provider", {'some' => 'data'} ) }

it "executes an http request" do
expect(http_client).to receive(:post).with("http://provider", '{"some":"data"}', {})
post_provider
end

it "returns the entity for the relation" do
expect(post_provider).to be_a(Entity)
end

context "with template params" do
let(:post_provider) { entity._link("pb:version-tag").expand(version: "1", tag: "prod").post({'some' => 'data'} ) }

it "posts to the expanded URL" do
expect(http_client).to receive(:post).with("http://provider/version/1/tag/prod", '{"some":"data"}', {})
post_provider
end
end
end

describe "can?" do
context "when the relation exists" do
it "returns true" do
expect(subject.can?('pb:provider')).to be true
end
end

context "when the relation does not exist" do
it "returns false" do
expect(subject.can?('pb:consumer')).to be false
end
end
end

describe 'fetch' do
context 'when the key exist' do
it 'returns fetched value' do
expect(subject.fetch('pb:provider')).to be do
{href: 'http://provider'}
end
end
end
context "when the key doesn't not exist" do
it 'returns nil' do
expect(subject.fetch('i-dont-exist')).to be nil
end
end
end
end
end
end
Loading

0 comments on commit 4d93bf9

Please sign in to comment.