apii 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []