service-client 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ - rbx-19mode
7
+ notifications:
8
+ email: false
9
+ campfire: "rumspringa:c0a20e6caab984176e5c72f84241b5f419662257@504871"
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # 0.0.14 / 2013-08-10
2
+
3
+ * Adds a way to specify Faraday builder
4
+
5
+ # 0.0.13 / 2013-07-03
6
+
7
+ * Adds error message to ServiceErrors
8
+
9
+ # 0.0.12
10
+
11
+ * Fixes GET request parameter handling in Faraday adapter
12
+ * Adds JSON Content-Type to each request
13
+
14
+ # 0.0.11
15
+
16
+ * Escapes hashes and arrays in GET query parameters properly
17
+
18
+ # 0.0.10
19
+
20
+ * Makes GET requests use query parameters instead of a JSON body
21
+
22
+ # 0.0.9
23
+
24
+ - Does not error out on empty response bodies anymore
25
+
26
+ # 0.0.8 - 07th November 2012
27
+
28
+ - Handles 304 - Not Modified responses correctly
29
+
30
+ # 0.0.7 - 03rd October 2012
31
+
32
+ - Fixes the wrongly named Authorization HTTP header
33
+
34
+ # 0.0.6 - 03rd October 2012
35
+
36
+ - Moves the token to be passed in to each request instead of per client instance
37
+
38
+ # 0.0.5 - 03rd October 2012
39
+
40
+ - Adds OAuth authentication
41
+
42
+ # 0.0.4 - 14th August 2012
43
+
44
+ - Fixed bug that prevented the default adapter to be used correctly.
45
+
46
+ # 0.0.3 - 13th August 2012
47
+
48
+ The start.
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in service-client.gemspec
4
+ gemspec
5
+
6
+ gem 'rake'
7
+
8
+ group :development, :test do
9
+ gem 'realweb'
10
+ gem 'guard-minitest'
11
+ gem 'guard-bundler'
12
+ gem 'rack-test'
13
+ end
14
+
15
+ platforms :jruby do
16
+ gem "jruby-openssl"
17
+ end
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ guard :bundler do
2
+ watch('Gemfile')
3
+ end
4
+
5
+ guard 'minitest' do
6
+ watch(%r|^spec/(.*)_spec\.rb|)
7
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
8
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "spec/client_spec.rb" }
9
+ watch(%r|^spec/spec_helper\.rb|) { "spec" }
10
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Thorben Schröder
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Service::Client
2
+
3
+ Service::Client is a generic client gem to access our services. It is the base for explicit clients for each service so that those explicit clients are easy and fast to implement and maintain.
4
+
5
+ This gem should not be used as an "end-user solution".
6
+
7
+ ## Usage
8
+
9
+ ### Client creation
10
+
11
+ Each client must be instantiated with a base URL to a service.
12
+
13
+ ```ruby
14
+ client = Service::Client.new('http://some=service.example.com/')
15
+ ```
16
+
17
+ ### Creating routes
18
+
19
+ A client route describes a single REST HTTP end-point at the service.
20
+
21
+ ```ruby
22
+ client.urls.add(:author, :post, '/authors/')
23
+ client.urls.add(:author, :get, '/authors/:id:')
24
+ client.urls.add(:review, :post, '/author/:author_id:/books/:book_id:')
25
+ ```
26
+
27
+ ### Requests
28
+
29
+ ```ruby
30
+ client.post(client.urls.author, token, name: 'Peter Lustig')
31
+ client.get(client.urls.author(123), token)
32
+ client.post(client.urls.review(author_id: 123, book_id: 456), token, name: 'Ronald Review', comment: 'This book is the bomb!')
33
+ ```
34
+
35
+ Each ``Service::Client`` instance supports the ``get``, ``post``, ``put`` and ``delete`` methods. They all share the same method signature of:
36
+
37
+ ```ruby
38
+ client.method(URL, TOKEN, BODY_HASH)
39
+ ```
40
+
41
+ The URL is a relative URL to the base url the client has been created with. TOKEN is an OAuth token to authenticat the request. The BODY_HASH is any Ruby hash. The hash becomes the body of the HTTP request after it has been dumped to JSON.
42
+
43
+ The ``client.urls`` method makes all the created routes available as an easy to use URL builder. The URL builder takes zero arguments when the URL does not have any arguments. It can also take an array of the URL paramters in their respective order or a hash to built the URL by it's named parameters.
44
+
45
+ ### Response
46
+
47
+ #### Success
48
+
49
+ If the HTTP response comes with a 200 status code, the client returns a ``Service::Client::Response`` object. That allows you to query for the JSON decoded data that came along with the body of the HTTP response. If the body was empty that data is just ``true``. You can also reach for the raw HTTP response:
50
+
51
+ ```ruby
52
+ response = client.get(client.urls.author(123), token)
53
+ puts "retrieved a book written by #{response.data['name']} with an HTTP status code of #{response.raw.status}"
54
+ ```
55
+
56
+ #### Redirections
57
+
58
+ For any redirecting responses the client raises a ``Service::Client::Redirection`` which can be queried for the redirection location:
59
+
60
+ ```ruby
61
+ begin
62
+ client.get(client.urls.author(123), token)
63
+ rescue Client::Service::Redirection => redirection
64
+ puts "The client has been redirected to: #{redirection.location}"
65
+ end
66
+ ```
67
+
68
+ #### Errors
69
+
70
+ For any other response codes the client raises a ``Service::Client::ServiceError`` if the response body was a JSON encoded object with an ``error`` key on the root level.
71
+
72
+ ```ruby
73
+ begin
74
+ client.get(client.urls.author(678), token)
75
+ rescue Client::Service::ServiceError => e
76
+ puts "A service error has occured. Error description: #{e.error}"
77
+ end
78
+ ```
79
+
80
+ If the body was not JSON encoded at all or did not include the ``error`` key on the root level a ``Service::Client::ResponseError`` is raised.
81
+
82
+ ```ruby
83
+ begin
84
+ client.get(client.urls.author(678), token)
85
+ rescue Client::Service::Error => e
86
+ puts "An error has occured. Error code: #{e.response.status} Error body: #{e.response.body}"
87
+ end
88
+ ```
89
+
90
+ ### Raw HTTP requests
91
+
92
+ The client exposes an interface that can be used to issue raw HTTP requests to any URL relative to the given base URL at creation time.
93
+
94
+ ```ruby
95
+ response = client.raw.post('/authors/123/books', JSON.dump({title: 'The guide to a higher enlightment', isbn: '1234567'}))
96
+ ```
97
+
98
+ The raw client supports the ``get``, ``post``, ``put`` and ``delete`` methods. They all share the same method signature of:
99
+
100
+ ```ruby
101
+ client.raw.method(URL, BODY, OPTIONS)
102
+ ```
103
+
104
+ The options are a hash. Possible arguments are:
105
+
106
+ * **``headers``**: A hash (string:string) to set the request headers.
107
+
108
+ ### Raw responses
109
+
110
+ Raw requests return a ``Rack::Response`` object that makes it easy to access the status code, headers and body of the response.
111
+
112
+ ```ruby
113
+ response = client.raw.get('/author/123/books')
114
+ response.body # "[{title: 'The guide to a higher enlightment', isbn: '1234567', id: 456}, {title: 'Some book', isbn: '23464527', id: 789}]"
115
+ response.status # 200
116
+ response.header # {"Some-Header" => "Some Value", "Another-Header" => "Another Value"}
117
+ ```
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.pattern = "spec/**/*_spec.rb"
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,103 @@
1
+ require "service-client/version"
2
+ require "service-client/error"
3
+ require "service-client/routing_error"
4
+ require "service-client/service_error"
5
+ require "service-client/response_error"
6
+ require "service-client/raw_interface"
7
+ require "service-client/adapter/faraday"
8
+ require "service-client/url_pattern"
9
+ require "service-client/bound_route"
10
+ require "service-client/route"
11
+ require "service-client/route_collection"
12
+ require "service-client/base_response"
13
+ require "service-client/response"
14
+ require "service-client/redirection"
15
+
16
+ require 'json'
17
+ require 'cgi'
18
+
19
+ module Service
20
+ class Client
21
+ attr_reader :base_url
22
+
23
+ def initialize(base_url)
24
+ @base_url = base_url
25
+ end
26
+
27
+ def raw
28
+ @raw_interface ||= RawInterface.new(self)
29
+ end
30
+
31
+ def routes
32
+ @routes ||= RouteCollection.new
33
+ end
34
+ alias urls routes
35
+
36
+ def get(bound_route, token, body_hash = nil)
37
+ request(:get, token, bound_route, body_hash)
38
+ end
39
+
40
+ def put(bound_route, token, body_hash = nil)
41
+ request(:put, token, bound_route, body_hash)
42
+ end
43
+
44
+ def post(bound_route, token, body_hash = nil)
45
+ request(:post, token, bound_route, body_hash)
46
+ end
47
+
48
+ def delete(bound_route, token, body_hash = nil)
49
+ request(:delete, token, bound_route, body_hash)
50
+ end
51
+
52
+ protected
53
+ def request(method, token, bound_route, body_hash)
54
+ url = bound_route.url_for_method(method)
55
+
56
+ body = nil
57
+ if method == :get
58
+ url = append_body_hash_to_url(url, body_hash)
59
+ else
60
+ body = body_hash ? JSON.dump(body_hash) : ''
61
+ end
62
+
63
+ headers = {
64
+ 'AUTHORIZATION' => "Bearer #{token}",
65
+ 'Content-Type' => 'application/json'
66
+ }
67
+
68
+ raw_response = raw.request(method, url, body, headers: headers)
69
+ case raw_response.status
70
+ when 200, 201, 304
71
+ Response.new(raw_response)
72
+ when 301, 302, 303, 307
73
+ raise Redirection.new(raw_response)
74
+ else
75
+ error = nil
76
+ begin
77
+ error = JSON.parse(raw_response.body.first)['error']
78
+ rescue JSON::ParserError
79
+ # treat invalid JSON the same as non-present error field
80
+ end
81
+ if error
82
+ raise ServiceError.new(error)
83
+ else
84
+ raise ResponseError.new(raw_response)
85
+ end
86
+ end
87
+ end
88
+
89
+ def append_body_hash_to_url(url, body_hash)
90
+ return url if !body_hash || body_hash.empty?
91
+
92
+ uri = URI.parse(url)
93
+
94
+ if body_hash && !body_hash.empty?
95
+ uri.query += '&' if uri.query
96
+ uri.query ||= ''
97
+ uri.query += Faraday::Utils.build_nested_query(body_hash)
98
+ end
99
+
100
+ uri.to_s
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,61 @@
1
+ require 'faraday'
2
+ require 'uri'
3
+ require 'rack'
4
+
5
+ module Service
6
+ class Client::Adapter
7
+ class Faraday
8
+ def initialize(options = {})
9
+ @adapter = options.delete :adapter
10
+ @builder = options.delete :builder
11
+ end
12
+
13
+ def request(method, url, body, options)
14
+ uri = URI.parse(url)
15
+
16
+ connection = create_connection(uri)
17
+
18
+ response = send_request(connection, method, uri, body, options)
19
+
20
+ Rack::Response.new(response.body || '', response.status, response.headers)
21
+ end
22
+
23
+ protected
24
+ def send_request(connection, method, uri, body, options)
25
+ connection.send(method) do |request|
26
+ request.url path(uri)
27
+ request.body = body
28
+ if method == :get && uri.query
29
+ request.params = ::Faraday::Utils.parse_nested_query(uri.query)
30
+ end
31
+ request.headers = options[:headers] || {}
32
+ end
33
+ end
34
+
35
+ def path(uri)
36
+ "#{uri.path}"
37
+ end
38
+
39
+ def base_url(uri)
40
+ "#{uri.scheme}://#{auth(uri)}#{uri.host}:#{uri.port}"
41
+ end
42
+
43
+ def auth(uri)
44
+ (uri.user && uri.password) ? "#{uri.user}:#{uri.password}@" : ''
45
+ end
46
+
47
+ def create_connection(uri)
48
+ ::Faraday.new(:url => base_url(uri)) do |faraday|
49
+ # if this returns false it skips the adapter selection later
50
+ builder_response = @builder ? @builder.call(faraday) : true
51
+
52
+ if @adapter && builder_response
53
+ faraday.adapter *@adapter
54
+ else
55
+ faraday.adapter ::Faraday.default_adapter
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,9 @@
1
+ module Service::Client::BaseResponse
2
+ def initialize(raw_response)
3
+ @raw_response = raw_response
4
+ end
5
+
6
+ def raw
7
+ @raw_response
8
+ end
9
+ end
@@ -0,0 +1,40 @@
1
+ class Service::Client::BoundRoute
2
+ def initialize(route, args)
3
+ @route = route
4
+ @args = args || []
5
+ end
6
+
7
+ def url_for_method(method)
8
+ pattern = @route.pattern_for(method)
9
+
10
+ raise Service::Client::RoutingError.new("Method #{method} unsupported!") unless pattern
11
+
12
+ pattern.filled_with(options_for_pattern(pattern))
13
+ end
14
+
15
+ protected
16
+ def options_for_pattern(pattern)
17
+ if args_are_a_hash?
18
+ @args.first
19
+ else
20
+ options_for_pattern_from_args_array(pattern)
21
+ end
22
+ end
23
+
24
+ def args_are_a_hash?
25
+ @args.size == 1 && @args.first.kind_of?(Hash)
26
+ end
27
+
28
+ def options_for_pattern_from_args_array(pattern)
29
+ if @args.size != pattern.placeholders.size
30
+ raise Service::Client::RoutingError.new("Number of URL arguments does not match! Given: #{@args.inspect} Expected: #{pattern.placeholders.inspect}")
31
+ end
32
+
33
+ cloned_args = @args.clone
34
+ options = {}
35
+ pattern.placeholders.each do |placeholder|
36
+ options[placeholder] = cloned_args.shift
37
+ end
38
+ options
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ class Service::Client
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,48 @@
1
+ require 'uri'
2
+
3
+ module Service
4
+ class Client
5
+ class RawInterface
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def get(url, body, options)
11
+ request(:get, url, body, options)
12
+ end
13
+
14
+ def put(url, body, options)
15
+ request(:put, url, body, options)
16
+ end
17
+
18
+ def post(url, body, options)
19
+ request(:post, url, body, options)
20
+ end
21
+
22
+ def delete(url, body, options)
23
+ request(:delete, url, body, options)
24
+ end
25
+
26
+ def adapter
27
+ @adapter ||= default_adapter
28
+ end
29
+
30
+ def adapter=(new_adapter)
31
+ @adapter = new_adapter
32
+ end
33
+
34
+ def request(method, url, body, options)
35
+ adapter.request(method, absolutize_url(url), body, options)
36
+ end
37
+
38
+ protected
39
+ def default_adapter
40
+ Adapter::Faraday.new
41
+ end
42
+
43
+ def absolutize_url(url)
44
+ URI::join(@client.base_url, url).to_s
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ class Service::Client::Redirection < Exception
2
+ include Service::Client::BaseResponse
3
+
4
+ def location
5
+ raw.header.detect {|k,v| k.to_s.downcase == 'location'}.last
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+
3
+ class Service::Client::Response
4
+ include Service::Client::BaseResponse
5
+
6
+ attr_reader :data
7
+
8
+ def initialize(raw_response)
9
+ super(raw_response)
10
+ body = raw.body.first
11
+ @data = body.empty? ? true : JSON.parse(body)
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class Service::Client::ResponseError < Service::Client::Error
2
+ attr_reader :response
3
+
4
+ def initialize(response)
5
+ @response = response
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ class Service::Client::Route
2
+ def add_pattern(method, pattern)
3
+ patterns[method] = Service::Client::UrlPattern.new(pattern)
4
+ end
5
+
6
+ def bind(*args)
7
+ Service::Client::BoundRoute.new(self, args)
8
+ end
9
+
10
+ def pattern_for(method)
11
+ patterns[method]
12
+ end
13
+
14
+ protected
15
+ def patterns
16
+ @patterns ||= {}
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ class Service::Client::RouteCollection
2
+ def add(name, method, pattern)
3
+ name = name.to_sym
4
+
5
+ route = routes[name] ||= Service::Client::Route.new
6
+ route.add_pattern(method, pattern)
7
+ end
8
+
9
+ protected
10
+ def routes
11
+ @routes ||= {}
12
+ end
13
+
14
+ def method_missing(name, *args)
15
+ raise Service::Client::RoutingError.new("No route named #{name}") unless route = routes[name.to_sym]
16
+ route.bind(*args)
17
+ end
18
+ end
@@ -0,0 +1,2 @@
1
+ class Service::Client::RoutingError < Service::Client::Error
2
+ end
@@ -0,0 +1,8 @@
1
+ class Service::Client::ServiceError < Service::Client::Error
2
+ attr_reader :error
3
+
4
+ def initialize(error)
5
+ super(error)
6
+ @error = error
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ class Service::Client::UrlPattern
2
+ def initialize(pattern)
3
+ @pattern = pattern
4
+ @pattern.scan(/:([^:]+):/).each do |placeholder|
5
+ placeholders << placeholder.first.to_sym
6
+ end
7
+ end
8
+
9
+ def filled_with(options)
10
+ url = @pattern.clone
11
+ placeholders.each do |placeholder|
12
+ url.gsub!(/:#{Regexp.escape(placeholder.to_s)}:/, options[placeholder].to_s)
13
+ end
14
+ url
15
+ end
16
+
17
+ def placeholders
18
+ @placeholders ||= []
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Service
2
+ class Client
3
+ VERSION = "0.0.14"
4
+ end
5
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/service-client/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Thorben Schröder"]
6
+ gem.email = ["stillepost@gmail.com"]
7
+ gem.description = %q{Service::Client is a generic client gem to access our services. It is the base for explicit clients for each service so that those explicit clients are easy and fast to implement and maintain.}
8
+ gem.summary = %q{Service::Client is a generic client gem to access our services.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "service-client"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Service::Client::VERSION
17
+
18
+ gem.add_dependency 'faraday', '0.8.1'
19
+ gem.add_dependency 'json'
20
+ end
@@ -0,0 +1,73 @@
1
+ require_relative '../spec_helper'
2
+ require 'realweb'
3
+ require 'json'
4
+
5
+ describe Service::Client::Adapter::Faraday do
6
+ before do
7
+ $__service_client_test_server ||= RealWeb.start_server_in_thread(File.expand_path("../test_server.ru", __FILE__))
8
+ @server = $__service_client_test_server
9
+ @url = @server.base_uri.to_s
10
+ @adapter = Service::Client::Adapter::Faraday.new
11
+ end
12
+
13
+ after do
14
+ unless $__service_client_stop_defined
15
+ self.class.class_eval do
16
+ at_exit do
17
+ puts "Shutting down server"
18
+ $__service_client_test_server.stop
19
+ end
20
+ end
21
+ end
22
+ $__service_client_stop_defined = true
23
+ end
24
+
25
+ headers = {
26
+ 'USER-AGENT' => 'service-client-spec',
27
+ 'REFERER' => 'http://super.example.com/'
28
+ }
29
+ body = 'This is a test'
30
+ [:get, :put, :post, :delete].each do |method|
31
+ it "sends correct #{method} requests" do
32
+ response = @adapter.request(method, @url, body, {headers: headers})
33
+ response.status.must_equal 200
34
+ request = JSON.parse(response.body.first)
35
+ request['body'].must_equal body
36
+ request['method'].must_equal method.to_s.upcase
37
+ headers.each do |key, value|
38
+ request['headers'][key].must_equal value
39
+ end
40
+ end
41
+ end
42
+
43
+ it "can use other Faraday adapters" do
44
+ ran = false
45
+ app = lambda {|env| ran = true; [200, {'Content-Type' => 'text/html'}, ["ran"]]}
46
+ adapter = Service::Client::Adapter::Faraday.new(adapter: [:rack, app])
47
+ adapter.request(:get, 'http://example.com/adapter_change', '', {})
48
+ ran.must_equal true
49
+ end
50
+
51
+ describe 'faraday builder' do
52
+ it 'can be used' do
53
+ ran = false
54
+ builder_called = false
55
+ app = lambda {|env| ran = true; [200, {'Content-Type' => 'text/html'}, ["ran"]]}
56
+ adapter = Service::Client::Adapter::Faraday.new(adapter: [:rack, app], builder: lambda {|faraday| builder_called = true})
57
+ adapter.request(:get, 'http://example.com/adapter_change', '', {})
58
+ ran.must_equal true
59
+ builder_called.must_equal true
60
+ end
61
+
62
+ it 'can skip the adapter call' do
63
+ ran = false
64
+ builder_called = false
65
+ app = lambda {|env| ran = true; [200, {'Content-Type' => 'text/html'}, ["ran"]]}
66
+ adapter = Service::Client::Adapter::Faraday.new(adapter: [:rack, app], builder: lambda {|faraday| builder_called = true; false})
67
+ adapter.request(:get, 'http://example.com/adapter_change', '', {})
68
+ # must be false because the rack adapter is not used as the builder returns false
69
+ ran.must_equal false
70
+ builder_called.must_equal true
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+
3
+ run lambda {|env|
4
+ request = Rack::Request.new(env)
5
+ method = request.request_method
6
+ headers = Hash[env.select {|k,v| k =~ /^HTTP_/}.map {|k,v| [k.gsub(/^HTTP_/, '').gsub('_', '-'), v]}]
7
+ body = request.body.read
8
+ [200, { 'Content-Type' => 'application/json' }, [JSON.dump(
9
+ method: method,
10
+ headers: headers,
11
+ body: body
12
+ )]]
13
+ }
@@ -0,0 +1,195 @@
1
+ require_relative './spec_helper'
2
+ require 'json'
3
+
4
+ TOKEN = '123'
5
+
6
+ def must_send_request(method, url, json = nil, options = {}, &blck)
7
+ @client.raw.adapter.expect :request, Rack::Response.new('', 200, {}), [method, url, json ? JSON.dump(json) : nil, options]
8
+ yield
9
+ @client.raw.adapter.verify
10
+ @client.raw.adapter = MiniTest::Mock.new
11
+ end
12
+
13
+ def must_raise_response_error(body)
14
+ raw_response = Rack::Response.new(body, 500, {})
15
+ @client.raw.adapter.expect :request, raw_response, [:get, 'http://example.com/authors/123', body, {}]
16
+ begin
17
+ @client.get(@client.urls.author(123), TOKEN)
18
+ flunk "Must raise Service::Client::ResponseError but didn't!"
19
+ rescue Service::Client::ResponseError => e
20
+ e.response.status.must_equal 500
21
+ e.response.body.must_equal [body]
22
+ end
23
+ end
24
+
25
+ describe Service::Client do
26
+ before do
27
+ @client = Service::Client.new('http://example.com')
28
+ @client.raw.adapter = MiniTest::Mock.new
29
+ end
30
+
31
+ describe "raw interface" do
32
+ it "is exposed" do
33
+ @client.raw.must_be_instance_of Service::Client::RawInterface
34
+ end
35
+
36
+ args = ['/bla', :body, :options]
37
+ url, body, options = args
38
+ [:get, :put, :post, :delete].each do |method|
39
+ it "passes #{method} requests through to the HTTP adapter" do
40
+ @client.raw.adapter.expect :request, true, [method, 'http://example.com/bla', body, options]
41
+ @client.raw.send(method, *args)
42
+ @client.raw.adapter.verify
43
+ end
44
+ end
45
+ end
46
+
47
+ it "uses the faraday adapter as a default" do
48
+ Service::Client.new('http://example.com').raw.adapter.must_be_instance_of Service::Client::Adapter::Faraday
49
+ end
50
+
51
+ describe "high level interface" do
52
+ before do
53
+ @client.urls.add(:author, :post, '/authors/')
54
+ @client.urls.add(:author, :get, '/authors/:id:')
55
+ @client.urls.add(:author_with_query, :get, '/authors/:id:/another-fixed-part?blub=1')
56
+ @client.urls.add(:review, :post, '/authors/:author_id:/books/:book_id:')
57
+ end
58
+
59
+ it "calls the right url with the right method after adding it" do
60
+ must_send_request(:post, 'http://example.com/authors/', {name: 'Peter Lustig'}, headers: {'AUTHORIZATION' => "Bearer #{TOKEN}", 'Content-Type' => 'application/json'}) do
61
+ @client.post(@client.urls.author, TOKEN, name: 'Peter Lustig')
62
+ end
63
+
64
+ must_send_request(:get, 'http://example.com/authors/123', nil, headers: {'AUTHORIZATION' => "Bearer #{TOKEN}", 'Content-Type' => 'application/json'}) do
65
+ @client.get(@client.urls.author(123), TOKEN)
66
+ end
67
+
68
+ must_send_request(:post, 'http://example.com/authors/123/books/456', {name: 'Ronald Review', comment: 'This book is the bomb!'}, headers: {'AUTHORIZATION' => "Bearer #{TOKEN}", 'Content-Type' => 'application/json'}) do
69
+ @client.post(@client.urls.review(author_id: 123, book_id: 456), TOKEN, name: 'Ronald Review', comment: 'This book is the bomb!')
70
+ end
71
+
72
+ must_send_request(:post, 'http://example.com/authors/123/books/456', {name: 'Ronald Review', comment: 'This book is the bomb!'}, headers: {'AUTHORIZATION' => "Bearer #{TOKEN}", 'Content-Type' => 'application/json'}) do
73
+ @client.post(@client.urls.review(123, 456), TOKEN, name: 'Ronald Review', comment: 'This book is the bomb!')
74
+ end
75
+ end
76
+
77
+ it "uses query parameters instead of JSON bodies for GET requests" do
78
+ must_send_request(:get, 'http://example.com/authors/123?some=arguments&are=cool%26not%3Dbody&array%5B%5D=12&array%5B%5D=gla&hash%5Bblub%5D=123&hash%5Bbla%5D=moep', nil, headers: {'AUTHORIZATION' => "Bearer #{TOKEN}", 'Content-Type' => 'application/json'}) do
79
+ @client.get(@client.urls.author(123), TOKEN, {some: 'arguments', are: "cool&not=body", array: ["12", 'gla'], hash: {"blub" => "123", "bla" => "moep"}})
80
+ end
81
+
82
+ must_send_request(:get, 'http://example.com/authors/123/another-fixed-part?blub=1&some=arguments&are=cool%26not%3Dbody', nil, headers: {'AUTHORIZATION' => "Bearer #{TOKEN}", 'Content-Type' => 'application/json'}) do
83
+ @client.get(@client.urls.author_with_query(123), TOKEN, {some: 'arguments', are: "cool&not=body"})
84
+ end
85
+ end
86
+
87
+ it "raises an error when no route for a given method/resource combination exist" do
88
+ lambda {
89
+ @client.post(@client.urls.author(123), TOKEN)
90
+ }.must_raise Service::Client::RoutingError
91
+
92
+ lambda {
93
+ @client.get(@client.urls.author, TOKEN)
94
+ }.must_raise Service::Client::RoutingError
95
+
96
+ lambda {
97
+ @client.get(@client.urls.review(123, 456), TOKEN, name: 'Ronald Review', comment: 'This book is the bomb!')
98
+ }.must_raise Service::Client::RoutingError
99
+
100
+ lambda {
101
+ @client.get(@client.urls.comments, TOKEN)
102
+ }.must_raise Service::Client::RoutingError
103
+ end
104
+
105
+ describe "responses" do
106
+ describe "successful" do
107
+ statuses = [200, 201]
108
+ statuses.each do |status|
109
+ describe "with status #{status}" do
110
+ before do
111
+ @body = JSON.dump(name: 'Peter Lustig', age: 76)
112
+ @headers = {}
113
+ raw_response = Rack::Response.new(@body, status, @headers)
114
+ @client.raw.adapter.expect :request, raw_response, [:get, 'http://example.com/authors/123', '', {}]
115
+ @response = @client.get(@client.urls.author(123), TOKEN)
116
+ end
117
+
118
+ it "has the raw data" do
119
+ @response.raw.status.must_equal status
120
+ @response.raw.body.must_equal [@body]
121
+ @headers.each do |key, value|
122
+ @response.raw.header[key].must_equal value
123
+ end
124
+ end
125
+
126
+ it "parses them" do
127
+ @response.data['name'].must_equal 'Peter Lustig'
128
+ @response.data['age'].must_equal 76
129
+ end
130
+
131
+ it "returns true for data if the response is empty" do
132
+ raw_response = Rack::Response.new('', status, {})
133
+ @client.raw.adapter.expect :request, raw_response, [:get, 'http://example.com/authors/456', '', {}]
134
+ @response = @client.get(@client.urls.author(456), TOKEN)
135
+ @response.data.must_equal true
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ describe "redirections" do
142
+ statuses = [301, 302, 303, 307]
143
+ statuses.each do |status|
144
+ it "raises a Service::Client::Redirection with the location for HTTP status #{status}" do
145
+ @client.raw.adapter.expect :request, Rack::Response.new('', status, {Location: 'http://example.com/somewhere/else'}), [:get, 'http://example.com/authors/123', '', {}]
146
+
147
+ begin
148
+ @client.get(@client.urls.author(123), TOKEN)
149
+ flunk "Must raise Service::Client::Redirection but didn't!"
150
+ rescue Service::Client::Redirection => e
151
+ e.location.must_equal 'http://example.com/somewhere/else'
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ describe "errors" do
158
+ describe "when error field is present" do
159
+ before do
160
+ @body = JSON.dump(error: 'This is why!')
161
+ end
162
+
163
+ error_states = [400, 401, 403, 404, 500]
164
+ error_states.each do |error_state|
165
+ it "raises an Service::Client::ServiceError for HTTP status code #{error_state}" do
166
+ raw_response = Rack::Response.new(@body, error_state, {})
167
+ @client.raw.adapter.expect :request, raw_response, [:get, 'http://example.com/authors/123', @body, {}]
168
+
169
+ begin
170
+ @client.get(@client.urls.author(123), TOKEN)
171
+ flunk "Must raise Service::Client::ServiceError but didn't!"
172
+ rescue Service::Client::ServiceError => e
173
+ e.error.must_equal 'This is why!'
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ describe "raises Service::Client::ResponseError when body is" do
180
+ it "empty" do
181
+ must_raise_response_error('')
182
+ end
183
+
184
+ it "invalid JSON" do
185
+ must_raise_response_error('some stuff but not json')
186
+ end
187
+
188
+ it "valid JSON but has no error field" do
189
+ must_raise_response_error(JSON.dump(missing: 'error', field: true))
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,5 @@
1
+ Bundler.setup
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require 'service-client'
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: service-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.14
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Thorben Schröder
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: faraday
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.8.1
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: Service::Client is a generic client gem to access our services. It is
47
+ the base for explicit clients for each service so that those explicit clients are
48
+ easy and fast to implement and maintain.
49
+ email:
50
+ - stillepost@gmail.com
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - .rvmrc
57
+ - .travis.yml
58
+ - CHANGELOG.md
59
+ - Gemfile
60
+ - Guardfile
61
+ - LICENSE
62
+ - README.md
63
+ - Rakefile
64
+ - lib/service-client.rb
65
+ - lib/service-client/adapter/faraday.rb
66
+ - lib/service-client/base_response.rb
67
+ - lib/service-client/bound_route.rb
68
+ - lib/service-client/error.rb
69
+ - lib/service-client/raw_interface.rb
70
+ - lib/service-client/redirection.rb
71
+ - lib/service-client/response.rb
72
+ - lib/service-client/response_error.rb
73
+ - lib/service-client/route.rb
74
+ - lib/service-client/route_collection.rb
75
+ - lib/service-client/routing_error.rb
76
+ - lib/service-client/service_error.rb
77
+ - lib/service-client/url_pattern.rb
78
+ - lib/service-client/version.rb
79
+ - service-client.gemspec
80
+ - spec/adapter/faraday_spec.rb
81
+ - spec/adapter/test_server.ru
82
+ - spec/client_spec.rb
83
+ - spec/spec_helper.rb
84
+ homepage: ''
85
+ licenses: []
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.8.25
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Service::Client is a generic client gem to access our services.
108
+ test_files:
109
+ - spec/adapter/faraday_spec.rb
110
+ - spec/adapter/test_server.ru
111
+ - spec/client_spec.rb
112
+ - spec/spec_helper.rb