light-services 0.5.4 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.github/config/rubocop_linter_action.yml +4 -0
  3. data/.github/workflows/ci.yml +63 -0
  4. data/.gitignore +3 -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 +127 -27
  17. data/lib/light/services/base_with_context.rb +32 -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 +77 -34
  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 +62 -0
  29. data/lib/light/services/version.rb +1 -1
  30. data/light-services.gemspec +21 -24
  31. metadata +34 -127
  32. data/.codeclimate.yml +0 -16
  33. data/.ruby-gemset +0 -1
  34. data/.travis.yml +0 -29
  35. data/Appraisals +0 -21
  36. data/gemfiles/rails_4_0.gemfile +0 -7
  37. data/gemfiles/rails_4_0.gemfile.lock +0 -115
  38. data/gemfiles/rails_4_1.gemfile +0 -7
  39. data/gemfiles/rails_4_1.gemfile.lock +0 -119
  40. data/gemfiles/rails_4_2.gemfile +0 -7
  41. data/gemfiles/rails_4_2.gemfile.lock +0 -146
  42. data/gemfiles/rails_5_0.gemfile +0 -7
  43. data/gemfiles/rails_5_0.gemfile.lock +0 -150
  44. data/gemfiles/rails_5_1.gemfile +0 -7
  45. data/gemfiles/rails_5_1.gemfile.lock +0 -150
  46. data/lib/light/services/callbacks.rb +0 -54
  47. data/lib/light/services/outputs.rb +0 -70
  48. 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
 
@@ -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,57 +1,157 @@
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
+ # Steps
30
+ step :load_defaults_and_validate
10
31
 
11
32
  # Getters
12
- attr_reader :errors, :warnings
33
+ attr_reader :outputs, :arguments, :errors, :warnings
13
34
 
14
- def initialize(args = {})
15
- @args = args
35
+ def initialize(args = {}, config = {}, parent_service = nil)
36
+ @config = Light::Services.config.merge(config)
37
+ @parent_service = parent_service
16
38
 
17
- initialize_params
18
- initialize_outputs
39
+ @outputs = Collection::Outputs.new(self)
40
+ @arguments = Collection::Arguments.new(self, args)
19
41
 
20
- @errors = Light::Services::Messages.new
21
- @warnings = Light::Services::Messages.new
22
- end
42
+ @launched_steps = []
23
43
 
24
- def call
25
- run_service
44
+ initialize_errors
45
+ initialize_warnings
26
46
  end
27
47
 
28
48
  def success?
29
- errors.blank?
49
+ !errors?
50
+ end
51
+
52
+ def failed?
53
+ errors?
30
54
  end
31
55
 
32
- def any_warnings?
33
- warnings.any?
56
+ def errors?
57
+ @errors.any?
58
+ end
59
+
60
+ def warnings?
61
+ @warnings.any?
62
+ end
63
+
64
+ def call
65
+ run_steps
66
+ run_steps_with_always
67
+
68
+ copy_warnings_to_parent_service
69
+ copy_errors_to_parent_service
34
70
  end
35
71
 
36
72
  class << self
37
- def call(args = {})
73
+ # TODO: Create `run!`
74
+ def run(args = {})
38
75
  new(args).tap(&:call)
39
76
  end
40
77
 
41
- alias run call
78
+ def with(service_or_config = {}, config = {})
79
+ service = service_or_config.is_a?(Hash) ? nil : service_or_config
80
+ config = service ? config : service_or_config
81
+
82
+ BaseWithContext.new(self, service, config)
83
+ end
42
84
  end
43
85
 
44
86
  private
45
87
 
46
- # Getters
47
- attr_reader :args
48
-
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?
88
+ def initialize_errors
89
+ @errors = Messages.new(
90
+ break_on_add: @config[:break_on_error],
91
+ raise_on_add: @config[:raise_on_error],
92
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_error]
93
+ )
94
+ end
95
+
96
+ def initialize_warnings
97
+ @warnings = Messages.new(
98
+ break_on_add: @config[:break_on_warning],
99
+ raise_on_add: @config[:raise_on_warning],
100
+ rollback_on_add: @config[:use_transactions] && @config[:rollback_on_warning]
101
+ )
102
+ end
103
+
104
+ def run_steps
105
+ within_transaction do
106
+ self.class.steps.each do |name, step|
107
+ @launched_steps << name if step.run(self)
108
+
109
+ break if @errors.break? || @warnings.break?
110
+ end
111
+ end
112
+ end
113
+
114
+ # Run steps with parameter `always` if they weren't launched because of errors/warnings
115
+ def run_steps_with_always
116
+ self.class.steps.each do |name, step|
117
+ next if !step.always || @launched_steps.include?(name)
118
+
119
+ @launched_steps << name if step.run(self)
120
+ end
121
+ end
122
+
123
+ def copy_warnings_to_parent_service
124
+ return if !@parent_service || !@config[:load_warnings]
125
+
126
+ @parent_service.warnings.copy_from(
127
+ @warnings,
128
+ break: @config[:self_break_on_warning],
129
+ rollback: @config[:self_rollback_on_warning]
130
+ )
131
+ end
132
+
133
+ def copy_errors_to_parent_service
134
+ return if !@parent_service || !@config[:load_errors]
135
+
136
+ @parent_service.errors.copy_from(
137
+ @errors,
138
+ break: @config[:self_break_on_error],
139
+ rollback: @config[:self_rollback_on_error]
140
+ )
141
+ end
142
+
143
+ def load_defaults_and_validate
144
+ @outputs.load_defaults
145
+ @arguments.load_defaults
146
+ @arguments.validate!
147
+ end
148
+
149
+ def within_transaction
150
+ if @config[:use_transactions] && defined?(ActiveRecord::Base)
151
+ ActiveRecord::Base.transaction(requires_new: true) { yield }
152
+ else
153
+ yield
154
+ end
55
155
  end
56
156
  end
57
157
  end
@@ -0,0 +1,32 @@
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
+ return args unless @parent_service
26
+
27
+ # TODO: Do we need `.dup` here?
28
+ @parent_service.arguments.extend_with_context(args)
29
+ end
30
+ end
31
+ end
32
+ 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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Collection to store arguments and outputs values
4
+ module Light
5
+ module Services
6
+ module Collection
7
+ class Base
8
+ # Includes
9
+ extend Forwardable
10
+
11
+ # Settings
12
+ def_delegators :@storage, :key?, :to_h
13
+
14
+ def initialize(instance, storage = {})
15
+ @instance = instance
16
+ @storage = storage
17
+
18
+ return if storage.is_a?(Hash)
19
+
20
+ raise Light::Services::ArgTypeError, "#{instance.class} - arguments must be a Hash"
21
+ end
22
+
23
+ def set(key, value)
24
+ @storage[key] = value
25
+ end
26
+
27
+ def get(key)
28
+ @storage[key]
29
+ end
30
+
31
+ def load_defaults
32
+ settings_collection.each do |name, settings|
33
+ next if !settings.default_exists || key?(name)
34
+
35
+ set(name, deep_dup(settings.default))
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def deep_dup(object)
42
+ Marshal.load(Marshal.dump(object))
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end