light-service 0.13.0 → 0.17.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 +4 -4
- data/.github/workflows/project-build.yml +28 -0
- data/.travis.yml +3 -9
- data/Appraisals +0 -4
- data/Gemfile +0 -2
- data/README.md +287 -51
- data/RELEASES.md +21 -2
- data/gemfiles/activesupport_5.gemfile +0 -1
- data/gemfiles/activesupport_6.gemfile +0 -1
- 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/action.rb +61 -4
- data/lib/light-service/context/key_verifier.rb +18 -1
- data/lib/light-service/context.rb +5 -3
- data/lib/light-service/errors.rb +1 -0
- data/lib/light-service/organizer/reduce_if_else.rb +21 -0
- data/lib/light-service/organizer/with_reducer.rb +12 -7
- data/lib/light-service/organizer/with_reducer_factory.rb +1 -1
- data/lib/light-service/organizer/with_reducer_log_decorator.rb +3 -0
- data/lib/light-service/organizer.rb +16 -3
- data/lib/light-service/version.rb +1 -1
- data/lib/light-service.rb +1 -0
- data/light-service.gemspec +6 -1
- data/spec/acceptance/after_actions_spec.rb +17 -0
- data/spec/acceptance/around_each_spec.rb +15 -0
- data/spec/acceptance/log_from_organizer_spec.rb +1 -1
- data/spec/acceptance/organizer/add_to_context_spec.rb +27 -0
- 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_else_spec.rb +60 -0
- data/spec/acceptance/organizer/reduce_if_spec.rb +6 -0
- data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
- data/spec/action_optional_expected_keys_spec.rb +82 -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 +5 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/test_doubles.rb +37 -0
- metadata +87 -9
- data/gemfiles/activesupport_3.gemfile +0 -8
- data/gemfiles/activesupport_4.gemfile +0 -8
- data/resources/orchestrators_deprecated.svg +0 -10
data/RELEASES.md
CHANGED
@@ -1,8 +1,27 @@
|
|
1
1
|
A brief list of new features and changes introduced with the specified version.
|
2
2
|
|
3
|
+
### 0.17.0
|
4
|
+
* [Fix around_action hook for nested actions](https://github.com/adomokos/light-service/pull/217)
|
5
|
+
* [Add ReduceIfElse macro](https://github.com/adomokos/light-service/pull/218)
|
6
|
+
* [Implement support for default values for optional expected keys](https://github.com/adomokos/light-service/pull/219)
|
7
|
+
* [Add light-service.js implementation to README](https://github.com/adomokos/light-service/pull/222)
|
8
|
+
|
9
|
+
### 0.16.0
|
10
|
+
* [Drop Ruby 2.4 support](https://github.com/adomokos/light-service/pull/207)
|
11
|
+
* [Fix callback current action](https://github.com/adomokos/light-service/pull/209)
|
12
|
+
* [Add Context accessors](https://github.com/adomokos/light-service/pull/211)
|
13
|
+
* [Switched to GH Actions from Travis CI](https://github.com/adomokos/light-service/pull/212)
|
14
|
+
|
15
|
+
### 0.15.0
|
16
|
+
* [Add Rails Generators](https://github.com/adomokos/light-service/pull/194) - LightService actions and organizers can be generated with generators
|
17
|
+
* [Add CodeCov](https://github.com/adomokos/light-service/pull/195) - Upload code coverage report to codecov.io
|
18
|
+
* [Remove ActiveSupport 3 checks](https://github.com/adomokos/light-service/pull/197) - They are unsupported, no need to tests them any more.
|
19
|
+
|
20
|
+
### 0.14.0
|
21
|
+
* [Add 'organized_by' to context](https://github.com/adomokos/light-service/pull/192) - Context now have an #organized_by attribute
|
22
|
+
|
3
23
|
### 0.13.0
|
4
|
-
* [Add 'add_to_context' and 'add_aliases'](https://github.com/adomokos/light-service/pull/172)
|
5
|
-
* Updating Ruby compatibility, minor fixes
|
24
|
+
* [Add 'add_to_context' and 'add_aliases'](https://github.com/adomokos/light-service/pull/172) - Updating Ruby compatibility, minor fixes
|
6
25
|
|
7
26
|
### 0.12.0
|
8
27
|
* [Per organizer logger](https://github.com/adomokos/light-service/pull/162)
|
@@ -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/action.rb
CHANGED
@@ -15,6 +15,12 @@ module LightService
|
|
15
15
|
|
16
16
|
module Macros
|
17
17
|
def expects(*args)
|
18
|
+
if expect_key_having_default?(args)
|
19
|
+
available_defaults[args.first] = args.last[:default]
|
20
|
+
|
21
|
+
args = [args.first]
|
22
|
+
end
|
23
|
+
|
18
24
|
expected_keys.concat(args)
|
19
25
|
end
|
20
26
|
|
@@ -30,8 +36,8 @@ module LightService
|
|
30
36
|
@promised_keys ||= []
|
31
37
|
end
|
32
38
|
|
33
|
-
def executed
|
34
|
-
define_singleton_method :execute do |context =
|
39
|
+
def executed(*_args, &block)
|
40
|
+
define_singleton_method :execute do |context = Context.make|
|
35
41
|
action_context = create_action_context(context)
|
36
42
|
return action_context if action_context.stop_processing?
|
37
43
|
|
@@ -43,7 +49,11 @@ module LightService
|
|
43
49
|
|
44
50
|
catch(:jump_when_failed) do
|
45
51
|
call_before_action(action_context)
|
46
|
-
|
52
|
+
|
53
|
+
execute_action(action_context, &block)
|
54
|
+
|
55
|
+
# Reset the stored action in case it was changed downstream
|
56
|
+
action_context.current_action = self
|
47
57
|
call_after_action(action_context)
|
48
58
|
end
|
49
59
|
end
|
@@ -63,8 +73,34 @@ module LightService
|
|
63
73
|
|
64
74
|
private
|
65
75
|
|
76
|
+
def execute_action(context)
|
77
|
+
if around_action_context?(context)
|
78
|
+
context.around_actions.call(context) do
|
79
|
+
yield(context)
|
80
|
+
context
|
81
|
+
end
|
82
|
+
else
|
83
|
+
yield(context)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def available_defaults
|
88
|
+
@available_defaults ||= {}
|
89
|
+
end
|
90
|
+
|
91
|
+
def expect_key_having_default?(key)
|
92
|
+
return false unless key.size == 2 && key.last.is_a?(Hash)
|
93
|
+
return true if key.last.key?(:default)
|
94
|
+
|
95
|
+
bad_key = key.last.keys.first
|
96
|
+
err_msg = "Specify defaults with a `default` key. You have #{bad_key}."
|
97
|
+
raise UnusableExpectKeyDefaultError, err_msg
|
98
|
+
end
|
99
|
+
|
66
100
|
def create_action_context(context)
|
67
|
-
|
101
|
+
usable_defaults(context).each do |ctx_key, default|
|
102
|
+
context[ctx_key] = extract_default(default, context)
|
103
|
+
end
|
68
104
|
|
69
105
|
LightService::Context.make(context)
|
70
106
|
end
|
@@ -73,6 +109,22 @@ module LightService
|
|
73
109
|
expected_keys + promised_keys
|
74
110
|
end
|
75
111
|
|
112
|
+
def missing_expected_keys(context)
|
113
|
+
expected_keys - context.keys
|
114
|
+
end
|
115
|
+
|
116
|
+
def usable_defaults(context)
|
117
|
+
available_defaults.slice(
|
118
|
+
*(missing_expected_keys(context) & available_defaults.keys)
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
def extract_default(default, context)
|
123
|
+
return default unless default.respond_to?(:call)
|
124
|
+
|
125
|
+
default.call(context)
|
126
|
+
end
|
127
|
+
|
76
128
|
def call_before_action(context)
|
77
129
|
invoke_callbacks(context[:_before_actions], context)
|
78
130
|
end
|
@@ -90,6 +142,11 @@ module LightService
|
|
90
142
|
|
91
143
|
context
|
92
144
|
end
|
145
|
+
|
146
|
+
def around_action_context?(context)
|
147
|
+
context.instance_of?(Context) &&
|
148
|
+
context.around_actions.respond_to?(:call)
|
149
|
+
end
|
93
150
|
end
|
94
151
|
end
|
95
152
|
end
|