light-services 0.6.3 → 2.0.0.rc3

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/config/rubocop_linter_action.yml +4 -0
  3. data/.github/workflows/ci.yml +63 -0
  4. data/.gitignore +11 -4
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +22 -8
  7. data/CHANGELOG.md +1 -0
  8. data/CODE_OF_CONDUCT.md +55 -30
  9. data/Gemfile +16 -2
  10. data/Gemfile.lock +102 -0
  11. data/LICENSE.txt +1 -1
  12. data/README.md +3 -149
  13. data/Rakefile +2 -2
  14. data/bin/console +4 -4
  15. data/lib/light/services.rb +4 -7
  16. data/lib/light/services/base.rb +151 -30
  17. data/lib/light/services/base_with_context.rb +33 -0
  18. data/lib/light/services/class_based_collection/base.rb +88 -0
  19. data/lib/light/services/class_based_collection/mount.rb +33 -0
  20. data/lib/light/services/collection/arguments.rb +34 -0
  21. data/lib/light/services/collection/base.rb +55 -0
  22. data/lib/light/services/collection/outputs.rb +16 -0
  23. data/lib/light/services/config.rb +63 -0
  24. data/lib/light/services/exceptions.rb +3 -2
  25. data/lib/light/services/messages.rb +67 -45
  26. data/lib/light/services/settings/argument.rb +50 -0
  27. data/lib/light/services/settings/output.rb +34 -0
  28. data/lib/light/services/settings/step.rb +77 -0
  29. data/lib/light/services/version.rb +1 -1
  30. data/light-services.gemspec +21 -25
  31. metadata +28 -122
  32. data/.codeclimate.yml +0 -18
  33. data/.ruby-gemset +0 -1
  34. data/.ruby-version +0 -1
  35. data/.travis.yml +0 -33
  36. data/Appraisals +0 -13
  37. data/gemfiles/rails_5_0.gemfile +0 -7
  38. data/gemfiles/rails_5_0.gemfile.lock +0 -151
  39. data/gemfiles/rails_5_1.gemfile +0 -7
  40. data/gemfiles/rails_5_1.gemfile.lock +0 -151
  41. data/gemfiles/rails_5_2.gemfile +0 -7
  42. data/gemfiles/rails_5_2.gemfile.lock +0 -159
  43. data/lib/light/services/callbacks.rb +0 -54
  44. data/lib/light/services/outputs.rb +0 -70
  45. data/lib/light/services/parameters.rb +0 -96
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
data/bin/console CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'bundler/setup'
5
- require 'light/services'
4
+ require "bundler/setup"
5
+ require "light/services"
6
6
 
7
7
  # You can add fixtures and/or initialization code here to make experimenting
8
8
  # with your gem easier. You can also use a different console, if you like.
@@ -11,5 +11,5 @@ require 'light/services'
11
11
  # require "pry"
12
12
  # Pry.start
13
13
 
14
- require 'irb'
15
- IRB.start
14
+ require "irb"
15
+ IRB.start(__FILE__)
@@ -1,12 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'light/services/version'
4
- require 'light/services/exceptions'
5
- require 'light/services/messages'
6
- require 'light/services/parameters'
7
- require 'light/services/outputs'
8
- require 'light/services/callbacks'
9
- require 'light/services/base'
3
+ require "light/services/config"
4
+ require "light/services/version"
5
+ require "light/services/exceptions"
6
+ require "light/services/base"
10
7
 
11
8
  module Light
12
9
  module Services
@@ -1,64 +1,185 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "light/services/messages"
4
+ require "light/services/base_with_context"
5
+
6
+ require "light/services/settings/step"
7
+ require "light/services/settings/output"
8
+ require "light/services/settings/argument"
9
+
10
+ require "light/services/collection/base"
11
+ require "light/services/collection/outputs"
12
+ require "light/services/collection/arguments"
13
+
14
+ require "light/services/class_based_collection/base"
15
+ require "light/services/class_based_collection/mount"
16
+
17
+ # Base class for all service objects
3
18
  module Light
4
19
  module Services
5
20
  class Base
6
21
  # Includes
7
- include Light::Services::Parameters
8
- include Light::Services::Outputs
9
- include Light::Services::Callbacks
22
+ extend ClassBasedCollection::Mount
23
+
24
+ # Settings
25
+ mount_class_based_collection :steps, item_class: Settings::Step, shortcut: :step
26
+ mount_class_based_collection :outputs, item_class: Settings::Output, shortcut: :output
27
+ mount_class_based_collection :arguments, item_class: Settings::Argument, shortcut: :arg, allow_redefine: true
28
+
29
+ # Arguments
30
+ # TODO: Rename internal arguments
31
+ arg :benchmark, default: false
32
+ arg :deepness, default: 0, context: true
33
+
34
+ # Steps
35
+ step :load_defaults_and_validate
36
+ step :log_header, if: :benchmark?
10
37
 
11
38
  # Getters
12
- attr_reader :errors, :warnings
39
+ attr_reader :outputs, :arguments, :errors, :warnings
13
40
 
14
- def initialize(args = {})
15
- @args = args
41
+ def initialize(args = {}, config = {}, parent_service = nil)
42
+ @config = Light::Services.config.merge(config)
43
+ @parent_service = parent_service
16
44
 
17
- initialize_params
18
- initialize_outputs
45
+ @outputs = Collection::Outputs.new(self)
46
+ @arguments = Collection::Arguments.new(self, args)
19
47
 
20
- @errors = Light::Services::Messages.new
21
- @warnings = Light::Services::Messages.new
22
- end
48
+ @done = false
49
+ @launched_steps = []
23
50
 
24
- def call
25
- within_transaction { run_service }
51
+ initialize_errors
52
+ initialize_warnings
26
53
  end
27
54
 
28
55
  def success?
29
- errors.blank?
56
+ !errors?
57
+ end
58
+
59
+ def failed?
60
+ errors?
61
+ end
62
+
63
+ def errors?
64
+ @errors.any?
30
65
  end
31
66
 
32
- def any_warnings?
33
- warnings.any?
67
+ def warnings?
68
+ @warnings.any?
69
+ end
70
+
71
+ def done!
72
+ @done = true
73
+ end
74
+
75
+ def done?
76
+ @done
77
+ end
78
+
79
+ def call
80
+ time = Benchmark.ms do
81
+ run_steps
82
+ run_steps_with_always
83
+
84
+ copy_warnings_to_parent_service
85
+ copy_errors_to_parent_service
86
+ end
87
+
88
+ return unless benchmark
89
+
90
+ log "🟢 Finished #{self.class} in #{time}ms"
91
+ puts
34
92
  end
35
93
 
36
94
  class << self
37
- def call(args = {})
95
+ # TODO: Create `run!`
96
+ def run(args = {})
38
97
  new(args).tap(&:call)
39
98
  end
40
99
 
41
- alias run call
100
+ def with(service_or_config = {}, config = {})
101
+ service = service_or_config.is_a?(Hash) ? nil : service_or_config
102
+ config = service ? config : service_or_config
103
+
104
+ BaseWithContext.new(self, service, config)
105
+ end
106
+ end
107
+
108
+ # TODO: Add possibility to specify logger
109
+ def log(message)
110
+ puts "#{' ' * deepness}→ #{message}"
42
111
  end
43
112
 
44
113
  private
45
114
 
46
- # Getters
47
- attr_reader :args
115
+ def initialize_errors
116
+ @errors = Messages.new(
117
+ break_on_add: @config[:break_on_error],
118
+ raise_on_add: @config[:raise_on_error],
119
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_error]
120
+ )
121
+ end
48
122
 
49
- def run_service
50
- run_callbacks(:before)
51
- run if success?
52
- run_callbacks(:after) if success?
53
- run_callbacks(:finally, force_run: true)
54
- success?
123
+ def initialize_warnings
124
+ @warnings = Messages.new(
125
+ break_on_add: @config[:break_on_warning],
126
+ raise_on_add: @config[:raise_on_warning],
127
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_warning]
128
+ )
55
129
  end
56
130
 
57
- def within_transaction
58
- if defined?(ActiveRecord::Base)
59
- ActiveRecord::Base.transaction do
60
- yield
131
+ def run_steps
132
+ within_transaction do
133
+ self.class.steps.each do |name, step|
134
+ @launched_steps << name if step.run(self, benchmark: benchmark)
135
+
136
+ break if @errors.break? || @warnings.break?
61
137
  end
138
+ end
139
+ end
140
+
141
+ # Run steps with parameter `always` if they weren't launched because of errors/warnings
142
+ def run_steps_with_always
143
+ self.class.steps.each do |name, step|
144
+ next if !step.always || @launched_steps.include?(name)
145
+
146
+ @launched_steps << name if step.run(self)
147
+ end
148
+ end
149
+
150
+ def copy_warnings_to_parent_service
151
+ return if !@parent_service || !@config[:load_warnings]
152
+
153
+ @parent_service.warnings.copy_from(
154
+ @warnings,
155
+ break: @config[:self_break_on_warning],
156
+ rollback: @config[:self_rollback_on_warning]
157
+ )
158
+ end
159
+
160
+ def copy_errors_to_parent_service
161
+ return if !@parent_service || !@config[:load_errors]
162
+
163
+ @parent_service.errors.copy_from(
164
+ @errors,
165
+ break: @config[:self_break_on_error],
166
+ rollback: @config[:self_rollback_on_error]
167
+ )
168
+ end
169
+
170
+ def load_defaults_and_validate
171
+ @outputs.load_defaults
172
+ @arguments.load_defaults
173
+ @arguments.validate!
174
+ end
175
+
176
+ def log_header
177
+ log "🏎 Run service #{self.class}"
178
+ end
179
+
180
+ def within_transaction
181
+ if @config[:use_transactions] && defined?(ActiveRecord::Base)
182
+ ActiveRecord::Base.transaction(requires_new: true) { yield }
62
183
  else
63
184
  yield
64
185
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class allows to run service object with context (parent class and custom config)
4
+ module Light
5
+ module Services
6
+ class BaseWithContext
7
+ def initialize(service_class, parent_service, config)
8
+ @service_class = service_class
9
+ @config = config
10
+ @parent_service = parent_service
11
+
12
+ return if parent_service.nil? || parent_service.is_a?(Light::Services::Base)
13
+
14
+ raise Light::Services::ArgTypeError, "#{parent_service.class} - must be a subclass of Light::Services::Base"
15
+ end
16
+
17
+ # TODO: Create `run!`
18
+ def run(args = {})
19
+ @service_class.new(extend_arguments(args), @config, @parent_service).tap(&:call)
20
+ end
21
+
22
+ private
23
+
24
+ def extend_arguments(args)
25
+ # TODO: Do we need `.dup` here?
26
+ args = @parent_service.arguments.extend_with_context(args) if @parent_service
27
+ args[:deepness] += 1 if args[:deepness]
28
+
29
+ args
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Create class based collections for storing arguments settings, steps settings and outputs settings
4
+ #
5
+ # General functionality:
6
+ # 1. Collection automatically loads data from parent classes
7
+ # 2. It's possible to redefine items if needed (e.g. arguments)
8
+ # 3. We can add items into collection after or before another items
9
+ #
10
+ module Light
11
+ module Services
12
+ module ClassBasedCollection
13
+ class Base
14
+ # TODO: Add `prepend: true`
15
+ def initialize(item_class, allow_redefine)
16
+ @item_class = item_class
17
+ @allow_redefine = allow_redefine
18
+ @collection = {}
19
+ end
20
+
21
+ def add(klass, name, opts = {})
22
+ @collection[klass] ||= all_from_superclass(klass)
23
+
24
+ validate_name!(klass, name)
25
+ validate_opts!(klass, name, opts)
26
+
27
+ item = @item_class.new(name, klass, opts)
28
+
29
+ if opts[:before] || opts[:after]
30
+ insert_item(klass, name, opts, item)
31
+ else
32
+ @collection[klass][name] = item
33
+ end
34
+ end
35
+
36
+ def find_index(klass, name)
37
+ index = @collection[klass].keys.index(name)
38
+
39
+ return index if index
40
+
41
+ # TODO: Update `NoStepError` because it maybe not only step
42
+ raise Light::Services::NoStepError, "Cannot find #{@item_class} `#{name}` in service #{klass}"
43
+ end
44
+
45
+ def remove(klass, name)
46
+ @collection[klass] ||= all_from_superclass(klass)
47
+ @collection[klass].delete(name)
48
+ end
49
+
50
+ def all(klass)
51
+ @collection[klass] || all_from_superclass(klass)
52
+ end
53
+
54
+ private
55
+
56
+ def all_from_superclass(klass)
57
+ if klass.superclass <= Light::Services::Base
58
+ all(klass.superclass).dup
59
+ else
60
+ {}
61
+ end
62
+ end
63
+
64
+ def validate_name!(klass, name)
65
+ if !@allow_redefine && all(klass).key?(name)
66
+ # TODO: Update error class
67
+ raise Light::Services::Error, "#{@item_class} with name `#{name}` already exists in service #{klass}"
68
+ end
69
+ end
70
+
71
+ def validate_opts!(klass, name, opts)
72
+ if opts[:before] && opts[:after]
73
+ # TODO: Update error class
74
+ raise Light::Services::Error, "You cannot specify `before` and `after` " \
75
+ "for #{@item_class} `#{name}` in service #{klass} at the same time"
76
+ end
77
+ end
78
+
79
+ def insert_item(klass, name, opts, item)
80
+ index = find_index(klass, opts[:before] || opts[:after])
81
+ index = opts[:before] ? index : index + 1
82
+
83
+ @collection[klass] = @collection[klass].to_a.insert(index, [name, item]).to_h
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This class allows to mount class based collections to service objects
4
+ #
5
+ # Usage:
6
+ #
7
+ # mount_class_based_collection :steps, klass: Settings::Step, shortcut: :step
8
+ # mount_class_based_collection :outputs, klass: Settings::Output, shortcut: :output
9
+ # mount_class_based_collection :arguments, klass: Settings::Argument, shortcut: :arg, allow_redefine: true
10
+ #
11
+ module Light
12
+ module Services
13
+ module ClassBasedCollection
14
+ module Mount
15
+ def mount_class_based_collection(collection_name, item_class:, shortcut:, allow_redefine: false)
16
+ class_variable_set("@@#{collection_name}", ClassBasedCollection::Base.new(item_class, allow_redefine))
17
+
18
+ define_singleton_method shortcut do |item_name, opts = {}|
19
+ class_variable_get("@@#{collection_name}").add(self, item_name, opts)
20
+ end
21
+
22
+ define_singleton_method "remove_#{shortcut}" do |item_name|
23
+ class_variable_get("@@#{collection_name}").remove(self, item_name)
24
+ end
25
+
26
+ define_singleton_method collection_name do
27
+ class_variable_get("@@#{collection_name}").all(self)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collection to store, merge and validate arguments
4
+ module Light
5
+ module Services
6
+ module Collection
7
+ class Arguments < Base
8
+ def extend_with_context(args)
9
+ settings_collection.each do |name, settings|
10
+ next if !settings.context || args.key?(name) || !key?(name)
11
+
12
+ args[settings.name] = get(name)
13
+ end
14
+
15
+ args
16
+ end
17
+
18
+ def validate!
19
+ settings_collection.each do |name, settings|
20
+ next if settings.optional && (!key?(name) || get(name).nil?)
21
+
22
+ settings.valid_type?(get(name))
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def settings_collection
29
+ @instance.class.arguments
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end