light-service 0.10.3 → 0.15.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 (49) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +6 -0
  4. data/.travis.yml +12 -11
  5. data/Appraisals +4 -4
  6. data/Gemfile +0 -2
  7. data/README.md +240 -34
  8. data/RELEASES.md +20 -1
  9. data/gemfiles/activesupport_4.gemfile +0 -1
  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/context.rb +6 -2
  21. data/lib/light-service/localization_adapter.rb +1 -1
  22. data/lib/light-service/organizer.rb +18 -0
  23. data/lib/light-service/organizer/with_reducer.rb +11 -6
  24. data/lib/light-service/organizer/with_reducer_factory.rb +11 -7
  25. data/lib/light-service/organizer/with_reducer_log_decorator.rb +5 -2
  26. data/lib/light-service/version.rb +1 -1
  27. data/light-service.gemspec +9 -4
  28. data/spec/acceptance/custom_log_from_organizer_spec.rb +60 -0
  29. data/spec/acceptance/fail_spec.rb +42 -16
  30. data/spec/acceptance/organizer/add_aliases_spec.rb +28 -0
  31. data/spec/acceptance/organizer/add_to_context_spec.rb +30 -0
  32. data/spec/acceptance/organizer/execute_spec.rb +1 -1
  33. data/spec/acceptance/organizer/iterate_spec.rb +7 -0
  34. data/spec/acceptance/organizer/reduce_if_spec.rb +38 -0
  35. data/spec/acceptance/organizer/reduce_until_spec.rb +6 -0
  36. data/spec/action_spec.rb +8 -0
  37. data/spec/lib/generators/action_generator_advanced_spec.rb +43 -0
  38. data/spec/lib/generators/action_generator_simple_spec.rb +37 -0
  39. data/spec/lib/generators/full_generator_test_blobs.rb +193 -0
  40. data/spec/lib/generators/organizer_generator_advanced_spec.rb +37 -0
  41. data/spec/lib/generators/organizer_generator_simple_spec.rb +37 -0
  42. data/spec/organizer_spec.rb +42 -14
  43. data/spec/sample/provides_free_shipping_action_spec.rb +1 -1
  44. data/spec/spec_helper.rb +7 -2
  45. data/spec/test_doubles.rb +77 -0
  46. metadata +104 -15
  47. data/gemfiles/activesupport_3.gemfile.lock +0 -76
  48. data/gemfiles/activesupport_4.gemfile.lock +0 -82
  49. data/gemfiles/activesupport_5.gemfile.lock +0 -82
@@ -1,6 +1,25 @@
1
1
  A brief list of new features and changes introduced with the specified version.
2
2
 
3
- ### 0.10.2
3
+ ### 0.15.0
4
+ * [Add Rails Generators](https://github.com/adomokos/light-service/pull/194) - LightService actions and organizers can be generated with generators
5
+ * [Add CodeCov](https://github.com/adomokos/light-service/pull/195) - Upload code coverage report to codecov.io
6
+ * [Remove ActiveSupport 3 checks](https://github.com/adomokos/light-service/pull/197) - They are unsupported, no need to tests them any more.
7
+
8
+
9
+ ### 0.14.0
10
+ * [Add 'organized_by' to context](https://github.com/adomokos/light-service/pull/192) - Context now have an #organized_by attribute
11
+
12
+ ### 0.13.0
13
+ * [Add 'add_to_context' and 'add_aliases'](https://github.com/adomokos/light-service/pull/172) - Updating Ruby compatibility, minor fixes
14
+
15
+ ### 0.12.0
16
+ * [Per organizer logger](https://github.com/adomokos/light-service/pull/162)
17
+ * [Fix 'fail_and_return!' not accepting 'error_code' option](https://github.com/adomokos/light-service/pull/168)
18
+
19
+ ### 0.11.0
20
+ * [Switch to 'each_with_object' in WithReducer](https://github.com/adomokos/light-service/pull/149).
21
+
22
+ ### 0.10.3
4
23
  * [Adding ContextFactory](https://github.com/adomokos/light-service/pull/147).
5
24
 
6
25
  ### 0.10.2
@@ -3,6 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "activesupport", "~> 4.0"
6
- gem "appraisal", "~> 2.0"
7
6
 
8
7
  gemspec :path => "../"
@@ -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? -%>
@@ -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
 
@@ -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)
@@ -115,8 +116,10 @@ module LightService
115
116
 
116
117
  def define_accessor_methods_for_keys(keys)
117
118
  return if keys.nil?
119
+
118
120
  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
@@ -34,7 +34,7 @@ module LightService
34
34
  scope = i18n_scope_from_class(action_class, type)
35
35
  options[:scope] = scope
36
36
 
37
- I18n.t(key, options)
37
+ I18n.t(key, **options)
38
38
  end
39
39
 
40
40
  def i18n_scope_from_class(action_class, type)
@@ -56,6 +56,24 @@ module LightService
56
56
  def with_callback(action, steps)
57
57
  WithCallback.run(self, action, steps)
58
58
  end
59
+
60
+ def log_with(logger)
61
+ @logger = logger
62
+ end
63
+
64
+ def logger
65
+ @logger
66
+ end
67
+
68
+ def add_to_context(**args)
69
+ args.map do |key, value|
70
+ execute(->(ctx) { ctx[key.to_sym] = value })
71
+ end
72
+ end
73
+
74
+ def add_aliases(args)
75
+ execute(->(ctx) { ctx.assign_aliases(ctx.aliases.merge(args)) })
76
+ end
59
77
  end
60
78
 
61
79
  module Macros