functional-light-service 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +72 -0
  5. data/.travis.yml +22 -0
  6. data/Appraisals +3 -0
  7. data/CHANGELOG.md +37 -0
  8. data/CODE_OF_CONDUCT.md +22 -0
  9. data/Gemfile +6 -0
  10. data/LICENSE +22 -0
  11. data/README.md +1424 -0
  12. data/Rakefile +12 -0
  13. data/VERSION +1 -0
  14. data/functional-light-service.gemspec +26 -0
  15. data/gemfiles/activesupport_5.gemfile +8 -0
  16. data/gemfiles/activesupport_5.gemfile.lock +82 -0
  17. data/lib/functional-light-service/action.rb +102 -0
  18. data/lib/functional-light-service/configuration.rb +24 -0
  19. data/lib/functional-light-service/context/key_verifier.rb +118 -0
  20. data/lib/functional-light-service/context.rb +165 -0
  21. data/lib/functional-light-service/errors.rb +6 -0
  22. data/lib/functional-light-service/functional/enum.rb +254 -0
  23. data/lib/functional-light-service/functional/maybe.rb +14 -0
  24. data/lib/functional-light-service/functional/monad.rb +66 -0
  25. data/lib/functional-light-service/functional/null.rb +74 -0
  26. data/lib/functional-light-service/functional/option.rb +99 -0
  27. data/lib/functional-light-service/functional/result.rb +122 -0
  28. data/lib/functional-light-service/localization_adapter.rb +44 -0
  29. data/lib/functional-light-service/organizer/execute.rb +14 -0
  30. data/lib/functional-light-service/organizer/iterate.rb +22 -0
  31. data/lib/functional-light-service/organizer/reduce_if.rb +17 -0
  32. data/lib/functional-light-service/organizer/reduce_until.rb +20 -0
  33. data/lib/functional-light-service/organizer/scoped_reducable.rb +13 -0
  34. data/lib/functional-light-service/organizer/verify_call_method_exists.rb +28 -0
  35. data/lib/functional-light-service/organizer/with_callback.rb +26 -0
  36. data/lib/functional-light-service/organizer/with_reducer.rb +71 -0
  37. data/lib/functional-light-service/organizer/with_reducer_factory.rb +18 -0
  38. data/lib/functional-light-service/organizer/with_reducer_log_decorator.rb +105 -0
  39. data/lib/functional-light-service/organizer.rb +105 -0
  40. data/lib/functional-light-service/testing/context_factory.rb +40 -0
  41. data/lib/functional-light-service/testing.rb +1 -0
  42. data/lib/functional-light-service/version.rb +3 -0
  43. data/lib/functional-light-service.rb +29 -0
  44. data/resources/fail_actions.png +0 -0
  45. data/resources/light-service.png +0 -0
  46. data/resources/organizer_and_actions.png +0 -0
  47. data/resources/skip_actions.png +0 -0
  48. data/spec/acceptance/add_numbers_spec.rb +11 -0
  49. data/spec/acceptance/after_actions_spec.rb +71 -0
  50. data/spec/acceptance/around_each_spec.rb +19 -0
  51. data/spec/acceptance/before_actions_spec.rb +98 -0
  52. data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
  53. data/spec/acceptance/fail_spec.rb +24 -0
  54. data/spec/acceptance/include_warning_spec.rb +29 -0
  55. data/spec/acceptance/log_from_organizer_spec.rb +154 -0
  56. data/spec/acceptance/message_localization_spec.rb +118 -0
  57. data/spec/acceptance/not_having_call_method_warning_spec.rb +39 -0
  58. data/spec/acceptance/organizer/around_each_with_reduce_if_spec.rb +42 -0
  59. data/spec/acceptance/organizer/context_failure_and_skipping_spec.rb +65 -0
  60. data/spec/acceptance/organizer/execute_spec.rb +46 -0
  61. data/spec/acceptance/organizer/iterate_spec.rb +37 -0
  62. data/spec/acceptance/organizer/reduce_if_spec.rb +51 -0
  63. data/spec/acceptance/organizer/reduce_until_spec.rb +43 -0
  64. data/spec/acceptance/organizer/with_callback_spec.rb +110 -0
  65. data/spec/acceptance/rollback_spec.rb +132 -0
  66. data/spec/acceptance/skip_all_warning_spec.rb +20 -0
  67. data/spec/acceptance/testing/context_factory_spec.rb +54 -0
  68. data/spec/action_expected_keys_spec.rb +63 -0
  69. data/spec/action_expects_and_promises_spec.rb +93 -0
  70. data/spec/action_promised_keys_spec.rb +122 -0
  71. data/spec/action_spec.rb +89 -0
  72. data/spec/context/inspect_spec.rb +57 -0
  73. data/spec/context_spec.rb +197 -0
  74. data/spec/examples/amount_spec.rb +77 -0
  75. data/spec/examples/controller_spec.rb +63 -0
  76. data/spec/examples/validate_address_spec.rb +37 -0
  77. data/spec/lib/deterministic/class_mixin_spec.rb +24 -0
  78. data/spec/lib/deterministic/currify_spec.rb +88 -0
  79. data/spec/lib/deterministic/monad_axioms.rb +44 -0
  80. data/spec/lib/deterministic/monad_spec.rb +45 -0
  81. data/spec/lib/deterministic/null_spec.rb +58 -0
  82. data/spec/lib/deterministic/option_spec.rb +133 -0
  83. data/spec/lib/deterministic/result/failure_spec.rb +65 -0
  84. data/spec/lib/deterministic/result/result_map_spec.rb +154 -0
  85. data/spec/lib/deterministic/result/result_shared.rb +24 -0
  86. data/spec/lib/deterministic/result/success_spec.rb +41 -0
  87. data/spec/lib/deterministic/result_spec.rb +63 -0
  88. data/spec/lib/enum_spec.rb +112 -0
  89. data/spec/localization_adapter_spec.rb +83 -0
  90. data/spec/organizer/with_reducer_spec.rb +56 -0
  91. data/spec/organizer_key_aliases_spec.rb +29 -0
  92. data/spec/organizer_spec.rb +93 -0
  93. data/spec/readme_spec.rb +47 -0
  94. data/spec/sample/calculates_order_tax_action_spec.rb +16 -0
  95. data/spec/sample/calculates_tax_spec.rb +30 -0
  96. data/spec/sample/looks_up_tax_percentage_action_spec.rb +53 -0
  97. data/spec/sample/provides_free_shipping_action_spec.rb +25 -0
  98. data/spec/sample/tax/calculates_order_tax_action.rb +9 -0
  99. data/spec/sample/tax/calculates_tax.rb +11 -0
  100. data/spec/sample/tax/looks_up_tax_percentage_action.rb +27 -0
  101. data/spec/sample/tax/provides_free_shipping_action.rb +10 -0
  102. data/spec/spec_helper.rb +24 -0
  103. data/spec/support.rb +1 -0
  104. data/spec/test_doubles.rb +552 -0
  105. data/spec/testing/context_factory/iterate_spec.rb +39 -0
  106. data/spec/testing/context_factory/reduce_if_spec.rb +40 -0
  107. data/spec/testing/context_factory/reduce_until_spec.rb +40 -0
  108. data/spec/testing/context_factory/with_callback_spec.rb +38 -0
  109. data/spec/testing/context_factory_spec.rb +55 -0
  110. metadata +285 -0
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+
6
+ require 'rspec/core/rake_task'
7
+ require 'rubocop/rake_task'
8
+ RuboCop::RakeTask.new
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ task(:default).clear
12
+ task :default => %i[spec rubocop]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.4
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/functional-light-service/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Boscolo Michele"]
6
+ gem.email = ["miboscol@gmail.com"]
7
+ gem.description = %q{A service skeleton with an emphasis on simplicity with a pinch a functional programming}
8
+ gem.summary = %q{A service skeleton with an emphasis on simplicity with a pinch a functional programming}
9
+ gem.homepage = "https://github.com/sphynx79/functional-light-service"
10
+ gem.license = "MIT"
11
+
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.name = "functional-light-service"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = FunctionalLightService::VERSION
18
+
19
+ # gem.add_dependency("activesupport", ">= 5.2.2")
20
+
21
+ gem.add_development_dependency("activesupport", "~> 5.2.0")
22
+ gem.add_development_dependency("rspec", "~> 3.0")
23
+ gem.add_development_dependency("simplecov", "~> 0.16.1")
24
+ gem.add_development_dependency("rubocop", "~> 0.63.1")
25
+ gem.add_development_dependency("pry", "~> 0.12.2")
26
+ end
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activesupport", "~> 5.0"
6
+ gem "appraisal", "~> 2.0"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,82 @@
1
+ PATH
2
+ remote: ../
3
+ specs:
4
+ light-service (0.6.1)
5
+ activesupport (>= 3.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (5.0.0.1)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (~> 0.7)
13
+ minitest (~> 5.1)
14
+ tzinfo (~> 1.1)
15
+ appraisal (2.1.0)
16
+ bundler
17
+ rake
18
+ thor (>= 0.14.0)
19
+ ast (2.3.0)
20
+ coderay (1.1.1)
21
+ concurrent-ruby (1.0.2)
22
+ diff-lcs (1.2.5)
23
+ docile (1.1.5)
24
+ i18n (0.7.0)
25
+ json (2.0.2)
26
+ method_source (0.8.2)
27
+ minitest (5.9.0)
28
+ parser (2.3.1.2)
29
+ ast (~> 2.2)
30
+ powerpack (0.1.1)
31
+ pry (0.10.4)
32
+ coderay (~> 1.1.0)
33
+ method_source (~> 0.8.1)
34
+ slop (~> 3.4)
35
+ rainbow (2.1.0)
36
+ rake (11.2.2)
37
+ rspec (3.5.0)
38
+ rspec-core (~> 3.5.0)
39
+ rspec-expectations (~> 3.5.0)
40
+ rspec-mocks (~> 3.5.0)
41
+ rspec-core (3.5.2)
42
+ rspec-support (~> 3.5.0)
43
+ rspec-expectations (3.5.0)
44
+ diff-lcs (>= 1.2.0, < 2.0)
45
+ rspec-support (~> 3.5.0)
46
+ rspec-mocks (3.5.0)
47
+ diff-lcs (>= 1.2.0, < 2.0)
48
+ rspec-support (~> 3.5.0)
49
+ rspec-support (3.5.0)
50
+ rubocop (0.42.0)
51
+ parser (>= 2.3.1.1, < 3.0)
52
+ powerpack (~> 0.1)
53
+ rainbow (>= 1.99.1, < 3.0)
54
+ ruby-progressbar (~> 1.7)
55
+ unicode-display_width (~> 1.0, >= 1.0.1)
56
+ ruby-progressbar (1.8.1)
57
+ simplecov (0.12.0)
58
+ docile (~> 1.1.0)
59
+ json (>= 1.8, < 3)
60
+ simplecov-html (~> 0.10.0)
61
+ simplecov-html (0.10.0)
62
+ slop (3.6.0)
63
+ thor (0.19.1)
64
+ thread_safe (0.3.5)
65
+ tzinfo (1.2.2)
66
+ thread_safe (~> 0.1)
67
+ unicode-display_width (1.1.1)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ activesupport (~> 5.0)
74
+ appraisal (~> 2.0)
75
+ light-service!
76
+ pry (~> 0.10)
77
+ rspec (~> 3.0)
78
+ rubocop (~> 0.36)
79
+ simplecov (~> 0.11)
80
+
81
+ BUNDLED WITH
82
+ 1.12.5
@@ -0,0 +1,102 @@
1
+ require 'active_support/deprecation'
2
+
3
+ module FunctionalLightService
4
+ module Action
5
+ def self.extended(base_class)
6
+ base_class.extend Macros
7
+ base_class.extend FunctionalLightService::Prelude::Result
8
+ end
9
+
10
+ def self.included(base_class)
11
+ msg = "including FunctionalLightService::Action is deprecated. " \
12
+ "Please use `extend FunctionalLightService::Action` instead"
13
+ ActiveSupport::Deprecation.warn(msg)
14
+ base_class.extend Macros
15
+ end
16
+
17
+ module Macros
18
+ def expects(*args)
19
+ expected_keys.concat(args)
20
+ end
21
+
22
+ def promises(*args)
23
+ promised_keys.concat(args)
24
+ end
25
+
26
+ def expected_keys
27
+ @expected_keys ||= []
28
+ end
29
+
30
+ def promised_keys
31
+ @promised_keys ||= []
32
+ end
33
+
34
+ def ctx(*args)
35
+ @ctx ||= args
36
+ end
37
+
38
+ def executed
39
+ define_singleton_method :execute do |context = {}|
40
+ action_context = create_action_context(context)
41
+ return action_context if action_context.stop_processing?
42
+
43
+ @ctx = action_context
44
+
45
+ # Store the action within the context
46
+ action_context.current_action = self
47
+
48
+ Context::KeyVerifier.verify_keys(action_context, self) do
49
+ action_context.define_accessor_methods_for_keys(all_keys)
50
+
51
+ catch(:jump_when_failed) do
52
+ call_before_action(action_context)
53
+ yield(action_context)
54
+ call_after_action(action_context)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def rolled_back
61
+ msg = "`rolled_back` macro can not be invoked again"
62
+ raise msg if respond_to?(:rollback)
63
+
64
+ define_singleton_method :rollback do |context = {}|
65
+ yield(context)
66
+
67
+ context
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def create_action_context(context)
74
+ return context if context.is_a? FunctionalLightService::Context
75
+
76
+ FunctionalLightService::Context.make(context)
77
+ end
78
+
79
+ def all_keys
80
+ expected_keys + promised_keys
81
+ end
82
+
83
+ def call_before_action(context)
84
+ invoke_callbacks(context[:_before_actions], context)
85
+ end
86
+
87
+ def call_after_action(context)
88
+ invoke_callbacks(context[:_after_actions], context)
89
+ end
90
+
91
+ def invoke_callbacks(callbacks, context)
92
+ return context unless callbacks
93
+
94
+ callbacks.each do |cb|
95
+ cb.call(context)
96
+ end
97
+
98
+ context
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,24 @@
1
+ module FunctionalLightService
2
+ class Configuration
3
+ class << self
4
+ attr_writer :logger, :localization_adapter
5
+
6
+ def logger
7
+ @logger = _default_logger unless instance_variable_defined?("@logger")
8
+ @logger
9
+ end
10
+
11
+ def localization_adapter
12
+ @localization_adapter ||= LocalizationAdapter.new
13
+ end
14
+
15
+ private
16
+
17
+ def _default_logger
18
+ logger = Logger.new("/dev/null")
19
+ logger.level = Logger::WARN
20
+ logger
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,118 @@
1
+ module FunctionalLightService
2
+ class Context
3
+ class KeyVerifier
4
+ attr_reader :context, :action
5
+
6
+ def initialize(context, action)
7
+ @context = context
8
+ @action = action
9
+ end
10
+
11
+ def are_all_keys_in_context?(keys)
12
+ not_found_keys = keys_not_found(keys)
13
+ not_found_keys.none?
14
+ end
15
+
16
+ def keys_not_found(keys)
17
+ keys ||= context.keys
18
+ keys - context.keys
19
+ end
20
+
21
+ def format_keys(keys)
22
+ keys.map { |k| ":#{k}" }.join(', ')
23
+ end
24
+
25
+ def error_message
26
+ "#{type_name} #{format_keys(keys_not_found(keys))} " \
27
+ "to be in the context during #{action}"
28
+ end
29
+
30
+ def throw_error_predicate(_keys)
31
+ raise NotImplementedError, 'Sorry, you have to override length'
32
+ end
33
+
34
+ def verify
35
+ return context if context.failure?
36
+
37
+ if throw_error_predicate(keys)
38
+ Configuration.logger.error error_message
39
+ raise error_to_throw, error_message
40
+ end
41
+
42
+ context
43
+ end
44
+
45
+ def self.verify_keys(context, action, &block)
46
+ ReservedKeysVerifier.new(context, action).verify
47
+ ExpectedKeyVerifier.new(context, action).verify
48
+
49
+ block.call
50
+
51
+ PromisedKeyVerifier.new(context, action).verify
52
+ end
53
+ end
54
+
55
+ class ExpectedKeyVerifier < KeyVerifier
56
+ def type_name
57
+ "expected"
58
+ end
59
+
60
+ def keys
61
+ action.expected_keys
62
+ end
63
+
64
+ def error_to_throw
65
+ ExpectedKeysNotInContextError
66
+ end
67
+
68
+ def throw_error_predicate(keys)
69
+ !are_all_keys_in_context?(keys)
70
+ end
71
+ end
72
+
73
+ class PromisedKeyVerifier < KeyVerifier
74
+ def type_name
75
+ "promised"
76
+ end
77
+
78
+ def keys
79
+ action.promised_keys
80
+ end
81
+
82
+ def error_to_throw
83
+ PromisedKeysNotInContextError
84
+ end
85
+
86
+ def throw_error_predicate(keys)
87
+ !are_all_keys_in_context?(keys)
88
+ end
89
+ end
90
+
91
+ class ReservedKeysVerifier < KeyVerifier
92
+ def violated_keys
93
+ (action.promised_keys + action.expected_keys) & reserved_keys
94
+ end
95
+
96
+ def error_message
97
+ "promised or expected keys cannot be a " \
98
+ "reserved key: [#{format_keys(violated_keys)}]"
99
+ end
100
+
101
+ def keys
102
+ violated_keys
103
+ end
104
+
105
+ def error_to_throw
106
+ ReservedKeysInContextError
107
+ end
108
+
109
+ def throw_error_predicate(keys)
110
+ keys.any?
111
+ end
112
+
113
+ def reserved_keys
114
+ %i[message error_code current_action].freeze
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,165 @@
1
+ require 'active_support/deprecation'
2
+
3
+ module FunctionalLightService
4
+ # rubocop:disable ClassLength
5
+ class Context < Hash
6
+ include FunctionalLightService::Prelude::Option
7
+ include FunctionalLightService::Prelude::Result
8
+ attr_accessor :outcome, :current_action
9
+
10
+ def initialize(context = {},
11
+ outcome = Success(:message => '', :error => nil))
12
+ @outcome = outcome
13
+ @skip_remaining = false
14
+ context.to_hash.each { |k, v| self[k] = v }
15
+ end
16
+
17
+ def self.make(context = {})
18
+ unless context.is_a?(Hash) || context.is_a?(FunctionalLightService::Context)
19
+ msg = 'Argument must be Hash or FunctionalLightService::Context'
20
+ raise ArgumentError, msg
21
+ end
22
+
23
+ context = new(context) unless context.is_a?(Context)
24
+
25
+ context.assign_aliases(context.delete(:_aliases)) if context[:_aliases]
26
+ context
27
+ end
28
+
29
+ def add_to_context(values)
30
+ merge! values
31
+ end
32
+
33
+ def success?
34
+ @outcome.success?
35
+ end
36
+
37
+ def failure?
38
+ success? == false
39
+ end
40
+
41
+ def skip_remaining?
42
+ @skip_remaining
43
+ end
44
+
45
+ def reset_skip_remaining!
46
+ @outcome = Success(nil)
47
+ @skip_remaining = false
48
+ end
49
+
50
+ def message
51
+ @outcome.value.dig(:message)
52
+ end
53
+
54
+ def error_code
55
+ @outcome.value.dig(:error)
56
+ end
57
+
58
+ def succeed!(message = nil, options = {})
59
+ message = Configuration.localization_adapter.success(message,
60
+ current_action,
61
+ options)
62
+ @outcome = Success(:message => message)
63
+ end
64
+
65
+ def fail!(message = nil, options_or_error_code = {})
66
+ options_or_error_code ||= {}
67
+
68
+ if options_or_error_code.is_a?(Hash)
69
+ error_code = options_or_error_code.delete(:error_code)
70
+ options = options_or_error_code
71
+ else
72
+ error_code = options_or_error_code
73
+ options = {}
74
+ end
75
+
76
+ message = Configuration.localization_adapter.failure(message,
77
+ current_action,
78
+ options)
79
+
80
+ @outcome = Failure(:message => message, :error => error_code)
81
+ end
82
+
83
+ def fail_and_return!(*args)
84
+ fail!(*args)
85
+ throw(:jump_when_failed, *args)
86
+ end
87
+
88
+ def fail_with_rollback!(message = nil, error_code = nil)
89
+ fail!(message, error_code)
90
+ raise FailWithRollbackError
91
+ end
92
+
93
+ def skip_all!(message = nil)
94
+ warning_msg = "Using skip_all! has been deprecated, " \
95
+ "please use `skip_remaining!` instead."
96
+ ActiveSupport::Deprecation.warn(warning_msg)
97
+
98
+ skip_remaining!(message)
99
+ end
100
+
101
+ def skip_remaining!(message = nil)
102
+ @outcome = Success(:message => message)
103
+ @skip_remaining = true
104
+ end
105
+
106
+ def stop_processing?
107
+ failure? || skip_remaining?
108
+ end
109
+
110
+ def define_accessor_methods_for_keys(keys)
111
+ return if keys.nil?
112
+
113
+ keys.each do |key|
114
+ next if respond_to?(key.to_sym)
115
+
116
+ define_singleton_method(key.to_s) { fetch(key) }
117
+ define_singleton_method("#{key}=") { |value| self[key] = value }
118
+ end
119
+ end
120
+
121
+ def assign_aliases(aliases)
122
+ @aliases = aliases
123
+
124
+ aliases.each_pair do |key, key_alias|
125
+ self[key_alias] = self[key]
126
+ end
127
+ end
128
+
129
+ def aliases
130
+ @aliases ||= {}
131
+ end
132
+
133
+ def [](key)
134
+ key = aliases.key(key) || key
135
+ return super(key)
136
+ end
137
+
138
+ def fetch(key, default = nil, &blk)
139
+ self[key] ||= if block_given?
140
+ super(key, &blk)
141
+ else
142
+ super
143
+ end
144
+ end
145
+
146
+ def inspect
147
+ "#{self.class}(#{self}, " \
148
+ + "success: #{success?}, " \
149
+ + "message: #{check_nil(message)}, " \
150
+ + "error_code: #{check_nil(error_code)}, " \
151
+ + "skip_remaining: #{@skip_remaining}, " \
152
+ + "aliases: #{@aliases}" \
153
+ + ")"
154
+ end
155
+
156
+ private
157
+
158
+ def check_nil(value)
159
+ return 'nil' unless value
160
+
161
+ "'#{value}'"
162
+ end
163
+ end
164
+ # rubocop:enable ClassLength
165
+ end
@@ -0,0 +1,6 @@
1
+ module FunctionalLightService
2
+ class FailWithRollbackError < StandardError; end
3
+ class ExpectedKeysNotInContextError < StandardError; end
4
+ class PromisedKeysNotInContextError < StandardError; end
5
+ class ReservedKeysInContextError < StandardError; end
6
+ end