light-services 0.6.0 → 2.0.0.rc1

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 (51) 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 +8 -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 +101 -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 +142 -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 +47 -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 +74 -41
  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 +71 -0
  29. data/lib/light/services/version.rb +1 -1
  30. data/light-services.gemspec +21 -24
  31. metadata +28 -129
  32. data/.codeclimate.yml +0 -18
  33. data/.ruby-gemset +0 -1
  34. data/.ruby-version +0 -1
  35. data/.travis.yml +0 -36
  36. data/Appraisals +0 -25
  37. data/gemfiles/rails_4_0.gemfile +0 -7
  38. data/gemfiles/rails_4_0.gemfile.lock +0 -115
  39. data/gemfiles/rails_4_1.gemfile +0 -7
  40. data/gemfiles/rails_4_1.gemfile.lock +0 -118
  41. data/gemfiles/rails_4_2.gemfile +0 -7
  42. data/gemfiles/rails_4_2.gemfile.lock +0 -144
  43. data/gemfiles/rails_5_0.gemfile +0 -7
  44. data/gemfiles/rails_5_0.gemfile.lock +0 -151
  45. data/gemfiles/rails_5_1.gemfile +0 -7
  46. data/gemfiles/rails_5_1.gemfile.lock +0 -151
  47. data/gemfiles/rails_5_2.gemfile +0 -7
  48. data/gemfiles/rails_5_2.gemfile.lock +0 -159
  49. data/lib/light/services/callbacks.rb +0 -54
  50. data/lib/light/services/outputs.rb +0 -70
  51. 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,176 @@
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
+ @launched_steps = []
23
49
 
24
- def call
25
- within_transaction { run_service }
50
+ initialize_errors
51
+ initialize_warnings
26
52
  end
27
53
 
28
54
  def success?
29
- errors.blank?
55
+ !errors?
30
56
  end
31
57
 
32
- def any_warnings?
33
- warnings.any?
58
+ def failed?
59
+ errors?
60
+ end
61
+
62
+ def errors?
63
+ @errors.any?
64
+ end
65
+
66
+ def warnings?
67
+ @warnings.any?
68
+ end
69
+
70
+ def call
71
+ time = Benchmark.ms do
72
+ run_steps
73
+ run_steps_with_always
74
+
75
+ copy_warnings_to_parent_service
76
+ copy_errors_to_parent_service
77
+ end
78
+
79
+ return unless benchmark
80
+
81
+ log "🟢 Finished #{self.class} in #{time}ms"
82
+ puts
34
83
  end
35
84
 
36
85
  class << self
37
- def call(args = {})
86
+ # TODO: Create `run!`
87
+ def run(args = {})
38
88
  new(args).tap(&:call)
39
89
  end
40
90
 
41
- alias run call
91
+ def with(service_or_config = {}, config = {})
92
+ service = service_or_config.is_a?(Hash) ? nil : service_or_config
93
+ config = service ? config : service_or_config
94
+
95
+ BaseWithContext.new(self, service, config)
96
+ end
97
+ end
98
+
99
+ # TODO: Add possibility to specify logger
100
+ def log(message)
101
+ puts "#{' ' * deepness}→ #{message}"
42
102
  end
43
103
 
44
104
  private
45
105
 
46
- # Getters
47
- attr_reader :args
106
+ def initialize_errors
107
+ @errors = Messages.new(
108
+ break_on_add: @config[:break_on_error],
109
+ raise_on_add: @config[:raise_on_error],
110
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_error]
111
+ )
112
+ end
48
113
 
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?
114
+ def initialize_warnings
115
+ @warnings = Messages.new(
116
+ break_on_add: @config[:break_on_warning],
117
+ raise_on_add: @config[:raise_on_warning],
118
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_warning]
119
+ )
55
120
  end
56
121
 
57
- def within_transaction
58
- if defined?(ActiveRecord::Base)
59
- ActiveRecord::Base.transaction do
60
- yield
122
+ def run_steps
123
+ within_transaction do
124
+ self.class.steps.each do |name, step|
125
+ @launched_steps << name if step.run(self, benchmark: benchmark)
126
+
127
+ break if @errors.break? || @warnings.break?
61
128
  end
129
+ end
130
+ end
131
+
132
+ # Run steps with parameter `always` if they weren't launched because of errors/warnings
133
+ def run_steps_with_always
134
+ self.class.steps.each do |name, step|
135
+ next if !step.always || @launched_steps.include?(name)
136
+
137
+ @launched_steps << name if step.run(self)
138
+ end
139
+ end
140
+
141
+ def copy_warnings_to_parent_service
142
+ return if !@parent_service || !@config[:load_warnings]
143
+
144
+ @parent_service.warnings.copy_from(
145
+ @warnings,
146
+ break: @config[:self_break_on_warning],
147
+ rollback: @config[:self_rollback_on_warning]
148
+ )
149
+ end
150
+
151
+ def copy_errors_to_parent_service
152
+ return if !@parent_service || !@config[:load_errors]
153
+
154
+ @parent_service.errors.copy_from(
155
+ @errors,
156
+ break: @config[:self_break_on_error],
157
+ rollback: @config[:self_rollback_on_error]
158
+ )
159
+ end
160
+
161
+ def load_defaults_and_validate
162
+ @outputs.load_defaults
163
+ @arguments.load_defaults
164
+ @arguments.validate!
165
+ end
166
+
167
+ def log_header
168
+ log "🏎 Run service #{self.class}"
169
+ end
170
+
171
+ def within_transaction
172
+ if @config[:use_transactions] && defined?(ActiveRecord::Base)
173
+ ActiveRecord::Base.transaction(requires_new: true) { yield }
62
174
  else
63
175
  yield
64
176
  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