light-services 0.6.0 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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