nxt_state_machine 0.1.0

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +67 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +348 -0
  10. data/Rakefile +6 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/lib/nxt_state_machine/callable.rb +63 -0
  14. data/lib/nxt_state_machine/callback_registry.rb +35 -0
  15. data/lib/nxt_state_machine/error_callback_registry.rb +38 -0
  16. data/lib/nxt_state_machine/errors/error.rb +1 -0
  17. data/lib/nxt_state_machine/errors/event_already_registered.rb +5 -0
  18. data/lib/nxt_state_machine/errors/event_without_transitions.rb +5 -0
  19. data/lib/nxt_state_machine/errors/initial_state_already_defined.rb +7 -0
  20. data/lib/nxt_state_machine/errors/invalid_callback_option.rb +5 -0
  21. data/lib/nxt_state_machine/errors/missing_configuration.rb +5 -0
  22. data/lib/nxt_state_machine/errors/state_already_registered.rb +5 -0
  23. data/lib/nxt_state_machine/errors/transition_already_registered.rb +5 -0
  24. data/lib/nxt_state_machine/errors/transition_halted.rb +12 -0
  25. data/lib/nxt_state_machine/errors/transition_not_defined.rb +5 -0
  26. data/lib/nxt_state_machine/errors/unknown_state_error.rb +5 -0
  27. data/lib/nxt_state_machine/event.rb +49 -0
  28. data/lib/nxt_state_machine/event_registry.rb +11 -0
  29. data/lib/nxt_state_machine/integrations/active_record.rb +77 -0
  30. data/lib/nxt_state_machine/integrations/attr_accessor.rb +69 -0
  31. data/lib/nxt_state_machine/integrations/hash.rb +67 -0
  32. data/lib/nxt_state_machine/state.rb +17 -0
  33. data/lib/nxt_state_machine/state_machine.rb +179 -0
  34. data/lib/nxt_state_machine/state_registry.rb +12 -0
  35. data/lib/nxt_state_machine/transition/around_callback_chain.rb +26 -0
  36. data/lib/nxt_state_machine/transition/proxy.rb +31 -0
  37. data/lib/nxt_state_machine/transition/store.rb +19 -0
  38. data/lib/nxt_state_machine/transition.rb +87 -0
  39. data/lib/nxt_state_machine/version.rb +3 -0
  40. data/lib/nxt_state_machine.rb +96 -0
  41. data/nxt_state_machine.gemspec +46 -0
  42. metadata +202 -0
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ StateAlreadyRegistered = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ TransitionAlreadyRegistered = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,12 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ class TransitionHalted < Error
4
+ def initialize(*args, **opts)
5
+ super(*args)
6
+ @options = opts
7
+ end
8
+
9
+ attr_reader :options
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ TransitionNotDefined = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module NxtStateMachine
2
+ module Errors
3
+ UnknownStateError = Class.new(Error)
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ module NxtStateMachine
2
+ class Event
3
+ include NxtRegistry
4
+
5
+ def initialize(name, state_machine:, &block)
6
+ @state_machine = state_machine
7
+ @name = name
8
+ @event_transitions = registry("#{name} event transitions")
9
+
10
+ configure(&block)
11
+
12
+ ensure_event_has_transitions
13
+ end
14
+
15
+ attr_reader :name, :state_machine, :event_transitions
16
+
17
+ delegate :before_transition,
18
+ :after_transition,
19
+ :around_transition,
20
+ :on_error,
21
+ :on_error!,
22
+ :any_state,
23
+ :all_states,
24
+ :all_states_except,
25
+ to: :state_machine
26
+
27
+ def transitions(from:, to:, &block)
28
+ Array(from).each do |from_state|
29
+ transition = Transition.new(name, from: from_state, to: to, state_machine: state_machine, &block)
30
+ state_machine.transitions << transition
31
+ event_transitions.register(from_state, transition)
32
+ end
33
+ end
34
+
35
+ alias_method :transition, :transitions
36
+
37
+ private
38
+
39
+ def configure(&block)
40
+ instance_exec(&block)
41
+ end
42
+
43
+ def ensure_event_has_transitions
44
+ return if event_transitions.size > 0
45
+
46
+ raise NxtStateMachine::Errors::EventWithoutTransitions, "No transitions for event :#{name} defined"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ module NxtStateMachine
2
+ class EventRegistry < NxtRegistry::Registry
3
+ def initialize
4
+ super :events do
5
+ on_key_already_registered do |key|
6
+ raise NxtStateMachine::Errors::EventAlreadyRegistered, "An event with the name '#{key}' was already registered!"
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,77 @@
1
+ module NxtStateMachine
2
+ module ActiveRecord
3
+ module ClassMethods
4
+ def state_machine(name = :default, state_attr: :state, target: nil, &config)
5
+ machine = super(
6
+ name,
7
+ state_attr: state_attr,
8
+ target: target,
9
+ &config
10
+ )
11
+
12
+ machine.get_state_with do |target|
13
+ if target
14
+ if target.send(state_attr).nil? && target.new_record?
15
+ target.assign_attributes(state_attr => machine.initial_state.to_s)
16
+ end
17
+
18
+ current_state = target.send(state_attr)
19
+ current_state&.to_sym
20
+ end
21
+ end
22
+
23
+ machine.set_state_with do |target, transition|
24
+ target.transaction do
25
+ transition.run_before_callbacks
26
+ result = set_state(target, transition, state_attr, :save)
27
+ transition.run_after_callbacks
28
+
29
+ result
30
+ end
31
+ rescue StandardError => error
32
+ target.assign_attributes(state_attr => transition.from.to_s)
33
+
34
+ if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
35
+ false
36
+ else
37
+ raise
38
+ end
39
+ end
40
+
41
+ machine.set_state_with! do |target, transition|
42
+ target.transaction do
43
+ transition.run_before_callbacks
44
+ result = set_state(target, transition, state_attr, :save!)
45
+ transition.run_after_callbacks
46
+
47
+ result
48
+ end
49
+ rescue StandardError
50
+ target.assign_attributes(state_attr => transition.from.to_s)
51
+ raise
52
+ end
53
+
54
+ machine
55
+ end
56
+ end
57
+
58
+ module InstanceMethods
59
+ private
60
+
61
+ def set_state(target, transition, state_attr, method)
62
+ transition.execute do |block|
63
+ result = block ? block.call : nil
64
+ target.assign_attributes(state_attr => transition.to.to_s)
65
+ set_state_result = target.send(method) || halt_transition
66
+ block ? result : set_state_result
67
+ end
68
+ end
69
+ end
70
+
71
+ def self.included(base)
72
+ base.include(NxtStateMachine)
73
+ base.include(InstanceMethods)
74
+ base.extend(ClassMethods)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,69 @@
1
+ module NxtStateMachine
2
+ module AttrAccessor
3
+ module ClassMethods
4
+ def state_machine(name = :default, state_attr: :state, target: nil, &config)
5
+ machine = super(
6
+ name,
7
+ state_attr: state_attr,
8
+ target: target,
9
+ &config
10
+ )
11
+
12
+ machine.get_state_with do |target|
13
+ if target.send(state_attr).nil?
14
+ target.send("#{state_attr}=", initial_state.enum)
15
+ end
16
+
17
+ target.send(state_attr)
18
+ end
19
+
20
+ machine.set_state_with do |target, transition|
21
+ transition.run_before_callbacks
22
+ result = set_state(target, transition, state_attr)
23
+ transition.run_after_callbacks
24
+
25
+ result
26
+ rescue StandardError => error
27
+ target.send("#{state_attr}=", transition.from.enum)
28
+
29
+ if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
30
+ false
31
+ else
32
+ raise
33
+ end
34
+ end
35
+
36
+ machine.set_state_with! do |target, transition|
37
+ transition.run_before_callbacks
38
+ result = set_state(target, transition, state_attr)
39
+ transition.run_after_callbacks
40
+
41
+ result
42
+ rescue StandardError
43
+ target.send("#{state_attr}=", transition.from.enum)
44
+ raise
45
+ end
46
+
47
+ machine
48
+ end
49
+ end
50
+
51
+ module InstanceMethods
52
+ private
53
+
54
+ def set_state(target, transition, state_attr)
55
+ transition.execute do |block|
56
+ result = block ? block.call : nil
57
+ set_state_result = target.send("#{state_attr}=", transition.to.enum)
58
+ block ? result : set_state_result
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.include(NxtStateMachine)
65
+ base.include(InstanceMethods)
66
+ base.extend(ClassMethods)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,67 @@
1
+ module NxtStateMachine
2
+ module Hash
3
+ module ClassMethods
4
+ def state_machine(name = :default, state_attr: :state, target: nil, &config)
5
+ machine = super(
6
+ name,
7
+ state_attr: state_attr,
8
+ target: target,
9
+ &config
10
+ )
11
+
12
+ machine.get_state_with do |target|
13
+ if target[state_attr].nil?
14
+ target[state_attr] = initial_state.enum
15
+ end
16
+
17
+ target[state_attr]
18
+ end
19
+
20
+ machine.set_state_with do |target, transition|
21
+ transition.run_before_callbacks
22
+ result = set_state(target, transition, state_attr)
23
+ transition.run_after_callbacks
24
+ result
25
+ rescue StandardError => error
26
+ target[state_attr] = transition.from.enum
27
+
28
+ if error.is_a?(NxtStateMachine::Errors::TransitionHalted)
29
+ false
30
+ else
31
+ raise
32
+ end
33
+ end
34
+
35
+ machine.set_state_with! do |target, transition|
36
+ transition.run_before_callbacks
37
+ result = set_state(target, transition, state_attr)
38
+ transition.run_after_callbacks
39
+
40
+ result
41
+ rescue StandardError
42
+ target[state_attr] = transition.from.enum
43
+ raise
44
+ end
45
+
46
+ machine
47
+ end
48
+ end
49
+
50
+ module InstanceMethods
51
+ private
52
+
53
+ def set_state(target, transition, state_attr)
54
+ transition.execute do |block|
55
+ result = block ? block.call : nil
56
+ set_state_result = target[state_attr] = transition.to.enum || halt_transition
57
+ block ? result : set_state_result
58
+ end
59
+ end
60
+ end
61
+
62
+ def self.included(base)
63
+ base.include(NxtStateMachine)
64
+ base.extend(ClassMethods)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,17 @@
1
+ module NxtStateMachine
2
+ class State
3
+ def initialize(enum, machine, **opts)
4
+ @enum = enum
5
+ @machine = machine
6
+ @initial = opts.delete(:initial)
7
+ @transitions = []
8
+ @options = opts.with_indifferent_access
9
+ end
10
+
11
+ attr_accessor :enum, :initial, :transitions, :machine, :options
12
+
13
+ def to_s
14
+ enum.to_s
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,179 @@
1
+ module NxtStateMachine
2
+ class StateMachine
3
+ def initialize(name, class_context, event_registry, **opts)
4
+ @name = name
5
+ @class_context = class_context
6
+ @options = opts
7
+
8
+ @states = NxtStateMachine::StateRegistry.new
9
+ @transitions = Transition::Store.new
10
+ @events = event_registry
11
+ @callbacks = CallbackRegistry.new
12
+ @error_callback_registry = ErrorCallbackRegistry.new
13
+
14
+ @initial_state = nil
15
+ end
16
+
17
+ attr_reader :class_context, :states, :transitions, :events, :options, :callbacks, :name, :error_callback_registry
18
+ attr_accessor :initial_state
19
+
20
+ def get_state_with(method = nil, &block)
21
+ method_or_block = (method || block)
22
+ @get_state_with ||= method_or_block && Callable.new(method_or_block) ||
23
+ raise_missing_configuration_error(:get_state_with)
24
+ end
25
+
26
+ def set_state_with(method = nil, &block)
27
+ method_or_block = (method || block)
28
+ @set_state_with ||= method_or_block && Callable.new(method_or_block) ||
29
+ raise_missing_configuration_error(:set_state_with)
30
+ end
31
+
32
+ def set_state_with!(method = nil, &block)
33
+ method_or_block = (method || block)
34
+ @set_state_with_bang ||= method_or_block && Callable.new(method_or_block) ||
35
+ raise_missing_configuration_error(:set_state_with!)
36
+ end
37
+
38
+ def state(*names, **opts, &block)
39
+ defaults = { initial: false }
40
+ opts.reverse_merge!(defaults)
41
+ machine = self
42
+
43
+ Array(names).map do |name|
44
+ if opts.fetch(:initial) && initial_state.present?
45
+ raise NxtStateMachine::Errors::InitialStateAlreadyDefined, ":#{initial_state.enum} was already defined as the initial state"
46
+ else
47
+ state = new_state_class(&block).new(name, self, opts)
48
+ states.register(name, state)
49
+ self.initial_state = state if opts.fetch(:initial)
50
+
51
+ class_context.define_method "#{name}?" do
52
+ # States internally are always strings
53
+ machine.current_state_name(self) == name
54
+ end
55
+
56
+ state
57
+ end
58
+ end
59
+ end
60
+
61
+ def transitions
62
+ @transitions ||= events.values.flat_map(&:event_transitions)
63
+ end
64
+
65
+ def all_transitions_from_to(from: all_states, to: all_states)
66
+ transitions.select { |transition| transition.transitions_from_to?(from, to) }
67
+ end
68
+
69
+ def any_state
70
+ states.values.map(&:enum)
71
+ end
72
+
73
+ alias_method :all_states, :any_state
74
+
75
+ def all_states_except(*excluded)
76
+ all_states - excluded
77
+ end
78
+
79
+ def event(name, &block)
80
+ event = Event.new(name, state_machine: self, &block)
81
+ events.register(name, event)
82
+
83
+ class_context.define_method name do |*args, **opts|
84
+ event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
85
+ transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
86
+ transition.prepare(name, self, :set_state_with, *args, **opts)
87
+ end
88
+
89
+ class_context.define_method "#{name}!" do |*args, **opts|
90
+ event.state_machine.can_transition!(name, event.state_machine.current_state_name(self))
91
+ transition = event.event_transitions.resolve(event.state_machine.current_state_name(self))
92
+ transition.prepare("#{name}!", self, :set_state_with!, *args, **opts)
93
+ end
94
+
95
+ class_context.define_method "can_#{name}?" do
96
+ event.state_machine.can_transition?(name, event.state_machine.current_state_name(self))
97
+ end
98
+ end
99
+
100
+ def can_transition?(event_name, from)
101
+ normalized_event_name = event_name
102
+ event = events.resolve(normalized_event_name)
103
+ event && event.event_transitions.key?(from)
104
+ end
105
+
106
+ def can_transition!(event, from)
107
+ return true if can_transition?(event, from)
108
+ raise NxtStateMachine::Errors::TransitionNotDefined, "No transition :#{event} for state :#{from} defined"
109
+ end
110
+
111
+ def before_transition(from:, to:, run: nil, &block)
112
+ callbacks.register(from, to, :before, run, block)
113
+ end
114
+
115
+ def after_transition(from:, to:, run: nil, &block)
116
+ callbacks.register(from, to, :after, run, block)
117
+ end
118
+
119
+ def on_error(error = StandardError, from:, to:, run: nil, &block)
120
+ error_callback_registry.register(from, to, error, run, block)
121
+ end
122
+
123
+ def on_error!(error = StandardError, from:, to:, run: nil, &block)
124
+ error_callback_registry.register!(from, to, error, run, block)
125
+ end
126
+
127
+ def around_transition(from:, to:, run: nil, &block)
128
+ callbacks.register(from, to, :around, run, block)
129
+ end
130
+
131
+ def configure(&block)
132
+ instance_exec(&block)
133
+ self
134
+ end
135
+
136
+ def run_before_callbacks(transition, context)
137
+ run_callbacks(transition, :before, context)
138
+ end
139
+
140
+ def run_after_callbacks(transition, context)
141
+ run_callbacks(transition, :after, context)
142
+ end
143
+
144
+ def find_error_callback(error, transition)
145
+ error_callback_registry.resolve(error, transition)
146
+ end
147
+
148
+ def run_callbacks(transition, kind, context)
149
+ current_callbacks = callbacks.resolve(transition, kind)
150
+
151
+ current_callbacks.each do |callback|
152
+ Callable.new(callback).with_context(context).call(transition)
153
+ end
154
+ end
155
+
156
+ def current_state_name(context)
157
+ get_state_with.with_context(context).call(target(context))
158
+ end
159
+
160
+ def target(context)
161
+ @target_method ||= options[:target] || :itself
162
+ context.send(@target_method)
163
+ end
164
+
165
+ private
166
+
167
+ def raise_missing_configuration_error(method)
168
+ raise NxtStateMachine::Errors::MissingConfiguration, "Configuration method :#{method} was not defined"
169
+ end
170
+
171
+ def new_state_class(&block)
172
+ if block
173
+ Class.new(NxtStateMachine::State, &block)
174
+ else
175
+ NxtStateMachine::State
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,12 @@
1
+ module NxtStateMachine
2
+ class StateRegistry < NxtRegistry::Registry
3
+ def initialize
4
+ super :states do
5
+ on_key_already_registered do |key|
6
+ raise NxtStateMachine::Errors::StateAlreadyRegistered,
7
+ "A state with the name '#{key}' was already registered!"
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ module NxtStateMachine
2
+ class Transition::AroundCallbackChain
3
+
4
+ def initialize(transition, context, state_machine)
5
+ @transition = transition
6
+ @context = context
7
+ @state_machine = state_machine
8
+ end
9
+
10
+ def build(proxy)
11
+ return proxy unless callbacks.any?
12
+
13
+ callbacks.map { |c| Callable.new(c).with_context(context) }.reverse.inject(proxy) do |previous, callback|
14
+ -> { callback.call(previous, transition) }
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def callbacks
21
+ @callbacks ||= state_machine.callbacks.resolve(transition).kind(:around)
22
+ end
23
+
24
+ attr_reader :transition, :context, :state_machine
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ module NxtStateMachine
2
+ class Transition::Proxy
3
+ def initialize(event, state_machine, transition, context)
4
+ @event = event
5
+ @transition = transition
6
+ @state_machine = state_machine
7
+ @context = context
8
+ end
9
+
10
+ def call(&block)
11
+ proxy = if block.arity == 1
12
+ Proc.new do
13
+ block.call(transition.block_proxy)
14
+ end
15
+ else
16
+ block
17
+ end
18
+
19
+ around_callback_chain(proxy).call
20
+ end
21
+
22
+ private
23
+
24
+ def around_callback_chain(proxy)
25
+ @around_callback_chain ||= Transition::AroundCallbackChain.new(transition, context, state_machine).build(proxy)
26
+ end
27
+
28
+ attr_reader :proxy, :transition, :state_machine, :context, :event
29
+ end
30
+ end
31
+
@@ -0,0 +1,19 @@
1
+ module NxtStateMachine
2
+ class Transition::Store < Array
3
+ def <<(transition)
4
+ ensure_transition_unique(transition)
5
+ super
6
+ end
7
+
8
+ alias_method :add, :<<
9
+
10
+ private
11
+
12
+ def ensure_transition_unique(transition)
13
+ return unless find { |other| other.from.enum == transition.from.enum && other.to.enum == transition.to.enum }
14
+
15
+ raise NxtStateMachine::Errors::TransitionAlreadyRegistered,
16
+ "A transition from :#{transition.from.enum} to :#{transition.to.enum} was already registered"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,87 @@
1
+ module NxtStateMachine
2
+ class Transition
3
+ def initialize(name, from:, to:, state_machine:, &block)
4
+ @name = name
5
+ @from = state_machine.states.resolve(from)
6
+ @to = state_machine.states.resolve(to)
7
+ @state_machine = state_machine
8
+ @block = block
9
+
10
+ # TODO: Write a spec that verifies that transitions are unique
11
+ ensure_states_exist
12
+ end
13
+
14
+ attr_reader :name, :from, :to
15
+
16
+ # TODO: Probably would make sense if we could also define the event name to be passed in
17
+ # => This way we could differentiate what event triggered the callback!!!
18
+
19
+ def prepare(event, context, set_state_with_method, *args, **opts)
20
+ # This exposes the transition block on the transition_to_execute so it can be executed later in :set_state_with
21
+ current_transition = clone
22
+ current_transition.send(:context=, context)
23
+ current_transition.send(:event=, event)
24
+ current_transition.send(:block_proxy=, nil)
25
+
26
+ # block_proxy only is set when the transition accepts a block!
27
+ if block
28
+ proxy = Proc.new do
29
+ # if the block takes arguments we always pass the transition as the first one
30
+ args.prepend(current_transition) if block.arity > 0
31
+ context.instance_exec(*args, **opts, &block)
32
+ end
33
+
34
+ current_transition.send(:block_proxy=, proxy)
35
+ end
36
+
37
+ state_machine.send(
38
+ set_state_with_method
39
+ ).with_context(
40
+ context
41
+ ).call(state_machine.target(context), current_transition)
42
+ end
43
+
44
+ def execute(&block)
45
+ # This is called on the cloned transition from above!
46
+ Transition::Proxy.new(event, state_machine,self, context).call(&block)
47
+ rescue StandardError => error
48
+ callback = state_machine.find_error_callback(error, self)
49
+ raise unless callback
50
+
51
+ Callable.new(callback).with_context(context).call(error, self)
52
+ end
53
+
54
+ alias_method :with_around_callbacks, :execute
55
+
56
+ def run_before_callbacks
57
+ state_machine.run_before_callbacks(self, context)
58
+ end
59
+
60
+ def run_after_callbacks
61
+ state_machine.run_after_callbacks(self, context)
62
+ end
63
+
64
+ def transitions_from_to?(from_state, to_state)
65
+ from.enum.in?(Array(from_state)) && to.enum.in?(Array(to_state))
66
+ end
67
+
68
+ def id
69
+ @id ||= "#{from.to_s}_#{to.to_s}"
70
+ end
71
+
72
+ attr_reader :block_proxy, :event
73
+
74
+ private
75
+
76
+ delegate :all_states, to: :state_machine
77
+
78
+ attr_reader :block, :state_machine
79
+ attr_accessor :context
80
+ attr_writer :block_proxy, :event
81
+
82
+ def ensure_states_exist
83
+ raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{from} registered" unless state_machine.states.key?(from.enum)
84
+ raise NxtStateMachine::Errors::UnknownStateError, "No state with :#{to} registered" unless state_machine.states.key?(to.enum)
85
+ end
86
+ end
87
+ end