weasel_diesel 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+