light-service 0.11.0 → 0.16.0

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 (55) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/project-build.yml +28 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +6 -0
  5. data/.travis.yml +7 -11
  6. data/Appraisals +4 -4
  7. data/Gemfile +0 -2
  8. data/README.md +257 -42
  9. data/RELEASES.md +21 -0
  10. data/gemfiles/activesupport_5.gemfile +0 -1
  11. data/gemfiles/{activesupport_3.gemfile → activesupport_6.gemfile} +1 -2
  12. data/lib/generators/light_service/action_generator.rb +90 -0
  13. data/lib/generators/light_service/generator_utils.rb +45 -0
  14. data/lib/generators/light_service/organizer_generator.rb +66 -0
  15. data/lib/generators/light_service/templates/action_spec_template.erb +31 -0
  16. data/lib/generators/light_service/templates/action_template.erb +30 -0
  17. data/lib/generators/light_service/templates/organizer_spec_template.erb +20 -0
  18. data/lib/generators/light_service/templates/organizer_template.erb +22 -0
  19. data/lib/light-service.rb +1 -0
  20. data/lib/light-service/action.rb +3 -0
  21. data/lib/light-service/context.rb +8 -4
  22. data/lib/light-service/context/key_verifier.rb +18 -1
  23. data/lib/light-service/localization_adapter.rb +1 -1
  24. data/lib/light-service/organizer.rb +27 -0
  25. data/lib/light-service/organizer/with_reducer.rb +8 -1
  26. data/lib/light-service/organizer/with_reducer_factory.rb +11 -7
  27. data/lib/light-service/organizer/with_reducer_log_decorator.rb +5 -2
  28. data/lib/light-service/version.rb +1 -1
  29. data/light-service.gemspec +10 -4
  30. data/spec/acceptance/after_actions_spec.rb +17 -0
  31. data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
  32. data/spec/acceptance/fail_spec.rb +42 -16
  33. data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
  34. data/spec/acceptance/organizer/add_to_context_spec.rb +57 -0
  35. data/spec/acceptance/organizer/execute_spec.rb +1 -1
  36. data/spec/acceptance/organizer/execute_with_add_to_context_spec.rb +28 -0
  37. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  38. data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
  39. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  40. data/spec/action_spec.rb +8 -0
  41. data/spec/lib/generators/action_generator_advanced_spec.rb +43 -0
  42. data/spec/lib/generators/action_generator_simple_spec.rb +37 -0
  43. data/spec/lib/generators/full_generator_test_blobs.rb +193 -0
  44. data/spec/lib/generators/organizer_generator_advanced_spec.rb +37 -0
  45. data/spec/lib/generators/organizer_generator_simple_spec.rb +37 -0
  46. data/spec/organizer_spec.rb +21 -0
  47. data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
  48. data/spec/spec_helper.rb +7 -2
  49. data/spec/test_doubles.rb +47 -0
  50. metadata +111 -21
  51. data/gemfiles/activesupport_3.gemfile.lock +0 -76
  52. data/gemfiles/activesupport_4.gemfile +0 -8
  53. data/gemfiles/activesupport_4.gemfile.lock +0 -82
  54. data/gemfiles/activesupport_5.gemfile.lock +0 -82
  55. data/resources/orchestrators_deprecated.svg +0 -10
data/RELEASES.md CHANGED
@@ -1,5 +1,26 @@
1
1
  A brief list of new features and changes introduced with the specified version.
2
2
 
3
+ ### 0.16.0
4
+ * [Drop Ruby 2.4 support](https://github.com/adomokos/light-service/pull/207)
5
+ * [Fix callback current action](https://github.com/adomokos/light-service/pull/209)
6
+ * [Add Context accessors](https://github.com/adomokos/light-service/pull/211)
7
+ * [Switched to GH Actions from Travis CI](https://github.com/adomokos/light-service/pull/212)
8
+
9
+ ### 0.15.0
10
+ * [Add Rails Generators](https://github.com/adomokos/light-service/pull/194) - LightService actions and organizers can be generated with generators
11
+ * [Add CodeCov](https://github.com/adomokos/light-service/pull/195) - Upload code coverage report to codecov.io
12
+ * [Remove ActiveSupport 3 checks](https://github.com/adomokos/light-service/pull/197) - They are unsupported, no need to tests them any more.
13
+
14
+ ### 0.14.0
15
+ * [Add 'organized_by' to context](https://github.com/adomokos/light-service/pull/192) - Context now have an #organized_by attribute
16
+
17
+ ### 0.13.0
18
+ * [Add 'add_to_context' and 'add_aliases'](https://github.com/adomokos/light-service/pull/172) - Updating Ruby compatibility, minor fixes
19
+
20
+ ### 0.12.0
21
+ * [Per organizer logger](https://github.com/adomokos/light-service/pull/162)
22
+ * [Fix 'fail_and_return!' not accepting 'error_code' option](https://github.com/adomokos/light-service/pull/168)
23
+
3
24
  ### 0.11.0
4
25
  * [Switch to 'each_with_object' in WithReducer](https://github.com/adomokos/light-service/pull/149).
5
26
 
@@ -3,6 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "activesupport", "~> 5.0"
6
- gem "appraisal", "~> 2.0"
7
6
 
8
7
  gemspec :path => "../"
@@ -2,7 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 3.0"
6
- gem "appraisal", "~> 2.0"
5
+ gem "activesupport", "~> 6.0"
7
6
 
8
7
  gemspec :path => "../"
@@ -0,0 +1,90 @@
1
+ require_relative './generator_utils'
2
+
3
+ module LightService
4
+ module Generators
5
+ class ActionGenerator < Rails::Generators::Base
6
+ include GeneratorUtils
7
+
8
+ argument :name, :type => :string
9
+ argument :keys,
10
+ :type => :hash,
11
+ :default => { "expects" => '', "promises" => '' },
12
+ :banner => "expects:one,thing promises:something,else"
13
+
14
+ class_option :dir,
15
+ :type => :string,
16
+ :default => "actions",
17
+ :desc => "Path to write actions to"
18
+
19
+ class_option :tests,
20
+ :type => :boolean,
21
+ :default => true,
22
+ :desc => "Generate tests (currently only RSpec supported)"
23
+
24
+ class_option :roll_back,
25
+ :type => :boolean,
26
+ :default => true,
27
+ :desc => "Add a roll back block"
28
+
29
+ source_root File.expand_path('templates', __dir__)
30
+
31
+ desc <<~DESCRIPTION
32
+ Description:
33
+ Will create the boilerplate for an action. Pass it an action name, e.g.
34
+ foo_bar, or FooBar - will create FooBar in app/actions/foo_bar.rb
35
+ foo/bar, or Foo::Bar - will create Foo::Bar in app/actions/foo/bar.rb
36
+
37
+ Expects & Promises:
38
+ Specify a list of expected context keys by passing expects and a comma separated
39
+ list of keys. Adds keys to the `expects` list, creates convenience variables in
40
+ the action, and generates a stub context in generated specs.
41
+
42
+ expects:foo,bar,baz
43
+
44
+ Specify promised context keys in the same manner as 'expects' above. This adds
45
+ keys to the `promises` list, and creates stub expectations in generated specs.
46
+
47
+ promises:quux,quark
48
+
49
+ Options:
50
+ Skip rspec test creation with --no-tests
51
+ Skip ActionRollback creation with --no-roll-back
52
+ Write actions to a specified dir with --dir="services". Default is "actions" in app/actions
53
+
54
+ Full Example:
55
+ rails g light_service:action My::Awesome::Action expects:foo,bar promises:baz,qux
56
+ DESCRIPTION
57
+
58
+ # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
59
+ def create_action
60
+ gen_vals = create_required_gen_vals_from(name)
61
+
62
+ @module_path = gen_vals[:module_path]
63
+ @class_name = gen_vals[:class_name]
64
+ @full_class_name = gen_vals[:full_class_name]
65
+ @expects = keys["expects"].to_s.downcase.split(',')
66
+ @promises = keys["promises"].to_s.downcase.split(',')
67
+
68
+ file_name = gen_vals[:file_name]
69
+ file_path = gen_vals[:file_path]
70
+
71
+ root_dir = options.dir.downcase
72
+ action_dir = File.join('app', root_dir, *file_path)
73
+ action_file = "#{action_dir}/#{file_name}"
74
+
75
+ make_nested_dir(action_dir)
76
+ template("action_template.erb", action_file)
77
+
78
+ return unless must_gen_tests?
79
+
80
+ spec_dir = File.join('spec', root_dir, *file_path)
81
+ spec_file_name = gen_vals[:spec_file_name]
82
+ spec_file = "#{spec_dir}/#{spec_file_name}"
83
+
84
+ make_nested_dir(spec_dir)
85
+ template("action_spec_template.erb", spec_file)
86
+ end
87
+ # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,45 @@
1
+ module LightService
2
+ module Generators
3
+ module GeneratorUtils
4
+ def make_nested_dir(dir)
5
+ FileUtils.mkdir_p(dir)
6
+ end
7
+
8
+ def supported_test_frameworks
9
+ %i[rspec]
10
+ end
11
+
12
+ def test_framework_supported?
13
+ supported_test_frameworks.include? test_framework
14
+ end
15
+
16
+ # Don't know a better way to get to this value, unfortunately.
17
+ def test_framework
18
+ # Rails.application.config.generators.options[:rails][:test_framework]
19
+ # When/if Minitest is supported, this will need to be updated to detect
20
+ # the selected test framework, and switch templates accordingly
21
+ :rspec
22
+ end
23
+
24
+ def must_gen_tests?
25
+ options.tests? && test_framework_supported?
26
+ end
27
+
28
+ # rubocop:disable Metrics/AbcSize
29
+ def create_required_gen_vals_from(name)
30
+ path_parts = name.underscore.split('/')
31
+
32
+ {
33
+ :path_parts => path_parts,
34
+ :file_path => path_parts.reverse.drop(1).reverse,
35
+ :module_path => path_parts.reverse.drop(1).reverse.join('/').classify,
36
+ :class_name => path_parts.last.classify,
37
+ :file_name => "#{path_parts.last}.rb",
38
+ :spec_file_name => "#{path_parts.last}_spec.rb",
39
+ :full_class_name => name.classify
40
+ }
41
+ end
42
+ # rubocop:enable Metrics/AbcSize
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,66 @@
1
+ require_relative './generator_utils'
2
+
3
+ module LightService
4
+ module Generators
5
+ class OrganizerGenerator < Rails::Generators::Base
6
+ include GeneratorUtils
7
+
8
+ argument :name, :type => :string
9
+
10
+ class_option :dir,
11
+ :type => :string,
12
+ :default => "organizers",
13
+ :desc => "Path to write organizers to"
14
+
15
+ class_option :tests,
16
+ :type => :boolean,
17
+ :default => true,
18
+ :desc => "Generate tests (currently only RSpec supported)"
19
+
20
+ source_root File.expand_path('templates', __dir__)
21
+
22
+ desc <<~DESCRIPTION
23
+ Description:
24
+ Will create the boilerplate for an organizer. Pass it an organizer name, e.g.
25
+ thing_maker, or ThingMaker - will create ThingMaker in app/organizers/thing_maker.rb
26
+ thing/maker, or Thing::Maker - will create Thing::Maker in app/organizers/thing/maker.rb
27
+
28
+ Options:
29
+ Skip rspec test creation with --no-tests
30
+ Write organizers to a specified dir with --dir="workflows". Default is "organizers" in app/organizers
31
+
32
+ Full Example:
33
+ rails g light_service:organizer My::Awesome::Organizer
34
+ DESCRIPTION
35
+
36
+ # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
37
+ def create_organizer
38
+ gen_vals = create_required_gen_vals_from(name)
39
+
40
+ @module_path = gen_vals[:module_path]
41
+ @class_name = gen_vals[:class_name]
42
+ @full_class_name = gen_vals[:full_class_name]
43
+
44
+ file_name = gen_vals[:file_name]
45
+ file_path = gen_vals[:file_path]
46
+
47
+ root_dir = options.dir.downcase
48
+ organizer_dir = File.join('app', root_dir, *file_path)
49
+ organizer_file = "#{organizer_dir}/#{file_name}"
50
+
51
+ make_nested_dir(organizer_dir)
52
+ template("organizer_template.erb", organizer_file)
53
+
54
+ return unless must_gen_tests?
55
+
56
+ spec_dir = File.join('spec', root_dir, *file_path)
57
+ spec_file_name = gen_vals[:spec_file_name]
58
+ spec_file = "#{spec_dir}/#{spec_file_name}"
59
+
60
+ make_nested_dir(spec_dir)
61
+ template("organizer_spec_template.erb", spec_file)
62
+ end
63
+ # rubocop:enable Metrics/MethodLength,Metrics/AbcSize
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= @full_class_name %>, type: :action do
6
+ subject { described_class.execute(ctx) }
7
+
8
+ let(:ctx) do
9
+ {
10
+ <%- if @expects.any? -%>
11
+ <%- @expects.each do |key| -%>
12
+ <%= key %>: nil,
13
+ <%- end -%>
14
+ <%- end -%>
15
+ }
16
+ end
17
+
18
+ context "when executed" do
19
+ xit "is expected to be successful" do
20
+ expect(subject).to be_a_success
21
+ end
22
+ <%- if @promises.any? -%>
23
+ <%- @promises.each do |key| -%>
24
+
25
+ xit "is expected to promise '<%= key %>'" do
26
+ expect(subject.<%= key %>).to eq Some<%= key.classify %>Class
27
+ end
28
+ <%- end -%>
29
+ <%- end -%>
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ <%- indent = !@module_path.empty? ? ' ' : '' -%>
2
+ # frozen_string_literal: true
3
+
4
+ <%= "module #{@module_path}\n" unless @module_path.empty? -%>
5
+ <%= "#{indent}class #{@class_name}" %>
6
+ <%= indent %>extend ::LightService::Action
7
+
8
+ <%- if @expects.any? -%>
9
+ <%= indent %>expects <%= @expects.map { |k| ":#{k}" }.join(', ') %>
10
+ <%- end -%>
11
+ <%- if @promises.any? -%>
12
+ <%= indent %>promises <%= @promises.map { |k| ":#{k}" }.join(', ') %>
13
+ <%- end -%>
14
+ <%- if (@expects + @promises).any? -%>
15
+
16
+ <%- end -%>
17
+ <%= indent %>executed do |ctx|
18
+ <%- if @expects.any? -%>
19
+ <%- @expects.each do |key| -%>
20
+ <%= indent %><%= key %> = ctx.<%= key %>
21
+ <%- end -%>
22
+ <%- end -%>
23
+ <%= indent %>end
24
+ <%- if options.roll_back -%>
25
+
26
+ <%= indent %>rolled_back do |ctx|
27
+ <%= indent %>end
28
+ <%- end -%>
29
+ <%= indent %>end
30
+ <%= 'end' unless @module_path.empty? -%>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe <%= @full_class_name %>, type: :organizer do
6
+ subject { described_class.call(ctx) }
7
+
8
+ let(:ctx) do
9
+ {
10
+ #foo: 'something foo',
11
+ #bar: { baz: qux },
12
+ }
13
+ end
14
+
15
+ context "when called" do
16
+ xit "is expected to be successful" do
17
+ expect(subject).to be_a_success
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ <%- indent = !@module_path.empty? ? ' ' : '' -%>
2
+ # frozen_string_literal: true
3
+
4
+ <%= "module #{@module_path}\n" unless @module_path.empty? -%>
5
+ <%= "#{indent}class #{@class_name}" %>
6
+ <%= indent %>extend ::LightService::Organizer
7
+
8
+ <%= indent %>def self.call(params)
9
+ <%= indent %> with(
10
+ <%= indent %> #foo: params[:foo],
11
+ <%= indent %> #bar: params[:bar]
12
+ <%= indent %> ).reduce(actions)
13
+ <%= indent %>end
14
+
15
+ <%= indent %>def self.actions
16
+ <%= indent %> [
17
+ <%= indent %> #<%= "#{@module_path}::" if @module_path.present? %>OneAction,
18
+ <%= indent %> #<%= "#{@module_path}::" if @module_path.present? %>TwoAction,
19
+ <%= indent %> ]
20
+ <%= indent %>end
21
+ <%= indent %>end
22
+ <%= 'end' unless @module_path.empty? -%>
data/lib/light-service.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'logger'
2
+ require 'active_support/core_ext/string'
2
3
 
3
4
  require 'light-service/version'
4
5
 
@@ -44,6 +44,9 @@ module LightService
44
44
  catch(:jump_when_failed) do
45
45
  call_before_action(action_context)
46
46
  yield(action_context)
47
+
48
+ # Reset the stored action in case it was changed downstream
49
+ action_context.current_action = self
47
50
  call_after_action(action_context)
48
51
  end
49
52
  end
@@ -8,7 +8,7 @@ module LightService
8
8
 
9
9
  # rubocop:disable ClassLength
10
10
  class Context < Hash
11
- attr_accessor :message, :error_code, :current_action
11
+ attr_accessor :message, :error_code, :current_action, :organized_by
12
12
 
13
13
  def initialize(context = {},
14
14
  outcome = Outcomes::SUCCESS,
@@ -18,6 +18,7 @@ module LightService
18
18
  @message = message
19
19
  @error_code = error_code
20
20
  @skip_remaining = false
21
+
21
22
  context.to_hash.each { |k, v| self[k] = v }
22
23
  end
23
24
 
@@ -88,7 +89,7 @@ module LightService
88
89
 
89
90
  def fail_and_return!(*args)
90
91
  fail!(*args)
91
- throw(:jump_when_failed, *args)
92
+ throw(:jump_when_failed)
92
93
  end
93
94
 
94
95
  def fail_with_rollback!(message = nil, error_code = nil)
@@ -114,9 +115,11 @@ module LightService
114
115
  end
115
116
 
116
117
  def define_accessor_methods_for_keys(keys)
117
- return if keys.nil?
118
- keys.each do |key|
118
+ return if keys.blank?
119
+
120
+ Array(keys).each do |key|
119
121
  next if respond_to?(key.to_sym)
122
+
120
123
  define_singleton_method(key.to_s) { fetch(key) }
121
124
  define_singleton_method("#{key}=") { |value| self[key] = value }
122
125
  end
@@ -161,6 +164,7 @@ module LightService
161
164
 
162
165
  def check_nil(value)
163
166
  return 'nil' unless value
167
+
164
168
  "'#{value}'"
165
169
  end
166
170
  end
@@ -111,7 +111,24 @@ module LightService
111
111
  end
112
112
 
113
113
  def reserved_keys
114
- %i[message error_code current_action].freeze
114
+ %i[message error_code current_action organized_by].freeze
115
+ end
116
+ end
117
+
118
+ class ReservedKeysViaOrganizerVerifier < ReservedKeysVerifier
119
+ def initialize(context_data)
120
+ @context = LightService::Context.make(context_data)
121
+ end
122
+
123
+ def violated_keys
124
+ context.keys.map(&:to_sym) & reserved_keys
125
+ end
126
+
127
+ def error_message
128
+ <<~ERR
129
+ reserved keys cannot be added to the context
130
+ reserved key: [#{format_keys(violated_keys)}]
131
+ ERR
115
132
  end
116
133
  end
117
134
  end