johac 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c9a6e0cd9ab2cb5c7aba657db7c59a726d7ec85a
4
+ data.tar.gz: c25864469ee9ace99f7b21fc7982a1bec72a8f69
5
+ SHA512:
6
+ metadata.gz: c6a3065a97f85e39e4a05963b559eb0d9260200c7898953485b58217df614f6c99f44846d9616ce50995fd7d1af9e83fba414ac528818e0cd9f838654d954ac7
7
+ data.tar.gz: 5bd7feade84a9af0a72e0341bf8ddb048867ce778d6aff22ee0f310d6d9a0ff38500d091c712cc5867d911eda69a4fec8687a96b65a985fc26376be97f021b2b
@@ -0,0 +1,73 @@
1
+ module Johac
2
+
3
+ # Base class for Johac API Client.
4
+ #
5
+ # Extend this class and provide API specific calls
6
+ class Client
7
+
8
+ attr_reader :config
9
+
10
+ # HTTP lib inclusion
11
+ #
12
+ # Supplies one method, {#connection}.
13
+ #
14
+ # The {#connection} method should return an object that responds to HTTP verbs (eg. #get, #post, #put, #delete).
15
+ include Connection
16
+
17
+ # @param config [Johac::Config]
18
+ def initialize(config=nil)
19
+ @config = Johac.merged_config(config)
20
+ end
21
+
22
+ # Reference to +base_uri+ set on {Johac.config} in {Johac.configure} or in the constructor.
23
+ #
24
+ # @return [String]
25
+ def uri
26
+ config.base_uri
27
+ end
28
+
29
+ # Reference to the current environment. Set explicitly in {Johac.configure} or via
30
+ # environment variables +JOHAC_ENV+ or +RAILS_ENV+.
31
+ #
32
+ # @return [String] One of "testing", "development", or "production"
33
+ def env
34
+ config.env
35
+ end
36
+
37
+ protected
38
+
39
+ def head(path, options={})
40
+ request { connection.head(path, options[:query], options[:headers]) }
41
+ end
42
+
43
+ def get(path, options={})
44
+ request { connection.get(path, options[:query], options[:headers]) }
45
+ end
46
+
47
+ def delete(path, options={})
48
+ request { connection.delete(path, options[:query], options[:headers]) }
49
+ end
50
+
51
+ def post(path, options={})
52
+ request { connection.post(path, options[:body], options[:headers]) }
53
+ end
54
+
55
+ def put(path, options={})
56
+ request { connection.put(path, options[:body], options[:headers]) }
57
+ end
58
+
59
+ def patch(path, options={})
60
+ request { connection.patch(path, options[:body], options[:headers]) }
61
+ end
62
+
63
+ private
64
+
65
+ # Wrap the response or error, or raise an exception if configured
66
+ def request(&block)
67
+ Johac::Response.new(yield)
68
+ rescue => e
69
+ @config.raise_exceptions ? (raise e) : Johac::Response.new(e)
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,185 @@
1
+ module Johac
2
+
3
+ # Provides {#connection}.
4
+ #
5
+ # Uses the {https://github.com/lostisland/faraday faraday} lib to handle HTTP requests.
6
+ #
7
+ # @see https://github.com/lostisland/faraday
8
+ module Connection
9
+
10
+ private
11
+
12
+ def connection
13
+ @connection ||= Faraday.new(:url => uri) do |faraday|
14
+ faraday.use :johac_connection_exceptions
15
+
16
+ faraday.request :json
17
+ faraday.request :johac_headers
18
+ faraday.request :johac_auth, @config.auth_scheme, @config.access_key, @config.secret_key
19
+
20
+ # Retry requests
21
+ faraday.request :retry, {
22
+ max: 2,
23
+ interval: 0.5,
24
+ backoff_factor: 3,
25
+ interval_randomness: 0.5,
26
+
27
+ # All connection errors + 429, 503, and 504
28
+ exceptions: [
29
+ Johac::Error::ConnectionError,
30
+ Johac::Error::RetryableError
31
+ ]
32
+ }
33
+
34
+ # apparently the response middleware acts like a stack, not a queue
35
+ faraday.response :json
36
+
37
+ # Raise connection errors as exceptions
38
+ faraday.response :johac_raise_error
39
+
40
+ faraday.adapter :johac_persistent do |http|
41
+ # http param is an instance of Net::HTTP::Persistent
42
+ #
43
+ http.open_timeout = 2
44
+ http.read_timeout = 60
45
+ http.idle_timeout = 120
46
+
47
+ # http.keep_alive
48
+ # http.max_requests
49
+ # http.socket_options
50
+ http.debug_output = STDOUT if env == 'development'
51
+ end
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ # Custom middleware.
58
+ module Connection::Middleware
59
+
60
+ # Wrapper of {Faraday::Adapter::NetHttpPersistent} adapter that allows for
61
+ # a block to be given when the faraday connection is created. Block is
62
+ # passed the newly created {Net::HTTP::Persistent} instance to be modified
63
+ # if desired.
64
+ #
65
+ # Allows for configuration of HTTP connection.
66
+ #
67
+ # @see https://github.com/lostisland/faraday
68
+ # @see https://github.com/drbrain/net-http-persistent
69
+ class PersistentAdapter < Faraday::Adapter::NetHttpPersistent
70
+
71
+ Faraday::Adapter.register_middleware :johac_persistent => self
72
+
73
+ # Extend Faraday::Adapter::NetHttpPersistent's initialize method with an
74
+ # optional block.
75
+ #
76
+ # @yield [Net::HTTP::Persistent] Stores the passed block for use when
77
+ # creating a new HTTP connection.
78
+ def initialize(app, &block)
79
+ @config_block = block
80
+ super(app)
81
+ end
82
+
83
+ # Yield HTTP connection to supplied block.
84
+ def with_net_http_connection(env, &block)
85
+ http = super(env) { |v| v }
86
+ @config_block.call(http) if @config_block
87
+ yield http
88
+ end
89
+
90
+ end
91
+
92
+ # Write custom user agent to all requests.
93
+ class JohacHeaders < Faraday::Middleware
94
+
95
+ Faraday::Request.register_middleware :johac_headers => self
96
+
97
+ AgentString = "johac/#{Johac::Version} ruby/#{RUBY_VERSION}".freeze
98
+ Accept = 'application/json'.freeze
99
+
100
+ def call(env)
101
+ env[:request_headers]['User-Agent'] = AgentString
102
+ env[:request_headers]['Accept'] = Accept
103
+ @app.call(env)
104
+ end
105
+
106
+ end
107
+
108
+ # Adds Authorization header to all requests.
109
+ class Authorization < Faraday::Middleware
110
+
111
+ Faraday::Request.register_middleware :johac_auth => self
112
+
113
+ def initialize(app, scheme, access_key, secret_key)
114
+ @scheme = scheme
115
+ @access_key = access_key
116
+ @secret_key = secret_key
117
+ super(app)
118
+ end
119
+
120
+ def call(env)
121
+ if auth = case @scheme
122
+ when :basic then "Basic #{basic_auth_token}"
123
+ when :hmac then "hmac #{hmac_auth_token(env.url.path)}"
124
+ end
125
+ env[:request_headers]['Authorization'] = auth
126
+ end
127
+ @app.call(env)
128
+ end
129
+
130
+ private
131
+
132
+ def basic_auth_token
133
+ Base64.strict_encode64("#{@access_key}:#{@secret_key}")
134
+ end
135
+
136
+ # TODO: Need a hook for generating the value
137
+ def hmac_auth_token(token)
138
+ OpenSSL::HMAC.hexdigest('sha1', @secret_key, token)
139
+ end
140
+
141
+ end
142
+
143
+ # Will raise some {Johac::Error::ResponseError} from an HTTP response using status codes.
144
+ class RaiseError < Faraday::Response::Middleware
145
+
146
+ Faraday::Response.register_middleware :johac_raise_error => self
147
+
148
+ def on_complete(env)
149
+ if e = ::Johac::Error::ResponseError.from_response(env.status, env.response_headers, env.body)
150
+ raise e
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ # Will raise some {Johac::Error::ConnectionError} if something happens with the connection.
157
+ class Exceptions < Faraday::Middleware
158
+
159
+ Faraday::Middleware.register_middleware :johac_connection_exceptions => self
160
+
161
+ def call(env)
162
+ @app.call(env)
163
+ rescue Faraday::Error::ConnectionFailed => e
164
+ raise ::Johac::Error::ConnectionError, e.message
165
+ rescue Faraday::Error::ResourceNotFound => e
166
+ raise ::Johac::Error::ConnectionError, e.message
167
+ rescue Faraday::Error::ParsingError => e
168
+ raise ::Johac::Error::ConnectionError, e.message
169
+ rescue Faraday::Error::TimeoutError => e
170
+ raise ::Johac::Error::ConnectionError, e.message
171
+ rescue Faraday::Error::SSLError => e
172
+ raise ::Johac::Error::ConnectionError, e.message
173
+ rescue Faraday::Error::ClientError => e
174
+ raise ::Johac::Error::ConnectionError, e.message
175
+ rescue Faraday::Error => e
176
+ raise ::Johac::Error::ConnectionError, e.message
177
+ rescue Net::HTTP::Persistent::Error => e
178
+ raise ::Johac::Error::ConnectionError, e.message, e.backtrace
179
+ end
180
+
181
+ end
182
+
183
+ end
184
+
185
+ end
@@ -0,0 +1,66 @@
1
+ module Johac
2
+
3
+ # {Johac} exception overrides.
4
+ #
5
+ # Rescue {Johac::Error Johac::Error} to catch all Johac errors.
6
+ class Error < StandardError
7
+
8
+ # Exception to be used when dealing with HTTP responses from a Johac API.
9
+ class ResponseError < Error
10
+
11
+ attr_reader :code
12
+ attr_reader :body
13
+
14
+ # Will attempt to decode a response body via JSON, and look for the 'message' key in the resulting (assumed) hash. If response body cannot be parsed via JSON the entire response body is set as the message for the exception.
15
+ #
16
+ # @param body [String] Response body.
17
+ # @param headers [Hash] Response headers.
18
+ def initialize(code, body, headers)
19
+ @code = code
20
+ @body = if headers['Content-Type'] == 'application/json'
21
+ JSON.parse(body) rescue body
22
+ else
23
+ body
24
+ end
25
+ super(body)
26
+ end
27
+
28
+ # If a problem is detected in an HTTP response, build the proper exception, otherwise return nil.
29
+ #
30
+ # @param status_code [Integer] HTTP response code.
31
+ # @param body [String] Response body.
32
+ # @param headers [Hash] Response headers.
33
+ #
34
+ # @return [Johac::Error::RetryableError] A client or server error which may be retried
35
+ # @return [Johac::Error::ClientError] The client has made a mistake and should take corrective action.
36
+ # @return [Johac::Error::ServerError] If there was a problem server-side, we're on it.
37
+ # @return [nil] If no error was detected in the response.
38
+ def self.from_response(status_code, headers, body)
39
+ if klass = case status_code
40
+ when 429 then RetryableError
41
+ when 503 then RetryableError
42
+ when 504 then RetryableError
43
+ when 400..499 then ClientError
44
+ when 500..599 then ServerError
45
+ end
46
+ klass.new(status_code, body, headers)
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ # An error that we should retry
53
+ class RetryableError < ResponseError; end
54
+
55
+ # There was a problem on our side, we're working on it.
56
+ class ServerError < ResponseError; end
57
+
58
+ # There was a problem with the requested or submitted resource.
59
+ class ClientError < ResponseError; end
60
+
61
+ # Error specific to the http connection.
62
+ class ConnectionError < Error; end
63
+
64
+ end
65
+
66
+ end
@@ -0,0 +1,128 @@
1
+ module Johac
2
+ class Response
3
+
4
+ attr_reader :response
5
+ attr_reader :exception
6
+
7
+ include Enumerable
8
+
9
+ def initialize(result)
10
+ if result.kind_of?(Faraday::Response)
11
+ @response = result
12
+ else
13
+ @exception = result
14
+ end
15
+ end
16
+
17
+ # Determine if the request failed
18
+ #
19
+ # @return true if response failed (http or expcetion)
20
+ def error?
21
+ exception != nil || status >= 400
22
+ end
23
+
24
+ # HTTP Status code
25
+ #
26
+ # @return response status code
27
+ def status
28
+ response_status || exception_status || 0
29
+ end
30
+
31
+ # HTTP Status code
32
+ #
33
+ # @return response status code
34
+ def code
35
+ status
36
+ end
37
+
38
+ # HTTP Response body as a JSON parsed object
39
+ #
40
+ # @return Hash/Array of the JSON parsed body, or empty hash
41
+ def body
42
+ response_body || exception_body || {}
43
+ end
44
+
45
+ # @return OpenStruct object of hash
46
+ def object
47
+ OpenStruct.new(body)
48
+ end
49
+
50
+ # Map body hash to another value using a block
51
+ #
52
+ # @param block [Block] mapping function block
53
+ #
54
+ # @return result of block
55
+ def map_object(&block)
56
+ yield body
57
+ end
58
+
59
+ # Dig for a item in the body
60
+ #
61
+ # @param args [Varargs] path of value
62
+ #
63
+ # @see {Hash.dig}
64
+ # @see {Array.dig}
65
+ #
66
+ # @return value of key path
67
+ def dig(*args)
68
+ body.dig(*args)
69
+ end
70
+
71
+ # Enumerate over response body opject
72
+ #
73
+ # @see {Enumerable}
74
+ # @param block [Block] to invoke
75
+ def each(&block)
76
+ body.each(&block)
77
+ end
78
+
79
+ # Invoke a block of code if the response fails, with
80
+ # the exception as the paramter.
81
+ #
82
+ # @param block [Block] to invoke
83
+ def on_error(&block)
84
+ if error?
85
+ yield exception
86
+ end
87
+ self
88
+ end
89
+
90
+ # Invoke a block of code if the response succeeds, with the content
91
+ # as a parameter
92
+ #
93
+ # @param block [Block] to invoke
94
+ def on_success(&block)
95
+ unless error?
96
+ yield body
97
+ end
98
+ self
99
+ end
100
+
101
+ protected
102
+
103
+ def response_status
104
+ response? ? response.status : nil
105
+ end
106
+
107
+ def response_body
108
+ response? ? response.body : nil
109
+ end
110
+
111
+ def exception_status
112
+ response_error? ? exception.code : nil
113
+ end
114
+
115
+ def exception_body
116
+ response_error? ? exception.body : nil
117
+ end
118
+
119
+ def response_error?
120
+ exception.kind_of?(Johac::Error::ResponseError)
121
+ end
122
+
123
+ def response?
124
+ response != nil
125
+ end
126
+
127
+ end
128
+ end
@@ -0,0 +1,3 @@
1
+ module Johac
2
+ Version = '0.9.0'
3
+ end
data/lib/johac.rb ADDED
@@ -0,0 +1,95 @@
1
+ $: << File.dirname(__FILE__)
2
+
3
+ require 'json'
4
+ require 'faraday'
5
+ require 'faraday_middleware'
6
+ require 'net/http/persistent'
7
+ require 'base64'
8
+ require 'ostruct'
9
+
10
+ module Johac
11
+
12
+ class << self
13
+
14
+ # Simple config class used to store global {Johac::Client} settings.
15
+ #
16
+ # @attr base_uri [String] Hostname of Johac API.
17
+ # @attr auth_scheme [Symbol] Authorization scheme to be used on all requests, +:basic+ or +:hmac+.
18
+ # @attr access_key [String] Public access key used for authorization of requests.
19
+ # @attr secret_key [String] Private key used for authorization of requests.
20
+ # @attr env [String] Environment. 'testing', 'development', or 'production'.
21
+ Config = Struct.new(:base_uri, :auth_scheme, :access_key, :secret_key, :env, :logger, :raise_exceptions)
22
+
23
+
24
+ # Configure the global defaults for all clients built.
25
+ #
26
+ # @yield [config] Passes a config object to the block.
27
+ # @yieldparam config [Johac::Config] Config object.
28
+ #
29
+ # @example
30
+ # Johac.defaults do |config|
31
+ # config.base_uri = 'http://api.myservice.com'
32
+ # config.auth_scheme = :basic
33
+ # config.access_key = 'user'
34
+ # config.secret_key = 'password'
35
+ # end
36
+ #
37
+ # @return [Johac::Config]
38
+ def defaults(&block)
39
+ c = @config ||= Config.new
40
+ c.env = environment || 'production'
41
+ yield c
42
+ c
43
+ end
44
+
45
+ # Return the config object for the global {Johac::Client} instance.
46
+ # @return [Johac::Config]
47
+ def config
48
+ unless defined? @config
49
+ raise "#{self.name} not configured. Configure via #{self.name}.configure { |cfg| ... }, or via client constructor"
50
+ end
51
+ @config
52
+ end
53
+
54
+ # Merge the new config with defaults
55
+ def merged_config(overrides)
56
+ case overrides
57
+ when Hash
58
+ dup_config.tap do |conf|
59
+ overrides.each do |k, v|
60
+ conf[k] = v
61
+ end
62
+ end
63
+ when Struct
64
+ dup_config.tap do |conf|
65
+ overrides.each_pair do |k, v|
66
+ conf[k] = v
67
+ end
68
+ end
69
+ else
70
+ config
71
+ end
72
+ end
73
+
74
+ protected
75
+
76
+ def environment
77
+ ENV.values_at('JOHAC_ENV', 'RAILS_ENV').compact.first
78
+ end
79
+
80
+ private
81
+
82
+ def dup_config
83
+ defined?(@config) ? @config.dup : Config.new
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+
90
+ require 'johac/version'
91
+ require 'johac/error'
92
+ require 'johac/connection'
93
+ require 'johac/response'
94
+ require 'johac/client'
95
+
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: johac
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Dan Simpson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.15.2
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.9.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 0.15.2
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.9.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: faraday_middleware
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.12.2
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.12.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: net-http-persistent
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.9'
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.9.4
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '2.9'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.9.4
67
+ - !ruby/object:Gem::Dependency
68
+ name: rake
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '10.4'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 10.4.2
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '10.4'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 10.4.2
87
+ - !ruby/object:Gem::Dependency
88
+ name: rack
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '1.4'
94
+ type: :development
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '1.4'
101
+ - !ruby/object:Gem::Dependency
102
+ name: webmock
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: 3.3.0
108
+ type: :development
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: 3.3.0
115
+ - !ruby/object:Gem::Dependency
116
+ name: yard
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: 0.8.7.6
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: 0.8.7.6
129
+ - !ruby/object:Gem::Dependency
130
+ name: minitest
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '4.7'
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 4.7.5
139
+ type: :development
140
+ prerelease: false
141
+ version_requirements: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '4.7'
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: 4.7.5
149
+ description: Opintionated library for implement HTTP+JSON API clients
150
+ email: dan.simpson@gmail.com
151
+ executables: []
152
+ extensions: []
153
+ extra_rdoc_files: []
154
+ files:
155
+ - lib/johac.rb
156
+ - lib/johac/client.rb
157
+ - lib/johac/connection.rb
158
+ - lib/johac/error.rb
159
+ - lib/johac/response.rb
160
+ - lib/johac/version.rb
161
+ homepage: https://github.com/dansimpson/johac
162
+ licenses:
163
+ - MIT
164
+ metadata: {}
165
+ post_install_message:
166
+ rdoc_options: []
167
+ require_paths:
168
+ - lib
169
+ required_ruby_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ requirements: []
180
+ rubyforge_project:
181
+ rubygems_version: 2.6.13
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: JSON Over HTTP API Client
185
+ test_files: []
186
+ has_rdoc: true