wsdsl 0.0.1

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/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