saddle 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +33 -0
- data/LICENSE +19 -0
- data/README.md +39 -0
- data/lib/saddle.rb +32 -0
- data/lib/saddle/endpoint.rb +108 -0
- data/lib/saddle/method_tree_builder.rb +113 -0
- data/lib/saddle/middleware/airbrake.rb +30 -0
- data/lib/saddle/middleware/default_response.rb +24 -0
- data/lib/saddle/middleware/parse_json.rb +36 -0
- data/lib/saddle/middleware/ruby_timeout.rb +19 -0
- data/lib/saddle/middleware/statsd_logging.rb +45 -0
- data/lib/saddle/options.rb +80 -0
- data/lib/saddle/requester.rb +159 -0
- data/lib/saddle/version.rb +3 -0
- data/saddle.gemspec +28 -0
- data/spec/saddle_client_spec.rb +34 -0
- metadata +23 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZTEzYTVkN2VkMjExYWQ4ZDg1OWY1NDFjMTI1M2E1NzliNTYwYTE3ZA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MzUyNzE1NDk1MDcxMmEzNzc2NzYyYjI0NjI5MGU0YmI0OWEwYzc4OQ==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
MDliMzRkYjYyY2VhMjlmNTgzNzM2ZTgxNjhiZGI2M2EzMzc5YmMwM2JmN2Ux
|
10
|
+
ZjgyOGU4NjZlZGNjN2Q4NzNiOGIyYzgzY2U2NmExY2E2OTRlZThjM2JmMDMy
|
11
|
+
MTgxMDE2ZjZmMmE3OTA2OTE4YTE3YmE5NDQ4ODVmZGVjMmFmNDY=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZDM5MWY5NDRiYzBiMmM1NjM1ZTQzNzc1YmE1N2VjZDI1MjM0ZmU0ZGE3MWU4
|
14
|
+
NDI2ZTNiODI2MmYxYjZjMjA0MGUzYzUwZjNhOGRmMGEzNmY3OTBkODVkYTFl
|
15
|
+
MDJiMDZiYzA5YmY5OTJiNjg2MGY0YmJlZTExYTNiYTBkMjNlNTU=
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
GEM
|
2
|
+
remote: https://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (3.2.13)
|
5
|
+
i18n (= 0.6.1)
|
6
|
+
multi_json (~> 1.0)
|
7
|
+
diff-lcs (1.2.1)
|
8
|
+
faraday (0.8.6)
|
9
|
+
multipart-post (~> 1.1)
|
10
|
+
faraday_middleware (0.9.0)
|
11
|
+
faraday (>= 0.7.4, < 0.9)
|
12
|
+
i18n (0.6.1)
|
13
|
+
multi_json (1.7.2)
|
14
|
+
multipart-post (1.2.0)
|
15
|
+
rspec (2.13.0)
|
16
|
+
rspec-core (~> 2.13.0)
|
17
|
+
rspec-expectations (~> 2.13.0)
|
18
|
+
rspec-mocks (~> 2.13.0)
|
19
|
+
rspec-core (2.13.1)
|
20
|
+
rspec-expectations (2.13.0)
|
21
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
22
|
+
rspec-instafail (0.2.4)
|
23
|
+
rspec-mocks (2.13.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
activesupport (>= 3.0)
|
30
|
+
faraday (~> 0.8.6)
|
31
|
+
faraday_middleware (~> 0.9.0)
|
32
|
+
rspec (~> 2.13.0)
|
33
|
+
rspec-instafail (~> 0.2)
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2013, Mike Lewis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# saddle
|
2
|
+
|
3
|
+
Hey nerd! Wrangle your SOA!
|
4
|
+
|
5
|
+
|
6
|
+
Saddle makes writing service clients as easy as giving high fives.©™®℗☃✓
|
7
|
+
|
8
|
+
It's a full-featured generic consumer layer for you to build API client implementations on top of.
|
9
|
+
|
10
|
+
|
11
|
+
## about
|
12
|
+
|
13
|
+
Ok, I love high fives, but what does Saddle do for me?
|
14
|
+
|
15
|
+
I'm glad you asked fellow nerd! Do you like automatic retries? Automatic multi-part file posting? I know I sure do!
|
16
|
+
|
17
|
+
Do you like sending your POSTs url-encoded? That's okay, I still love you anyways and Saddle has your back. Does your rampant OCD refuse to let you post in anything less structured than JSON? High five nerd, Saddle bleeds OCD. Just set your :post_style flag and fuhgedaboutit!
|
18
|
+
|
19
|
+
For an example of a concrete implementation, see [saddle-example](https://github.com/mLewisLogic/saddle-example)
|
20
|
+
|
21
|
+
|
22
|
+
## features
|
23
|
+
* set default connection settings for your implementation
|
24
|
+
* post urlencoded or JSON
|
25
|
+
* auto-parse JSON responses
|
26
|
+
* automatic retries and exception throwing
|
27
|
+
|
28
|
+
|
29
|
+
## guide
|
30
|
+
1. Inherit your endpoint from BaseEndpoint and call .get or .post within its action methods
|
31
|
+
2. Place those endpoints in the *endpoints* directory at the root of your client. Nest them if you like.
|
32
|
+
3. Inherit your client from Saddle
|
33
|
+
4. Call client.endpoint.action()
|
34
|
+
|
35
|
+
|
36
|
+
## todo
|
37
|
+
* xml posting
|
38
|
+
* xml parsing
|
39
|
+
* clean up custom middleware loading interface
|
data/lib/saddle.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'saddle/method_tree_builder'
|
2
|
+
require 'saddle/options'
|
3
|
+
require 'saddle/requester'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
# Ghost ride the whip.
|
8
|
+
# Inherit your client implementation from Saddle::Client
|
9
|
+
# then call YourCrayClient.create to get a client instance.
|
10
|
+
|
11
|
+
|
12
|
+
module Saddle
|
13
|
+
|
14
|
+
class Client
|
15
|
+
|
16
|
+
extend MethodTreeBuilder
|
17
|
+
extend Options
|
18
|
+
|
19
|
+
|
20
|
+
# Once your implementation is written, this is the magic you need to
|
21
|
+
# create a client instance.
|
22
|
+
def self.create(opt={})
|
23
|
+
self.build_tree(
|
24
|
+
Saddle::Requester.new(
|
25
|
+
default_options.merge(opt)
|
26
|
+
)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
module Saddle
|
6
|
+
|
7
|
+
# This base endpoint is what all implementation endpoints should inherit
|
8
|
+
# from. It automatically provides tree construction and traversal
|
9
|
+
# functionality. It also abstracts away url construction and requests to
|
10
|
+
# the underlying requester instance.
|
11
|
+
|
12
|
+
class BaseEndpoint
|
13
|
+
|
14
|
+
attr_reader :requester, :relative_path, :parent
|
15
|
+
|
16
|
+
# Each endpoint needs to have a requester in order to ... make ... uh ... requests.
|
17
|
+
def initialize(requester, relative_path=nil, parent=nil)
|
18
|
+
@requester = requester
|
19
|
+
@relative_path = relative_path
|
20
|
+
@parent = parent.is_a?(BaseEndpoint) ? parent : nil
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Provide GET functionality for the implementer class
|
25
|
+
def get(action, params={}, options={})
|
26
|
+
request(:get, action, params, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Provide POST functionality for the implementer class
|
30
|
+
def post(action, params={}, options={})
|
31
|
+
request(:post, action, params, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Provide PUT functionality for the implementer class
|
35
|
+
def put(action, params={}, options={})
|
36
|
+
request(:put, action, params, options)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Provide DELETE functionality for the implementer class
|
40
|
+
def delete(action, params={}, options={})
|
41
|
+
request(:delete, action, params, options)
|
42
|
+
end
|
43
|
+
|
44
|
+
def request(method, action, params={}, options={})
|
45
|
+
# Augment in interesting options
|
46
|
+
options[:saddle] = {
|
47
|
+
:call_chain => path_array,
|
48
|
+
:action => action,
|
49
|
+
}
|
50
|
+
@requester.send(method, path(action), params, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
# Get the url path for this endpoint/action combo
|
55
|
+
def path(action=nil)
|
56
|
+
paths = path_array
|
57
|
+
paths << action unless action.nil?
|
58
|
+
'/' + paths.join('/')
|
59
|
+
end
|
60
|
+
|
61
|
+
def path_array
|
62
|
+
endpoint_chain.map(&:relative_path).reject{|p| p.nil?}
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get the parent chain that led to this endpoint
|
66
|
+
def endpoint_chain
|
67
|
+
chain = []
|
68
|
+
node = self
|
69
|
+
until node.nil?
|
70
|
+
chain << node
|
71
|
+
node = node.parent
|
72
|
+
end
|
73
|
+
chain.reverse
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# Create an endpoint instance and foist it upon this node
|
78
|
+
def build_and_attach_node(endpoint_class, method_name)
|
79
|
+
endpoint_instance = endpoint_class.new(@requester, method_name, self)
|
80
|
+
self.instance_variable_set("@#{method_name}", endpoint_instance)
|
81
|
+
self.class.class_eval { define_method(method_name) { endpoint_instance } }
|
82
|
+
endpoint_instance
|
83
|
+
end
|
84
|
+
|
85
|
+
# This will create a resource endpoint, based upon the parameters
|
86
|
+
# of this current node endpoint
|
87
|
+
def create_resource_endpoint(endpoint_class, resource_id)
|
88
|
+
endpoint_class.new(
|
89
|
+
@requester,
|
90
|
+
(path_array + [resource_id]).join('/')
|
91
|
+
# no parent so that it can free up memory
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
# This endpoint will be automatically constructed into the node
|
100
|
+
# traversal tree.
|
101
|
+
class TraversalEndpoint < BaseEndpoint; end
|
102
|
+
|
103
|
+
|
104
|
+
# This endpoint is used for constructing resource-style endpoints. This
|
105
|
+
# means it will NOT be automatically added into the traversal tree.
|
106
|
+
class ResourceEndpoint < BaseEndpoint; end
|
107
|
+
|
108
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
require 'saddle/endpoint'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
# This mixin provides functionality for the construction of the endpoint
|
8
|
+
# tree. It will start at the root directory and namespace of the client
|
9
|
+
# implementation. It will then load the 'endpoints' directory and
|
10
|
+
# build the endpoint tree based upon module/class namespaces
|
11
|
+
|
12
|
+
|
13
|
+
module Saddle::MethodTreeBuilder
|
14
|
+
|
15
|
+
# Build out the endpoint structure from the root of the implementation
|
16
|
+
def build_tree(requester)
|
17
|
+
root_node = build_root_node(requester)
|
18
|
+
# If we have an 'endpoints' directory, build it out
|
19
|
+
if knows_root? && Dir.exists?(endpoints_directory)
|
20
|
+
Dir["#{endpoints_directory}/**/*.rb"].each { |f| load(f) }
|
21
|
+
build_node_children(self.endpoints_module, root_node, requester)
|
22
|
+
end
|
23
|
+
root_node
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Build our root node here. The root node is special in that it lives below
|
28
|
+
# the 'endpoints' directory, and so we need to manually check if it exists.
|
29
|
+
def build_root_node(requester)
|
30
|
+
if knows_root?
|
31
|
+
root_endpoint_file = File.join(
|
32
|
+
@@implementation_root,
|
33
|
+
'root_endpoint.rb'
|
34
|
+
)
|
35
|
+
if File.file?(root_endpoint_file)
|
36
|
+
# Load it and create our base endpoint
|
37
|
+
load(root_endpoint_file)
|
38
|
+
self.implementation_module::RootEndpoint.new(requester)
|
39
|
+
else
|
40
|
+
# 'root_endpoint.rb' doesn't exist, so create a dummy endpoint
|
41
|
+
Saddle::BaseEndpoint.new(requester)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
# we don't even have an implementation root, so create a dummy endpoint
|
45
|
+
Saddle::BaseEndpoint.new(requester)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
# Build out the traversal tree by module namespace
|
51
|
+
def build_node_children(current_module, current_node, requester)
|
52
|
+
current_module.constants.each do |const_symbol|
|
53
|
+
const = current_module.const_get(const_symbol)
|
54
|
+
|
55
|
+
if const.class == Module
|
56
|
+
# A module means that it's a branch
|
57
|
+
# Build the branch out with a base endpoint
|
58
|
+
branch_node = current_node.build_and_attach_node(
|
59
|
+
Saddle::BaseEndpoint,
|
60
|
+
ActiveSupport::Inflector.underscore(const_symbol)
|
61
|
+
)
|
62
|
+
# Build out the branch's endpoints on the new branch node
|
63
|
+
self.build_node_children(const, branch_node, requester)
|
64
|
+
end
|
65
|
+
|
66
|
+
if const < Saddle::TraversalEndpoint
|
67
|
+
# A class means that it's a node
|
68
|
+
# Build out this endpoint on the current node
|
69
|
+
current_node.build_and_attach_node(
|
70
|
+
const,
|
71
|
+
ActiveSupport::Inflector.underscore(const_symbol)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# Get the module that the client implementation belongs to. This will act
|
79
|
+
# as the root namespace for endpoint traversal and construction
|
80
|
+
def implementation_module
|
81
|
+
::ActiveSupport::Inflector.constantize(
|
82
|
+
self.name.split('::')[0..-2].join('::')
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Get the Endpoints module that lives within this implementation's
|
87
|
+
# namespace
|
88
|
+
def endpoints_module
|
89
|
+
implementation_module.const_get('Endpoints')
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get the path to the 'endpoints' directory, based upon the client
|
93
|
+
# class that inherited Saddle
|
94
|
+
def endpoints_directory
|
95
|
+
File.join(@@implementation_root, 'endpoints')
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
# When Saddle is inherited, we store the root of the implementation class
|
101
|
+
# This is so that we know where to look for relative files, like
|
102
|
+
# the endpoints directory
|
103
|
+
def inherited(obj)
|
104
|
+
path, = caller[0].partition(":")
|
105
|
+
@@implementation_root = File.dirname(path)
|
106
|
+
end
|
107
|
+
|
108
|
+
# If this client was not fully constructed, it may not even have an
|
109
|
+
# implementation root. Allow that behavior and avoid firesystem searching.
|
110
|
+
def knows_root?
|
111
|
+
defined?(@@implementation_root)
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
|
4
|
+
module Saddle::Middleware
|
5
|
+
|
6
|
+
# Public: Reports exceptions to airbrake
|
7
|
+
#
|
8
|
+
class Airbrake < Faraday::Middleware
|
9
|
+
|
10
|
+
def initialize(app, airbrake_api_key)
|
11
|
+
super(app)
|
12
|
+
::Airbrake.configure do |config|
|
13
|
+
config.api_key = airbrake_api_key
|
14
|
+
# TODO: filter sensitive info
|
15
|
+
# config.params_filters.concat(Security::SENSITIVE_PARAMS_STR)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
begin
|
21
|
+
@app.call(env)
|
22
|
+
rescue => e
|
23
|
+
::Airbrake.notify(e)
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
|
4
|
+
module Saddle::Middleware
|
5
|
+
|
6
|
+
# Public: Returns a default response in the case of an exception
|
7
|
+
# Expects default_response to be defined in the request of connection options, otherwise rethrows exception
|
8
|
+
class DefaultResponse < Faraday::Middleware
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
begin
|
12
|
+
@app.call(env)
|
13
|
+
rescue => e
|
14
|
+
if res = env[:request][:default_response]
|
15
|
+
return ::Faraday::Response.new(:body => res)
|
16
|
+
else
|
17
|
+
raise
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'faraday_middleware/response_middleware'
|
2
|
+
|
3
|
+
|
4
|
+
module Saddle::Middleware
|
5
|
+
|
6
|
+
# Public: Parse response bodies as JSON.
|
7
|
+
class ParseJson < FaradayMiddleware::ResponseMiddleware
|
8
|
+
MIME_TYPE = 'application/json'.freeze
|
9
|
+
|
10
|
+
dependency do
|
11
|
+
require 'json' unless defined?(::JSON)
|
12
|
+
end
|
13
|
+
|
14
|
+
define_parser do |body|
|
15
|
+
::JSON.parse body unless body.strip.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def parse_response?(env)
|
20
|
+
type = response_type(env)
|
21
|
+
super and has_body?(env) and (type.empty? or type == MIME_TYPE)
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_body?(env)
|
25
|
+
body = env[:body] and !(body.respond_to?(:to_str) and body.empty?)
|
26
|
+
end
|
27
|
+
|
28
|
+
def response_type(env)
|
29
|
+
type = env[:response_headers][CONTENT_TYPE].to_s
|
30
|
+
type = type.split(';', 2).first if type.index(';')
|
31
|
+
type
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
|
4
|
+
module Saddle::Middleware
|
5
|
+
|
6
|
+
# Public: Enforces a ruby timeout on the request
|
7
|
+
# :timeout must be present in the request or client options
|
8
|
+
class RubyTimeout < Faraday::Middleware
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
timeout = env[:request][:timeout] # nil or 0 means no timeout
|
12
|
+
Timeout.timeout(timeout) do
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'statsd'
|
2
|
+
|
3
|
+
module Saddle::Middleware
|
4
|
+
|
5
|
+
# Public: Wraps request with statsd logging
|
6
|
+
# Expects statsd_path in request options. However, if using saddle and no statsd_path is specified
|
7
|
+
# will read call_chain and action and use them to construct a statsd_path
|
8
|
+
class StatsdLogging < Faraday::Middleware
|
9
|
+
attr_accessor :graphite_host, :graphite_port, :namespace
|
10
|
+
|
11
|
+
def initialize(app, graphite_host, graphite_port=nil, namespace=nil)
|
12
|
+
super(app)
|
13
|
+
@graphite_host = graphite_host
|
14
|
+
@graphite_port = graphite_port
|
15
|
+
@namespace = namespace
|
16
|
+
self.statsd
|
17
|
+
end
|
18
|
+
|
19
|
+
def statsd
|
20
|
+
if(@statsd.nil?)
|
21
|
+
@statsd = Statsd.new(@graphite_host, @graphite_port)
|
22
|
+
@statsd.namespace = @namespace if @namespace
|
23
|
+
end
|
24
|
+
return @statsd
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(env)
|
28
|
+
if env[:request][:statsd_path]
|
29
|
+
statsd_path = env[:request][:statsd_path]
|
30
|
+
elsif env[:request][:saddle] && env[:request][:saddle][:call_chain] && env[:request][:saddle][:action]
|
31
|
+
statsd_path = (env[:request][:saddle][:call_chain] + [env[:request][:saddle][:action]]).join(".")
|
32
|
+
end
|
33
|
+
|
34
|
+
if statsd_path
|
35
|
+
self.statsd.time statsd_path do
|
36
|
+
@app.call(env)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
@app.call(env)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
# Default options for a client. Override whatever you need to for
|
5
|
+
# your specific implementation
|
6
|
+
|
7
|
+
|
8
|
+
module Saddle::Options
|
9
|
+
|
10
|
+
# Construct our default options, based upon the class methods
|
11
|
+
def default_options
|
12
|
+
{
|
13
|
+
:host => host,
|
14
|
+
:port => port,
|
15
|
+
:use_ssl => use_ssl,
|
16
|
+
:post_style => post_style,
|
17
|
+
:response_style => response_style,
|
18
|
+
:num_retries => num_retries,
|
19
|
+
:timeout => timeout,
|
20
|
+
:additional_middlewares => additional_middlewares,
|
21
|
+
:stubs => stubs,
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
# The default host for this client
|
26
|
+
def host
|
27
|
+
'localhost'
|
28
|
+
end
|
29
|
+
|
30
|
+
# The default port for this client
|
31
|
+
def port
|
32
|
+
80
|
33
|
+
end
|
34
|
+
|
35
|
+
# Should this client use SSL by default?
|
36
|
+
def use_ssl
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
# The POST/PUT style for this client
|
41
|
+
# options are [:json, :urlencoded]
|
42
|
+
def post_style
|
43
|
+
:json
|
44
|
+
end
|
45
|
+
|
46
|
+
# How to parse results
|
47
|
+
# options are [:json, :urlencoded]
|
48
|
+
def response_style
|
49
|
+
:json
|
50
|
+
end
|
51
|
+
|
52
|
+
# Default number of retries per request
|
53
|
+
def num_retries
|
54
|
+
3
|
55
|
+
end
|
56
|
+
|
57
|
+
# Default timeout per request (in seconds)
|
58
|
+
def timeout
|
59
|
+
30
|
60
|
+
end
|
61
|
+
|
62
|
+
# Override this to add additional middleware to the request stack
|
63
|
+
# ex:
|
64
|
+
#
|
65
|
+
# require 'my_middleware'
|
66
|
+
# def self.default_middleware
|
67
|
+
# [MyMiddleware]
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
###
|
71
|
+
def additional_middlewares
|
72
|
+
[]
|
73
|
+
end
|
74
|
+
|
75
|
+
# If the Typhoeus adapter is being used, pass stubs to it for testing.
|
76
|
+
def stubs
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
|
4
|
+
require 'saddle/middleware/default_response'
|
5
|
+
require 'saddle/middleware/parse_json'
|
6
|
+
require 'saddle/middleware/ruby_timeout'
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
# The requester handles setting up the network connecting and brokering
|
11
|
+
# requests.
|
12
|
+
|
13
|
+
|
14
|
+
module Saddle
|
15
|
+
|
16
|
+
class Requester
|
17
|
+
|
18
|
+
VALID_BODY_STYLES = [:json, :urlencoded]
|
19
|
+
|
20
|
+
# Available options
|
21
|
+
## host - host to connect to (default: localhost)
|
22
|
+
## port - port to connect on (default: 80)
|
23
|
+
## use_ssl - true if we should use https (default: false)
|
24
|
+
## post_style - :json or :urlencoded (default: :json)
|
25
|
+
## response_style - :json or :urlencoded (default: :json)
|
26
|
+
## num_retries - number of times to retry each request (default: 3)
|
27
|
+
## timeout - timeout in seconds
|
28
|
+
## additional_middleware - an Array of more middlewares to apply to the top of the stack
|
29
|
+
## - each middleware consists of hash of klass, and optionally args (an array)
|
30
|
+
## stubs - test stubs for specs
|
31
|
+
def initialize(opt={})
|
32
|
+
@host = opt[:host] || 'localhost'
|
33
|
+
raise ':host must be a string' unless @host.is_a?(String)
|
34
|
+
@port = opt[:port] || 80
|
35
|
+
raise ':port must be an integer' unless @port.is_a?(Fixnum)
|
36
|
+
@use_ssl = opt[:use_ssl] || false
|
37
|
+
raise ':use_ssl must be true or false' unless (@use_ssl.is_a?(TrueClass) || @use_ssl.is_a?(FalseClass))
|
38
|
+
@post_style = opt[:post_style] || :json
|
39
|
+
raise ":post_style must be in: #{VALID_BODY_STYLES.join(',')}" unless VALID_BODY_STYLES.include?(@post_style)
|
40
|
+
@response_style = opt[:response_style] || :json
|
41
|
+
raise ":response_style must be in: #{VALID_BODY_STYLES.join(',')}" unless VALID_BODY_STYLES.include?(@response_style)
|
42
|
+
@num_retries = opt[:num_retries] || 3
|
43
|
+
raise ':num_retries must be an integer' unless @num_retries.is_a?(Fixnum)
|
44
|
+
@timeout = opt[:timeout]
|
45
|
+
unless @timeout.nil?
|
46
|
+
raise ':timeout must be a number or nil' unless @timeout.is_a?(Numeric)
|
47
|
+
end
|
48
|
+
@additional_middlewares = opt[:additional_middlewares] || []
|
49
|
+
raise ':additional_middleware must be an Array' unless @additional_middlewares.is_a?(Array)
|
50
|
+
raise 'invalid middleware found' unless @additional_middlewares.all? { |m| m[:klass] < Faraday::Middleware }
|
51
|
+
raise 'middleware arguments must be an array' unless @additional_middlewares.all? { |m| m[:args].nil? || m[:args].is_a?(Array) }
|
52
|
+
@stubs = opt[:stubs] || nil
|
53
|
+
unless @stubs.nil?
|
54
|
+
raise ':stubs must be a Faraday::Adapter::Test::Stubs' unless @stubs.is_a?(Faraday::Adapter::Test::Stubs)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Make a GET request
|
60
|
+
def get(url, params={}, options={})
|
61
|
+
response = connection.get do |req|
|
62
|
+
req.options.merge! options
|
63
|
+
req.url url, params
|
64
|
+
end
|
65
|
+
response.body
|
66
|
+
end
|
67
|
+
|
68
|
+
# Handle request logic for PUT or POST
|
69
|
+
def post_or_put(f, url, params={}, options={})
|
70
|
+
response = connection.send(f) do |req|
|
71
|
+
req.options.merge! options
|
72
|
+
req.url url
|
73
|
+
# Handle different supported post styles
|
74
|
+
case @post_style
|
75
|
+
when :json
|
76
|
+
req.headers['Content-Type'] = 'application/json'
|
77
|
+
req.body = params.to_json
|
78
|
+
when :urlencoded
|
79
|
+
req.params = params
|
80
|
+
else
|
81
|
+
raise RuntimeError(":post_style must be one of: #{VALID_POST_STYLES.join(',')}")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
response.body
|
85
|
+
end
|
86
|
+
|
87
|
+
# Make a POST request
|
88
|
+
def post(url, params={}, options={})
|
89
|
+
post_or_put(:post, url, params, options)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Make a PUT request
|
93
|
+
def put(url, params={}, options={})
|
94
|
+
post_or_put(:put, url, params, options)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Make a DELETE request
|
98
|
+
def delete(url, params={}, options={})
|
99
|
+
response = connection.delete do |req|
|
100
|
+
req.options.merge! options
|
101
|
+
req.url url, params
|
102
|
+
end
|
103
|
+
response.body
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# Construct a base url using this requester's settings
|
111
|
+
def base_url
|
112
|
+
"http#{'s' if @use_ssl}://#{@host}:#{@port}"
|
113
|
+
end
|
114
|
+
|
115
|
+
# Build a connection instance, wrapped in the middleware that we want
|
116
|
+
def connection
|
117
|
+
@connection ||= Faraday.new(base_url) do |builder|
|
118
|
+
# Config options
|
119
|
+
unless @timeout.nil?
|
120
|
+
builder.options[:timeout] = @timeout
|
121
|
+
end
|
122
|
+
|
123
|
+
# Support default return values upon exception
|
124
|
+
builder.use Saddle::Middleware::DefaultResponse
|
125
|
+
|
126
|
+
# Apply additional implementation-specific middlewares
|
127
|
+
@additional_middlewares.each do |m|
|
128
|
+
builder.use m[:klass], *m[:args]
|
129
|
+
end
|
130
|
+
|
131
|
+
# Hard timeout on the entire request
|
132
|
+
builder.use Saddle::Middleware::RubyTimeout
|
133
|
+
|
134
|
+
# Support multi-part encoding if there is a file attached
|
135
|
+
builder.request :multipart
|
136
|
+
# Handle retries
|
137
|
+
if @num_retries
|
138
|
+
builder.request :retry, @num_retries
|
139
|
+
end
|
140
|
+
|
141
|
+
# Set up our adapter
|
142
|
+
if @stubs.nil?
|
143
|
+
# Use the default adapter
|
144
|
+
builder.adapter :net_http
|
145
|
+
else
|
146
|
+
# Use the test adapter
|
147
|
+
builder.adapter :test, @stubs
|
148
|
+
end
|
149
|
+
|
150
|
+
# Raise exceptions on 4xx and 5xx errors
|
151
|
+
builder.response :raise_error
|
152
|
+
# Handle parsing out the response if it's JSON
|
153
|
+
builder.use Saddle::Middleware::ParseJson
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
data/saddle.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/saddle/version', __FILE__)
|
3
|
+
|
4
|
+
lib = File.expand_path('../lib', __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = 'saddle'
|
9
|
+
s.version = Saddle::VERSION
|
10
|
+
|
11
|
+
s.authors = ['Mike Lewis', 'Naseem Hakim']
|
12
|
+
s.email = 'mike@cleverkoala.com'
|
13
|
+
s.description = %q{Makes writing API clients as easy as giving high fives}
|
14
|
+
s.summary = %q{
|
15
|
+
A generic client wrapper for building service-specific wrappers. Base functionality, meant to be extended to concrete implementations.
|
16
|
+
}
|
17
|
+
s.homepage = 'https://github.com/mLewisLogic/saddle'
|
18
|
+
s.license = 'MIT'
|
19
|
+
|
20
|
+
s.require_path = 'lib'
|
21
|
+
s.files = `git ls-files`.split($\)
|
22
|
+
s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
23
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
24
|
+
|
25
|
+
s.add_dependency 'activesupport', '>= 3.0'
|
26
|
+
s.add_dependency 'faraday', '~> 0.8.6'
|
27
|
+
s.add_dependency 'faraday_middleware', '~> 0.9.0'
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'saddle'
|
2
|
+
|
3
|
+
describe Saddle::Client do
|
4
|
+
|
5
|
+
context "instance" do
|
6
|
+
before :each do
|
7
|
+
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
|
8
|
+
stub.get('/test') {
|
9
|
+
[
|
10
|
+
200,
|
11
|
+
{'Content-Type' => 'application/x-www-form-urlencoded'},
|
12
|
+
'success'
|
13
|
+
]
|
14
|
+
}
|
15
|
+
stub.get('/test.json') {
|
16
|
+
[
|
17
|
+
200,
|
18
|
+
{'Content-Type' => 'application/json'},
|
19
|
+
{'success' => true}.to_json
|
20
|
+
]
|
21
|
+
}
|
22
|
+
end
|
23
|
+
@client = Saddle::Client.create(:stubs => stubs)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should be able to request urlencoded" do
|
27
|
+
@client.requester.get('/test').should == 'success'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should be able to request JSON encoded" do
|
31
|
+
@client.requester.get('/test.json')['success'].should == true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saddle
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Lewis
|
@@ -58,7 +58,26 @@ email: mike@cleverkoala.com
|
|
58
58
|
executables: []
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
|
-
files:
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- .rspec
|
64
|
+
- Gemfile
|
65
|
+
- Gemfile.lock
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- lib/saddle.rb
|
69
|
+
- lib/saddle/endpoint.rb
|
70
|
+
- lib/saddle/method_tree_builder.rb
|
71
|
+
- lib/saddle/middleware/airbrake.rb
|
72
|
+
- lib/saddle/middleware/default_response.rb
|
73
|
+
- lib/saddle/middleware/parse_json.rb
|
74
|
+
- lib/saddle/middleware/ruby_timeout.rb
|
75
|
+
- lib/saddle/middleware/statsd_logging.rb
|
76
|
+
- lib/saddle/options.rb
|
77
|
+
- lib/saddle/requester.rb
|
78
|
+
- lib/saddle/version.rb
|
79
|
+
- saddle.gemspec
|
80
|
+
- spec/saddle_client_spec.rb
|
62
81
|
homepage: https://github.com/mLewisLogic/saddle
|
63
82
|
licenses:
|
64
83
|
- MIT
|
@@ -84,4 +103,5 @@ signing_key:
|
|
84
103
|
specification_version: 4
|
85
104
|
summary: A generic client wrapper for building service-specific wrappers. Base functionality,
|
86
105
|
meant to be extended to concrete implementations.
|
87
|
-
test_files:
|
106
|
+
test_files:
|
107
|
+
- spec/saddle_client_spec.rb
|