wsdsl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/response.rb ADDED
@@ -0,0 +1,301 @@
1
+ class WSDSL
2
+ # Response DSL class
3
+ # @api public
4
+ class Response
5
+
6
+ # The list of all the elements inside the response
7
+ #
8
+ # @return [Array<WSDSL::Response::Element>]
9
+ # @api public
10
+ attr_reader :elements
11
+
12
+ def initialize
13
+ @elements = []
14
+ end
15
+
16
+ # Defines a new element and yields the content of an optional block
17
+ # Each new element is then stored in the elements array.
18
+ #
19
+ # @param [Hash] opts Options used to define the element
20
+ # @option opts [String, Symbol] :name The element name
21
+ # @option opts [String, Symbol] :type The optional type
22
+ #
23
+ # @yield [WSDSL::Response::Element] the newly created element
24
+ # @example create an element called 'my_stats'.
25
+ # service.response do |response|
26
+ # response.element(:name => "my_stats", :type => 'Leaderboard')
27
+ # end
28
+ #
29
+ # @return [Array<WSDSL::Response::Element>]
30
+ # @api public
31
+ def element(opts={})
32
+ el = Element.new(opts[:name], opts[:type])
33
+ yield(el) if block_given?
34
+ @elements << el
35
+ end
36
+
37
+ # Returns a response element object based on its name
38
+ # @param [String, Symbol] The element name we want to match
39
+ #
40
+ # @return [WSDSL::Response::Element]
41
+ # @api public
42
+ def element_named(name)
43
+ @elements.find{|e| e.name.to_s == name.to_s}
44
+ end
45
+
46
+ # The Response element class describing each element of a service response.
47
+ # Instances are usually not instiated directly but via the Response#element accessor.
48
+ #
49
+ # @see WSDSL::Response#element
50
+ # @api public
51
+ class Element
52
+
53
+ # @return [String, #to_s] The name of the element
54
+ # @api public
55
+ attr_reader :name
56
+
57
+ # @api public
58
+ attr_reader :type
59
+
60
+ # @return [Array<WSDSL::Response::Element::Attribute>] An array of attributes
61
+ # @api public
62
+ attr_reader :attributes
63
+
64
+ # @return [Array] An array of vectors/arrays
65
+ # @api public
66
+ attr_reader :vectors
67
+
68
+ # @return [WSDSL::Documentation::ElementDoc] Response element documentation
69
+ # @api public
70
+ attr_reader :doc
71
+
72
+ # @return [NilClass, Array<WSDSL::Response::Element>] The optional nested elements
73
+ attr_reader :elements
74
+
75
+ # param [String, Symbol] name The name of the element
76
+ # param [String, Symbol] type The optional type of the element
77
+ # @api public
78
+ def initialize(name, type=nil)
79
+ # sets a documentation placeholder since the response doc is defined at the same time
80
+ # the response is defined.
81
+ @doc = Documentation::ElementDoc.new(name)
82
+ @name = name
83
+ @type = type
84
+ @attributes = []
85
+ @vectors = []
86
+ # we don't need to initialize the nested elements, by default they should be nil
87
+ end
88
+
89
+ # sets a new attribute and returns the entire list of attributes
90
+ #
91
+ # @param [Hash] opts An element's attribute options
92
+ # @option opts [String, Symbol] attribute_name The name of the attribute, the value being the type
93
+ # @option opts [String, Symbol] :doc The attribute documentation
94
+ # @option opts [String, Symbol] :mock An optional mock value used by service related tools
95
+ #
96
+ # @example Creation of a response attribute called 'best_lap_time'
97
+ # service.response do |response|
98
+ # response.element(:name => "my_stats", :type => 'Leaderboard') do |e|
99
+ # e.attribute "best_lap_time" => :float, :doc => "Best lap time in seconds."
100
+ # end
101
+ # end
102
+ #
103
+ # @return [Array<WSDSL::Response::Attribute>]
104
+ # @api public
105
+ def attribute(opts)
106
+ raise ArgumentError unless opts.is_a?(Hash)
107
+ # extract the documentation part and add it where it belongs
108
+ new_attribute = Attribute.new(opts)
109
+ @attributes << new_attribute
110
+ # document the attribute if description available
111
+ # we might want to have a placeholder message when a response attribute isn't defined
112
+ if opts.has_key?(:doc)
113
+ @doc.attribute(new_attribute.name, opts[:doc])
114
+ end
115
+ @attributes
116
+ end
117
+
118
+ # Defines an array aka vector of elements.
119
+ #
120
+ # @param [Hash] opts A hash representing the array information, usually a name and a type.
121
+ # @option opts [String, Symbol] :name The name of the defined array
122
+ # @option opts [String, Symbol] :type The class name of the element inside the array
123
+ #
124
+ # @param [Proc] &block
125
+ # A block to execute against the newly created array.
126
+ #
127
+ # @example Defining an element array called 'player_creation_rating'
128
+ # element.array :name => 'player_creation_rating', :type => 'PlayerCreationRating' do |a|
129
+ # a.attribute :comments => :string
130
+ # a.attribute :player_id => :integer
131
+ # a.attribute :rating => :integer
132
+ # a.attribute :username => :string
133
+ # end
134
+ # @yield [Vector] the newly created array/vector instance
135
+ # @see Vector#initialize
136
+ #
137
+ # @return [Array<WSDSL::Response::Element::Vector>]
138
+ # @api public
139
+ def array(opts)
140
+ vector = Vector.new(opts)
141
+ yield(vector) if block_given?
142
+ @vectors << vector
143
+ end
144
+
145
+ # Returns the arrays/vectors contained in the response.
146
+ # This is an alias to access @vectors
147
+ # @see @vectors
148
+ #
149
+ # @return [Array<WSDSL::Response::Element::Vector>]
150
+ # @api public
151
+ def arrays
152
+ @vectors
153
+ end
154
+
155
+ # Defines a new element and yields the content of an optional block
156
+ # Each new element is then stored in the elements array.
157
+ #
158
+ # @param [Hash] opts Options used to define the element
159
+ # @option opts [String, Symbol] :name The element name
160
+ # @option opts [String, Symbol] :type The optional type
161
+ #
162
+ # @yield [WSDSL::Response::Element] the newly created element
163
+ # @example create an element called 'my_stats'.
164
+ # service.response do |response|
165
+ # response.element(:name => "my_stats", :type => 'Leaderboard')
166
+ # end
167
+ #
168
+ # @return [Array<WSDSL::Response::Element>]
169
+ # @api public
170
+ def element(opts={})
171
+ el = Element.new(opts[:name], opts[:type])
172
+ yield(el) if block_given?
173
+ @elements ||= []
174
+ @elements << el
175
+ end
176
+
177
+ # Response element's attribute class
178
+ # @api public
179
+ class Attribute
180
+
181
+ # @return [String, #to_s] The attribute's name.
182
+ # @api public
183
+ attr_reader :name
184
+
185
+ # @return [Symbol, String, #to_s] The attribute's type such as boolean, string etc..
186
+ # @api public
187
+ attr_reader :type
188
+
189
+ # @return [String] The documentation associated with this attribute.
190
+ # @api public
191
+ attr_reader :doc
192
+
193
+ # @see {Attribute#new}
194
+ # @return [Hash, Nil, Object] Could be a hash, nil or any object depending on how the attribute is created.
195
+ # @api public
196
+ attr_reader :opts
197
+
198
+ # Takes a Hash or an Array and extract the attribute name, type
199
+ # doc and extra options.
200
+ # If the passed objects is a Hash, the name will be extract from
201
+ # the first key and the type for the first value.
202
+ # An entry keyed by :doc will be used for the doc and the rest will go
203
+ # as extra options.
204
+ #
205
+ # If an Array is passed, the elements will be 'shifted' in this order:
206
+ # name, type, doc, type
207
+ #
208
+ # @param [Hash, Array] o_params
209
+ #
210
+ # @api public
211
+ def initialize(o_params)
212
+ params = o_params.dup
213
+ if params.is_a?(Hash)
214
+ @name, @type = params.shift
215
+ @doc = params.delete(:doc) if params.has_key?(:doc)
216
+ @opts = params
217
+ elsif params.is_a?(Array)
218
+ @name = params.shift
219
+ @type = params.shift
220
+ @doc = params.shift
221
+ @opts = params
222
+ end
223
+ end
224
+ end
225
+
226
+ # Array of objects inside an element
227
+ # @api public
228
+ class Vector
229
+
230
+ # @api public
231
+ attr_reader :name
232
+
233
+ # @api public
234
+ attr_reader :obj_type
235
+
236
+ # @api public
237
+ attr_accessor :attributes
238
+
239
+ # A vector can have nested elements.
240
+ # This value is nil by default.
241
+ #
242
+ # @return [NilClass, Array<WSDSL::Response::Element>]
243
+ # @see #element
244
+ # @api public
245
+ attr_reader :elements
246
+
247
+ # Initialize a Vector object, think about it as an array of objects of a certain type.
248
+ # It is recommended to passthe type argument as a string so the constant doesn't need to be resolved.
249
+ # In other words, if you say you are creating a vector of Foo objects, the Foo class doesn't need to be
250
+ # loaded yet. That makes service parsing easier and avoids dependency challenges.
251
+ #
252
+ # @param [Hash] opts A hash representing the vector information, usually a name and a type, both as strings
253
+ # @option opts [String] :name The array's name
254
+ # @option opts [Symbol, String] :type The type of the objects inside the array
255
+ #
256
+ # @example
257
+ # Vector.new(:name => 'player_creation_rating', :type => 'PlayerCreationRating')
258
+ #
259
+ # @api public
260
+ def initialize(opts)
261
+ @name = opts[:name]
262
+ @obj_type = opts[:type]
263
+ @attributes = []
264
+ end
265
+
266
+ # Sets a vector attribute
267
+ #
268
+ # @param (see Attribute#initialize)
269
+ # @api public
270
+ def attribute(opts)
271
+ raise ArgumentError unless opts.is_a?(Hash)
272
+ @attributes << Attribute.new(opts)
273
+ end
274
+
275
+ # Defines a new element and yields the content of an optional block
276
+ # Each new element is then stored in the elements array.
277
+ #
278
+ # @param [Hash] opts Options used to define the element
279
+ # @option opts [String, Symbol] :name The element name
280
+ # @option opts [String, Symbol] :type The optional type
281
+ #
282
+ # @yield [WSDSL::Response::Element] the newly created element
283
+ # @example create an element called 'my_stats'.
284
+ # service.response do |response|
285
+ # response.element(:name => "my_stats", :type => 'Leaderboard')
286
+ # end
287
+ #
288
+ # @return [Array<WSDSL::Response::Element>]
289
+ # @api public
290
+ def element(opts={})
291
+ el = Element.new(opts[:name], opts[:type])
292
+ yield(el) if block_given?
293
+ @elements ||= []
294
+ @elements << el
295
+ end
296
+
297
+ end # of Vector
298
+ end # of Element
299
+
300
+ end # of Response
301
+ end
data/lib/ws_list.rb ADDED
@@ -0,0 +1,29 @@
1
+ # Wrapper module to keep track of all defined services
2
+ #
3
+ # @api public
4
+ module WSList
5
+
6
+ module_function
7
+
8
+ # Add a service to the array tracking
9
+ # the playco services
10
+ #
11
+ # @param [WSDSL] The service to add.
12
+ # @return [Array<WSDSL>] All the added services.
13
+ # @api public
14
+ def add(service)
15
+ @list ||= []
16
+ @list << service unless @list.find{|s| s.url == service.url && s.verb == service.verb}
17
+ @list
18
+ end
19
+
20
+ # Returns an array of services
21
+ #
22
+ # @return [Array<WSDSL>] All the added services.
23
+ # @api public
24
+ def all
25
+ @list || []
26
+ end
27
+
28
+ end
29
+
data/lib/wsdsl.rb ADDED
@@ -0,0 +1,332 @@
1
+ unless Object.const_defined?(:Extlib)
2
+ require File.expand_path('inflection', File.dirname(__FILE__))
3
+ end
4
+ require File.expand_path('params', File.dirname(__FILE__))
5
+ require File.expand_path('response', File.dirname(__FILE__))
6
+ require File.expand_path('documentation', File.dirname(__FILE__))
7
+ require File.expand_path('ws_list', File.dirname(__FILE__))
8
+
9
+ # WSDSL offers a web service DSL to define web services,
10
+ # their params, http verbs, formats expected as well as the documentation
11
+ # for all these aspects of a web service.
12
+ #
13
+ # This DSL is only meant to describe a web service and isn't meant to cover any type
14
+ # of implementation details. It is meant to be framework/tool agnostic.
15
+ #
16
+ # However, tools can be built around the Web Service DSL data structure to extract documentation,
17
+ # generate routing information, verify that an incoming request is valid, generate automated tests...
18
+ #
19
+ #
20
+ #
21
+ # WSDSL
22
+ # |
23
+ # |__ service options (name, url, SSL, auth required formats, verbs, controller name, action, version)
24
+ # |__ defined_params (instance of WSDSL::Params)
25
+ # | | | |_ Optional param rules
26
+ # | | |_ Required param rules
27
+ # | |_ Namespaced params (array containing nested optional and required rules)
28
+ # |__ response (instance of WSDSL::Response)
29
+ # | |_ elements (array of elements with each element having a name, type, attributes and vectors
30
+ # | | |_ attributes (array of WSDSL::Response::Attribute, each attribute has a name, a type, a doc and some extra options)
31
+ # | |_ vectors (array of WSDSL::Response::Vector), each vector has a name, obj_type, & an array of attributes
32
+ # | |_ attributes (array of WSDSL::Response::Attribute, each attribute has a name, a type and a doc)
33
+ # |__ doc (instance of WSDSL::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
+ #
38
+ #
39
+ #
40
+ # |_ response (instance of Documentation.new)
41
+ # |_ elements (array of instances of WSDSL::Documentation::ElementDoc, each element has a name and a list of attributes)
42
+ # |_ attributes (Hash with the key being the attribute name and the value being the attribute's documentation)
43
+ #
44
+ # @since 0.1
45
+ # @api public
46
+ class WSDSL
47
+
48
+ # Returns the service url
49
+ #
50
+ # @return [String] The service url
51
+ # @api public
52
+ attr_reader :url
53
+
54
+ # List of all the service params
55
+ #
56
+ # @return [Array<WSDSL::Params>]
57
+ # @api public
58
+ attr_reader :defined_params
59
+
60
+ # Documentation instance containing all the service doc
61
+ #
62
+ # @return [WSDSL::Documentation]
63
+ # @api public
64
+ attr_reader :doc
65
+
66
+ # The HTTP verb supported
67
+ #
68
+ # @return [Symbol]
69
+ # @api public
70
+ attr_reader :verb
71
+
72
+ # Service's version
73
+ #
74
+ # @return [String]
75
+ # @api public
76
+ attr_reader :version
77
+
78
+ # Controller instance associated with the service
79
+ #
80
+ # @return [WSController]
81
+ # @api public
82
+ attr_reader :controller
83
+
84
+ # Name of the controller action associated with the service
85
+ #
86
+ # @return [String]
87
+ # @api public
88
+ attr_reader :action
89
+
90
+ # Name of the controller associated with the service
91
+ #
92
+ # @return [String]
93
+ # @api public
94
+ attr_reader :controller_name
95
+
96
+ # Name of the service
97
+ #
98
+ # @return [String]
99
+ # @api public
100
+ attr_reader :name
101
+
102
+ # Is SSL required?
103
+ #
104
+ # @return [Boolean]
105
+ # @api public
106
+ attr_reader :ssl
107
+
108
+ # Is authentication required?
109
+ #
110
+ # @return [Boolean]
111
+ # @api public
112
+ attr_reader :auth_required
113
+
114
+
115
+ # Service constructor which is usually used via {Kernel#describe_service}
116
+ #
117
+ # @param [String] url Service's url
118
+ # @see #describe_service See how this class is usually initialized using `describe_service`
119
+ # @api public
120
+ def initialize(url)
121
+ @url = url
122
+ @defined_params = WSDSL::Params.new
123
+ @doc = WSDSL::Documentation.new
124
+ @response = WSDSL::Response.new
125
+ @name = extract_service_root_name(url)
126
+ @controller_name = "#{name.classify}Controller"
127
+ @action = extract_service_action(url)
128
+ @verb = :get
129
+ @formats = []
130
+ @version = '0.1'
131
+ @ssl = false
132
+ @auth_required = true
133
+ end
134
+
135
+ # Offers a way to dispatch the service at runtime
136
+ # Basically, it dispatches the request to the defined controller/action
137
+ # The full request cycle looks like that:
138
+ # client -> webserver -> rack -> env -> [service dispatcher] -> controller action -> rack -> webserver -> client
139
+ # @param [Object] app Reference object such as a Sinatra::Application to be passed to the controller.
140
+ #
141
+ # @return [#to_s] The response from the controller action
142
+ # @api private
143
+ def controller_dispatch(app)
144
+ unless @controller
145
+ if Object.const_defined?(@controller_name)
146
+ @controller = Object.const_get(@controller_name)
147
+ else
148
+ raise "The #{@controller_name} class was not found"
149
+ end
150
+ end
151
+ # We are passing the service object to the controller so the
152
+ # param verification could be done when the controller gets initialized.
153
+ @controller.new(app, self).send(@action)
154
+ end
155
+
156
+ # Returns the defined params
157
+ # for DSL use only!
158
+ # To keep the distinction between the request params and the service params
159
+ # using the +defined_params+ accessor is recommended.
160
+ # @see WSDSL::Params
161
+ #
162
+ # @return [WSDSL::Params] The defined params
163
+ # @api public
164
+ def params
165
+ if block_given?
166
+ yield(@defined_params)
167
+ else
168
+ @defined_params
169
+ end
170
+ end
171
+ alias :param :params
172
+
173
+ # Returns an array of required param rules
174
+ #
175
+ # @return [Array<WSDSL::Params::Rule>] Only the required param rules
176
+ # @api public
177
+ def required_rules
178
+ @defined_params.list_required
179
+ end
180
+
181
+ # Returns an array of optional param rules
182
+ #
183
+ # @return [Array<WSDSL::Params::Rule>]Only the optional param rules
184
+ # @api public
185
+ def optional_rules
186
+ @defined_params.list_optional
187
+ end
188
+
189
+ # Returns an array of namespaced params
190
+ # @see WSDSL::Params#namespaced_params
191
+ #
192
+ # @return [Array<WSDSL::Params>] the namespaced params
193
+ # @api public
194
+ def nested_params
195
+ @defined_params.namespaced_params
196
+ end
197
+
198
+ # Mark that the service doesn't require authentication.
199
+ # Note: Authentication is turned on by default
200
+ #
201
+ # @return [Boolean]
202
+ # @api public
203
+ def disable_auth
204
+ @auth_required = false
205
+ end
206
+
207
+ # Mark that the service requires a SSL connection
208
+ #
209
+ # @return [Boolean]
210
+ # @api public
211
+ def enable_ssl
212
+ @ssl = true
213
+ end
214
+
215
+ # Mark the current service as not accepting any params.
216
+ # This is purely for expressing the developer's objective since
217
+ # by default an error is raise if no params are defined and some
218
+ # params are sent.
219
+ #
220
+ # @return [Nil]
221
+ # @api public
222
+ def accept_no_params!
223
+ # no op operation since this is the default behavior
224
+ # unless params get defined. Makes sense for documentation tho.
225
+ end
226
+
227
+ # Returns the service response
228
+ # @yield The service response object
229
+ #
230
+ # @return [WSDSL::Response]
231
+ # @api public
232
+ def response
233
+ if block_given?
234
+ yield(@response)
235
+ else
236
+ @response
237
+ end
238
+ end
239
+
240
+ # Sets or returns the supported formats
241
+ # @param [String, Symbol] f_types Format type supported, such as :xml
242
+ #
243
+ # @return [Array<Symbol>] List of supported formats
244
+ # @api public
245
+ def formats(*f_types)
246
+ f_types.each{|f| @formats << f unless @formats.include?(f) }
247
+ @formats
248
+ end
249
+
250
+ # Sets the accepted HTTP verbs or return it if nothing is passed.
251
+ #
252
+ # @return [String, Symbol]
253
+ # @api public
254
+ def http_verb(s_verb=nil)
255
+ return @verb if s_verb.nil?
256
+ @verb = s_verb.to_sym
257
+ # Depending on the service settings and url, the service action might need to be updated.
258
+ # This is how we can support restful routes where a PUT request automatically uses the update method.
259
+ update_restful_action(@verb)
260
+ @verb
261
+ end
262
+
263
+ # Yields and returns the documentation object
264
+ # @yield [WSDSL::Documentation]
265
+ #
266
+ # @return [WSDSL::Documentation] The service documentation object
267
+ # @api public
268
+ def documentation
269
+ yield(doc)
270
+ end
271
+
272
+ SERVICE_ROOT_REGEXP = /(.*?)[\/\(\.]/
273
+ SERVICE_ACTION_REGEXP = /[\/\(\.]([a-z0-9_]+)[\/\(\.\?]/i
274
+ SERVICE_RESTFUL_SHOW_REGEXP = /\/:[a-z0-9_]+\.\w{3}$/
275
+
276
+ private
277
+
278
+ # extracts the service root name out of the url using a regexp
279
+ def extract_service_root_name(url)
280
+ url[SERVICE_ROOT_REGEXP, 1] || url
281
+ end
282
+
283
+ # extracts the action name out of the url using a regexp
284
+ # Defaults to the list action
285
+ def extract_service_action(url)
286
+ if url =~ SERVICE_RESTFUL_SHOW_REGEXP
287
+ 'show'
288
+ else
289
+ url[SERVICE_ACTION_REGEXP, 1] || 'list'
290
+ end
291
+ end
292
+
293
+ # Check if we need to use a restful route in which case we need
294
+ # to update the service action
295
+ def update_restful_action(verb)
296
+ if verb != :get && @action && @action == 'list'
297
+ case verb
298
+ when :post
299
+ @action = 'create'
300
+ when :put
301
+ @action = 'update'
302
+ when :delete
303
+ @action = 'destroy'
304
+ end
305
+ end
306
+ end
307
+
308
+ end
309
+
310
+ # Extending the top level module to add some helpers
311
+ #
312
+ # @api public
313
+ module Kernel
314
+
315
+ # Base DSL method called to describe a service
316
+ #
317
+ # @param [String] url The url of the service to add.
318
+ # @yield [WSDSL] The newly created service.
319
+ # @return [Array] The services already defined
320
+ # @example Describing a basic service
321
+ # describe_service "hello-world.xml" do |service|
322
+ # # describe the service
323
+ # end
324
+ #
325
+ # @api public
326
+ def describe_service(url, &block)
327
+ service = WSDSL.new(url)
328
+ yield service
329
+ WSList.add(service)
330
+ end
331
+
332
+ end