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.
@@ -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
@@ -0,0 +1,3 @@
1
+ class WeaselDiesel
2
+ VERSION = "1.0.0"
3
+ end
@@ -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
+