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.
- checksums.yaml +5 -5
- data/.github/workflows/project-build.yml +28 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +6 -0
- data/.travis.yml +7 -11
- data/Appraisals +4 -4
- data/Gemfile +0 -2
- data/README.md +257 -42
- data/RELEASES.md +21 -0
- data/gemfiles/activesupport_5.gemfile +0 -1
- data/gemfiles/{activesupport_3.gemfile → activesupport_6.gemfile} +1 -2
- data/lib/generators/light_service/action_generator.rb +90 -0
- data/lib/generators/light_service/generator_utils.rb +45 -0
- data/lib/generators/light_service/organizer_generator.rb +66 -0
- data/lib/generators/light_service/templates/action_spec_template.erb +31 -0
- data/lib/generators/light_service/templates/action_template.erb +30 -0
- data/lib/generators/light_service/templates/organizer_spec_template.erb +20 -0
- data/lib/generators/light_service/templates/organizer_template.erb +22 -0
- data/lib/light-service.rb +1 -0
- data/lib/light-service/action.rb +3 -0
- data/lib/light-service/context.rb +8 -4
- data/lib/light-service/context/key_verifier.rb +18 -1
- data/lib/light-service/localization_adapter.rb +1 -1
- data/lib/light-service/organizer.rb +27 -0
- data/lib/light-service/organizer/with_reducer.rb +8 -1
- data/lib/light-service/organizer/with_reducer_factory.rb +11 -7
- data/lib/light-service/organizer/with_reducer_log_decorator.rb +5 -2
- data/lib/light-service/version.rb +1 -1
- data/light-service.gemspec +10 -4
- data/spec/acceptance/after_actions_spec.rb +17 -0
- data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
- data/spec/acceptance/fail_spec.rb +42 -16
- data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
- data/spec/acceptance/organizer/add_to_context_spec.rb +57 -0
- data/spec/acceptance/organizer/execute_spec.rb +1 -1
- data/spec/acceptance/organizer/execute_with_add_to_context_spec.rb +28 -0
- data/spec/acceptance/organizer/iterate_spec.rb +7 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
- data/spec/action_spec.rb +8 -0
- data/spec/lib/generators/action_generator_advanced_spec.rb +43 -0
- data/spec/lib/generators/action_generator_simple_spec.rb +37 -0
- data/spec/lib/generators/full_generator_test_blobs.rb +193 -0
- data/spec/lib/generators/organizer_generator_advanced_spec.rb +37 -0
- data/spec/lib/generators/organizer_generator_simple_spec.rb +37 -0
- data/spec/organizer_spec.rb +21 -0
- data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -2
- data/spec/test_doubles.rb +47 -0
- metadata +111 -21
- data/gemfiles/activesupport_3.gemfile.lock +0 -76
- data/gemfiles/activesupport_4.gemfile +0 -8
- data/gemfiles/activesupport_4.gemfile.lock +0 -82
- data/gemfiles/activesupport_5.gemfile.lock +0 -82
- 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
|
|
@@ -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
data/lib/light-service/action.rb
CHANGED
@@ -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
|
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.
|
118
|
-
|
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
|