light-service 0.10.3 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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