smooth_operator 0.4.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +9 -9
  2. data/.gitignore +2 -1
  3. data/.rspec +4 -0
  4. data/Gemfile +13 -0
  5. data/README.md +258 -10
  6. data/console.rb +44 -0
  7. data/lib/smooth_operator/array_with_meta_data.rb +31 -0
  8. data/lib/smooth_operator/attribute_assignment.rb +102 -0
  9. data/lib/smooth_operator/attribute_methods.rb +87 -0
  10. data/lib/smooth_operator/attributes/base.rb +107 -0
  11. data/lib/smooth_operator/attributes/dirty.rb +29 -0
  12. data/lib/smooth_operator/attributes/normal.rb +15 -0
  13. data/lib/smooth_operator/delegation.rb +60 -0
  14. data/lib/smooth_operator/finder_methods.rb +43 -0
  15. data/lib/smooth_operator/helpers.rb +79 -0
  16. data/lib/smooth_operator/model_schema.rb +81 -0
  17. data/lib/smooth_operator/open_struct.rb +37 -0
  18. data/lib/smooth_operator/operator.rb +145 -0
  19. data/lib/smooth_operator/operators/faraday.rb +75 -0
  20. data/lib/smooth_operator/operators/typhoeus.rb +77 -0
  21. data/lib/smooth_operator/persistence.rb +144 -0
  22. data/lib/smooth_operator/relation/array_relation.rb +13 -0
  23. data/lib/smooth_operator/relation/association_reflection.rb +75 -0
  24. data/lib/smooth_operator/relation/associations.rb +75 -0
  25. data/lib/smooth_operator/relation/reflection.rb +41 -0
  26. data/lib/smooth_operator/relation/single_relation.rb +14 -0
  27. data/lib/smooth_operator/remote_call/base.rb +80 -0
  28. data/lib/smooth_operator/remote_call/errors/connection_failed.rb +20 -0
  29. data/lib/smooth_operator/remote_call/errors/timeout.rb +20 -0
  30. data/lib/smooth_operator/remote_call/faraday.rb +19 -0
  31. data/lib/smooth_operator/remote_call/typhoeus.rb +19 -0
  32. data/lib/smooth_operator/serialization.rb +79 -0
  33. data/lib/smooth_operator/translation.rb +27 -0
  34. data/lib/smooth_operator/validations.rb +15 -0
  35. data/lib/smooth_operator/version.rb +1 -1
  36. data/lib/smooth_operator.rb +26 -5
  37. data/smooth_operator.gemspec +12 -3
  38. data/spec/factories/user_factory.rb +34 -0
  39. data/spec/require_helper.rb +11 -0
  40. data/spec/smooth_operator/attribute_assignment_spec.rb +351 -0
  41. data/spec/smooth_operator/attributes_dirty_spec.rb +53 -0
  42. data/spec/smooth_operator/delegation_spec.rb +139 -0
  43. data/spec/smooth_operator/finder_methods_spec.rb +105 -0
  44. data/spec/smooth_operator/model_schema_spec.rb +31 -0
  45. data/spec/smooth_operator/operator_spec.rb +46 -0
  46. data/spec/smooth_operator/persistence_spec.rb +424 -0
  47. data/spec/smooth_operator/remote_call_spec.rb +320 -0
  48. data/spec/smooth_operator/serialization_spec.rb +80 -0
  49. data/spec/smooth_operator/validations_spec.rb +42 -0
  50. data/spec/spec_helper.rb +25 -0
  51. data/spec/support/helpers/persistence_helper.rb +38 -0
  52. data/spec/support/localhost_server.rb +97 -0
  53. data/spec/support/models/address.rb +14 -0
  54. data/spec/support/models/comment.rb +3 -0
  55. data/spec/support/models/post.rb +13 -0
  56. data/spec/support/models/user.rb +41 -0
  57. data/spec/support/models/user_with_address_and_posts.rb +89 -0
  58. data/spec/support/test_server.rb +165 -0
  59. metadata +108 -18
  60. data/lib/smooth_operator/base.rb +0 -30
  61. data/lib/smooth_operator/core.rb +0 -218
  62. data/lib/smooth_operator/http_handlers/typhoeus/base.rb +0 -58
  63. data/lib/smooth_operator/http_handlers/typhoeus/orm.rb +0 -34
  64. data/lib/smooth_operator/http_handlers/typhoeus/remote_call.rb +0 -28
  65. data/lib/smooth_operator/operator/base.rb +0 -43
  66. data/lib/smooth_operator/operator/exceptions.rb +0 -64
  67. data/lib/smooth_operator/operator/orm.rb +0 -118
  68. data/lib/smooth_operator/operator/remote_call.rb +0 -84
@@ -0,0 +1,77 @@
1
+ require 'typhoeus'
2
+ require "smooth_operator/remote_call/typhoeus"
3
+
4
+ module SmoothOperator
5
+
6
+ module Operators
7
+
8
+ module Typhoeus
9
+
10
+ extend self
11
+
12
+ def make_the_call(http_verb, resource_path, params, body, options)
13
+ request = ::Typhoeus::Request.new *typhoeus_request_args(http_verb, resource_path, params, body, options)
14
+
15
+ hydra = options[:hydra] || ::Typhoeus::Hydra::hydra
16
+
17
+ _remote_call = nil
18
+
19
+ hydra.queue(request)
20
+
21
+ request.on_complete do |typhoeus_response|
22
+ _remote_call = remote_call(typhoeus_response)
23
+
24
+ yield(_remote_call) if block_given?
25
+ end
26
+
27
+ hydra.run if Helpers.blank?(options[:hydra])
28
+
29
+ _remote_call
30
+ end
31
+
32
+
33
+ protected ################ PROTECTED ################
34
+
35
+ def typhoeus_request_args(http_verb, relative_path, params, body, options)
36
+ [url(options, relative_path), build_typhoeus_options(http_verb, params, body, options)]
37
+ end
38
+
39
+ def remote_call(typhoeus_response)
40
+ if typhoeus_response.return_code == :couldnt_connect
41
+ RemoteCall::Errors::ConnectionFailed
42
+ elsif typhoeus_response.timed_out?
43
+ RemoteCall::Errors::Timeout
44
+ else
45
+ RemoteCall::Typhoeus
46
+ end.new(typhoeus_response)
47
+ end
48
+
49
+
50
+ private ################### PRIVATE ###############
51
+
52
+ def build_typhoeus_options(http_verb, params, body, options)
53
+ typhoeus_options = { method: http_verb, headers: options[:headers].merge({ "Content-type" => "application/x-www-form-urlencoded" }) }
54
+
55
+ typhoeus_options[:timeout] = options[:timeout] if Helpers.present?(options[:timeout])
56
+
57
+ typhoeus_options[:body] = body if Helpers.present?(body)
58
+
59
+ typhoeus_options[:params] = params if Helpers.present?(params)
60
+
61
+ typhoeus_options[:userpwd] = "#{options[:endpoint_user]}:#{options[:endpoint_pass]}" if Helpers.present?(options[:endpoint_user])
62
+
63
+ typhoeus_options
64
+ end
65
+
66
+ def url(options, relative_path)
67
+ url = options[:endpoint]
68
+
69
+ slice = url[-1] != '/' ? '/' : ''
70
+
71
+ url = "#{url}#{slice}#{relative_path}" if Helpers.present?(relative_path)
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,144 @@
1
+ module SmoothOperator
2
+ module Persistence
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ attr_reader :last_remote_call
9
+
10
+ def get_primary_key
11
+ get_internal_data(self.class.primary_key)
12
+ end
13
+
14
+ def reload(relative_path = nil, data = {}, options = {})
15
+ raise 'UnknownPath' if Helpers.blank?(relative_path) && (!respond_to?(self.class.primary_key) || Helpers.blank?(get_primary_key))
16
+
17
+ persistence_call(:reload, relative_path, data, options) do |remote_call|
18
+ block_given? ? yield(remote_call) : remote_call.status
19
+ end
20
+ end
21
+
22
+ def new_record?(bypass_cache = false)
23
+ return @new_record if !bypass_cache && defined?(@new_record)
24
+
25
+ @new_record = Helpers.blank?(get_primary_key)
26
+ end
27
+
28
+ def marked_for_destruction?(bypass_cache = false)
29
+ return @marked_for_destruction if !bypass_cache && defined?(@marked_for_destruction)
30
+
31
+ @marked_for_destruction = ["true", "1", true].include?(get_internal_data(self.class.destroy_key))
32
+ end
33
+
34
+ def destroyed?
35
+ return @destroyed if defined?(@destroyed)
36
+
37
+ @destroyed = false
38
+ end
39
+
40
+ def persisted?
41
+ !(new_record? || destroyed?)
42
+ end
43
+
44
+ def save(relative_path = nil, data = {}, options = {})
45
+ data = data_with_object_attributes(data, options)
46
+
47
+ if new_record?
48
+ create(relative_path, data, options) { |remote_call| block_given? ? yield(remote_call) : remote_call.status }
49
+ else
50
+ update(relative_path, data, options) { |remote_call| block_given? ? yield(remote_call) : remote_call.status }
51
+ end
52
+ end
53
+
54
+ def save!(relative_path = nil, data = {}, options = {})
55
+ save(relative_path, data, options) do |remote_call|
56
+ block_given? ? yield(remote_call) : remote_call.status
57
+ end || raise('RecordNotSaved')
58
+ end
59
+
60
+ def destroy(relative_path = nil, data = {}, options = {})
61
+ return false unless persisted?
62
+
63
+ persistence_call(:destroy, relative_path, data, options) do |remote_call|
64
+ @destroyed = true if remote_call.status
65
+
66
+ block_given? ? yield(remote_call) : remote_call.status
67
+ end
68
+ end
69
+
70
+
71
+ protected ######################### PROTECTED ##################
72
+
73
+ def create(relative_path, data, options)
74
+ persistence_call(:create, relative_path, data, options) do |remote_call|
75
+ @new_record = false if remote_call.status
76
+
77
+ block_given? ? yield(remote_call) : remote_call
78
+ end
79
+ end
80
+
81
+ def update(relative_path, data, options)
82
+ persistence_call(:update, relative_path, data, options) do |remote_call|
83
+ block_given? ? yield(remote_call) : remote_call
84
+ end
85
+ end
86
+
87
+ def persistence_call(method, relative_path, data, options)
88
+ options ||= {}
89
+
90
+ http_verb = options[:http_verb] || self.class.methods_vs_http_verbs[method]
91
+
92
+ make_the_call(http_verb, relative_path, data, options) do |remote_call|
93
+ @last_remote_call = remote_call
94
+
95
+ if !@last_remote_call.error? && @last_remote_call.parsed_response.is_a?(Hash)
96
+ assign_attributes @last_remote_call.parsed_response, from_server: true
97
+ end
98
+
99
+ yield(remote_call)
100
+ end
101
+ end
102
+
103
+ def data_with_object_attributes(data, options)
104
+ data = Helpers.stringify_keys(data)
105
+
106
+ hash = serializable_hash(options[:serializable_options]).dup
107
+
108
+ hash.delete(self.class.primary_key)
109
+
110
+ { self.class.resource_name => hash }.merge(data)
111
+ end
112
+
113
+
114
+ module ClassMethods
115
+
116
+ METHODS_VS_HTTP_VERBS = { reload: :get, create: :post, update: :put, destroy: :delete }
117
+
118
+ def methods_vs_http_verbs
119
+ Helpers.get_instance_variable(self, :methods_vs_http_verbs, METHODS_VS_HTTP_VERBS.dup)
120
+ end
121
+
122
+ def primary_key
123
+ Helpers.get_instance_variable(self, :primary_key, 'id')
124
+ end
125
+
126
+ attr_writer :primary_key
127
+
128
+ def destroy_key
129
+ Helpers.get_instance_variable(self, :destroy_key, '_destroy')
130
+ end
131
+
132
+ attr_writer :destroy_key
133
+
134
+ METHODS_VS_HTTP_VERBS.keys.each do |method|
135
+ define_method("#{method}_http_verb=") { |http_verb| methods_vs_http_verbs[method] = http_verb }
136
+ end
137
+
138
+ def create(attributes = nil, relative_path = nil, data = {}, options = {})
139
+ new(attributes).tap { |object| object.save(relative_path, data, options) }
140
+ end
141
+
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,13 @@
1
+ require "smooth_operator/relation/single_relation"
2
+
3
+ module SmoothOperator
4
+ module Relation
5
+ class ArrayRelation < SingleRelation
6
+
7
+ def reload2
8
+ "TODO2"
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,75 @@
1
+ require "smooth_operator/relation/reflection"
2
+
3
+ module SmoothOperator
4
+ module Relation
5
+ class AssociationReflection < Reflection
6
+
7
+ attr_reader :related_reflection, :macro
8
+
9
+ def initialize(association, related_reflection, options)
10
+ super(association, options)
11
+ @macro = options[:macro] || macro_default(association)
12
+ @related_reflection = related_reflection
13
+ end
14
+
15
+ def primary_key
16
+ @primary_key ||= options[:primary_key] || :id
17
+ end
18
+
19
+ def foreign_key
20
+ @foreign_key ||= options[:foreign_key] || foreign_key_default
21
+ end
22
+
23
+ def set_relational_keys(origin, destination)
24
+ return nil if options[:standalone] == true
25
+
26
+ if has_many? || has_one?
27
+ set_foreign_key(destination, primary_key_of(origin))
28
+ elsif belongs_to?
29
+ set_foreign_key(origin, primary_key_of(destination))
30
+ end
31
+ end
32
+
33
+ def set_foreign_key(object, id)
34
+ setter = "#{foreign_key}="
35
+
36
+ if object.respond_to?(setter)
37
+ object.send(setter, id)
38
+ elsif object.respond_to?("send_to_representative")
39
+ object.send_to_representative(setter, id)
40
+ end
41
+ end
42
+
43
+ def primary_key_of(object)
44
+ object.send(primary_key)
45
+ end
46
+
47
+ def has_many?
48
+ macro == :has_many
49
+ end
50
+
51
+ def has_one?
52
+ macro == :has_one
53
+ end
54
+
55
+ def belongs_to?
56
+ macro == :belongs_to
57
+ end
58
+
59
+ private ################################# private
60
+
61
+ def macro_default(association)
62
+ Helpers.plural?(association) ? :has_many : :belongs_to
63
+ end
64
+
65
+ def foreign_key_default
66
+ if has_many? || has_one?
67
+ "#{related_reflection.single_name}_id"
68
+ elsif belongs_to?
69
+ "#{single_name}_id"
70
+ end.to_sym
71
+ end
72
+
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,75 @@
1
+ require "smooth_operator/relation/array_relation"
2
+ require "smooth_operator/relation/association_reflection"
3
+
4
+ module SmoothOperator
5
+ module Relation
6
+ module Associations
7
+
8
+ def relations
9
+ @relations ||= {}
10
+ end
11
+
12
+ def get_relation(relation_name)
13
+ relations[relation_name] ||= self.class.build_relation(relation_name, get_internal_data(relation_name))
14
+ end
15
+
16
+ def self.included(base)
17
+ base.extend(ClassMethods)
18
+ end
19
+
20
+ module ClassMethods
21
+
22
+ def has_many(nested_object_name, options = {})
23
+ accepts_nested_objects(nested_object_name, :has_many, options)
24
+ end
25
+
26
+ def has_one(nested_object_name, options = {})
27
+ accepts_nested_objects(nested_object_name, :has_one, options)
28
+ end
29
+
30
+ def belongs_to(nested_object_name, options = {})
31
+ accepts_nested_objects(nested_object_name, :belongs_to, options)
32
+ end
33
+
34
+ def reflections
35
+ Helpers.get_instance_variable(self, :reflections, {})
36
+ end
37
+
38
+ def reflect_on_association(association)
39
+ reflections[association]
40
+ end
41
+
42
+ def reflect_on_all_associations(macro = nil)
43
+ macro ? reflections.values.select { |reflection| reflection.macro == macro } : reflections.values
44
+ end
45
+
46
+ def build_relation(relation_name, data)
47
+ if reflections[relation_name.to_sym].has_many?
48
+ ArrayRelation.new(data || [], relation_name)
49
+ else
50
+ SingleRelation.new(data, relation_name)
51
+ end
52
+ end
53
+
54
+ protected ###################### PROTECTED ###################
55
+
56
+ def accepts_nested_objects(nested_object_name, macro, options = {})
57
+ default_options = { macro: macro }
58
+ options = options.is_a?(Hash) ? options.merge(default_options) : default_options
59
+ options = Helpers.symbolyze_keys(options)
60
+
61
+ reflection = AssociationReflection.new(nested_object_name, Reflection.new(name, {}), options)
62
+
63
+ self.send(:attr_accessor, "#{nested_object_name}_attributes".to_sym)
64
+ self.instance_variable_set("@reflections", reflections.merge(nested_object_name => reflection))
65
+
66
+ define_method("existing_#{nested_object_name}") { existing_nested_objects(nested_object_name) }
67
+ define_method("build_#{reflection.single_name}") { |attributes = {}, nested_object = nil| build_nested_object(nested_object_name, attributes, nested_object) }
68
+
69
+ schema(nested_object_name => reflection.klass)
70
+ end
71
+
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,41 @@
1
+ module SmoothOperator
2
+ module Relation
3
+ class Reflection
4
+
5
+ attr_reader :name, :klass, :options
6
+
7
+ def initialize(class_name, options)
8
+ options = options.is_a?(Hash) ? options : {}
9
+
10
+ @name, @options = class_name, options
11
+
12
+ @klass = options[:class_name] || klass_default(@name)
13
+
14
+ if options.include?(:class_name) && options[:class_name].nil?
15
+ @klass = nil
16
+ elsif @klass.is_a?(String)
17
+ @klass = @klass.constantize
18
+ end
19
+ end
20
+
21
+ def single_name
22
+ @single_name ||= options[:single_name] || name.to_s.singularize
23
+ end
24
+
25
+ def plural_name
26
+ @plural_name ||= options[:plural_name] || name.to_s.pluralize
27
+ end
28
+
29
+ private ################################# private
30
+
31
+ def klass_default(class_name)
32
+ if Helpers.plural?(class_name)
33
+ class_name.to_s.singularize.camelize
34
+ else
35
+ class_name.to_s.camelize
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,14 @@
1
+ module SmoothOperator
2
+ module Relation
3
+ class SingleRelation < ::SimpleDelegator
4
+
5
+ attr_reader :relation_name
6
+
7
+ def initialize(object, relation_name)
8
+ @relation_name = relation_name
9
+ super(object)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,80 @@
1
+ module SmoothOperator
2
+
3
+ module RemoteCall
4
+
5
+ class Base
6
+
7
+ extend Forwardable
8
+
9
+ attr_reader :response, :http_status, :body, :headers
10
+
11
+ attr_accessor :object
12
+
13
+ def initialize(response)
14
+ @response = response
15
+ end
16
+
17
+
18
+ def ok?
19
+ http_status.between?(200, 299) || http_status == 304
20
+ end
21
+
22
+ def not_processed?
23
+ http_status == 422
24
+ end
25
+
26
+ def error?
27
+ !ok? && !not_processed?
28
+ end
29
+
30
+ def client_error?
31
+ http_status.between?(400, 499)
32
+ end
33
+
34
+ def server_error?
35
+ http_status.between?(500, 599) || http_status == 0
36
+ end
37
+
38
+
39
+ def not_found?
40
+ http_status == 404
41
+ end
42
+
43
+ def timeout?
44
+ false
45
+ end
46
+
47
+ def connection_failed?
48
+ false
49
+ end
50
+
51
+
52
+ def parsed_response
53
+ return nil if body.nil?
54
+
55
+ require 'json' unless defined? JSON
56
+
57
+ begin
58
+ JSON.parse(body)
59
+ rescue JSON::ParserError
60
+ nil
61
+ end
62
+ end
63
+
64
+ def status
65
+ error? ? nil : ok?
66
+ end
67
+
68
+ def objects
69
+ object.respond_to?(:length) ? object : []
70
+ end
71
+
72
+ def data
73
+ object.nil? ? parsed_response : object
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+
80
+ end
@@ -0,0 +1,20 @@
1
+ module SmoothOperator
2
+ module RemoteCall
3
+ module Errors
4
+
5
+ class ConnectionFailed < Base
6
+
7
+ def initialize(response)
8
+ super
9
+ @http_status = 0
10
+ end
11
+
12
+ def connection_failed?
13
+ true
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module SmoothOperator
2
+ module RemoteCall
3
+ module Errors
4
+
5
+ class Timeout < Base
6
+
7
+ def initialize(response)
8
+ super
9
+ @http_status = 0
10
+ end
11
+
12
+ def timeout?
13
+ true
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ module SmoothOperator
2
+
3
+ module RemoteCall
4
+
5
+ class Faraday < Base
6
+
7
+ def initialize(response)
8
+ @response = response
9
+
10
+ @body = response.body
11
+ @headers = response.headers
12
+ @http_status = response.status
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,19 @@
1
+ module SmoothOperator
2
+
3
+ module RemoteCall
4
+
5
+ class Typhoeus < Base
6
+
7
+ def initialize(response)
8
+ @response = response
9
+
10
+ @body = response.body
11
+ @http_status = response.code
12
+ @headers = response.headers_hash
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,79 @@
1
+ module SmoothOperator
2
+
3
+ module Serialization
4
+
5
+ def to_hash(options = nil)
6
+ Helpers.symbolyze_keys(serializable_hash(options) || {})
7
+ end
8
+
9
+ # alias :attributes :to_hash
10
+ def attributes; to_hash; end
11
+
12
+ def to_json(options = nil)
13
+ require 'json' unless defined? JSON
14
+
15
+ JSON(serializable_hash(options) || {})
16
+ end
17
+
18
+ def read_attribute_for_serialization(attribute)
19
+ send(attribute)
20
+ end
21
+
22
+ def serializable_hash(options = nil)
23
+ hash = {}
24
+ options ||= {}
25
+
26
+ attribute_names(options).each do |attribute_name|
27
+ hash[attribute_name] = read_attribute_for_hashing(attribute_name, options)
28
+ end
29
+
30
+ method_names(options).each do |method_name|
31
+ hash[method_name.to_s] = send(method_name)
32
+ end
33
+
34
+ hash
35
+ end
36
+
37
+
38
+ protected ##################### PROTECTED ###################
39
+
40
+ # TODO: COMPLEX METHOD
41
+ def attribute_names(options)
42
+ attribute_names = internal_data.keys.sort
43
+
44
+ if only = options[:only]
45
+ attribute_names &= [*only].map(&:to_s)
46
+ elsif except = options[:except]
47
+ attribute_names -= [*except].map(&:to_s)
48
+ end
49
+
50
+ attribute_names
51
+ end
52
+
53
+ def method_names(options)
54
+ [*options[:methods]].select { |n| respond_to?(n) }
55
+ end
56
+
57
+ def read_attribute_for_hashing(attribute_name, options)
58
+ object = read_attribute_for_serialization(attribute_name)
59
+
60
+ _options = options[attribute_name] || options[attribute_name.to_sym]
61
+
62
+ if object.is_a?(Array)
63
+ object.map { |array_entry| attribute_to_hash(array_entry, _options) }
64
+ else
65
+ attribute_to_hash(object, _options)
66
+ end
67
+ end
68
+
69
+ def attribute_to_hash(object, options = nil)
70
+ if object.respond_to?(:serializable_hash)
71
+ Helpers.symbolyze_keys(object.serializable_hash(options))
72
+ else
73
+ object
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ end