light-services 0.5.4 → 2.0.0.beta1

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 (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