api_smith 1.0.0
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.
- 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
|
+
|