playerconnect-wsdsl 0.2.2

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,48 @@
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 [WSDSL] The service to add.
14
+ # @return [Array<WSDSL>] 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<WSDSL>] 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 [WSDSL] The found service.
35
+ #
36
+ # @api public
37
+ def self.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
+
47
+ end
48
+
data/lib/wsdsl.rb ADDED
@@ -0,0 +1,359 @@
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
+ # WSDSL 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
+ # WSDSL
20
+ # |
21
+ # |__ service options (name, url, SSL, auth required formats, verbs, controller name, action, version, extra)
22
+ # |__ defined_params (instance of WSDSL::Params)
23
+ # | | | |_ Optional param rules
24
+ # | | |_ Required param rules
25
+ # | |_ Namespaced params (array containing nested optional and required rules)
26
+ # |__ response (instance of WSDSL::Response)
27
+ # | |_ elements (array of elements with each element having a name, type, attributes and vectors
28
+ # | | |_ attributes (array of WSDSL::Response::Attribute, each attribute has a name, a type, a doc and some extra options)
29
+ # | |_ vectors (array of WSDSL::Response::Vector), each vector has a name, obj_type, & an array of attributes
30
+ # | |_ attributes (array of WSDSL::Response::Attribute, each attribute has a name, a type and a doc)
31
+ # |__ doc (instance of WSDSL::Documentation)
32
+ # | | | |_ overal) description
33
+ # | | |_ examples (array of examples as strings)
34
+ # | |_ params documentation (Hash with the key being the param name and the value being the param documentation)
35
+ # |_ response (instance of Documentation.new)
36
+ # |_ elements (array of instances of WSDSL::Documentation::ElementDoc, each element has a name and a list of attributes)
37
+ # |_ attributes (Hash with the key being the attribute name and the value being the attribute's documentation)
38
+ #
39
+ # @since 0.0.3
40
+ # @api public
41
+ class WSDSL
42
+
43
+ # Returns the service url
44
+ #
45
+ # @return [String] The service url
46
+ # @api public
47
+ attr_reader :url
48
+
49
+ # List of all the service params
50
+ #
51
+ # @return [Array<WSDSL::Params>]
52
+ # @api public
53
+ attr_reader :defined_params
54
+
55
+ # Documentation instance containing all the service doc
56
+ #
57
+ # @return [WSDSL::Documentation]
58
+ # @api public
59
+ attr_reader :doc
60
+
61
+ # The HTTP verb supported
62
+ #
63
+ # @return [Symbol]
64
+ # @api public
65
+ attr_reader :verb
66
+
67
+ # Service's version
68
+ #
69
+ # @return [String]
70
+ # @api public
71
+ attr_reader :version
72
+
73
+ # Controller instance associated with the service
74
+ #
75
+ # @return [WSController]
76
+ # @api public
77
+ attr_reader :controller
78
+
79
+ # Name of the controller action associated with the service
80
+ #
81
+ # @return [String]
82
+ # @api public
83
+ attr_accessor :action
84
+
85
+ # Name of the controller associated with the service
86
+ #
87
+ # @return [String]
88
+ # @api public
89
+ attr_accessor :controller_name
90
+
91
+ # Name of the service
92
+ #
93
+ # @return [String]
94
+ # @api public
95
+ attr_reader :name
96
+
97
+ # Is SSL required?
98
+ #
99
+ # @return [Boolean]
100
+ # @api public
101
+ attr_reader :ssl
102
+
103
+ # Is authentication required?
104
+ #
105
+ # @return [Boolean]
106
+ # @api public
107
+ attr_reader :auth_required
108
+
109
+ # Extra placeholder to store data in based on developer's discretion.
110
+ #
111
+ # @return [Hash] A hash storing extra data based.
112
+ # @api public
113
+ # @since 0.1
114
+ attr_reader :extra
115
+
116
+ # Service constructor which is usually used via {Kernel#describe_service}
117
+ #
118
+ # @param [String] url Service's url
119
+ # @see #describe_service See how this class is usually initialized using `describe_service`
120
+ # @api public
121
+ def initialize(url)
122
+ @url = url
123
+ @defined_params = WSDSL::Params.new
124
+ @doc = WSDSL::Documentation.new
125
+ @response = WSDSL::Response.new
126
+ @name = extract_service_root_name(url)
127
+ if WSDSL.use_pluralized_controllers
128
+ base_name = ExtlibCopy::Inflection.pluralize(ExtlibCopy::Inflection.singular(name))
129
+ @controller_name = "#{ExtlibCopy.classify(base_name)}Controller"
130
+ else
131
+ @controller_name = "#{ExtlibCopy.classify(name)}Controller"
132
+ end
133
+ @action = extract_service_action(url)
134
+ @verb = :get
135
+ @formats = []
136
+ @version = '0.1'
137
+ @ssl = false
138
+ @auth_required = true
139
+ @extra = {}
140
+ end
141
+
142
+ # Checks the WSDSL flag to see if the controller names are pluralized.
143
+ #
144
+ # @return [Boolean] The updated value, default to false
145
+ # @api public
146
+ # @since 0.1.1
147
+ def self.use_pluralized_controllers
148
+ @pluralized_controllers ||= false
149
+ end
150
+
151
+ # Sets a WSDSL global flag so all controller names will be automatically pluralized.
152
+ #
153
+ # @param [Boolean] True if the controllers are pluralized, False otherwise.
154
+ #
155
+ # @return [Boolean] The updated value
156
+ # @api public
157
+ # @since 0.1.1
158
+ def self.use_pluralized_controllers=(val)
159
+ @pluralized_controllers = val
160
+ end
161
+
162
+ # Offers a way to dispatch the service at runtime
163
+ # Basically, it dispatches the request to the defined controller/action
164
+ # The full request cycle looks like that:
165
+ # client -> webserver -> rack -> env -> [service dispatcher] -> controller action -> rack -> webserver -> client
166
+ # @param [Object] app Reference object such as a Sinatra::Application to be passed to the controller.
167
+ #
168
+ # @return [#to_s] The response from the controller action
169
+ # @api private
170
+ def controller_dispatch(app)
171
+ unless @controller
172
+ if Object.const_defined?(@controller_name)
173
+ @controller = Object.const_get(@controller_name)
174
+ else
175
+ raise "The #{@controller_name} class was not found"
176
+ end
177
+ end
178
+ # We are passing the service object to the controller so the
179
+ # param verification could be done when the controller gets initialized.
180
+ @controller.new(app, self).send(@action)
181
+ end
182
+
183
+ # Returns the defined params
184
+ # for DSL use only!
185
+ # To keep the distinction between the request params and the service params
186
+ # using the +defined_params+ accessor is recommended.
187
+ # @see WSDSL::Params
188
+ #
189
+ # @return [WSDSL::Params] The defined params
190
+ # @api public
191
+ def params
192
+ if block_given?
193
+ yield(@defined_params)
194
+ else
195
+ @defined_params
196
+ end
197
+ end
198
+ alias :param :params
199
+
200
+ # Returns an array of required param rules
201
+ #
202
+ # @return [Array<WSDSL::Params::Rule>] Only the required param rules
203
+ # @api public
204
+ def required_rules
205
+ @defined_params.list_required
206
+ end
207
+
208
+ # Returns an array of optional param rules
209
+ #
210
+ # @return [Array<WSDSL::Params::Rule>]Only the optional param rules
211
+ # @api public
212
+ def optional_rules
213
+ @defined_params.list_optional
214
+ end
215
+
216
+ # Returns an array of namespaced params
217
+ # @see WSDSL::Params#namespaced_params
218
+ #
219
+ # @return [Array<WSDSL::Params>] the namespaced params
220
+ # @api public
221
+ def nested_params
222
+ @defined_params.namespaced_params
223
+ end
224
+
225
+ # Mark that the service doesn't require authentication.
226
+ # Note: Authentication is turned on by default
227
+ #
228
+ # @return [Boolean]
229
+ # @api public
230
+ def disable_auth
231
+ @auth_required = false
232
+ end
233
+
234
+ # Mark that the service requires a SSL connection
235
+ #
236
+ # @return [Boolean]
237
+ # @api public
238
+ def enable_ssl
239
+ @ssl = true
240
+ end
241
+
242
+ # Mark the current service as not accepting any params.
243
+ # This is purely for expressing the developer's objective since
244
+ # by default an error is raise if no params are defined and some
245
+ # params are sent.
246
+ #
247
+ # @return [Nil]
248
+ # @api public
249
+ def accept_no_params!
250
+ # no op operation since this is the default behavior
251
+ # unless params get defined. Makes sense for documentation tho.
252
+ end
253
+
254
+ # Returns the service response
255
+ # @yield The service response object
256
+ #
257
+ # @return [WSDSL::Response]
258
+ # @api public
259
+ def response
260
+ if block_given?
261
+ yield(@response)
262
+ else
263
+ @response
264
+ end
265
+ end
266
+
267
+ # Sets or returns the supported formats
268
+ # @param [String, Symbol] f_types Format type supported, such as :xml
269
+ #
270
+ # @return [Array<Symbol>] List of supported formats
271
+ # @api public
272
+ def formats(*f_types)
273
+ f_types.each{|f| @formats << f unless @formats.include?(f) }
274
+ @formats
275
+ end
276
+
277
+ # Sets the accepted HTTP verbs or return it if nothing is passed.
278
+ #
279
+ # @return [String, Symbol]
280
+ # @api public
281
+ def http_verb(s_verb=nil)
282
+ return @verb if s_verb.nil?
283
+ @verb = s_verb.to_sym
284
+ # Depending on the service settings and url, the service action might need to be updated.
285
+ # This is how we can support restful routes where a PUT request automatically uses the update method.
286
+ update_restful_action(@verb)
287
+ @verb
288
+ end
289
+
290
+ # Yields and returns the documentation object
291
+ # @yield [WSDSL::Documentation]
292
+ #
293
+ # @return [WSDSL::Documentation] The service documentation object
294
+ # @api public
295
+ def documentation
296
+ yield(doc)
297
+ end
298
+
299
+ SERVICE_ROOT_REGEXP = /(.*?)[\/\(\.]/
300
+ SERVICE_ACTION_REGEXP = /[\/\(\.]([a-z0-9_]+)[\/\(\.\?]/i
301
+ SERVICE_RESTFUL_SHOW_REGEXP = /\/:[a-z0-9_]+\.\w{3}$/
302
+
303
+ private
304
+
305
+ # extracts the service root name out of the url using a regexp
306
+ def extract_service_root_name(url)
307
+ url[SERVICE_ROOT_REGEXP, 1] || url
308
+ end
309
+
310
+ # extracts the action name out of the url using a regexp
311
+ # Defaults to the list action
312
+ def extract_service_action(url)
313
+ if url =~ SERVICE_RESTFUL_SHOW_REGEXP
314
+ 'show'
315
+ else
316
+ url[SERVICE_ACTION_REGEXP, 1] || 'list'
317
+ end
318
+ end
319
+
320
+ # Check if we need to use a restful route in which case we need
321
+ # to update the service action
322
+ def update_restful_action(verb)
323
+ if verb != :get && @action && %w{list show}.include?(@action)
324
+ case verb
325
+ when :post
326
+ @action = 'create'
327
+ when :put
328
+ @action = 'update'
329
+ when :delete
330
+ @action = 'destroy'
331
+ end
332
+ end
333
+ end
334
+
335
+ end
336
+
337
+ # Extending the top level module to add some helpers
338
+ #
339
+ # @api public
340
+ module Kernel
341
+
342
+ # Base DSL method called to describe a service
343
+ #
344
+ # @param [String] url The url of the service to add.
345
+ # @yield [WSDSL] The newly created service.
346
+ # @return [Array] The services already defined
347
+ # @example Describing a basic service
348
+ # describe_service "hello-world.xml" do |service|
349
+ # # describe the service
350
+ # end
351
+ #
352
+ # @api public
353
+ def describe_service(url, &block)
354
+ service = WSDSL.new(url)
355
+ yield service
356
+ WSList.add(service)
357
+ end
358
+
359
+ end