rest-in-peace 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ coverage
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ rest-in-peace
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p547
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.2
6
+ script:
7
+ - bundle exec rake spec
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
9
+ end
10
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Nine Internet Solutions AG (nine.ch)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # REST in Peace [![Build Status](https://travis-ci.org/ninech/REST-in-Peace.svg)](https://travis-ci.org/ninech/REST-in-Peace) [![Code Climate](https://codeclimate.com/github/ninech/REST-in-Peace.png)](https://codeclimate.com/github/ninech/REST-in-Peace)
2
+
3
+ A ruby REST client that lets you feel like in heaven when consuming APIs.
4
+
5
+ ![logo](https://raw.githubusercontent.com/ninech/REST-in-Peace/master/images/rest_in_peace.gif)
6
+
7
+ ## Getting Started
8
+
9
+ 1. Add `REST-in-Peace` to your dependencies
10
+
11
+ gem 'rest-in-peace'
12
+
13
+ 2. Choose which http adapter you want to use
14
+
15
+ gem 'faraday'
16
+
17
+ ## Usage
18
+
19
+ ### HTTP Client Library
20
+
21
+ There is no dependency on a specific HTTP client library but the client has been tested with [Faraday](https://github.com/lostisland/faraday) only. You can use any other client library as long as it has the same API as Faraday.
22
+
23
+ ### Configuration
24
+
25
+ You need to define all the API endpoints you want to consume with `RESTinPeace`. Currently the four HTTP Verbs `GET`, `POST`, `PATCH` and `DELETE` are supported.
26
+
27
+ There are two sections where you can specify endpoints: `resource` and `collection`:
28
+
29
+ ```ruby
30
+ rest_in_peace do
31
+ resource do
32
+ get :reload, '/rip/:id'
33
+ end
34
+ collection do
35
+ get :find, '/rip/:id'
36
+ end
37
+ end
38
+ ```
39
+
40
+ #### HTTP Client Configuration
41
+
42
+ You need to specify the HTTP client library to use. You can either specify a block (for lazy loading) or a client instance directly.
43
+
44
+ ```ruby
45
+ class Resource
46
+ rest_in_peace do
47
+ use_api ->() { Faraday.new(url: 'http://rip.dev') }
48
+ end
49
+ end
50
+
51
+ class ResourceTwo
52
+ rest_in_peace do
53
+ use_api Faraday.new(url: 'http://rip.dev')
54
+ end
55
+ end
56
+ ```
57
+
58
+ #### Resource
59
+
60
+ If you define anything inside the `resource` block, it will define a method on the instances of the class:
61
+ ```ruby
62
+ class Resource
63
+ rest_in_peace do
64
+ resource do
65
+ get :reload, '/rip/:id'
66
+ post :create, '/rip'
67
+ end
68
+ end
69
+ end
70
+
71
+ resource = Resource.new(id: 1)
72
+ resource.create # calls "POST /rip"
73
+ resource.reload # calls "GET /rip/1"
74
+ ```
75
+
76
+ #### Collection
77
+
78
+ If you define anything inside the `collection` block, it will define a method on the class:
79
+
80
+ ```ruby
81
+ class Resource
82
+ rest_in_peace do
83
+ collection do
84
+ get :find, '/rip/:id'
85
+ get :find_on_other, '/other/:other_id/rip/:id'
86
+ end
87
+ end
88
+ end
89
+
90
+ resource = Resource.find(1) # calls "GET /rip/1"
91
+ resource = Resource.find_on_other(42, 1337) # calls "GET /other/42/rip/1337"
92
+ ```
93
+
94
+ #### Pagination
95
+
96
+ You can define your own pagination module which will be mixed in when calling the API:
97
+
98
+ ```ruby
99
+ class Resource
100
+ rest_in_peace do
101
+ collection do
102
+ get :all, '/rips', paginate_with: MyClient::Paginator
103
+ end
104
+ end
105
+ end
106
+ ```
107
+
108
+ An example pagination mixin with HTTP headers can be found in the [examples directory](https://github.com/ninech/REST-in-Peace/blob/master/examples) of this repo.
109
+
110
+ #### Complete Configuration
111
+
112
+ ```ruby
113
+ require 'my_client/paginator'
114
+ require 'rest_in_peace'
115
+
116
+ module MyClient
117
+ class Fabric < Struct.new(:id, :name, :ip)
118
+ include RESTinPeace
119
+
120
+ rest_in_peace do
121
+ use_api ->() { MyClient.api }
122
+
123
+ resource do
124
+ patch :save, '/fabrics/:id'
125
+ post :create, '/fabrics'
126
+ delete :destroy, '/fabrics/:id'
127
+ get :reload, '/fabrics/:id'
128
+ end
129
+
130
+ collection do
131
+ get :all, '/fabrics', paginate_with: MyClient::Paginator
132
+ get :find, '/fabrics/:id'
133
+ end
134
+ end
135
+ end
136
+ end
137
+ ```
138
+
139
+ ## Helpers
140
+
141
+ ### SSL Configuration for Faraday
142
+
143
+ There is a helper class which can be used to create a Faraday compatible SSL configuration hash (with support for client certificates).
144
+
145
+ ```ruby
146
+ ssl_config = {
147
+ "api_url" => "https://api-backend.dev:3443",
148
+ "use_cert" => true,
149
+ "ssl_cert_client" => "/etc/ssl/private/client.crt",
150
+ "ssl_key_client" => "/etc/ssl/private/client.key",
151
+ "ssl_ca" => "/etc/ssl/certs/ca-chain.crt"
152
+ }
153
+
154
+ ssl_config_creator = RESTinPeace::SSLConfigCreator.new(ssl_config, :peer)
155
+ ssl_config_creator.faraday_options.inspect
156
+ # =>
157
+ {
158
+ :client_cert => #<OpenSSL::X509::Certificate>,
159
+ :client_key => Long key is long,
160
+ :ca_file => "/etc/ssl/certs/ca-chain.crt",
161
+ :verify_mode => 1
162
+ }
163
+ ```
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec) do |task|
6
+ task.rspec_opts = '--format doc --profile'
7
+ end
8
+
9
+ task default: :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,26 @@
1
+ module MyClient
2
+ module Paginator
3
+ def get
4
+ Enumerator.new do |yielder|
5
+ @params.merge!(page: 1)
6
+ result = api.get(url, @params)
7
+ current_page = result.env.response_headers['X-Page'].to_i
8
+ total_pages = result.env.response_headers['X-Total-Pages'].to_i
9
+
10
+ loop do
11
+ # Yield the results we got in the body.
12
+ result.body.each do |item|
13
+ yielder << @klass.new(item)
14
+ end
15
+
16
+ # Only make a request to get the next page if we have not
17
+ # reached the last page yet.
18
+ raise StopIteration if current_page == total_pages
19
+ @params.merge!(page: current_page + 1)
20
+ result = api.get(url, @params)
21
+ current_page = result.env.response_headers['X-Page'].to_i
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
Binary file
@@ -0,0 +1 @@
1
+ require 'rest_in_peace'
@@ -0,0 +1,53 @@
1
+ require 'rest_in_peace/template_sanitizer'
2
+ require 'rest_in_peace/response_converter'
3
+
4
+ module RESTinPeace
5
+ class ApiCall
6
+ def initialize(api, url_template, klass, params)
7
+ @api = api
8
+ @url_template = url_template
9
+ @klass = klass
10
+ @params = params
11
+ end
12
+
13
+ def get
14
+ response = api.get(url, params)
15
+ convert_response(response)
16
+ end
17
+
18
+ def post
19
+ response = api.post(url, params)
20
+ convert_response(response)
21
+ end
22
+
23
+ def patch
24
+ response = api.patch(url, params)
25
+ convert_response(response)
26
+ end
27
+
28
+ def delete
29
+ response = api.delete(url, params)
30
+ convert_response(response)
31
+ end
32
+
33
+ def url
34
+ sanitizer.url
35
+ end
36
+
37
+ def params
38
+ sanitizer.leftover_params
39
+ end
40
+
41
+ def sanitizer
42
+ @sanitizer ||= RESTinPeace::TemplateSanitizer.new(@url_template, @params)
43
+ end
44
+
45
+ def convert_response(response)
46
+ RESTinPeace::ResponseConverter.new(response, @klass).result
47
+ end
48
+
49
+ def api
50
+ @api.respond_to?(:call) ? @api.call : @api
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ require 'rest_in_peace/template_sanitizer'
2
+ require 'rest_in_peace/api_call'
3
+
4
+ module RESTinPeace
5
+ class DefinitionProxy
6
+ class CollectionMethodDefinitions
7
+ def initialize(target)
8
+ @target = target
9
+ end
10
+
11
+ def get(method_name, url_template, default_params = {})
12
+ @target.send(:define_singleton_method, method_name) do |*args|
13
+ if args.last.is_a?(Hash)
14
+ params = default_params.merge(args.pop)
15
+ else
16
+ params = default_params.dup
17
+ tokens = RESTinPeace::TemplateSanitizer.new(url_template, {}).tokens
18
+ tokens.each do |token|
19
+ params.merge!(token.to_sym => args.shift)
20
+ end
21
+ end
22
+
23
+ call = RESTinPeace::ApiCall.new(api, url_template, self, params)
24
+ call.extend(params.delete(:paginate_with)) if params[:paginate_with]
25
+ call.get
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,39 @@
1
+ module RESTinPeace
2
+ class DefinitionProxy
3
+ class ResourceMethodDefinitions
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ def get(method_name, url_template, default_params = {})
9
+ @target.send(:define_method, method_name) do
10
+ call = RESTinPeace::ApiCall.new(api, url_template, self, to_h)
11
+ call.get
12
+ end
13
+ end
14
+
15
+ def patch(method_name, url_template)
16
+ @target.send(:define_method, method_name) do
17
+ call = RESTinPeace::ApiCall.new(api, url_template, self, to_h)
18
+ call.patch
19
+ end
20
+ end
21
+
22
+ def post(method_name, url_template)
23
+ @target.send(:define_method, method_name) do
24
+ call = RESTinPeace::ApiCall.new(api, url_template, self, to_h)
25
+ call.post
26
+ end
27
+ end
28
+
29
+ def delete(method_name, url_template, default_params = {})
30
+ @target.send(:define_method, method_name) do |params = {}|
31
+ merged_params = default_params.merge(to_h).merge(params)
32
+ call = RESTinPeace::ApiCall.new(api, url_template, self, merged_params)
33
+ call.delete
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,24 @@
1
+ require 'rest_in_peace/definition_proxy/resource_method_definitions'
2
+ require 'rest_in_peace/definition_proxy/collection_method_definitions'
3
+
4
+ module RESTinPeace
5
+ class DefinitionProxy
6
+ def initialize(target)
7
+ @target = target
8
+ end
9
+
10
+ def resource(&block)
11
+ method_definitions = RESTinPeace::DefinitionProxy::ResourceMethodDefinitions.new(@target)
12
+ method_definitions.instance_eval(&block)
13
+ end
14
+
15
+ def collection(&block)
16
+ method_definitions = RESTinPeace::DefinitionProxy::CollectionMethodDefinitions.new(@target)
17
+ method_definitions.instance_eval(&block)
18
+ end
19
+
20
+ def use_api(api)
21
+ @target.api = api
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,3 @@
1
+ module RESTinPeace
2
+ class DefaultError < Exception; end
3
+ end
@@ -0,0 +1,33 @@
1
+ module RESTinPeace
2
+ class ResponseConverter
3
+ def initialize(response, klass)
4
+ @response = response
5
+ @klass = klass
6
+ end
7
+
8
+ def result
9
+ case @response.body.class.to_s
10
+ when 'Array'
11
+ convert_from_array
12
+ when 'Hash'
13
+ convert_from_hash
14
+ else
15
+ raise "Don't know how to convert #{@response.body.class}"
16
+ end
17
+ end
18
+
19
+ def convert_from_array
20
+ @response.body.map do |entity|
21
+ convert_from_hash(entity)
22
+ end
23
+ end
24
+
25
+ def convert_from_hash(entity = @response.body)
26
+ klass.new entity
27
+ end
28
+
29
+ def klass
30
+ @klass.respond_to?(:new) ? @klass : @klass.class
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ require 'openssl'
2
+
3
+ module RESTinPeace
4
+ class SSLConfigCreator
5
+ def initialize(config, verify = :peer)
6
+ @config = config
7
+ @verify = verify
8
+ end
9
+
10
+ def faraday_options
11
+ {client_cert: client_cert, client_key: client_key, ca_file: ca_cert_path, verify_mode: verify_mode}
12
+ end
13
+
14
+ def client_cert
15
+ OpenSSL::X509::Certificate.new(open_file(client_cert_path))
16
+ end
17
+
18
+ def client_cert_path
19
+ path(@config[:ssl_cert_client])
20
+ end
21
+
22
+ def client_key
23
+ OpenSSL::PKey::RSA.new(open_file(client_key_path))
24
+ end
25
+
26
+ def client_key_path
27
+ path(@config[:ssl_key_client])
28
+ end
29
+
30
+ def ca_cert_path
31
+ path(@config[:ssl_ca])
32
+ end
33
+
34
+ def verify_mode
35
+ case @verify
36
+ when :peer
37
+ OpenSSL::SSL::VERIFY_PEER
38
+ else
39
+ raise "Unknown verify variant '#{@verify}'"
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def open_file(file)
46
+ File.open(file)
47
+ end
48
+
49
+ def path(file)
50
+ File.join(file)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ require 'rest_in_peace/errors'
2
+
3
+ module RESTinPeace
4
+ class TemplateSanitizer
5
+
6
+ class IncompleteParams < RESTinPeace::DefaultError; end
7
+
8
+ def initialize(url_template, params)
9
+ @url_template = url_template
10
+ @params = params.dup
11
+ end
12
+
13
+ def url
14
+ return @url if @url
15
+ @url = @url_template.dup
16
+ tokens.each do |token|
17
+ param = @params.delete(token.to_sym)
18
+ raise IncompleteParams, "Unknown parameter for token :#{token} found" unless param
19
+ @url.gsub!(%r{:#{token}}, param.to_s)
20
+ end
21
+ @url
22
+ end
23
+
24
+ def tokens
25
+ @url_template.scan(%r{:([a-z_]+)}).flatten
26
+ end
27
+
28
+ def leftover_params
29
+ @params.delete_if { |param| tokens.include?(param.to_s) }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,38 @@
1
+ require 'rest_in_peace/definition_proxy'
2
+
3
+ module RESTinPeace
4
+
5
+ def self.included(base)
6
+ base.send :extend, ClassMethods
7
+ end
8
+
9
+ def api
10
+ self.class.api
11
+ end
12
+
13
+ def initialize(attributes = {})
14
+ update_from_hash(attributes)
15
+ end
16
+
17
+ def to_h
18
+ Hash[each_pair.to_a]
19
+ end
20
+
21
+ protected
22
+
23
+ def update_from_hash(hash)
24
+ hash.each do |key, value|
25
+ next unless self.class.members.map(&:to_s).include?(key.to_s)
26
+ send("#{key}=", value)
27
+ end
28
+ end
29
+
30
+ module ClassMethods
31
+ attr_accessor :api
32
+
33
+ def rest_in_peace(&block)
34
+ definition_proxy = RESTinPeace::DefinitionProxy.new(self)
35
+ definition_proxy.instance_eval(&block)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ $:.push File.expand_path('../lib', __FILE__)
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'rest-in-peace'
7
+ s.version = File.read(File.expand_path('../VERSION', __FILE__)).strip
8
+ s.authors = ['Raffael Schmid']
9
+ s.email = ['raffael.schmid@nine.ch']
10
+ s.homepage = 'http://github.com/ninech/'
11
+ s.license = 'MIT'
12
+ s.summary = 'REST in peace'
13
+ s.description = 'Let your api REST in peace.'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ['lib']
19
+
20
+ s.add_development_dependency 'rake', '~> 10.0'
21
+ s.add_development_dependency 'rspec', '~> 3.0'
22
+ s.add_development_dependency 'guard', '~> 2.6.1'
23
+ s.add_development_dependency 'guard-rspec', '~> 4.2.0'
24
+ s.add_development_dependency 'simplecov', '~> 0.8.2'
25
+ end
@@ -0,0 +1,54 @@
1
+ require 'rest_in_peace/api_call'
2
+ require 'ostruct'
3
+
4
+ describe RESTinPeace::ApiCall do
5
+ let(:api) { double }
6
+ let(:url_template) { '/rip/:id' }
7
+ let(:klass) { OpenStruct }
8
+ let(:params) { {id: 1} }
9
+
10
+ let(:api_call) { RESTinPeace::ApiCall.new(api, url_template, klass, params) }
11
+
12
+ let(:response) { OpenStruct.new(body: []) }
13
+
14
+ describe '#get' do
15
+ context 'with enough parameters for the template' do
16
+ it 'calls the api with the parameters' do
17
+ expect(api).to receive(:get).with('/rip/1', {}).and_return(response)
18
+ api_call.get
19
+ end
20
+ end
21
+
22
+ context 'with more parameters than needed' do
23
+ let(:params) { { id: 1, name: 'test' } }
24
+ it 'uses also the additional parameters' do
25
+ expect(api).to receive(:get).with('/rip/1', { name: 'test' }).and_return(response)
26
+ api_call.get
27
+ end
28
+ end
29
+ end
30
+
31
+ describe '#post' do
32
+ let(:url_template) { '/rip' }
33
+ let(:params) { { name: 'test' } }
34
+ it 'calls the api with the parameters' do
35
+ expect(api).to receive(:post).with('/rip', { name: 'test' }).and_return(response)
36
+ api_call.post
37
+ end
38
+ end
39
+
40
+ describe '#patch' do
41
+ let(:params) { { id: 1, name: 'test' } }
42
+ it 'calls the api with the parameters' do
43
+ expect(api).to receive(:patch).with('/rip/1', { name: 'test' }).and_return(response)
44
+ api_call.patch
45
+ end
46
+ end
47
+
48
+ describe '#delete' do
49
+ it 'calls the api with the parameters' do
50
+ expect(api).to receive(:delete).with('/rip/1', {}).and_return(response)
51
+ api_call.delete
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,80 @@
1
+ require 'rest_in_peace'
2
+ require 'rest_in_peace/definition_proxy/collection_method_definitions'
3
+
4
+ describe RESTinPeace::DefinitionProxy::CollectionMethodDefinitions do
5
+ let(:method_name) { :find }
6
+ let(:url_template) { '/a/:id' }
7
+ let(:default_params) { {} }
8
+ let(:struct) { Struct.new(:id, :name) }
9
+ let(:target) do
10
+ Class.new(struct) do
11
+ include RESTinPeace
12
+ end
13
+ end
14
+ let(:definitions) { described_class.new(target) }
15
+ let(:api_call_double) { object_double(RESTinPeace::ApiCall.new(target.api, url_template, definitions, default_params)) }
16
+
17
+ subject { definitions }
18
+
19
+ before do
20
+ allow(RESTinPeace::ApiCall).to receive(:new).and_return(api_call_double)
21
+ end
22
+
23
+ context '#get' do
24
+ it 'defines a singleton method on the target' do
25
+ expect { subject.get(:find, '/a/:id', default_params) }.
26
+ to change { target.respond_to?(:find) }.from(false).to(true)
27
+ end
28
+
29
+ describe 'the created method' do
30
+ before do
31
+ allow(api_call_double).to receive(:get)
32
+ end
33
+
34
+ context 'without a paginator' do
35
+ it 'does not extend api call' do
36
+ expect(api_call_double).to_not receive(:extend)
37
+
38
+ subject.get(:find, '/a/:id', default_params)
39
+ target.find(1)
40
+ end
41
+ end
42
+
43
+ context 'with a paginator' do
44
+ it 'extends api call' do
45
+ expect(api_call_double).to receive(:extend)
46
+
47
+ subject.get(:find, '/a/:id', default_params.merge(paginate_with: Module))
48
+ target.find(1)
49
+ end
50
+ end
51
+
52
+ describe 'parameter and arguments handling' do
53
+ it 'converts the parameters' do
54
+ expect(RESTinPeace::ApiCall).to receive(:new).
55
+ with(target.api, '/a/:id', target, {id: 1}).
56
+ and_return(api_call_double)
57
+
58
+ subject.get(:find, '/a/:id', default_params)
59
+ target.find(1)
60
+ end
61
+
62
+ it 'appends the given attributes' do
63
+ expect(RESTinPeace::ApiCall).to receive(:new).
64
+ with(target.api, '/a', target, {name: 'daniele', last_name: 'in der o'}).
65
+ and_return(api_call_double)
66
+
67
+ subject.get(:all, '/a', {last_name: 'in der o'})
68
+ target.all(name: 'daniele')
69
+ end
70
+
71
+ it 'does not modify the default params' do
72
+ default_params = { per_page: 250 }
73
+ subject.get(:find, '/a/:id', default_params)
74
+ target.find(1)
75
+ expect(default_params).to eq({ per_page: 250 })
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,77 @@
1
+ require 'rest_in_peace'
2
+ require 'rest_in_peace/definition_proxy/resource_method_definitions'
3
+
4
+ describe RESTinPeace::DefinitionProxy::ResourceMethodDefinitions do
5
+ let(:struct) { Struct.new(:id, :name) }
6
+ let(:target) do
7
+ Class.new(struct) do
8
+ include RESTinPeace
9
+ end
10
+ end
11
+ let(:instance) { target.new }
12
+ let(:definitions) { described_class.new(target) }
13
+ let(:api_call_double) { object_double(RESTinPeace::ApiCall.new(target.api, '/a/:id', definitions, {})) }
14
+
15
+ subject { definitions }
16
+
17
+ before do
18
+ allow(RESTinPeace::ApiCall).to receive(:new).and_return(api_call_double)
19
+ end
20
+
21
+ shared_examples_for 'an instance method' do
22
+ it 'defines a singleton method on the target' do
23
+ expect { subject.send(http_verb, method_name, url_template) }.
24
+ to change { instance.respond_to?(method_name) }.from(false).to(true)
25
+ end
26
+
27
+ describe 'the created method' do
28
+ before do
29
+ allow(api_call_double).to receive(http_verb)
30
+ end
31
+
32
+ describe 'parameter and arguments handling' do
33
+ it 'uses the attributes of the class' do
34
+ expect(RESTinPeace::ApiCall).to receive(:new).
35
+ with(target.api, url_template, instance, instance.to_h).
36
+ and_return(api_call_double)
37
+
38
+ subject.send(http_verb, method_name, url_template)
39
+ instance.send(method_name)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ context '#get' do
46
+ it_behaves_like 'an instance method' do
47
+ let(:http_verb) { :get }
48
+ let(:method_name) { :reload }
49
+ let(:url_template) { '/a/:id' }
50
+ end
51
+ end
52
+
53
+ context '#patch' do
54
+ it_behaves_like 'an instance method' do
55
+ let(:http_verb) { :patch }
56
+ let(:method_name) { :save }
57
+ let(:url_template) { '/a/:id' }
58
+ end
59
+ end
60
+
61
+ context '#post' do
62
+ it_behaves_like 'an instance method' do
63
+ let(:http_verb) { :post }
64
+ let(:method_name) { :create }
65
+ let(:url_template) { '/a/:id' }
66
+ end
67
+ end
68
+
69
+ context '#delete' do
70
+ it_behaves_like 'an instance method' do
71
+ let(:http_verb) { :delete }
72
+ let(:method_name) { :destroy }
73
+ let(:url_template) { '/a/:id' }
74
+ end
75
+ end
76
+
77
+ end
@@ -0,0 +1,36 @@
1
+ require 'rest_in_peace/definition_proxy'
2
+
3
+ describe RESTinPeace::DefinitionProxy do
4
+ let(:resource_definitions) { object_double(RESTinPeace::DefinitionProxy::ResourceMethodDefinitions) }
5
+ let(:collection_definitions) { object_double(RESTinPeace::DefinitionProxy::CollectionMethodDefinitions) }
6
+ let(:target) { }
7
+ let(:proxy) { RESTinPeace::DefinitionProxy.new(target) }
8
+ let(:test_proc) { ->() {} }
9
+
10
+ subject { proxy }
11
+
12
+ before do
13
+ allow(RESTinPeace::DefinitionProxy::ResourceMethodDefinitions).
14
+ to receive(:new).with(target).and_return(resource_definitions)
15
+ allow(RESTinPeace::DefinitionProxy::CollectionMethodDefinitions).
16
+ to receive(:new).with(target).and_return(collection_definitions)
17
+ end
18
+
19
+ describe '#resource' do
20
+ it 'forwards the given block to a resource method definition' do
21
+ expect(resource_definitions).to receive(:instance_eval) do |&block|
22
+ expect(block).to be_instance_of(Proc)
23
+ end
24
+ subject.resource(&test_proc)
25
+ end
26
+ end
27
+
28
+ describe '#collection' do
29
+ it 'forwards the given block to a collection method definition' do
30
+ expect(collection_definitions).to receive(:instance_eval) do |&block|
31
+ expect(block).to be_instance_of(Proc)
32
+ end
33
+ subject.collection(&test_proc)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ require 'rest_in_peace/response_converter'
2
+ require 'ostruct'
3
+
4
+ describe RESTinPeace::ResponseConverter do
5
+ let(:element1) { { name: 'test1' } }
6
+ let(:element2) { { name: 'test2' } }
7
+ let(:response) { OpenStruct.new(body: response_body) }
8
+ let(:converter) { RESTinPeace::ResponseConverter.new(response, klass) }
9
+
10
+ describe '#result' do
11
+ subject { converter.result }
12
+
13
+ shared_examples_for 'an array input' do
14
+ let(:response_body) { [element1, element2] }
15
+ specify { expect(subject).to be_instance_of(Array) }
16
+ specify { expect(subject).to eq([OpenStruct.new(name: 'test1'), OpenStruct.new(name: 'test2')]) }
17
+ end
18
+
19
+ shared_examples_for 'a hash input' do
20
+ let(:response_body) { element1 }
21
+ specify { expect(subject).to be_instance_of(OpenStruct) }
22
+ specify { expect(subject).to eq(OpenStruct.new(name: 'test1')) }
23
+ end
24
+
25
+ context 'given type is a class' do
26
+ let(:klass) { OpenStruct }
27
+ context 'input is an array' do
28
+ it_behaves_like 'an array input'
29
+ end
30
+
31
+ context 'input is a hash' do
32
+ it_behaves_like 'a hash input'
33
+ end
34
+ end
35
+
36
+ context 'given type is an instance' do
37
+ let(:klass) { OpenStruct.new }
38
+ context 'input is an array' do
39
+ it_behaves_like 'an array input'
40
+ end
41
+
42
+ context 'input is a hash' do
43
+ it_behaves_like 'a hash input'
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ require 'rest_in_peace/template_sanitizer'
2
+
3
+ describe RESTinPeace::TemplateSanitizer do
4
+
5
+ let(:template_sanitizer) { RESTinPeace::TemplateSanitizer.new(url_template, params) }
6
+
7
+ describe '#url' do
8
+ subject { template_sanitizer.url }
9
+
10
+ context 'single token' do
11
+ let(:params) { { id: 1 } }
12
+ let(:url_template) { '/a/:id' }
13
+ specify { expect(subject).to eq('/a/1') }
14
+ end
15
+
16
+ context 'multiple token' do
17
+ let(:params) { { id: 2, a_id: 1 } }
18
+ let(:url_template) { '/a/:a_id/b/:id' }
19
+ specify { expect(subject).to eq('/a/1/b/2') }
20
+ end
21
+
22
+ context 'incomplete params' do
23
+ let(:params) { {} }
24
+ let(:url_template) { '/a/:id' }
25
+ specify { expect { subject }.to raise_error(RESTinPeace::TemplateSanitizer::IncompleteParams) }
26
+ end
27
+
28
+ context 'immutability of the url template' do
29
+ let(:params) { { id: 1 } }
30
+ let(:url_template) { '/a/:id' }
31
+ specify { expect { subject }.to_not change { url_template } }
32
+ end
33
+
34
+ context 'immutability of the params' do
35
+ let(:params) { { id: 1 } }
36
+ let(:url_template) { '/a/:id' }
37
+ specify { expect { subject }.to_not change { params } }
38
+ end
39
+ end
40
+
41
+ describe '#tokens' do
42
+ let(:params) { {} }
43
+
44
+ context 'single token' do
45
+ let(:url_template) { '/a/:id' }
46
+ subject { template_sanitizer.tokens }
47
+ specify { expect(subject).to eq(%w(id)) }
48
+ end
49
+
50
+ context 'multiple tokens' do
51
+ let(:url_template) { '/a/:a_id/b/:id' }
52
+ subject { template_sanitizer.tokens }
53
+ specify { expect(subject).to eq(%w(a_id id)) }
54
+ end
55
+ end
56
+
57
+ describe '#leftover_params' do
58
+ let(:params) { { id: 1, name: 'test' } }
59
+ let(:url_template) { '/a/:id' }
60
+ subject { template_sanitizer.leftover_params }
61
+
62
+ specify { expect(subject).to eq({name: 'test'}) }
63
+ end
64
+ end
@@ -0,0 +1,55 @@
1
+ require 'rest_in_peace'
2
+
3
+ describe RESTinPeace do
4
+
5
+ let(:struct) { Struct.new(:name) }
6
+ let(:extended_class) do
7
+ Class.new(struct) do
8
+ include RESTinPeace
9
+ end
10
+ end
11
+ let(:attributes) { { name: 'test' } }
12
+ let(:instance) { extended_class.new(attributes) }
13
+
14
+ describe '::api' do
15
+ subject { extended_class }
16
+ specify { expect(subject).to respond_to(:api).with(0).arguments }
17
+ end
18
+
19
+ describe '::rest_in_peace' do
20
+ subject { extended_class }
21
+ specify { expect(subject).to respond_to(:rest_in_peace).with(0).arguments }
22
+ let(:definition_proxy) { object_double(RESTinPeace::DefinitionProxy) }
23
+
24
+
25
+ it 'evaluates the given block inside the definition proxy' do
26
+ allow(RESTinPeace::DefinitionProxy).to receive(:new).with(subject).and_return(definition_proxy)
27
+ expect(definition_proxy).to receive(:instance_eval) do |&block|
28
+ expect(block).to be_instance_of(Proc)
29
+ end
30
+ subject.rest_in_peace { }
31
+ end
32
+ end
33
+
34
+ describe '#api' do
35
+ subject { instance }
36
+ specify { expect(subject).to respond_to(:api).with(0).arguments }
37
+ end
38
+
39
+ describe '#to_h' do
40
+ subject { instance }
41
+ specify { expect(subject).to respond_to(:to_h).with(0).arguments }
42
+ end
43
+
44
+ describe '#initialize' do
45
+ subject { instance }
46
+ specify { expect(subject.name).to eq('test') }
47
+
48
+ context 'unknown params' do
49
+ let(:attributes) { { name: 'test42', email: 'yolo@example.org' } }
50
+ specify { expect(subject.name).to eq('test42') }
51
+ specify { expect { subject.email }.to raise_error(NoMethodError) }
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,61 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ RSpec.configure do |config|
5
+ # These two settings work together to allow you to limit a spec run
6
+ # to individual examples or groups you care about by tagging them with
7
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
8
+ # get run.
9
+ config.filter_run :focus
10
+ config.run_all_when_everything_filtered = true
11
+
12
+ # Many RSpec users commonly either run the entire suite or an individual
13
+ # file, and it's useful to allow more verbose output when running an
14
+ # individual spec file.
15
+ if config.files_to_run.one?
16
+ # Use the documentation formatter for detailed output,
17
+ # unless a formatter has already been configured
18
+ # (e.g. via a command-line flag).
19
+ config.default_formatter = 'doc'
20
+ end
21
+
22
+ # Print the 10 slowest examples and example groups at the
23
+ # end of the spec run, to help surface which specs are running
24
+ # particularly slow.
25
+ config.profile_examples = 10
26
+
27
+ # Run specs in random order to surface order dependencies. If you find an
28
+ # order dependency and want to debug it, you can fix the order by providing
29
+ # the seed, which is printed after each run.
30
+ # --seed 1234
31
+ config.order = :random
32
+
33
+ # Seed global randomization in this process using the `--seed` CLI option.
34
+ # Setting this allows you to use `--seed` to deterministically reproduce
35
+ # test failures related to randomization by passing the same `--seed` value
36
+ # as the one that triggered the failure.
37
+ Kernel.srand config.seed
38
+
39
+ # rspec-expectations config goes here. You can use an alternate
40
+ # assertion/expectation library such as wrong or the stdlib/minitest
41
+ # assertions if you prefer.
42
+ config.expect_with :rspec do |expectations|
43
+ # Enable only the newer, non-monkey-patching expect syntax.
44
+ # For more details, see:
45
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
46
+ expectations.syntax = :expect
47
+ end
48
+
49
+ # rspec-mocks config goes here. You can use an alternate test double
50
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
51
+ config.mock_with :rspec do |mocks|
52
+ # Enable only the newer, non-monkey-patching expect syntax.
53
+ # For more details, see:
54
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
55
+ mocks.syntax = :expect
56
+
57
+ # Prevents you from mocking or stubbing a method that does not exist on
58
+ # a real object. This is generally recommended.
59
+ mocks.verify_partial_doubles = true
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rest-in-peace
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Raffael Schmid
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '10.0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '10.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '3.0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '3.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: guard
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.6.1
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.6.1
62
+ - !ruby/object:Gem::Dependency
63
+ name: guard-rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 4.2.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 4.2.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: simplecov
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 0.8.2
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 0.8.2
94
+ description: Let your api REST in peace.
95
+ email:
96
+ - raffael.schmid@nine.ch
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - .rspec
103
+ - .ruby-gemset
104
+ - .ruby-version
105
+ - .travis.yml
106
+ - Gemfile
107
+ - Guardfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - VERSION
112
+ - examples/pagination_with_headers.rb
113
+ - images/rest_in_peace.gif
114
+ - lib/rest-in-peace.rb
115
+ - lib/rest_in_peace.rb
116
+ - lib/rest_in_peace/api_call.rb
117
+ - lib/rest_in_peace/definition_proxy.rb
118
+ - lib/rest_in_peace/definition_proxy/collection_method_definitions.rb
119
+ - lib/rest_in_peace/definition_proxy/resource_method_definitions.rb
120
+ - lib/rest_in_peace/errors.rb
121
+ - lib/rest_in_peace/response_converter.rb
122
+ - lib/rest_in_peace/ssl_config_creator.rb
123
+ - lib/rest_in_peace/template_sanitizer.rb
124
+ - rest-in-peace.gemspec
125
+ - spec/rest_in_peace/api_call_spec.rb
126
+ - spec/rest_in_peace/definition_proxy/collection_method_definitions_spec.rb
127
+ - spec/rest_in_peace/definition_proxy/resource_method_definitions_spec.rb
128
+ - spec/rest_in_peace/definition_proxy_spec.rb
129
+ - spec/rest_in_peace/response_converter_spec.rb
130
+ - spec/rest_in_peace/template_sanitizer_spec.rb
131
+ - spec/rest_in_peace_spec.rb
132
+ - spec/spec_helper.rb
133
+ homepage: http://github.com/ninech/
134
+ licenses:
135
+ - MIT
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ none: false
142
+ requirements:
143
+ - - ! '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ! '>='
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 1.8.23.2
155
+ signing_key:
156
+ specification_version: 3
157
+ summary: REST in peace
158
+ test_files:
159
+ - spec/rest_in_peace/api_call_spec.rb
160
+ - spec/rest_in_peace/definition_proxy/collection_method_definitions_spec.rb
161
+ - spec/rest_in_peace/definition_proxy/resource_method_definitions_spec.rb
162
+ - spec/rest_in_peace/definition_proxy_spec.rb
163
+ - spec/rest_in_peace/response_converter_spec.rb
164
+ - spec/rest_in_peace/template_sanitizer_spec.rb
165
+ - spec/rest_in_peace_spec.rb
166
+ - spec/spec_helper.rb