apii 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ce6f4fc3f84231b335c3af2e506282b8bd721f5e
4
+ data.tar.gz: 7e0db9b90e07bb5e0a7df48c404775d39a7a7884
5
+ SHA512:
6
+ metadata.gz: e7b547658b29e81fec0cc00db093677d7962fa127457db37e98f45f0503da3814b667ea130c40358873b6c22fcdb28fac88f00e4d21d2ef177516b5a637dbe84
7
+ data.tar.gz: 6801bb1509b05649f9fe668820adbad05a4ceee77526191a475aed4e0805bad0cf77e8c0190864709e6a3cfa788ab442e669a6bf40e6888e31e8b674be3ebd15
@@ -0,0 +1,10 @@
1
+ pkg/*
2
+ doc/*
3
+ rdoc/*
4
+ *.gem
5
+ .bundle
6
+ Gemfile.lock
7
+ .gh_pages
8
+ coverage/
9
+ *#*
10
+ *~
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1 @@
1
+ inherit_from: .rubocop_todo.yml
@@ -0,0 +1,36 @@
1
+ # This configuration was generated by `rubocop --auto-gen-config`
2
+ # on 2014-06-01 12:18:38 +0200 using RuboCop version 0.22.0.
3
+ # The point is for the user to remove these configuration records
4
+ # one by one as the offenses are removed from the code base.
5
+ # Note that changes in the inspected code, or installation of new
6
+ # versions of RuboCop, may require this file to be generated again.
7
+
8
+ # Offense count: 1
9
+ # Configuration parameters: CountComments.
10
+ ClassLength:
11
+ Max: 142
12
+
13
+ # Offense count: 1
14
+ CyclomaticComplexity:
15
+ Max: 8
16
+
17
+ # Offense count: 5
18
+ Documentation:
19
+ Enabled: false
20
+
21
+ # Offense count: 53
22
+ LineLength:
23
+ Max: 704
24
+
25
+ # Offense count: 2
26
+ # Configuration parameters: CountComments.
27
+ MethodLength:
28
+ Max: 19
29
+
30
+ # Offense count: 2
31
+ MultilineBlockChain:
32
+ Enabled: false
33
+
34
+ # Offense count: 5
35
+ RescueModifier:
36
+ Enabled: false
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+
3
+ notifications:
4
+ email: false
5
+
6
+ rvm:
7
+ - 2.0
8
+ - 2.1
9
+ - ruby-head
10
+
11
+ matrix:
12
+ allow_failures:
13
+ - rvm: ruby-head
14
+
15
+ before_install:
16
+ - gem install fakeweb
17
+
18
+ script:
19
+ - "bundle exec rake spec"
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,6 @@
1
+ group 'gem' do
2
+ guard 'rspec', :cmd => "bundle exec rspec", :all_on_start => false, :all_after_pass => false, :failed_mode => :focus do
3
+ watch(%r{^spec/.+_spec\.rb$})
4
+ watch(%r{^lib/api/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
5
+ end
6
+ end
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2015 Paul Bonaud
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
@@ -0,0 +1,69 @@
1
+ # API wrapper
2
+
3
+ ## Description
4
+
5
+ This gem is a generic client for HTTP based APIs.
6
+
7
+ ## Installation
8
+
9
+ You can use this gem in your project by specifying it in your `Gemfile`:
10
+
11
+ ```
12
+ gem "api"
13
+ ```
14
+
15
+ or simply install it via the CLI:
16
+
17
+ ```
18
+ gem install api
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Create your own API client my inheriting from the Api::Client class and defining all mandatory defaults attributes in a Namespace::Default module as such:
24
+
25
+ ```
26
+ module ExampleApi
27
+ class Client < Api::Client
28
+ # Include mandatory modules
29
+ include Api::Configurable
30
+ include Api::Connection
31
+ include Api::Authentication
32
+ end
33
+
34
+ module Default
35
+ API_ENDPOINT = "http://example.org".freeze
36
+
37
+ class << self
38
+
39
+ # Include mandatory module
40
+ include Api::DefaultOptions
41
+
42
+ def api_endpoint
43
+ API_ENDPOINT
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ ```
50
+
51
+ Now get started requesting stuff from your api:
52
+
53
+ ```
54
+ c = ExampleApi::Client.new
55
+ => ...
56
+
57
+ c.root
58
+ => "<!doctype html>\n..."
59
+
60
+ c.last_response.data == c.get("/")
61
+ => true
62
+
63
+ c.get("/boom")
64
+ raises Api::NotFound: #<Sawyer::Response:0x000000029bb908>
65
+ ```
66
+
67
+ ## License
68
+
69
+ Code licensed under [MIT-LICENSE](https://github.com/paulrbr/api/blob/master/MIT-LICENSE)
@@ -0,0 +1,25 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
8
+
9
+ require 'api/version'
10
+ require 'rdoc/task'
11
+ RDoc::Task.new(:rdoc) do |rdoc|
12
+ rdoc.main = 'README.rdoc'
13
+ rdoc.rdoc_dir = 'rdoc'
14
+ rdoc.title = "Api #{Api::VERSION} documentation"
15
+ rdoc.rdoc_files.include('README*')
16
+ rdoc.rdoc_files.include('lib/**/*.rb')
17
+ rdoc.options << '--webcvs=http://github.com/paulrbr/api/tree/master/'
18
+ end
19
+
20
+ require 'gokdok'
21
+ Gokdok::Dokker.new do |gd|
22
+ gd.repo_url = 'git@github.com:paulrbr/api.git'
23
+ gd.doc_home = 'rdoc'
24
+ gd.remote_path = '.'
25
+ end
@@ -0,0 +1,37 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'api/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'apii'
6
+ s.licenses = ['MIT']
7
+ s.version = Api::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Paul Bonaud']
10
+ s.email = ['paul+gh@bonaud.fr']
11
+ s.homepage = 'http://github.com/paulrbr/api'
12
+ s.summary = %q(Easily build your API client.)
13
+ s.description = %q(This gem provides a generic API client to interact with any HTTP hypermedia APIs.)
14
+
15
+ s.rubyforge_project = 'api'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
20
+ s.require_paths = ['lib']
21
+
22
+ s.add_dependency 'hashie'
23
+ s.add_dependency 'faraday'
24
+ s.add_dependency 'sawyer'
25
+
26
+ s.add_development_dependency 'rake'
27
+ s.add_development_dependency 'rspec'
28
+ s.add_development_dependency 'webmock'
29
+ s.add_development_dependency 'vcr'
30
+ s.add_development_dependency 'multi_json'
31
+ s.add_development_dependency 'gokdok'
32
+ s.add_development_dependency 'guard'
33
+ s.add_development_dependency 'guard-rspec'
34
+ s.add_development_dependency 'rdoc'
35
+ s.add_development_dependency 'fakeweb'
36
+ s.add_development_dependency 'coveralls'
37
+ end
@@ -0,0 +1,45 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__)) unless
2
+ $LOAD_PATH.include?(File.dirname(__FILE__)) || $LOAD_PATH.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ require 'rubygems'
5
+ require 'json'
6
+
7
+ require 'api/errors'
8
+ require 'api/default'
9
+ require 'api/configurable'
10
+ require 'api/client'
11
+ require 'api/version'
12
+
13
+
14
+ # Ruby toolkit to build API clients
15
+ module Api
16
+
17
+ # class << self
18
+ # include Api::Configurable
19
+
20
+ # # API client based on configured options {Configurable}
21
+ # #
22
+ # # @return [Api::Client] API wrapper
23
+ # def client
24
+ # return @client if defined?(@client) && @client.same_options?(options)
25
+ # @client = Api::Client.new(options)
26
+ # end
27
+
28
+ # private
29
+
30
+ # def respond_to_missing?(method_name, include_private=false)
31
+ # client.respond_to?(method_name, include_private)
32
+ # end
33
+
34
+ # def method_missing(method_name, *args, &block)
35
+ # if client.respond_to?(method_name)
36
+ # return client.send(method_name, *args, &block)
37
+ # end
38
+
39
+ # super
40
+ # end
41
+
42
+ # end
43
+ end
44
+
45
+ # Api.reset!
@@ -0,0 +1,24 @@
1
+ module Api
2
+
3
+ # Authentication methods for {Api::Client}
4
+ module Authentication
5
+
6
+ # Indicates if the client was supplied an
7
+ # access token
8
+ #
9
+ # @return [Boolean]
10
+ def token_authenticated?
11
+ !@access_token.nil? && !@access_token.empty?
12
+ end
13
+
14
+ # Indicates if the client was supplied basic auth
15
+ # credentials
16
+ #
17
+ # @return [Boolean]
18
+ def basic_authenticated?
19
+ !@basic_login.nil? && !@basic_login.empty? &&
20
+ !@basic_password.nil? && !@basic_password.empty?
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ require 'api/authentication'
2
+ require 'api/configurable'
3
+ require 'api/connection'
4
+
5
+ module Api
6
+
7
+ # Client for an API
8
+ class Client
9
+
10
+ include Api::Authentication
11
+ include Api::Configurable
12
+ include Api::Connection
13
+
14
+ # Header keys that can be passed in options hash to {#get}
15
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
16
+
17
+ def initialize(options = {})
18
+ reset!(options)
19
+ end
20
+
21
+ # Text representation of the client, masking tokens and passwords
22
+ #
23
+ # @return [String]
24
+ def inspect
25
+ inspected = super
26
+
27
+ # mask password
28
+ inspected = inspected.gsub! @basic_password, "*******" if @basic_password
29
+ # Only show last 4 of token, secret
30
+ if @access_token
31
+ inspected = inspected.gsub! @access_token, "#{'*'*36}#{@access_token[36..-1]}"
32
+ end
33
+
34
+ inspected
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,110 @@
1
+ module Api
2
+
3
+ # Configuration options for {Client}, defaulting to values
4
+ # in {Default}
5
+ module Configurable
6
+ # @!attribute [w] access_token
7
+ # @return [String] access token for authentication
8
+ # @!attribute [w] access_token_prefix
9
+ # @return [String] access token prefix used before token in Authorization header
10
+ # @!attribute api_endpoint
11
+ # @return [String] Base URL for API requests.
12
+ # @!attribute api_version
13
+ # @return [String] Version of the api. default: v1
14
+ # @!attribute [w] basic_login
15
+ # @return [String] login for basic authentication
16
+ # @!attribute [w] basic_password
17
+ # @return [String] password for basic authentication
18
+ # @!attribute connection_options
19
+ # @see https://github.com/lostisland/faraday
20
+ # @return [Hash] Configure connection options for Faraday
21
+ # @!attribute sawyer_options
22
+ # @see https://github.com/lostisland/sawyer
23
+ # @return [Hash] Configure sawyer options
24
+ # @!attribute user_agent
25
+ # @return [String] Configure User-Agent header for requests.
26
+ # @!attribute auto_paginate
27
+ # @return [Boolean] Auto fetch next page of results until rate limit reached. Will only work with an Hypermedia API.
28
+ # @!attribute per_page
29
+ # @return [String] Configure page size for paginated results. API default: 30
30
+
31
+ attr_accessor :access_token, :access_token_prefix, :basic_login, :basic_password, :connection_options, :sawyer_options, :user_agent, :auto_paginate, :per_page
32
+ attr_writer :api_endpoint, :api_version
33
+
34
+ class << self
35
+
36
+ # List of configurable keys for {Api::Client}
37
+ # @return [Array] of option keys
38
+ def keys
39
+ @keys ||= [
40
+ :access_token,
41
+ :access_token_prefix,
42
+ :api_endpoint,
43
+ :api_version,
44
+ :auto_paginate,
45
+ :basic_login,
46
+ :basic_password,
47
+ :connection_options,
48
+ :sawyer_options,
49
+ :per_page,
50
+ :user_agent,
51
+ ]
52
+ end
53
+ end
54
+
55
+ # Set configuration options using a block
56
+ def configure
57
+ yield self
58
+ end
59
+
60
+ # Compares client options to a Hash of requested options
61
+ #
62
+ # @param opts [Hash] Options to compare with current client options
63
+ # @return [Boolean]
64
+ def same_options?(opts)
65
+ opts.hash == options.hash
66
+ end
67
+
68
+ # Reset configuration options to default values
69
+ def reset!(options = {})
70
+ default_class = module_defaults || class_defaults
71
+ Api::Configurable.keys.each do |key|
72
+ value = options[key] || default_class.options[key] || Api::Default.options[key]
73
+ instance_variable_set(:"@#{key}", value)
74
+ end
75
+ self
76
+ end
77
+
78
+ def api_endpoint
79
+ File.join(@api_endpoint, "")
80
+ end
81
+
82
+ def api_version
83
+ if @api_version.nil?
84
+ ""
85
+ else
86
+ File.join(@api_version, "")
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def options
93
+ Hash[Api::Configurable.keys.map{|key| [key, instance_variable_get(:"@#{key}")]}]
94
+ end
95
+
96
+ def module_defaults
97
+ try_defaults("#{self}::Default")
98
+ end
99
+
100
+ def class_defaults
101
+ try_defaults("#{self.class.to_s.split("::").first}::Default")
102
+ end
103
+
104
+ def try_defaults(object_name)
105
+ Object.const_get(object_name)
106
+ rescue NameError => _e
107
+ nil
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,186 @@
1
+ require 'api/response/raise_error'
2
+ require 'sawyer'
3
+
4
+ module Api
5
+
6
+ # Network layer for API clients.
7
+ module Connection
8
+
9
+ # Header keys that can be passed in options hash to {#get},{#head}
10
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
11
+
12
+ # Make a HTTP GET request
13
+ #
14
+ # @param url [String] The path, relative to {#api_endpoint/#api_version}
15
+ # @param options [Hash] Query and header params for request
16
+ # @return [Sawyer::Resource]
17
+ def get(url, options = {})
18
+ request :get, url, parse_query_and_convenience_headers(options)
19
+ end
20
+
21
+ # Make a HTTP POST request
22
+ #
23
+ # @param url [String] The path, relative to {#api_endpoint/#api_version}
24
+ # @param options [Hash] Body and header params for request
25
+ # @return [Sawyer::Resource]
26
+ def post(url, options = {})
27
+ request :post, url, options
28
+ end
29
+
30
+ # Make a HTTP PUT request
31
+ #
32
+ # @param url [String] The path, relative to {#api_endpoint/#api_version}
33
+ # @param options [Hash] Body and header params for request
34
+ # @return [Sawyer::Resource]
35
+ def put(url, options = {})
36
+ request :put, url, options
37
+ end
38
+
39
+ # Make a HTTP PATCH request
40
+ #
41
+ # @param url [String] The path, relative to {#api_endpoint/#api_version}
42
+ # @param options [Hash] Body and header params for request
43
+ # @return [Sawyer::Resource]
44
+ def patch(url, options = {})
45
+ request :patch, url, options
46
+ end
47
+
48
+ # Make a HTTP DELETE request
49
+ #
50
+ # @param url [String] The path, relative to {#api_endpoint/#api_version}
51
+ # @param options [Hash] Query and header params for request
52
+ # @return [Sawyer::Resource]
53
+ def delete(url, options = {})
54
+ request :delete, url, options
55
+ end
56
+
57
+ # Make a HTTP HEAD request
58
+ #
59
+ # @param url [String] The path, relative to {#api_endpoint/#api_version}
60
+ # @param options [Hash] Query and header params for request
61
+ # @return [Sawyer::Resource]
62
+ def head(url, options = {})
63
+ request :head, url, parse_query_and_convenience_headers(options)
64
+ end
65
+
66
+ # Make one or more HTTP GET requests, optionally fetching
67
+ # the next page of results from URL in Link response header based
68
+ # on value in {#auto_paginate}.
69
+ #
70
+ # @param url [String] The path, relative to {#api_endpoint/#api_version}
71
+ # @param options [Hash] Query and header params for request
72
+ # @param block [Block] Block to perform the data concatination of the
73
+ # multiple requests. The block is called with two parameters, the first
74
+ # contains the contents of the requests so far and the second parameter
75
+ # contains the latest response.
76
+ # @return [Sawyer::Resource]
77
+ def paginate(url, options = {}, &block)
78
+ opts = parse_query_and_convenience_headers(options.dup)
79
+ if @auto_paginate || @per_page
80
+ opts[:query][:per_page] ||= @per_page || (@auto_paginate ? 100 : nil)
81
+ end
82
+
83
+ data = request(:get, url, opts.dup)
84
+
85
+ if @auto_paginate
86
+ while @last_response.rels[:next] && rate_limit.remaining > 0
87
+ @last_response = @last_response.rels[:next].get(:headers => opts[:headers])
88
+ if block_given?
89
+ yield(data, @last_response)
90
+ else
91
+ data.concat(@last_response.data) if @last_response.data.is_a?(Array)
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ data
98
+ end
99
+
100
+ # Hypermedia agent for the API
101
+ #
102
+ # @return [Sawyer::Agent]
103
+ def agent
104
+ @agent ||= Sawyer::Agent.new(endpoint, sawyer_options) do |http|
105
+ http.headers[:content_type] = "application/json"
106
+ http.headers[:user_agent] = user_agent
107
+ if basic_authenticated?
108
+ http.basic_auth(@basic_login, @basic_password)
109
+ elsif token_authenticated?
110
+ http.authorization @access_token_prefix, @access_token
111
+ end
112
+ end
113
+ end
114
+
115
+ # Fetch the root resource for the API
116
+ #
117
+ # @return [Sawyer::Resource]
118
+ def root
119
+ get "/"
120
+ end
121
+
122
+ # Response for last HTTP request
123
+ #
124
+ # @return [Sawyer::Response]
125
+ def last_response
126
+ @last_response if defined? @last_response
127
+ end
128
+
129
+ protected
130
+
131
+ def endpoint
132
+ api_endpoint
133
+ end
134
+
135
+ private
136
+
137
+ def reset_agent
138
+ @agent = nil
139
+ end
140
+
141
+ def request(method, path, data, options = {})
142
+ if data.is_a?(Hash)
143
+ options[:query] = data.delete(:query) || {}
144
+ options[:headers] = data.delete(:headers) || {}
145
+ if accept = data.delete(:accept)
146
+ options[:headers][:accept] = accept
147
+ end
148
+ end
149
+
150
+ if api_version
151
+ path = File.join(api_version, path)
152
+ end
153
+
154
+ @last_response = response = agent.call(method, URI::Parser.new.escape(path.to_s), data, options)
155
+ raise_on_unsuccessful_status(response)
156
+ response.data
157
+ end
158
+
159
+ def raise_on_unsuccessful_status(response)
160
+ Response::RaiseError.try(response)
161
+ end
162
+
163
+ def sawyer_options
164
+ opts = @sawyer_options
165
+ conn_opts = @connection_options
166
+ opts[:faraday] = Faraday.new(conn_opts)
167
+
168
+ opts
169
+ end
170
+
171
+ def parse_query_and_convenience_headers(options)
172
+ headers = options.delete(:headers) { Hash.new }
173
+ CONVENIENCE_HEADERS.each do |h|
174
+ if header = options.delete(h)
175
+ headers[h] = header
176
+ end
177
+ end
178
+ query = options.delete(:query)
179
+ opts = {:query => options}
180
+ opts[:query].merge!(query) if query && query.is_a?(Hash)
181
+ opts[:headers] = headers unless headers.empty?
182
+
183
+ opts
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,90 @@
1
+ require "api/default_options"
2
+
3
+ module Api
4
+
5
+ # Default configuration options for {Client}
6
+ module Default
7
+
8
+ USER_AGENT = "Ruby toolkit API gem"
9
+
10
+ class << self
11
+
12
+ include Api::DefaultOptions
13
+
14
+ # Default access token from ENV
15
+ # @return [String]
16
+ def access_token
17
+ ENV['API_ACCESS_TOKEN']
18
+ end
19
+
20
+ # Default access token prefix
21
+ # @return [String]
22
+ def access_token_prefix
23
+ "token"
24
+ end
25
+
26
+ # Default API endpoint from ENV
27
+ # @return [String]
28
+ def api_endpoint
29
+ ENV['API_ENDPOINT']
30
+ end
31
+
32
+ # Default API version from ENV
33
+ # @return [String]
34
+ def api_version
35
+ ENV['API_VERSION']
36
+ end
37
+
38
+ # Default pagination preference from ENV
39
+ # @return [String]
40
+ def auto_paginate
41
+ ENV['API_AUTO_PAGINATE']
42
+ end
43
+
44
+ # Default login for basic auth from ENV
45
+ # @return [String]
46
+ def basic_login
47
+ ENV['API_LOGIN']
48
+ end
49
+
50
+ # Default password for basic auth from ENV
51
+ # @return [String]
52
+ def basic_password
53
+ ENV['API_PASSWORD']
54
+ end
55
+
56
+ # Default options for Faraday::Connection
57
+ # @return [Hash]
58
+ def connection_options
59
+ {
60
+ :headers => {
61
+ :user_agent => user_agent
62
+ }
63
+ }
64
+ end
65
+
66
+ # Default options for Sawyer::Agent
67
+ # @return [Hash]
68
+ def sawyer_options
69
+ {
70
+ :links_parser => Sawyer::LinkParsers::Simple.new
71
+ }
72
+ end
73
+
74
+ # Default pagination page size from ENV
75
+ # @return [Fixnum] Page size
76
+ def per_page
77
+ page_size = ENV['API_PER_PAGE']
78
+
79
+ page_size.to_i if page_size
80
+ end
81
+
82
+ # Default User-Agent header string from ENV or {USER_AGENT}
83
+ # @return [String]
84
+ def user_agent
85
+ ENV['API_USER_AGENT'] || USER_AGENT
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,14 @@
1
+ module Api
2
+
3
+ # Module to include in Default modules of your API toolkit
4
+ module DefaultOptions
5
+
6
+ # Configuration options
7
+ # @return [Hash]
8
+ def options
9
+ Hash[Api::Configurable.keys.select { |key| respond_to?(key) }
10
+ .map { |key| [key, send(key)] }]
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,60 @@
1
+ module Api
2
+
3
+ # All errors from this gem will inherit from this one.
4
+ class Error < StandardError
5
+ # Returns the appropriate Api::Error subclass based
6
+ # on status and response message
7
+ #
8
+ # @param [Hash] response HTTP response
9
+ # @return [Api::Error]
10
+ def self.from_response(response)
11
+ status = if response.respond_to?(:[])
12
+ response[:status].to_i
13
+ else
14
+ response.status
15
+ end
16
+
17
+ if klass = case status
18
+ when 400 then Api::BadRequest
19
+ when 401 then Api::Unauthorized
20
+ when 403 then Api::Unauthorized
21
+ when 404 then Api::NotFound
22
+ when 405 then Api::MethodNotAllowed
23
+ when 406 then Api::NotAcceptable
24
+ when 409 then Api::Conflict
25
+ when 415 then Api::UnsupportedMediaType
26
+ when 422 then Api::UnprocessableEntity
27
+ when 400..499 then Api::ClientError
28
+ when 500 then Api::InternalServerError
29
+ when 501 then Api::NotImplemented
30
+ when 502 then Api::BadGateway
31
+ when 503 then Api::ServiceUnavailable
32
+ when 500..599 then Api::ServerError
33
+ end
34
+ klass.new(response)
35
+ end
36
+ end
37
+
38
+ def initialize(response=nil)
39
+ @response = response
40
+ super
41
+ end
42
+ end
43
+
44
+ class ClientError < Error; end
45
+ class BadRequest < ClientError; end
46
+ class Unauthorized < ClientError; end
47
+ class Unauthorized < ClientError; end
48
+ class NotFound < ClientError; end
49
+ class MethodNotAllowed < ClientError; end
50
+ class NotAcceptable < ClientError; end
51
+ class Conflict < ClientError; end
52
+ class UnsupportedMediaType < ClientError; end
53
+ class UnprocessableEntity < ClientError; end
54
+ class InternalServerError < ClientError; end
55
+ class NotImplemented < ClientError; end
56
+ class BadGateway < ClientError; end
57
+ class ServiceUnavailable < ClientError; end
58
+ class ServerError < ClientError; end
59
+
60
+ end
@@ -0,0 +1,28 @@
1
+ require 'faraday'
2
+ require 'api/errors'
3
+
4
+ module Api
5
+ # Faraday response middleware
6
+ module Response
7
+
8
+ # This class raises an Api-flavored exception based
9
+ # HTTP status codes returned by the API
10
+ class RaiseError < Faraday::Response::Middleware
11
+
12
+ class << self
13
+ def try(response)
14
+ @middleware ||= RaiseError.new
15
+ @middleware.send(:on_complete, response)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def on_complete(response)
22
+ if error = Api::Error.from_response(response)
23
+ raise error
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module Api
2
+ VERSION = '0.0.4'
3
+ end
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe Api::Client do
4
+ before do
5
+ VCR.turn_off!
6
+ allow(Sawyer::LinkParsers::Simple).to receive(:new).and_return(Sawyer::LinkParsers::Simple)
7
+ @client = Api::Client.new
8
+ end
9
+
10
+ it "sets defaults" do
11
+ Api::Configurable.keys.each do |key|
12
+ expect(@client.instance_variable_get(:"@#{key}")).to eq(Api::Default.send(key))
13
+ end
14
+ end
15
+
16
+ describe ".configure" do
17
+ Api::Configurable.keys.each do |key|
18
+ it "sets the #{key.to_s.gsub('_', ' ')}" do
19
+ @client.configure do |config|
20
+ config.send("#{key}=", key)
21
+ end
22
+ expect(@client.instance_variable_get(:"@#{key}")).to eq(key)
23
+ end
24
+ end
25
+ end
26
+
27
+ describe ".inspect" do
28
+ before do
29
+ @client = Api::Client.new(access_token: "token123", basic_password: "mysecret")
30
+ end
31
+
32
+ it "masks access token" do
33
+ expect(@client.inspect).to_not match("token123")
34
+ end
35
+
36
+ it "masks basic auth password" do
37
+ expect(@client.inspect).to_not match("mysecret")
38
+ end
39
+ end
40
+
41
+ describe "A client implementation" do
42
+ before do
43
+ module MyApi
44
+ class Client < Api::Client
45
+ end
46
+
47
+ module Default
48
+ class << self
49
+ include Api::DefaultOptions
50
+
51
+ def api_endpoint
52
+ "http://example.org"
53
+ end
54
+ def method_missing(_method)
55
+ ""
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ @client = MyApi::Client.new
62
+ end
63
+
64
+ describe ".root" do
65
+ it "gets the root of the api" do
66
+ stub_get('/').to_return(:status => 200)
67
+ expect { @client.root }.to_not raise_error
68
+ expect(@client.last_response.data).to eq('')
69
+ end
70
+ end
71
+
72
+ context "with no authentication" do
73
+ it "does requests with no authorization header" do
74
+ stub_head('/hello').to_return(:status => 200)
75
+ expect { @client.head('/hello') }.to_not raise_error
76
+ expect(@client.last_response.headers).to eq({})
77
+ end
78
+ end
79
+
80
+ context "with token authentication" do
81
+ before do
82
+ @client.configure do |c|
83
+ c.access_token = "token123"
84
+ end
85
+ end
86
+
87
+ it "does requests with a token authorization header" do
88
+ stub_post('/hello').with(:headers => { 'Authorization' => 'token token123' }).to_return(:status => 200)
89
+ expect { @client.post('/hello') }.to_not raise_error
90
+ expect(@client.last_response.data).to eq('')
91
+ end
92
+ end
93
+
94
+ context "with basic authentication" do
95
+ before do
96
+ @client.configure do |c|
97
+ c.basic_login = "login"
98
+ c.basic_password = "password"
99
+ end
100
+ end
101
+
102
+ it "does requests with basic auth method" do
103
+ stub_put('/hello', basic_login: "login", basic_password: "password").to_return(:status => 200)
104
+ expect { @client.put('/hello') }.to_not raise_error
105
+ expect(@client.last_response.data).to eq('')
106
+ end
107
+ end
108
+
109
+ context "error handling" do
110
+ it "raises on 404" do
111
+ stub_delete('/booya').to_return(:status => 404)
112
+ expect { @client.delete('/booya') }.to raise_error Api::NotFound
113
+ end
114
+
115
+ it "raises on 401" do
116
+ stub_patch('/forbidden').to_return(:status => 401)
117
+ expect { @client.patch('/forbidden') }.to raise_error Api::Unauthorized
118
+ end
119
+
120
+ it "raises on 500" do
121
+ stub_get('/boom').to_return(:status => 500)
122
+ expect { @client.get('/boom') }.to raise_error Api::InternalServerError
123
+ end
124
+
125
+ it "raises for all supported codes" do
126
+ %w(400 403 405 406 409 415 422 499 501 502 503 504).each do |code|
127
+ stub_get('/error').to_return(:status => code.to_i)
128
+ expect { @client.get('/error') }.to raise_error
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,78 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
4
+ SimpleCov::Formatter::HTMLFormatter,
5
+ Coveralls::SimpleCov::Formatter
6
+ ]
7
+ SimpleCov.start
8
+
9
+ require 'json'
10
+ require 'api'
11
+ require 'rspec'
12
+ require 'webmock/rspec'
13
+
14
+ WebMock.disable_net_connect!(:allow => 'coveralls.io')
15
+
16
+ require 'vcr'
17
+ VCR.configure do |c|
18
+ c.hook_into :webmock
19
+ end
20
+
21
+ def api_url(url, opts = {})
22
+ return url if url =~ /^http/
23
+
24
+ basic_auth = opts[:basic_login] ? "#{opts[:basic_login]}:#{opts[:basic_password]}@" : ""
25
+ url = File.join("http://#{basic_auth}example.org", url)
26
+ uri = Addressable::URI.parse(url)
27
+
28
+ uri.to_s
29
+ end
30
+
31
+ def token_client
32
+ Api::Client.new(:access_token => test_token)
33
+ end
34
+
35
+ def test_token
36
+ ENV.fetch 'API_TEST_TOKEN', 'x' * 20
37
+ end
38
+
39
+ def stub_delete(url, opts = {})
40
+ stub_request(:delete, api_url(url, opts))
41
+ end
42
+
43
+ def stub_get(url, opts = {})
44
+ stub_request(:get, api_url(url, opts))
45
+ end
46
+
47
+ def stub_head(url, opts = {})
48
+ stub_request(:head, api_url(url, opts))
49
+ end
50
+
51
+ def stub_patch(url, opts = {})
52
+ stub_request(:patch, api_url(url, opts))
53
+ end
54
+
55
+ def stub_post(url, opts = {})
56
+ stub_request(:post, api_url(url, opts))
57
+ end
58
+
59
+ def stub_put(url, opts = {})
60
+ stub_request(:put, api_url(url, opts))
61
+ end
62
+
63
+ def fixture_path
64
+ File.expand_path("../fixtures", __FILE__)
65
+ end
66
+
67
+ def fixture(file)
68
+ File.new(fixture_path + '/' + file)
69
+ end
70
+
71
+ def json_response(file)
72
+ {
73
+ :body => fixture(file),
74
+ :headers => {
75
+ :content_type => 'application/json; charset=utf-8'
76
+ }
77
+ }
78
+ end
metadata ADDED
@@ -0,0 +1,264 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apii
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Paul Bonaud
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-10-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hashie
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sawyer
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: vcr
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: multi_json
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: gokdok
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: guard
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: guard-rspec
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rdoc
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: fakeweb
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: coveralls
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ description: This gem provides a generic API client to interact with any HTTP hypermedia
210
+ APIs.
211
+ email:
212
+ - paul+gh@bonaud.fr
213
+ executables: []
214
+ extensions: []
215
+ extra_rdoc_files: []
216
+ files:
217
+ - ".gitignore"
218
+ - ".rspec"
219
+ - ".rubocop.yml"
220
+ - ".rubocop_todo.yml"
221
+ - ".travis.yml"
222
+ - Gemfile
223
+ - Guardfile
224
+ - MIT-LICENSE
225
+ - README.md
226
+ - Rakefile
227
+ - api.gemspec
228
+ - lib/api.rb
229
+ - lib/api/authentication.rb
230
+ - lib/api/client.rb
231
+ - lib/api/configurable.rb
232
+ - lib/api/connection.rb
233
+ - lib/api/default.rb
234
+ - lib/api/default_options.rb
235
+ - lib/api/errors.rb
236
+ - lib/api/response/raise_error.rb
237
+ - lib/api/version.rb
238
+ - spec/client_spec.rb
239
+ - spec/spec_helper.rb
240
+ homepage: http://github.com/paulrbr/api
241
+ licenses:
242
+ - MIT
243
+ metadata: {}
244
+ post_install_message:
245
+ rdoc_options: []
246
+ require_paths:
247
+ - lib
248
+ required_ruby_version: !ruby/object:Gem::Requirement
249
+ requirements:
250
+ - - ">="
251
+ - !ruby/object:Gem::Version
252
+ version: '0'
253
+ required_rubygems_version: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ requirements: []
259
+ rubyforge_project: api
260
+ rubygems_version: 2.4.8
261
+ signing_key:
262
+ specification_version: 4
263
+ summary: Easily build your API client.
264
+ test_files: []