api_smith 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/api_smith.rb +16 -0
- data/lib/api_smith/base.rb +19 -0
- data/lib/api_smith/client.rb +319 -0
- data/lib/api_smith/smash.rb +212 -0
- data/lib/api_smith/version.rb +4 -0
- data/lib/api_smith/web_mock_extensions.rb +42 -0
- metadata +147 -0
data/lib/api_smith.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Provides a simple set of tools built on top of Hashie and HTTParty to make
|
2
|
+
# it easier to build clients for different apis.
|
3
|
+
|
4
|
+
# @see APISmith::Smash
|
5
|
+
# @see APISmith::Base
|
6
|
+
#
|
7
|
+
# @author Darcy Laycock
|
8
|
+
# @author Steve Webb
|
9
|
+
module APISmith
|
10
|
+
|
11
|
+
require 'api_smith/version'
|
12
|
+
require 'api_smith/smash'
|
13
|
+
require 'api_smith/client'
|
14
|
+
require 'api_smith/base'
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'api_smith/client'
|
2
|
+
|
3
|
+
module APISmith
|
4
|
+
# A base class for building api clients (with a specified endpoint and general
|
5
|
+
# shared options) on top of HTTParty, including response unpacking and transformation.
|
6
|
+
#
|
7
|
+
# Used to convert APISmith::Client to a class (versus a mixin), making it useable
|
8
|
+
# in certain other situations where it isn't necessarily useful otherwise.
|
9
|
+
#
|
10
|
+
# @author Darcy Laycock
|
11
|
+
# @author Steve Webb
|
12
|
+
class Base
|
13
|
+
include APISmith::Client
|
14
|
+
|
15
|
+
def initialize(*)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,319 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module APISmith
|
4
|
+
# A mixin providing the base set of functionality for building API clients.
|
5
|
+
#
|
6
|
+
# @see InstanceMethods
|
7
|
+
# @see ClassMethods
|
8
|
+
# @see HTTParty
|
9
|
+
#
|
10
|
+
# @author Darcy Laycock
|
11
|
+
# @author Steve Webb
|
12
|
+
module Client
|
13
|
+
|
14
|
+
# Hooks into the mixin process to add HTTParty and the two APISmith::Client
|
15
|
+
# components to the given parent automatically.
|
16
|
+
# @param [Class] parent the object this is being mixed into
|
17
|
+
def self.included(parent)
|
18
|
+
parent.class_eval do
|
19
|
+
include HTTParty
|
20
|
+
include InstanceMethods
|
21
|
+
extend ClassMethods
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# The most important part of the client functionality - namely, provides a set of tools
|
26
|
+
# and methods that make it possible to build API clients. This is where the bulk of the
|
27
|
+
# client work takes place.
|
28
|
+
module InstanceMethods
|
29
|
+
|
30
|
+
# Given a path relative to the endpoint (or `/`), will perform a GET request.
|
31
|
+
#
|
32
|
+
# If provided, will use a `:transformer` to convert the resultant data into
|
33
|
+
# a useable object. Also, in the case an object is nested, allows you to
|
34
|
+
# traverse an object.
|
35
|
+
#
|
36
|
+
# @param [String] path the relative path to the object, pre-normalisation
|
37
|
+
# @param [Hash] options the raw options to be passed to ``#request!``
|
38
|
+
# @see #request!
|
39
|
+
def get(path, options = {})
|
40
|
+
request! :get, path, options, :query
|
41
|
+
end
|
42
|
+
|
43
|
+
# Given a path relative to the endpoint (or `/`), will perform a POST request.
|
44
|
+
#
|
45
|
+
# If provided, will use a `:transformer` to convert the resultant data into
|
46
|
+
# a useable object. Also, in the case an object is nested, allows you to
|
47
|
+
# traverse an object.
|
48
|
+
#
|
49
|
+
# @param [String] path the relative path to the object, pre-normalisation
|
50
|
+
# @param [Hash] options the raw options to be passed to ``#request!``
|
51
|
+
# @see #request!
|
52
|
+
def post(path, options = {})
|
53
|
+
request! :post, path, options, :query, :body
|
54
|
+
end
|
55
|
+
|
56
|
+
# Given a path relative to the endpoint (or `/`), will perform a PUT request.
|
57
|
+
#
|
58
|
+
# If provided, will use a `:transformer` to convert the resultant data into
|
59
|
+
# a useable object. Also, in the case an object is nested, allows you to
|
60
|
+
# traverse an object.
|
61
|
+
#
|
62
|
+
# @param [String] path the relative path to the object, pre-normalisation
|
63
|
+
# @param [Hash] options the raw options to be passed to ``#request!``
|
64
|
+
# @see #request!
|
65
|
+
def put(path, options = {})
|
66
|
+
request! :put, path, options, :query, :body
|
67
|
+
end
|
68
|
+
|
69
|
+
# Given a path relative to the endpoint (or `/`), will perform a DELETE request.
|
70
|
+
#
|
71
|
+
# If provided, will use a `:transformer` to convert the resultant data into
|
72
|
+
# a useable object. Also, in the case an object is nested, allows you to
|
73
|
+
# traverse an object.
|
74
|
+
#
|
75
|
+
# @param [String] path the relative path to the object, pre-normalisation
|
76
|
+
# @param [Hash] options the raw options to be passed to ``#request!``
|
77
|
+
# @see #request!
|
78
|
+
def delete(path, options = {})
|
79
|
+
request! :delete, path, options, :query
|
80
|
+
end
|
81
|
+
|
82
|
+
# Performs a HTTP request using HTTParty, using a set of expanded options built up by the current client.
|
83
|
+
#
|
84
|
+
# @param [:get, :post, :put, :delete] method the http request method to use
|
85
|
+
# @param [String] path the request path, relative to either the endpoint or /.
|
86
|
+
# @param [Hash{Symbol => Object}] options the options for the given request.
|
87
|
+
# @param [Array<Symbol>] param_types the given parameter types (e.g. :body, :query) to add to the request
|
88
|
+
#
|
89
|
+
# @option options [true,false] :skip_endpoint If true, don't expand the given path before processing.
|
90
|
+
# @option options [Array] :response_container If present, it will traverse an array of objects to unpack
|
91
|
+
# @option options [Hash] :extra_request Extra raw, request options to pass in to the request
|
92
|
+
# @option options [Hash] :extra_body Any parameters to add to the request body
|
93
|
+
# @option options [Hash] :extra_query Any parameters to add to the request query string
|
94
|
+
# @option options [#call] :transform An object, invoked via #call, that takes the response and
|
95
|
+
# transformers it into a useable form.
|
96
|
+
#
|
97
|
+
# @return the response, defaults to the raw HTTParty::Response
|
98
|
+
#
|
99
|
+
# @see #path_for
|
100
|
+
# @see #extract_response
|
101
|
+
# @see #transform_response
|
102
|
+
def request!(method, path, options, *param_types)
|
103
|
+
# Merge in the default request options, e.g. those to be passed to HTTParty raw
|
104
|
+
request_options = merged_options_for(:request, options)
|
105
|
+
# Exapdn the path out into a full version when the endpoint is present.
|
106
|
+
full_path = request_options[:skip_endpoint] ? path : path_for(path)
|
107
|
+
# For each of the given param_types (e.g. :query, :body) will automatically
|
108
|
+
# merge in the options for the current request.
|
109
|
+
param_types.each do |type|
|
110
|
+
request_options[type] = merged_options_for(type, options)
|
111
|
+
end
|
112
|
+
# Finally, use HTTParty to get the response
|
113
|
+
response = self.class.send method, full_path, request_options
|
114
|
+
# Pre-process the response to check for errors.
|
115
|
+
check_response_errors response
|
116
|
+
# Unpack the response using the :response_container option
|
117
|
+
inner_response = extract_response path, response, options
|
118
|
+
# Finally, apply any transformations
|
119
|
+
transform_response inner_response, options
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# Provides a hook to handle checking errors on API responses. This is called
|
125
|
+
# post-fetch and pre-unpacking / transformation. It is passed the apis response
|
126
|
+
# post-decoding (meaning JSON etc have been parsed into normal ruby objects).
|
127
|
+
# @param [Object] response the raw decoded api response
|
128
|
+
def check_response_errors(response)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Merges in options of a given type into the base options, taking into account
|
132
|
+
#
|
133
|
+
# * Shared options (e.g. #base_query_options)
|
134
|
+
# * Instance-level options (e.g. #query_options)
|
135
|
+
# * Call-level options (e.g. the :extra_query option)
|
136
|
+
#
|
137
|
+
# @param [Symbol] type the type of options, one of :body, :query or :request
|
138
|
+
# @param [Hash] options the hash to check for the `:extra_{type}` option.
|
139
|
+
# @return [Hash] a hash of the merged options.
|
140
|
+
def merged_options_for(type, options)
|
141
|
+
base = send :"base_#{type}_options"
|
142
|
+
base.merge!(send(:"#{type}_options") || {})
|
143
|
+
base.merge! options.fetch(:"extra_#{type}", {})
|
144
|
+
base
|
145
|
+
end
|
146
|
+
|
147
|
+
# The base set of body parameters, common to all instances of the client.
|
148
|
+
# Ideally, in your client you'd override this to return other required
|
149
|
+
# parameters that are the same across all client subclasses e.g. the format.
|
150
|
+
#
|
151
|
+
# These will automatically be included in POST and PUT requests but not
|
152
|
+
# GET or DELETE requests.
|
153
|
+
#
|
154
|
+
# @example
|
155
|
+
# def base_body_options
|
156
|
+
# {:format => 'json'}
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
def base_body_options
|
160
|
+
{}
|
161
|
+
end
|
162
|
+
|
163
|
+
# The base set of query parameters, common to all instances of the client.
|
164
|
+
# Ideally, in your client you'd override this to return other required
|
165
|
+
# parameters that are the same across all client subclasses e.g. the format.
|
166
|
+
#
|
167
|
+
# These will automatically be included in all requests as part of the query
|
168
|
+
# string.
|
169
|
+
#
|
170
|
+
# @example
|
171
|
+
# def base_query_options
|
172
|
+
# {:format => 'json'}
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
def base_query_options
|
176
|
+
{}
|
177
|
+
end
|
178
|
+
|
179
|
+
# The base set of request options as accepted by HTTParty. These can be used to
|
180
|
+
# setup things like the normaliser HTTParty will use for parameters.
|
181
|
+
def base_request_options
|
182
|
+
{}
|
183
|
+
end
|
184
|
+
|
185
|
+
# Per-instance configurable query parameters.
|
186
|
+
# @return [Hash] the instance-specific query parameters.
|
187
|
+
# @see #add_query_options!
|
188
|
+
def query_options
|
189
|
+
@query_options ||= {}
|
190
|
+
end
|
191
|
+
|
192
|
+
# Per-instance configurable body parameters.
|
193
|
+
# @return [Hash] the instance-specific body parameters.
|
194
|
+
# @see #add_body_options!
|
195
|
+
def body_options
|
196
|
+
@body_options ||= {}
|
197
|
+
end
|
198
|
+
|
199
|
+
# Per-instance configurable request options.
|
200
|
+
# @return [Hash] the instance-specific request options.
|
201
|
+
# @see #add_request_options!
|
202
|
+
def request_options
|
203
|
+
@request_options ||= {}
|
204
|
+
end
|
205
|
+
|
206
|
+
# Merges in a hash of extra query parameters to the given request, applying
|
207
|
+
# them for every request that has query string parameters. Typically
|
208
|
+
# called from inside #initialize.
|
209
|
+
# @param [Hash{Symbol => Object}] value a hash of options to add recently
|
210
|
+
def add_query_options!(value)
|
211
|
+
query_options.merge! value
|
212
|
+
end
|
213
|
+
|
214
|
+
# Merges in a hash of extra body parameters to the given request, applying
|
215
|
+
# them for every request that has body parameters. Typically called from
|
216
|
+
# inside #initialize.
|
217
|
+
# @param [Hash{Symbol => Object}] value a hash of options to add recently
|
218
|
+
def add_body_options!(value)
|
219
|
+
body_options.merge! value
|
220
|
+
end
|
221
|
+
|
222
|
+
# Merges in a hash of request options to the given request, applying
|
223
|
+
# them for every request that has query string parameters. Typically called
|
224
|
+
# from inside #initialize.
|
225
|
+
# @param [Hash{Symbol => Object}] value a hash of options to add recently
|
226
|
+
def add_request_options!(value)
|
227
|
+
request_options.merge! value
|
228
|
+
end
|
229
|
+
|
230
|
+
# Given a path, expands it relative to the / and the defined endpoint
|
231
|
+
# for this class.
|
232
|
+
# @param [String] path the current, unexpanded path for the api call.
|
233
|
+
# @example With an endpoint of v1
|
234
|
+
# path_for('test') # => "/v1/test"
|
235
|
+
def path_for(path)
|
236
|
+
File.join(*['', endpoint, path].compact)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Given a path, response and options, will walk the response object
|
240
|
+
# (typically hashes and arrays) to unpack / extract the users response.
|
241
|
+
#
|
242
|
+
# Note that the response container will be found either via a :response_container
|
243
|
+
# option or, if not specified at all, the result of #default_response_container to
|
244
|
+
# 'get' the part of the response that the user cares about.
|
245
|
+
#
|
246
|
+
# @param [String] path the path used for the request#
|
247
|
+
# @param [Hash, Array] response the object returned from the api call
|
248
|
+
# @param [Hash] options the options passed to the api call
|
249
|
+
# @option options [Array<Symbol, String, Integer>] :response_container the container to unpack
|
250
|
+
# from, e.g. ["a", 1, "b"], %w(a 2 3) or something else.
|
251
|
+
def extract_response(path, response, options)
|
252
|
+
# First, get the response container options
|
253
|
+
response_container = options.fetch(:response_container) do
|
254
|
+
default_response_container(path, options)
|
255
|
+
end
|
256
|
+
# And then unpack then
|
257
|
+
if response_container
|
258
|
+
response_keys = Array(response_container)
|
259
|
+
response = response_keys.inject(response) do |r, key|
|
260
|
+
r.respond_to?(:[]) ? r[key] : r
|
261
|
+
end
|
262
|
+
end
|
263
|
+
response
|
264
|
+
end
|
265
|
+
|
266
|
+
# Takes a response and, if present, uses the :transform option to convert
|
267
|
+
# it into a useable object.
|
268
|
+
# @param [Hash, Array] response the object returned from the api call
|
269
|
+
# @param [Hash] options the options passed to the api call
|
270
|
+
# @option options [#call] :transform If present, passed the unpack response.
|
271
|
+
# @option option [#call] :transformer see the :transform option
|
272
|
+
# @return [Object] the transformed response, or the response itself if no :transform
|
273
|
+
# option is passed.
|
274
|
+
def transform_response(response, options)
|
275
|
+
transformer = options[:transform] || options[:transformer]
|
276
|
+
if transformer
|
277
|
+
transformer.call response
|
278
|
+
else
|
279
|
+
response
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Returns the current api endpoint, if present.
|
284
|
+
# @return [nil, String] the current endpoint
|
285
|
+
def endpoint
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
|
289
|
+
# A hook method to define the default response container for a given
|
290
|
+
# path and set of options to an API call. Intended to be used inside
|
291
|
+
# subclasses to make it possible to define a standardised way to unpack
|
292
|
+
# responses without having to pass a `:response_container` option.
|
293
|
+
# @param [String] path the current path to the request
|
294
|
+
# @param [Hash] options the set of options passed to #request!
|
295
|
+
# @return [nil, Array] the array of indices (either hash / array indices)
|
296
|
+
# to unpack the response via.
|
297
|
+
def default_response_container(path, options)
|
298
|
+
nil
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
# Class level methods to let you configure your api client.
|
304
|
+
module ClassMethods
|
305
|
+
|
306
|
+
# When present, lets you specify the api for the given client.
|
307
|
+
# @param [String, nil] value the endpoint to use.
|
308
|
+
# @example Setting a string endpoint
|
309
|
+
# endpoint 'v1'
|
310
|
+
# @example Unsetting the string endpoint
|
311
|
+
# endpoint nil
|
312
|
+
def endpoint(value = nil)
|
313
|
+
define_method(:endpoint) { value }
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
317
|
+
|
318
|
+
end
|
319
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'hashie/dash'
|
2
|
+
|
3
|
+
module APISmith
|
4
|
+
# Extends Hashie::Dash to suppress unknown keys when passing data, but
|
5
|
+
# is configurable to raises an UnknownKey exception when accessing keys in the
|
6
|
+
# Smash.
|
7
|
+
#
|
8
|
+
# APISmith::Smash is a subclass of Hashie::Dash that adds several features
|
9
|
+
# making it suitable for use in writing api clients. Namely,
|
10
|
+
#
|
11
|
+
# * The ability to silence exceptions on unknown keys (vs. Raising NoMethodError)
|
12
|
+
# * The ability to define conversion of incoming data via transformers
|
13
|
+
# * The ability to define aliases for keys via the from parameter.
|
14
|
+
#
|
15
|
+
# @author Darcy Laycock
|
16
|
+
# @author Steve Webb
|
17
|
+
#
|
18
|
+
# @example a simple, structured object with the most common use cases.
|
19
|
+
# class MyResponse < APISmith::Smash
|
20
|
+
# property :full_name, :from => :fullName
|
21
|
+
# property :value_percentage, :transformer => :to_f
|
22
|
+
# property :short_name
|
23
|
+
# property :created, :transformer => lambda { |v| Date.parse(v) }
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# response = MyResponse.new({
|
27
|
+
# :fullName => "Bob Smith",
|
28
|
+
# :value_percentage => "10.5",
|
29
|
+
# :short_name => 'Bob',
|
30
|
+
# :created => '2010-12-28'
|
31
|
+
# })
|
32
|
+
#
|
33
|
+
# p response.short_name # => "Bob"
|
34
|
+
# p response.full_name # => "Bob Smith"
|
35
|
+
# p response.value_percentage # => 10.5
|
36
|
+
# p response.created.class # => Date
|
37
|
+
#
|
38
|
+
class Smash < Hashie::Dash
|
39
|
+
# When we access an unknown property, we raise the unknown key instead of
|
40
|
+
# a NoMethodError on undefined keys so that we can do a target rescue.
|
41
|
+
class UnknownKey < StandardError; end
|
42
|
+
|
43
|
+
# Returns a class-specific hash of transformers, containing the attribute
|
44
|
+
# name mapped to the transformer that responds to call.
|
45
|
+
# @return The hash of transformers.
|
46
|
+
def self.transformers
|
47
|
+
(@transformers ||= {})
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a class-specific hash of incoming keys and their resultant
|
51
|
+
# property name, useful for mapping non-standard names (e.g. displayName)
|
52
|
+
# to their more ruby-like equivelant (e.g. display_name).
|
53
|
+
# @return The hash of key mappings.
|
54
|
+
def self.key_mapping
|
55
|
+
(@key_mapping ||= {})
|
56
|
+
end
|
57
|
+
|
58
|
+
# Test if the object should raise a NoMethodError exception on unknown
|
59
|
+
# property accessors or whether it should be silenced.
|
60
|
+
#
|
61
|
+
# @return true if an exception will be raised when accessing an unknown key
|
62
|
+
# else, false.
|
63
|
+
def self.exception_on_unknown_key?
|
64
|
+
defined?(@exception_on_unknown_key) && @exception_on_unknown_key
|
65
|
+
end
|
66
|
+
|
67
|
+
# Sets whether or not Smash should raise NoMethodError on an unknown key.
|
68
|
+
# Sets it for the current class.
|
69
|
+
#
|
70
|
+
# @param [Boolean] value true to throw exceptions.
|
71
|
+
def self.exception_on_unknown_key=(value)
|
72
|
+
@exception_on_unknown_key = value
|
73
|
+
end
|
74
|
+
self.exception_on_unknown_key = false
|
75
|
+
|
76
|
+
# Sets the transformer that is invoked when the given key is set.
|
77
|
+
#
|
78
|
+
# @param [Symbol] key The key should this transformer operate on
|
79
|
+
# @param [#call] value If a block isn't given, used to transform via #call.
|
80
|
+
# @param [Block] blk The block used to transform the key.
|
81
|
+
def self.transformer_for(key, value = nil, &blk)
|
82
|
+
if blk.nil? && value
|
83
|
+
blk = value.respond_to?(:call) ? value : value.to_sym.to_proc
|
84
|
+
end
|
85
|
+
raise ArgumentError, 'must provide a transformation' if blk.nil?
|
86
|
+
transformers[key.to_s] = blk
|
87
|
+
# For each subclass, set the transformer.
|
88
|
+
Array(@subclasses).each { |klass| klass.transformer_for(key, value) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Hook to make it inherit instance variables correctly. Called once
|
92
|
+
# the Smash is inherited from in another object to maintain state.
|
93
|
+
def self.inherited(klass)
|
94
|
+
super
|
95
|
+
klass.instance_variable_set '@transformers', transformers.dup
|
96
|
+
klass.instance_variable_set '@key_mapping', key_mapping.dup
|
97
|
+
klass.instance_variable_set '@exception_on_unknown_key', exception_on_unknown_key?
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create a new property (i.e., hash key) for this Object type. This method
|
101
|
+
# allows for converting property names and defining custom transformers for
|
102
|
+
# more complex types.
|
103
|
+
#
|
104
|
+
# @param [Symbol] property_name The property name (duh).
|
105
|
+
# @param [Hash] options
|
106
|
+
# @option options [String, Array<String>] :from Also accept values for this property when
|
107
|
+
# using the key(s) specified in from.
|
108
|
+
# @option options [Block] :transformer Specify a class or block to use when transforming the data.
|
109
|
+
def self.property(property_name, options = {})
|
110
|
+
super
|
111
|
+
if options[:from]
|
112
|
+
property_name = property_name.to_s
|
113
|
+
Array(options[:from]).each do |k|
|
114
|
+
key_mapping[k.to_s] = property_name
|
115
|
+
end
|
116
|
+
end
|
117
|
+
if options[:transformer]
|
118
|
+
transformer_for property_name, options[:transformer]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Does this Smash class contain a specific property (key),
|
123
|
+
# or does it have a key mapping (via :from)
|
124
|
+
#
|
125
|
+
# @param [Symbol] key the property to test for.
|
126
|
+
# @return [Boolean] true if this class contains the key; else, false.
|
127
|
+
def self.property?(key)
|
128
|
+
super || key_mapping.has_key?(key.to_s)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Automates type conversion (including on Array and Hashes) to this type.
|
132
|
+
# Used so we can pass this class similarily to how we pass lambdas as an
|
133
|
+
# object, primarily for use as transformers.
|
134
|
+
#
|
135
|
+
# @param [Object] the object to attempt to convert.
|
136
|
+
# @return [Array<Smash>, Smash] The converted object / array of objects if
|
137
|
+
# possible, otherwise nil.
|
138
|
+
def self.call(value)
|
139
|
+
if value.is_a?(Array)
|
140
|
+
value.map { |v| call v }.compact
|
141
|
+
elsif value.is_a?(Hash)
|
142
|
+
new value
|
143
|
+
else
|
144
|
+
nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Access the value responding to a key, normalising the key into a form
|
149
|
+
# we know (e.g. processing the from value to convert it to the actual
|
150
|
+
# property name).
|
151
|
+
#
|
152
|
+
# @param [Symbol] property the key to check for.
|
153
|
+
# @return The value corresponding to property. nil if it does not exist.
|
154
|
+
def [](property)
|
155
|
+
super transform_key(property)
|
156
|
+
rescue UnknownKey
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
|
160
|
+
# Sets the value for a given key. Transforms the key first (e.g. taking into
|
161
|
+
# account from values) and transforms the property using any transformers.
|
162
|
+
#
|
163
|
+
# @param [Symbol] property the key to set.
|
164
|
+
# @param [String] value the value to set.
|
165
|
+
# @return If the property exists value is returned; else, nil.
|
166
|
+
def []=(property, value)
|
167
|
+
key = transform_key(property)
|
168
|
+
super key, transform_property(key, value)
|
169
|
+
rescue UnknownKey
|
170
|
+
nil
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
# Overrides the Dashie check to raise a custom exception that we can
|
176
|
+
# rescue from when the key is unknown.
|
177
|
+
def assert_property_exists!(property)
|
178
|
+
has_property = self.class.property?(property)
|
179
|
+
unless has_property
|
180
|
+
exception = self.class.exception_on_unknown_key? ? NoMethodError : UnknownKey
|
181
|
+
raise exception, "The property '#{property}' is not defined on this #{self.class.name}"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Transforms a given key into it's normalised alternative, making it
|
186
|
+
# suitable for automatically mapping external objects into a useable
|
187
|
+
# local version.
|
188
|
+
# @param [Symbol, String] key the starting key, pre-transformation
|
189
|
+
# @return [String] the transformed key, ready for use internally.
|
190
|
+
def transform_key(key)
|
191
|
+
self.class.key_mapping[key.to_s] || default_key_transformation(key)
|
192
|
+
end
|
193
|
+
|
194
|
+
# By default, we transform the key using #to_s, making it useable
|
195
|
+
# as a hash index. If you want to, for example, add leading underscores,
|
196
|
+
# you're do so here.
|
197
|
+
def default_key_transformation(key)
|
198
|
+
key.to_s
|
199
|
+
end
|
200
|
+
|
201
|
+
# Given a key and a value, applies any incoming data transformations as appropriate.
|
202
|
+
# @param [String, Symbol] key the property key
|
203
|
+
# @param [Object] value the incoming value of the given property
|
204
|
+
# @return [Object] the transformed value for the given key
|
205
|
+
# @see Smash.transformer_for
|
206
|
+
def transform_property(key, value)
|
207
|
+
transformation = self.class.transformers[key.to_s]
|
208
|
+
transformation ? transformation.call(value) : value
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module APISmith
|
2
|
+
# A set of extensions to make using APISmith with WebMock (or most test utilities in general)
|
3
|
+
# simpler when it comes checking values. Please note this is primarily intended for use with
|
4
|
+
# rspec due to dependence on subject in some places.
|
5
|
+
#
|
6
|
+
# @author Darcy Laycock
|
7
|
+
# @author Steve Webb
|
8
|
+
module WebMockExtensions
|
9
|
+
|
10
|
+
# Returns the class of the current subject
|
11
|
+
# @return [Class] the subject class
|
12
|
+
def subject_api_class
|
13
|
+
subject.is_a?(Class) ? subject : subject.class
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns an instance of the subject class, created via allocate (vs. new)
|
17
|
+
# @return [Object] the instance
|
18
|
+
def subject_class_instance
|
19
|
+
@subject_class_instance ||= subject_api_class.allocate
|
20
|
+
end
|
21
|
+
|
22
|
+
# Expands the given path relative to the API for the current subject class.
|
23
|
+
# Namely, this makes it possible to convert a relative path to an endpoint-specified
|
24
|
+
# path.
|
25
|
+
# @param [String] path the path to expand, minus endpoint etc.
|
26
|
+
# @return [String] the expanded path
|
27
|
+
def api_url_for(path)
|
28
|
+
path = subject_class_instance.send(:path_for, path)
|
29
|
+
base_uri = subject_api_class.base_uri
|
30
|
+
File.join base_uri, path
|
31
|
+
end
|
32
|
+
|
33
|
+
# Short hand for #stub_request that lets you give it a relative path prior to expanding it.
|
34
|
+
# @param [:get, :post, :put, :delete] the verb for the request
|
35
|
+
# @param [String] the relative path for the api
|
36
|
+
# @return [Object] the result from stub_request
|
37
|
+
def stub_api(type, path)
|
38
|
+
stub_request(type, api_url_for(path))
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: api_smith
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Darcy Laycock
|
14
|
+
- Steve Webb
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-06-21 00:00:00 +08:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: httparty
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: hashie
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 15
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 0
|
48
|
+
version: "1.0"
|
49
|
+
type: :runtime
|
50
|
+
version_requirements: *id002
|
51
|
+
- !ruby/object:Gem::Dependency
|
52
|
+
name: rr
|
53
|
+
prerelease: false
|
54
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 3
|
60
|
+
segments:
|
61
|
+
- 0
|
62
|
+
version: "0"
|
63
|
+
type: :development
|
64
|
+
version_requirements: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: rspec
|
67
|
+
prerelease: false
|
68
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ~>
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 2
|
76
|
+
- 0
|
77
|
+
version: "2.0"
|
78
|
+
type: :development
|
79
|
+
version_requirements: *id004
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: fuubar
|
82
|
+
prerelease: false
|
83
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
84
|
+
none: false
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
hash: 3
|
89
|
+
segments:
|
90
|
+
- 0
|
91
|
+
version: "0"
|
92
|
+
type: :development
|
93
|
+
version_requirements: *id005
|
94
|
+
description: APISmith provides tools to make working with structured HTTP-based apis even easier.
|
95
|
+
email:
|
96
|
+
- sutto@thefrontiergroup.com.au
|
97
|
+
executables: []
|
98
|
+
|
99
|
+
extensions: []
|
100
|
+
|
101
|
+
extra_rdoc_files: []
|
102
|
+
|
103
|
+
files:
|
104
|
+
- lib/api_smith/base.rb
|
105
|
+
- lib/api_smith/client.rb
|
106
|
+
- lib/api_smith/smash.rb
|
107
|
+
- lib/api_smith/version.rb
|
108
|
+
- lib/api_smith/web_mock_extensions.rb
|
109
|
+
- lib/api_smith.rb
|
110
|
+
has_rdoc: true
|
111
|
+
homepage: http://github.com/thefrontiergroup
|
112
|
+
licenses: []
|
113
|
+
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
hash: 3
|
125
|
+
segments:
|
126
|
+
- 0
|
127
|
+
version: "0"
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
hash: 23
|
134
|
+
segments:
|
135
|
+
- 1
|
136
|
+
- 3
|
137
|
+
- 6
|
138
|
+
version: 1.3.6
|
139
|
+
requirements: []
|
140
|
+
|
141
|
+
rubyforge_project:
|
142
|
+
rubygems_version: 1.6.2
|
143
|
+
signing_key:
|
144
|
+
specification_version: 3
|
145
|
+
summary: A simple layer on top of HTTParty for building API's
|
146
|
+
test_files: []
|
147
|
+
|