weasel_diesel 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/Gemfile +4 -0
- data/Gemfile.lock +38 -0
- data/LICENSE +23 -0
- data/README.md +231 -0
- data/Rakefile +12 -0
- data/lib/documentation.rb +151 -0
- data/lib/framework_ext/sinatra.rb +30 -0
- data/lib/framework_ext/sinatra_controller.rb +80 -0
- data/lib/inflection.rb +460 -0
- data/lib/json_response_verification.rb +109 -0
- data/lib/params.rb +374 -0
- data/lib/params_verification.rb +268 -0
- data/lib/response.rb +457 -0
- data/lib/weasel_diesel.rb +415 -0
- data/lib/weasel_diesel/version.rb +3 -0
- data/lib/ws_list.rb +58 -0
- data/spec/hello_world_controller.rb +5 -0
- data/spec/hello_world_service.rb +20 -0
- data/spec/json_response_description_spec.rb +124 -0
- data/spec/json_response_verification_spec.rb +225 -0
- data/spec/params_verification_spec.rb +102 -0
- data/spec/preferences_service.rb +10 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/test_services.rb +102 -0
- data/spec/wsdsl_sinatra_ext_spec.rb +26 -0
- data/spec/wsdsl_spec.rb +314 -0
- data/weasel_diesel.gemspec +30 -0
- metadata +127 -0
@@ -0,0 +1,415 @@
|
|
1
|
+
require File.expand_path('inflection', File.dirname(__FILE__))
|
2
|
+
require File.expand_path('params', File.dirname(__FILE__))
|
3
|
+
require File.expand_path('response', File.dirname(__FILE__))
|
4
|
+
require File.expand_path('documentation', File.dirname(__FILE__))
|
5
|
+
require File.expand_path('ws_list', File.dirname(__FILE__))
|
6
|
+
|
7
|
+
# WeaselDiesel offers a web service DSL to define web services,
|
8
|
+
# their params, http verbs, formats expected as well as the documentation
|
9
|
+
# for all these aspects of a web service.
|
10
|
+
#
|
11
|
+
# This DSL is only meant to describe a web service and isn't meant to cover any type
|
12
|
+
# of implementation details. It is meant to be framework/tool agnostic.
|
13
|
+
#
|
14
|
+
# However, tools can be built around the Web Service DSL data structure to extract documentation,
|
15
|
+
# generate routing information, verify that an incoming request is valid, generate automated tests...
|
16
|
+
#
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# WeaselDiesel
|
20
|
+
# |
|
21
|
+
# |__ service options (name, url, SSL, auth required formats, verbs, controller name, action, version, extra)
|
22
|
+
# |__ defined_params (instance of WeaselDiesel::Params)
|
23
|
+
# | | | |_ Optional param rules
|
24
|
+
# | | |_ Required param rules
|
25
|
+
# | |_ Namespaced params (array containing nested optional and required rules)
|
26
|
+
# |__ response (instance of WeaselDiesel::Response)
|
27
|
+
# | |_ elements (array of elements with each element having a name, type, attributes and vectors
|
28
|
+
# | | | |_ attributes (array of WeaselDiesel::Response::Attribute, each attribute has a name, a type, a doc and some extra options)
|
29
|
+
# | | |_ vectors (array of WeaselDiesel::Response::Vector), each vector has a name, obj_type, & an array of attributes
|
30
|
+
# | | |_ attributes (array of WeaselDiesel::Response::Attribute, each attribute has a name, a type and a doc)
|
31
|
+
# | |_ arrays (like elements but represent an array of objects)
|
32
|
+
# |
|
33
|
+
# |__ doc (instance of WeaselDiesel::Documentation)
|
34
|
+
# | | | |_ overal) description
|
35
|
+
# | | |_ examples (array of examples as strings)
|
36
|
+
# | |_ params documentation (Hash with the key being the param name and the value being the param documentation)
|
37
|
+
# |_ response (instance of Documentation.new)
|
38
|
+
# |_ elements (array of instances of WeaselDiesel::Documentation::ElementDoc, each element has a name and a list of attributes)
|
39
|
+
# |_ attributes (Hash with the key being the attribute name and the value being the attribute's documentation)
|
40
|
+
#
|
41
|
+
# @since 0.0.3
|
42
|
+
# @api public
|
43
|
+
class WeaselDiesel
|
44
|
+
|
45
|
+
# Returns the service url
|
46
|
+
#
|
47
|
+
# @return [String] The service url
|
48
|
+
# @api public
|
49
|
+
attr_reader :url
|
50
|
+
|
51
|
+
# List of all the service params
|
52
|
+
#
|
53
|
+
# @return [Array<WeaselDiesel::Params>]
|
54
|
+
# @api public
|
55
|
+
attr_reader :defined_params
|
56
|
+
|
57
|
+
# Documentation instance containing all the service doc
|
58
|
+
#
|
59
|
+
# @return [WeaselDiesel::Documentation]
|
60
|
+
# @api public
|
61
|
+
attr_reader :doc
|
62
|
+
|
63
|
+
# The HTTP verb supported
|
64
|
+
#
|
65
|
+
# @return [Symbol]
|
66
|
+
# @api public
|
67
|
+
attr_reader :verb
|
68
|
+
|
69
|
+
# Service's version
|
70
|
+
#
|
71
|
+
# @return [String]
|
72
|
+
# @api public
|
73
|
+
attr_reader :version
|
74
|
+
|
75
|
+
# Controller instance associated with the service
|
76
|
+
#
|
77
|
+
# @return [WSController]
|
78
|
+
# @api public
|
79
|
+
attr_reader :controller
|
80
|
+
|
81
|
+
# Name of the controller action associated with the service
|
82
|
+
#
|
83
|
+
# @return [String]
|
84
|
+
# @api public
|
85
|
+
attr_accessor :action
|
86
|
+
|
87
|
+
# Name of the controller associated with the service
|
88
|
+
#
|
89
|
+
# @return [String]
|
90
|
+
# @api public
|
91
|
+
attr_accessor :controller_name
|
92
|
+
|
93
|
+
# Name of the service
|
94
|
+
#
|
95
|
+
# @return [String]
|
96
|
+
# @api public
|
97
|
+
attr_reader :name
|
98
|
+
|
99
|
+
# Is SSL required?
|
100
|
+
#
|
101
|
+
# @return [Boolean]
|
102
|
+
# @api public
|
103
|
+
attr_reader :ssl
|
104
|
+
|
105
|
+
# Is authentication required?
|
106
|
+
#
|
107
|
+
# @return [Boolean]
|
108
|
+
# @api public
|
109
|
+
attr_reader :auth_required
|
110
|
+
|
111
|
+
# Extra placeholder to store data in based on developer's discretion.
|
112
|
+
#
|
113
|
+
# @return [Hash] A hash storing extra data based.
|
114
|
+
# @api public
|
115
|
+
# @since 0.1
|
116
|
+
attr_reader :extra
|
117
|
+
|
118
|
+
# Service constructor which is usually used via {Kernel#describe_service}
|
119
|
+
#
|
120
|
+
# @param [String] url Service's url
|
121
|
+
# @see #describe_service See how this class is usually initialized using `describe_service`
|
122
|
+
# @api public
|
123
|
+
def initialize(url)
|
124
|
+
@url = url
|
125
|
+
@defined_params = WeaselDiesel::Params.new
|
126
|
+
@doc = WeaselDiesel::Documentation.new
|
127
|
+
@response = WeaselDiesel::Response.new
|
128
|
+
if WeaselDiesel.use_controller_dispatch
|
129
|
+
@name = extract_service_root_name(url)
|
130
|
+
if WeaselDiesel.use_pluralized_controllers
|
131
|
+
base_name = ExtlibCopy::Inflection.pluralize(ExtlibCopy::Inflection.singular(name))
|
132
|
+
@controller_name = "#{ExtlibCopy.classify(base_name)}Controller"
|
133
|
+
else
|
134
|
+
@controller_name = "#{ExtlibCopy.classify(name)}Controller"
|
135
|
+
end
|
136
|
+
@action = extract_service_action(url)
|
137
|
+
end
|
138
|
+
@verb = :get
|
139
|
+
@formats = []
|
140
|
+
@version = '0.1'
|
141
|
+
@ssl = false
|
142
|
+
@auth_required = true
|
143
|
+
@extra = {}
|
144
|
+
end
|
145
|
+
|
146
|
+
# Checks the WeaselDiesel flag to see if the controller names are pluralized.
|
147
|
+
#
|
148
|
+
# @return [Boolean] The updated value, default to false
|
149
|
+
# @api public
|
150
|
+
# @since 0.1.1
|
151
|
+
def self.use_pluralized_controllers
|
152
|
+
@pluralized_controllers ||= false
|
153
|
+
end
|
154
|
+
|
155
|
+
# Sets a WeaselDiesel global flag so all controller names will be automatically pluralized.
|
156
|
+
#
|
157
|
+
# @param [Boolean] True if the controllers are pluralized, False otherwise.
|
158
|
+
#
|
159
|
+
# @return [Boolean] The updated value
|
160
|
+
# @api public
|
161
|
+
# @since 0.1.1
|
162
|
+
def self.use_pluralized_controllers=(val)
|
163
|
+
@pluralized_controllers = val
|
164
|
+
end
|
165
|
+
|
166
|
+
# Checks the WeaselDiesel flag to see if controller are used to dispatch requests.
|
167
|
+
# This allows apps to use this DSL but route to controller/actions.
|
168
|
+
#
|
169
|
+
# @return [Boolean] The updated value, default to false
|
170
|
+
# @api public
|
171
|
+
# @since 0.3.0
|
172
|
+
def self.use_controller_dispatch
|
173
|
+
@controller_dispatch
|
174
|
+
end
|
175
|
+
|
176
|
+
# Sets a WeaselDiesel global flag so the controller settings can be generated
|
177
|
+
# Setting this flag will automatically set the controller/action names.
|
178
|
+
# @param [Boolean] True if the controllers are pluralized, False otherwise.
|
179
|
+
#
|
180
|
+
# @return [Boolean] The updated value
|
181
|
+
# @api public
|
182
|
+
# @since 0.1.1
|
183
|
+
def self.use_controller_dispatch=(val)
|
184
|
+
@controller_dispatch = val
|
185
|
+
end
|
186
|
+
|
187
|
+
# Offers a way to dispatch the service at runtime
|
188
|
+
# Basically, it dispatches the request to the defined controller/action
|
189
|
+
# The full request cycle looks like that:
|
190
|
+
# client -> webserver -> rack -> env -> [service dispatcher] -> controller action -> rack -> webserver -> client
|
191
|
+
# @param [Object] app Reference object such as a Sinatra::Application to be passed to the controller.
|
192
|
+
#
|
193
|
+
# @return [#to_s] The response from the controller action
|
194
|
+
# @api private
|
195
|
+
def controller_dispatch(app)
|
196
|
+
unless @controller
|
197
|
+
klass = @controller_name.split("::")
|
198
|
+
begin
|
199
|
+
@controller = klass.inject(Object) { |const,k| const.const_get(k) }
|
200
|
+
rescue NameError => e
|
201
|
+
raise "The #{@controller_name} class was not found"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
# We are passing the service object to the controller so the
|
205
|
+
# param verification could be done when the controller gets initialized.
|
206
|
+
@controller.new(app, self).send(@action)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Returns the defined params
|
210
|
+
# for DSL use only!
|
211
|
+
# To keep the distinction between the request params and the service params
|
212
|
+
# using the +defined_params+ accessor is recommended.
|
213
|
+
# @see WeaselDiesel::Params
|
214
|
+
#
|
215
|
+
# @return [WeaselDiesel::Params] The defined params
|
216
|
+
# @api public
|
217
|
+
def params
|
218
|
+
if block_given?
|
219
|
+
yield(@defined_params)
|
220
|
+
else
|
221
|
+
@defined_params
|
222
|
+
end
|
223
|
+
end
|
224
|
+
alias :param :params
|
225
|
+
|
226
|
+
# Returns true if the DSL defined any params
|
227
|
+
#
|
228
|
+
# @return [Boolean]
|
229
|
+
def params?
|
230
|
+
!(required_rules.empty? && optional_rules.empty? && nested_params.empty?)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns an array of required param rules
|
234
|
+
#
|
235
|
+
# @return [Array<WeaselDiesel::Params::Rule>] Only the required param rules
|
236
|
+
# @api public
|
237
|
+
def required_rules
|
238
|
+
@defined_params.list_required
|
239
|
+
end
|
240
|
+
|
241
|
+
# Returns an array of optional param rules
|
242
|
+
#
|
243
|
+
# @return [Array<WeaselDiesel::Params::Rule>]Only the optional param rules
|
244
|
+
# @api public
|
245
|
+
def optional_rules
|
246
|
+
@defined_params.list_optional
|
247
|
+
end
|
248
|
+
|
249
|
+
# Returns an array of namespaced params
|
250
|
+
# @see WeaselDiesel::Params#namespaced_params
|
251
|
+
#
|
252
|
+
# @return [Array<WeaselDiesel::Params>] the namespaced params
|
253
|
+
# @api public
|
254
|
+
def nested_params
|
255
|
+
@defined_params.namespaced_params
|
256
|
+
end
|
257
|
+
|
258
|
+
# Mark that the service doesn't require authentication.
|
259
|
+
# Note: Authentication is turned on by default
|
260
|
+
#
|
261
|
+
# @return [Boolean]
|
262
|
+
# @api public
|
263
|
+
def disable_auth
|
264
|
+
@auth_required = false
|
265
|
+
end
|
266
|
+
|
267
|
+
# Mark that the service requires a SSL connection
|
268
|
+
#
|
269
|
+
# @return [Boolean]
|
270
|
+
# @api public
|
271
|
+
def enable_ssl
|
272
|
+
@ssl = true
|
273
|
+
end
|
274
|
+
|
275
|
+
# Mark the current service as not accepting any params.
|
276
|
+
# This is purely for expressing the developer's objective since
|
277
|
+
# by default an error is raise if no params are defined and some
|
278
|
+
# params are sent.
|
279
|
+
#
|
280
|
+
# @return [Nil]
|
281
|
+
# @api public
|
282
|
+
def accept_no_params!
|
283
|
+
# no op operation since this is the default behavior
|
284
|
+
# unless params get defined. Makes sense for documentation tho.
|
285
|
+
end
|
286
|
+
|
287
|
+
# Returns the service response
|
288
|
+
# @yield The service response object
|
289
|
+
#
|
290
|
+
# @return [WeaselDiesel::Response]
|
291
|
+
# @api public
|
292
|
+
def response
|
293
|
+
if block_given?
|
294
|
+
yield(@response)
|
295
|
+
else
|
296
|
+
@response
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Sets or returns the supported formats
|
301
|
+
# @param [String, Symbol] f_types Format type supported, such as :xml
|
302
|
+
#
|
303
|
+
# @return [Array<Symbol>] List of supported formats
|
304
|
+
# @api public
|
305
|
+
def formats(*f_types)
|
306
|
+
f_types.each{|f| @formats << f unless @formats.include?(f) }
|
307
|
+
@formats
|
308
|
+
end
|
309
|
+
|
310
|
+
# Sets the accepted HTTP verbs or return it if nothing is passed.
|
311
|
+
#
|
312
|
+
# @return [String, Symbol]
|
313
|
+
# @api public
|
314
|
+
def http_verb(s_verb=nil)
|
315
|
+
return @verb if s_verb.nil?
|
316
|
+
@verb = s_verb.to_sym
|
317
|
+
# Depending on the service settings and url, the service action might need to be updated.
|
318
|
+
# This is how we can support restful routes where a PUT request automatically uses the update method.
|
319
|
+
update_restful_action(@verb)
|
320
|
+
@verb
|
321
|
+
end
|
322
|
+
|
323
|
+
# Yields and returns the documentation object
|
324
|
+
# @yield [WeaselDiesel::Documentation]
|
325
|
+
#
|
326
|
+
# @return [WeaselDiesel::Documentation] The service documentation object
|
327
|
+
# @api public
|
328
|
+
def documentation
|
329
|
+
yield(doc)
|
330
|
+
end
|
331
|
+
|
332
|
+
# Assign a route loading point to compare two routes.
|
333
|
+
# Using this point value, one can load routes with the more globbing
|
334
|
+
# routes later than short routes.
|
335
|
+
#
|
336
|
+
# @return [Integer] point value
|
337
|
+
def route_loading_point
|
338
|
+
url =~ /(.*?):(.*?)[\/\.](.*)/
|
339
|
+
return url.size if $1.nil?
|
340
|
+
# The shortest the prepend, the further the service should be loaded
|
341
|
+
prepend = $1.size
|
342
|
+
# The shortest the placeholder, the further it should be in the queue
|
343
|
+
place_holder = $2.size
|
344
|
+
# The shortest the trail, the further it should be in the queue
|
345
|
+
trail = $3.size
|
346
|
+
prepend + place_holder + trail
|
347
|
+
end
|
348
|
+
|
349
|
+
# Compare two services using the route loading point
|
350
|
+
def <=> (other)
|
351
|
+
route_loading_point <=> other.route_loading_point
|
352
|
+
end
|
353
|
+
|
354
|
+
SERVICE_ROOT_REGEXP = /(.*?)[\/\(\.]/
|
355
|
+
SERVICE_ACTION_REGEXP = /[\/\(\.]([a-z0-9_]+)[\/\(\.\?]/i
|
356
|
+
SERVICE_RESTFUL_SHOW_REGEXP = /\/:[a-z0-9_]+\.\w{3}$/
|
357
|
+
|
358
|
+
private
|
359
|
+
|
360
|
+
# extracts the service root name out of the url using a regexp
|
361
|
+
def extract_service_root_name(url)
|
362
|
+
url[SERVICE_ROOT_REGEXP, 1] || url
|
363
|
+
end
|
364
|
+
|
365
|
+
# extracts the action name out of the url using a regexp
|
366
|
+
# Defaults to the list action
|
367
|
+
def extract_service_action(url)
|
368
|
+
if url =~ SERVICE_RESTFUL_SHOW_REGEXP
|
369
|
+
'show'
|
370
|
+
else
|
371
|
+
url[SERVICE_ACTION_REGEXP, 1] || 'list'
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Check if we need to use a restful route in which case we need
|
376
|
+
# to update the service action
|
377
|
+
def update_restful_action(verb)
|
378
|
+
if verb != :get && @action && @action == 'list'
|
379
|
+
case verb
|
380
|
+
when :post
|
381
|
+
@action = 'create'
|
382
|
+
when :put
|
383
|
+
@action = 'update'
|
384
|
+
when :delete
|
385
|
+
@action = 'destroy'
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
end
|
391
|
+
|
392
|
+
# Extending the top level module to add some helpers
|
393
|
+
#
|
394
|
+
# @api public
|
395
|
+
module Kernel
|
396
|
+
|
397
|
+
# Base DSL method called to describe a service
|
398
|
+
#
|
399
|
+
# @param [String] url The url of the service to add.
|
400
|
+
# @yield [WeaselDiesel] The newly created service.
|
401
|
+
# @return [Array] The services already defined
|
402
|
+
# @example Describing a basic service
|
403
|
+
# describe_service "hello-world.xml" do |service|
|
404
|
+
# # describe the service
|
405
|
+
# end
|
406
|
+
#
|
407
|
+
# @api public
|
408
|
+
def describe_service(url, &block)
|
409
|
+
service = WeaselDiesel.new(url)
|
410
|
+
yield service
|
411
|
+
WSList.add(service)
|
412
|
+
service
|
413
|
+
end
|
414
|
+
|
415
|
+
end
|
data/lib/ws_list.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# Wrapper module to keep track of all defined services
|
2
|
+
#
|
3
|
+
# @api public
|
4
|
+
module WSList
|
5
|
+
|
6
|
+
class UnknownService < StandardError; end
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
# Add a service to the array tracking
|
11
|
+
# the playco services
|
12
|
+
#
|
13
|
+
# @param [WeaselDiesel] The service to add.
|
14
|
+
# @return [Array<WeaselDiesel>] All the added services.
|
15
|
+
# @api public
|
16
|
+
def add(service)
|
17
|
+
@list ||= []
|
18
|
+
@list << service unless @list.find{|s| s.url == service.url && s.verb == service.verb}
|
19
|
+
@list
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns an array of services
|
23
|
+
#
|
24
|
+
# @return [Array<WeaselDiesel>] All the added services.
|
25
|
+
# @api public
|
26
|
+
def all
|
27
|
+
@list || []
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a service based on its name
|
31
|
+
#
|
32
|
+
# @param [String] name The name of the service you are looking for.
|
33
|
+
# @raise [UnknownService] if a service with the passed name isn't found.
|
34
|
+
# @return [WeaselDiesel] The found service.
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def named(name)
|
38
|
+
service = all.find{|service| service.name == name}
|
39
|
+
if service.nil?
|
40
|
+
raise UnknownService, "Service named #{name} isn't available"
|
41
|
+
else
|
42
|
+
service
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a service based on its url
|
47
|
+
#
|
48
|
+
# @param [String] url The url of the service you are looking for.
|
49
|
+
# @return [Nil, WeaselDiesel] The found service.
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def [](url)
|
53
|
+
@list.find{|service| service.url == url}
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
end
|
58
|
+
|