light-service 0.14.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10ae707b6938f2898615668928728eb8e562fb9ed2f0ecf17e50deff76226a44
4
- data.tar.gz: c870f719d663dd1aec96d895196f57b2caaae1cb9e61d073bd5fcef7ce33cff5
3
+ metadata.gz: f400829da41766f56e7f0cb9cfe95aa028ef8ab34bf0bc5794bfafcff3cdcfb1
4
+ data.tar.gz: 41dbd504ac39a0804eb445b4af086dbcf9986264077994277e17fde98b7bdc3d
5
5
  SHA512:
6
- metadata.gz: 3062b1c4d10519f2f60c527ecedee3647d13c85419e5ad38db7ec7046b0cba68b122d044a1045724bc034c64d6e3b4afc3482cfa9bb8a87ee77ef5f847e0e728
7
- data.tar.gz: c240aec3f4244f7ebb17f06c98e21c03495c1718d34dafcd862209fc95323bde27db6e9144e38ad285d0b83b2182713d5d1690dc43f28c95b591dd9d56aec7b7
6
+ metadata.gz: e9a1148b82b6e1a0813400b0d23b013ff6f06965f3204bb3fa95101adc8a749223dd334887f7543622b4a829fd17f190c21dd5f601df3aecde8d8451459a76af
7
+ data.tar.gz: f0aec690e94896dcc13d6caa774003e49ddfa6b74c5acf9272fb7b9d3710d28db4a29601e725e3729deba820013c62b740739d5c6052c4c5ae93fd885cec6fda
@@ -20,7 +20,6 @@ script:
20
20
  - bundle exec rubocop
21
21
 
22
22
  gemfile:
23
- - gemfiles/activesupport_3.gemfile
24
23
  - gemfiles/activesupport_4.gemfile
25
24
  - gemfiles/activesupport_5.gemfile
26
25
  - gemfiles/activesupport_6.gemfile
data/Appraisals CHANGED
@@ -1,7 +1,3 @@
1
- appraise "activesupport-3" do
2
- gem "activesupport", "~> 3.0"
3
- end
4
-
5
1
  appraise "activesupport-4" do
6
2
  gem "activesupport", "~> 4.0"
7
3
  end
data/Gemfile CHANGED
@@ -2,5 +2,3 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in light_service.gemspec
4
4
  gemspec
5
-
6
- gem 'appraisal', '~> 2.0'
data/README.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/light-service.svg)](https://rubygems.org/gems/light-service)
4
4
  [![Build Status](https://secure.travis-ci.org/adomokos/light-service.svg)](http://travis-ci.org/adomokos/light-service)
5
+ [![codecov](https://codecov.io/gh/adomokos/light-service/branch/master/graph/badge.svg)](https://codecov.io/gh/adomokos/light-service)
5
6
  [![Code Climate](https://codeclimate.com/github/adomokos/light-service.svg)](https://codeclimate.com/github/adomokos/light-service)
6
7
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT)
7
8
  [![Download Count](https://ruby-gem-downloads-badge.herokuapp.com/light-service?type=total)](https://rubygems.org/gems/light-service)
8
9
 
9
- <br><br>
10
+ <br>
10
11
 
11
12
  ![Orchestrators-Deprecated](resources/orchestrators_deprecated.svg)
12
13
  <br>Version 0.9.0 deprecates Orchestrators and moves all their functionalities into Organizers. Please check out [this PR](https://github.com/adomokos/light-service/pull/132) to see the changes.
@@ -15,6 +16,11 @@
15
16
 
16
17
  ## Table of Content
17
18
  * [Why LightService?](#why-lightservice)
19
+ * [Getting Started](#getting-started)
20
+ * [Requirements](#requirements)
21
+ * [Installation](#installation)
22
+ * [Your first action](#your-first-action)
23
+ * [Your first organizer](#your-first-organizer)
18
24
  * [Stopping the Series of Actions](#stopping-the-series-of-actions)
19
25
  * [Failing the Context](#failing-the-context)
20
26
  * [Skipping the Rest of the Actions](#skipping-the-rest-of-the-actions)
@@ -27,7 +33,7 @@
27
33
  * [Localizing Messages](#localizing-messages)
28
34
  * [Orchestrator Logic in Organizers](#orchestrator-logic-in-organizers)
29
35
  * [ContextFactory for Faster Action Testing](#contextfactory-for-faster-action-testing)
30
-
36
+ * [Rails support](#rails-support)
31
37
 
32
38
  ## Why LightService?
33
39
 
@@ -175,7 +181,143 @@ end
175
181
  I gave a [talk at RailsConf 2013](http://www.adomokos.com/2013/06/simple-and-elegant-rails-code-with.html) on
176
182
  simple and elegant Rails code where I told the story of how LightService was extracted from the projects I had worked on.
177
183
 
184
+ ## Getting started
185
+
186
+ ### Requirements
187
+
188
+ This gem requires ruby 2.x. Use of [generators](#rails-support) requires Rails 5+ (tested on Rails 5.x & 6.x only. Will probably work on
189
+ Rails versions as old as 3.2)
190
+
191
+ ### Installation
192
+
193
+ In your Gemfile:
194
+
195
+ ```ruby
196
+ gem 'light-service'
197
+ ```
198
+
199
+ And then
200
+
201
+ ```shell
202
+ bundle install
203
+ ```
204
+
205
+ Or install it yourself as:
206
+
207
+ ```shell
208
+ gem install light-service
209
+ ```
210
+
211
+ ### Your first action
212
+
213
+ LightService's building blocks are actions that are normally composed within an organizer, but can be run independently.
214
+ Let's make a simple greeter action. Each action can take an optional list of expected inputs and promised outputs. If
215
+ these are specified and missing at action start and stop respectively, an exception will be thrown.
216
+
217
+ ```ruby
218
+ class GreetsPerson
219
+ extend ::LightService::Action
220
+
221
+ expects :name
222
+ promises :greeting
223
+
224
+ executed do |context|
225
+ context.greeting = "Hey there, #{name}. You enjoying LightService so far?"
226
+ end
227
+ end
228
+ ```
178
229
 
230
+ When an action is run, you have access to its returned context, and the status of the action. You can invoke an
231
+ action by calling `.execute` on its class with `key: value` arguments, and inspect its status and context like so:
232
+
233
+ ```ruby
234
+ outcome = GreetsPerson.execute(name: "Han")
235
+
236
+ if outcome.success?
237
+ puts outcome.greeting # which was a promised context value
238
+ elsif outcome.failure?
239
+ puts "Rats... I can't say hello to you"
240
+ end
241
+ ```
242
+
243
+ You will notice that actions are set up to promote simplicity, i.e. they either succeed or fail, and they have
244
+ very clear inputs and outputs. Ideally, they should do [exactly one thing](https://en.wikipedia.org/wiki/Single-responsibility_principle). This makes them as easy to test as unit tests.
245
+
246
+ ### Your first organizer
247
+
248
+ LightService provides a facility to compose actions using organizers. This is great when you have a business process
249
+ to execute that has multiple steps. By composing actions that do exactly one thing, you can sequence simple
250
+ actions together to perform complex multi-step business processes in a clear manner that is very easy
251
+ to reason about.
252
+
253
+ There are advanced ways to sequence actions that can be found later in the README, but we'll keep this simple for now.
254
+ First, let's add a second action that we can sequence to run after the `GreetsPerson` action from above:
255
+
256
+ ```ruby
257
+ class RandomlyAwardsPrize
258
+ extend ::LightService::Action
259
+
260
+ expects :name, :greeting
261
+ promises :did_i_win
262
+
263
+ executed do |context|
264
+ prize_num = "#{context.name}__#{context.greeting}".length
265
+ prizes = ["jelly beans", "ice cream", "pie"]
266
+ did_i_win = rand((1..prize_num)) % 7 == 0
267
+ did_i_lose = rand((1..prize_num)) % 13 == 0
268
+
269
+ if did_i_lose
270
+ # When failing, send a message as an argument, readable from the return context
271
+ context.fail!("you are exceptionally unlucky")
272
+ else
273
+ # You can specify 'optional' context items by treating context like a hash.
274
+ # Useful for when you may or may not be returning extra data. Ideally, selecting
275
+ # a prize should be a separate action that is only run if you win.
276
+ context[:prize] = "lifetime supply of #{prizes.sample}" if did_i_win
277
+ context.did_i_win = did_i_win
278
+ end
279
+ end
280
+ end
281
+ ```
282
+
283
+ And here's the organizer that ties the two together. You implement a `call` class method that takes some arguments and
284
+ from there sends them to `with` in `key: value` format which forms the initial state of the context. From there, chain
285
+ `reduce` to `with` and send it a list of action class names in sequence. The organizer will call each action, one
286
+ after the other, and build up the context as it goes along.
287
+
288
+ ```ruby
289
+ class WelcomeAPotentiallyLuckyPerson
290
+ extend LightService::Organizer
291
+
292
+ def self.call(name)
293
+ with(:name => name).reduce(GreetsPerson, RandomlyAwardsPrize)
294
+ end
295
+ end
296
+ ```
297
+
298
+ When an organizer is run, you have access to the context as it passed through all actions, and the overall status
299
+ of the organized execution. You can invoke an organizer by calling `.call` on the class with the expected arguments,
300
+ and inspect its status and context just like you would an action:
301
+
302
+ ```ruby
303
+ outcome = WelcomeAPotentiallyLuckyPerson.call("Han")
304
+
305
+ if outcome.success?
306
+ puts outcome.greeting # which was a promised context value
307
+
308
+ if outcome.did_i_win
309
+ puts "And you've won a prize! Lucky you. Please see the front desk for your #{outcome.prize}."
310
+ end
311
+ else # outcome.failure? is true, and we can pull the failure message out of the context for feedback to the user.
312
+ puts "Rats... I can't say hello to you, because #{outcome.message}."
313
+ end
314
+ ```
315
+
316
+ Because organizers generally run through complex business logic, and every action has the potential to cause a failure,
317
+ testing an organizer is functionally equivalent to an integration test.
318
+
319
+ For further examples, please visit the project's [Wiki](https://github.com/adomokos/light-service/wiki) and review
320
+ the ["Why LightService" section](#why-lightservice) above.
179
321
 
180
322
  ## Stopping the Series of Actions
181
323
  When nothing unexpected happens during the organizer's call, the returned `context` will be successful. Here is how you can check for this:
@@ -857,28 +999,52 @@ This context then can be passed to the action under test, freeing you up from th
857
999
 
858
1000
  In case your organizer has more logic in its `call` method, you could create your own test organizer in your specs like you can see it in this [acceptance test](spec/acceptance/testing/context_factory_spec.rb#L4-L11). This is reusable in all your action tests.
859
1001
 
860
- ## Requirements
1002
+ ## Rails support
1003
+
1004
+ LightService includes Rails generators for creating both Organizers and Actions along with corresponding tests. Currently only RSpec is
1005
+ supported ([PR's for supporting MiniTest are welcome](https://github.com/adomokos/light-service/pulls))
861
1006
 
862
- This gem requires ruby 2.x
1007
+ Note: Generators are namespaced to `light_service` not `light-service` due to Rake name constraints.
863
1008
 
864
- ## Installation
865
- Add this line to your application's Gemfile:
1009
+ ### Organizer generation
866
1010
 
867
- gem 'light-service'
1011
+ ```shell
1012
+ rails generate light_service:organizer My::SuperFancy::Organizer
1013
+ # -- or
1014
+ rails generate light_service:organizer my/super_fancy/organizer
1015
+ ```
868
1016
 
869
- And then execute:
1017
+ Options for this generator are:
870
1018
 
871
- $ bundle
1019
+ * `--dir=<SOME_DIR>`. `<SOME_DIR>` defaults to `organizers`. Will write organizers to `/app/organizers`, and specs to `/spec/organizers`
1020
+ * `--no-tests`. Default is `--tests`. Will generate a test file matching the namespace you've supplied.
872
1021
 
873
- Or install it yourself as:
1022
+ ### Action generation
1023
+
1024
+ ```shell
1025
+ rails generate light_service:action My::SuperFancy::Action
1026
+ # -- or
1027
+ rails generate light_service:action my/super_fancy/action
1028
+ ```
1029
+
1030
+ Options for this generator are:
874
1031
 
875
- $ gem install light-service
1032
+ * `--dir=<SOME_DIR>`. `<SOME_DIR>` defaults to `actions`. Will write actions to `/app/actions`, and specs to `/spec/actions`
1033
+ * `--no-tests`. Defaults is `--tests`. Will generate a test file matching the namespace you've supplied.
1034
+ * `--no-roll-back`. Default is `--roll-back`. Will generate a `rolled_back` block for you to implement with [roll back functionality](#action-rollback).
1035
+
1036
+ ### Advanced action generation
1037
+
1038
+ You are able to optionally specify `expects` and/or `promises` keys during generation
1039
+
1040
+ ```shell
1041
+ rails generate light_service:action CrankWidget expects:one_fish,two_fish promises:red_fish,blue_fish
1042
+ ```
876
1043
 
877
- ## Usage
878
- Based on the refactoring example above, just create an organizer object that calls the
879
- actions in order and write code for the actions. That's it.
1044
+ When specifying `expects`, convenience variables will be initialized in the `executed` block so that you don't have to call
1045
+ them through the context. A stub context will be created in the test file using these keys too.
880
1046
 
881
- For further examples, please visit the project's [Wiki](https://github.com/adomokos/light-service/wiki).
1047
+ When specifying `promises`, specs will be created testing for their existence after executing the action.
882
1048
 
883
1049
  ## Contributing
884
1050
  1. Fork it
@@ -1,5 +1,11 @@
1
1
  A brief list of new features and changes introduced with the specified version.
2
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
+
3
9
  ### 0.14.0
4
10
  * [Add 'organized_by' to context](https://github.com/adomokos/light-service/pull/192) - Context now have an #organized_by attribute
5
11
 
@@ -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 => "../"
@@ -3,6 +3,5 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "activesupport", "~> 6.0"
6
- gem "appraisal", "~> 2.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,3 +1,3 @@
1
1
  module LightService
2
- VERSION = "0.14.0".freeze
2
+ VERSION = "0.15.0".freeze
3
3
  end
@@ -18,8 +18,12 @@ Gem::Specification.new do |gem|
18
18
 
19
19
  gem.add_runtime_dependency("activesupport", ">= 3.0.0")
20
20
 
21
+ gem.add_development_dependency("generator_spec", "~> 0.9.4")
22
+ gem.add_development_dependency("test-unit", "~> 3.0") # Needed for generator specs.
23
+ gem.add_development_dependency("appraisal", "~> 2.3")
21
24
  gem.add_development_dependency("rspec", "~> 3.0")
22
25
  gem.add_development_dependency("simplecov", "~> 0.17")
26
+ gem.add_development_dependency("codecov", "~> 0.1")
23
27
  gem.add_development_dependency("rubocop", "~> 0.68.0")
24
28
  gem.add_development_dependency("rubocop-performance", "~> 1.2.0")
25
29
  gem.add_development_dependency("pry", "~> 0.12.2")
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative '../../../lib/generators/light_service/action_generator.rb'
4
+ require_relative './full_generator_test_blobs'
5
+
6
+ describe LightService::Generators::ActionGenerator, :type => :generator do
7
+ destination File.expand_path('tmp', __dir__)
8
+
9
+ context "when generating an advanced action" do
10
+ before(:all) do
11
+ prepare_destination
12
+ run_generator
13
+ end
14
+
15
+ after(:all) do
16
+ FileUtils.rm_rf destination_root
17
+ end
18
+
19
+ arguments %w[
20
+ my/fancy/action
21
+ expects:foo,bar
22
+ promises:baz,qux
23
+ --no-roll-back
24
+ --dir=services
25
+ ]
26
+
27
+ specify do
28
+ expect(destination_root).to(have_structure do
29
+ directory "app/services/my/fancy" do
30
+ file "action.rb" do
31
+ contains FullGeneratorTestBlobs.advanced_action_blob
32
+ end
33
+ end
34
+
35
+ directory "spec/services/my/fancy" do
36
+ file "action_spec.rb" do
37
+ contains FullGeneratorTestBlobs.advanced_action_spec_blob
38
+ end
39
+ end
40
+ end)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative '../../../lib/generators/light_service/action_generator.rb'
4
+ require_relative './full_generator_test_blobs'
5
+
6
+ describe LightService::Generators::ActionGenerator, :type => :generator do
7
+ destination File.expand_path('tmp', __dir__)
8
+
9
+ context "when generating a simple action" do
10
+ before(:all) do
11
+ prepare_destination
12
+ run_generator
13
+ end
14
+
15
+ after(:all) do
16
+ FileUtils.rm_rf destination_root
17
+ end
18
+
19
+ arguments %w[my_action]
20
+
21
+ specify do
22
+ expect(destination_root).to(have_structure do
23
+ directory "app/actions" do
24
+ file "my_action.rb" do
25
+ contains FullGeneratorTestBlobs.simple_action_blob
26
+ end
27
+ end
28
+
29
+ directory "spec/actions" do
30
+ file "my_action_spec.rb" do
31
+ contains FullGeneratorTestBlobs.simple_action_spec_blob
32
+ end
33
+ end
34
+ end)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,193 @@
1
+ # rubocop:disable Metrics/ClassLength, Metrics/MethodLength
2
+ class FullGeneratorTestBlobs
3
+ def self.simple_action_blob
4
+ <<~BLOB
5
+ # frozen_string_literal: true
6
+
7
+ class MyAction
8
+ extend ::LightService::Action
9
+
10
+ executed do |ctx|
11
+ end
12
+
13
+ rolled_back do |ctx|
14
+ end
15
+ end
16
+ BLOB
17
+ end
18
+
19
+ def self.simple_action_spec_blob
20
+ <<~BLOB
21
+ # frozen_string_literal: true
22
+
23
+ require 'rails_helper'
24
+
25
+ RSpec.describe MyAction, type: :action do
26
+ subject { described_class.execute(ctx) }
27
+
28
+ let(:ctx) do
29
+ {
30
+ }
31
+ end
32
+
33
+ context "when executed" do
34
+ xit "is expected to be successful" do
35
+ expect(subject).to be_a_success
36
+ end
37
+ end
38
+ end
39
+ BLOB
40
+ end
41
+
42
+ # There's some weird whitespace issue which prevents
43
+ # using HEREDOCS :(
44
+ def self.advanced_action_blob
45
+ "# frozen_string_literal: true\n" \
46
+ "\n" \
47
+ "module My::Fancy\n" \
48
+ " class Action\n" \
49
+ " extend ::LightService::Action\n" \
50
+ "\n" \
51
+ " expects :foo, :bar\n" \
52
+ " promises :baz, :qux\n" \
53
+ "\n" \
54
+ " executed do |ctx|\n" \
55
+ " foo = ctx.foo\n" \
56
+ " bar = ctx.bar\n" \
57
+ " end\n" \
58
+ " end\n" \
59
+ "end"
60
+ end
61
+
62
+ def self.advanced_action_spec_blob
63
+ <<~BLOB
64
+ # frozen_string_literal: true
65
+
66
+ require 'rails_helper'
67
+
68
+ RSpec.describe My::Fancy::Action, type: :action do
69
+ subject { described_class.execute(ctx) }
70
+
71
+ let(:ctx) do
72
+ {
73
+ foo: nil,
74
+ bar: nil,
75
+ }
76
+ end
77
+
78
+ context "when executed" do
79
+ xit "is expected to be successful" do
80
+ expect(subject).to be_a_success
81
+ end
82
+
83
+ xit "is expected to promise 'baz'" do
84
+ expect(subject.baz).to eq SomeBazClass
85
+ end
86
+
87
+ xit "is expected to promise 'qux'" do
88
+ expect(subject.qux).to eq SomeQuxClass
89
+ end
90
+ end
91
+ end
92
+ BLOB
93
+ end
94
+
95
+ def self.simple_organizer_blob
96
+ <<~BLOB
97
+ # frozen_string_literal: true
98
+
99
+ class MyOrganizer
100
+ extend ::LightService::Organizer
101
+
102
+ def self.call(params)
103
+ with(
104
+ #foo: params[:foo],
105
+ #bar: params[:bar]
106
+ ).reduce(actions)
107
+ end
108
+
109
+ def self.actions
110
+ [
111
+ #OneAction,
112
+ #TwoAction,
113
+ ]
114
+ end
115
+ end
116
+ BLOB
117
+ end
118
+
119
+ def self.simple_organizer_spec_blob
120
+ <<~BLOB
121
+ # frozen_string_literal: true
122
+
123
+ require 'rails_helper'
124
+
125
+ RSpec.describe MyOrganizer, type: :organizer do
126
+ subject { described_class.call(ctx) }
127
+
128
+ let(:ctx) do
129
+ {
130
+ #foo: 'something foo',
131
+ #bar: { baz: qux },
132
+ }
133
+ end
134
+
135
+ context "when called" do
136
+ xit "is expected to be successful" do
137
+ expect(subject).to be_a_success
138
+ end
139
+ end
140
+ end
141
+ BLOB
142
+ end
143
+
144
+ def self.advanced_organizer_blob
145
+ "# frozen_string_literal: true\n" \
146
+ "\n" \
147
+ "module My::Fancy\n" \
148
+ " class Organizer\n" \
149
+ " extend ::LightService::Organizer\n" \
150
+ "\n" \
151
+ " def self.call(params)\n" \
152
+ " with(\n" \
153
+ " #foo: params[:foo],\n" \
154
+ " #bar: params[:bar]\n" \
155
+ " ).reduce(actions)\n" \
156
+ " end\n" \
157
+ "\n" \
158
+ " def self.actions\n" \
159
+ " [\n" \
160
+ " #My::Fancy::OneAction,\n" \
161
+ " #My::Fancy::TwoAction,\n" \
162
+ " ]\n" \
163
+ " end\n" \
164
+ " end\n" \
165
+ "end"
166
+ end
167
+
168
+ def self.advanced_organizer_spec_blob
169
+ <<~BLOB
170
+ # frozen_string_literal: true
171
+
172
+ require 'rails_helper'
173
+
174
+ RSpec.describe My::Fancy::Organizer, type: :organizer do
175
+ subject { described_class.call(ctx) }
176
+
177
+ let(:ctx) do
178
+ {
179
+ #foo: 'something foo',
180
+ #bar: { baz: qux },
181
+ }
182
+ end
183
+
184
+ context "when called" do
185
+ xit "is expected to be successful" do
186
+ expect(subject).to be_a_success
187
+ end
188
+ end
189
+ end
190
+ BLOB
191
+ end
192
+ end
193
+ # rubocop:enable Metrics/ClassLength, Metrics/MethodLength
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative '../../../lib/generators/light_service/organizer_generator.rb'
4
+ require_relative './full_generator_test_blobs'
5
+
6
+ describe LightService::Generators::OrganizerGenerator, :type => :generator do
7
+ destination File.expand_path('tmp', __dir__)
8
+
9
+ context "when generating an advanced organizer" do
10
+ before(:all) do
11
+ prepare_destination
12
+ run_generator
13
+ end
14
+
15
+ after(:all) do
16
+ FileUtils.rm_rf destination_root
17
+ end
18
+
19
+ arguments %w[my/fancy/organizer --dir=processes]
20
+
21
+ specify do
22
+ expect(destination_root).to(have_structure do
23
+ directory "app/processes/my/fancy" do
24
+ file "organizer.rb" do
25
+ contains FullGeneratorTestBlobs.advanced_organizer_blob
26
+ end
27
+ end
28
+
29
+ directory "spec/processes/my/fancy" do
30
+ file "organizer_spec.rb" do
31
+ contains FullGeneratorTestBlobs.advanced_organizer_spec_blob
32
+ end
33
+ end
34
+ end)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ require_relative '../../../lib/generators/light_service/organizer_generator.rb'
4
+ require_relative './full_generator_test_blobs'
5
+
6
+ describe LightService::Generators::OrganizerGenerator, :type => :generator do
7
+ destination File.expand_path('tmp', __dir__)
8
+
9
+ context "when generating a simple organizer" do
10
+ before(:all) do
11
+ prepare_destination
12
+ run_generator
13
+ end
14
+
15
+ after(:all) do
16
+ FileUtils.rm_rf destination_root
17
+ end
18
+
19
+ arguments %w[my_organizer]
20
+
21
+ specify do
22
+ expect(destination_root).to(have_structure do
23
+ directory "app/organizers" do
24
+ file "my_organizer.rb" do
25
+ contains FullGeneratorTestBlobs.simple_organizer_blob
26
+ end
27
+ end
28
+
29
+ directory "spec/organizers" do
30
+ file "my_organizer_spec.rb" do
31
+ contains FullGeneratorTestBlobs.simple_organizer_spec_blob
32
+ end
33
+ end
34
+ end)
35
+ end
36
+ end
37
+ end
@@ -8,8 +8,10 @@ if ENV['RUN_COVERAGE_REPORT']
8
8
  add_filter 'vendor/'
9
9
  add_filter %r{^/spec/}
10
10
  end
11
-
12
11
  SimpleCov.minimum_coverage_by_file 90
12
+
13
+ require 'codecov'
14
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
13
15
  end
14
16
 
15
17
  require 'light-service'
@@ -19,5 +21,7 @@ require 'pry'
19
21
  require 'support'
20
22
  require 'test_doubles'
21
23
  require 'stringio'
24
+ require 'fileutils'
25
+ require 'generator_spec'
22
26
 
23
27
  I18n.enforce_available_locales = true
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: light-service
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Attila Domokos
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-02 00:00:00.000000000 Z
11
+ date: 2020-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,48 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: generator_spec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.9.4
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.9.4
41
+ - !ruby/object:Gem::Dependency
42
+ name: test-unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: appraisal
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.3'
27
69
  - !ruby/object:Gem::Dependency
28
70
  name: rspec
29
71
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +94,20 @@ dependencies:
52
94
  - - "~>"
53
95
  - !ruby/object:Gem::Version
54
96
  version: '0.17'
97
+ - !ruby/object:Gem::Dependency
98
+ name: codecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.1'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.1'
55
111
  - !ruby/object:Gem::Dependency
56
112
  name: rubocop
57
113
  requirement: !ruby/object:Gem::Requirement
@@ -112,10 +168,16 @@ files:
112
168
  - README.md
113
169
  - RELEASES.md
114
170
  - Rakefile
115
- - gemfiles/activesupport_3.gemfile
116
171
  - gemfiles/activesupport_4.gemfile
117
172
  - gemfiles/activesupport_5.gemfile
118
173
  - gemfiles/activesupport_6.gemfile
174
+ - lib/generators/light_service/action_generator.rb
175
+ - lib/generators/light_service/generator_utils.rb
176
+ - lib/generators/light_service/organizer_generator.rb
177
+ - lib/generators/light_service/templates/action_spec_template.erb
178
+ - lib/generators/light_service/templates/action_template.erb
179
+ - lib/generators/light_service/templates/organizer_spec_template.erb
180
+ - lib/generators/light_service/templates/organizer_template.erb
119
181
  - lib/light-service.rb
120
182
  - lib/light-service/action.rb
121
183
  - lib/light-service/configuration.rb
@@ -179,6 +241,11 @@ files:
179
241
  - spec/action_spec.rb
180
242
  - spec/context/inspect_spec.rb
181
243
  - spec/context_spec.rb
244
+ - spec/lib/generators/action_generator_advanced_spec.rb
245
+ - spec/lib/generators/action_generator_simple_spec.rb
246
+ - spec/lib/generators/full_generator_test_blobs.rb
247
+ - spec/lib/generators/organizer_generator_advanced_spec.rb
248
+ - spec/lib/generators/organizer_generator_simple_spec.rb
182
249
  - spec/localization_adapter_spec.rb
183
250
  - spec/organizer/with_reducer_spec.rb
184
251
  - spec/organizer_key_aliases_spec.rb
@@ -258,6 +325,11 @@ test_files:
258
325
  - spec/action_spec.rb
259
326
  - spec/context/inspect_spec.rb
260
327
  - spec/context_spec.rb
328
+ - spec/lib/generators/action_generator_advanced_spec.rb
329
+ - spec/lib/generators/action_generator_simple_spec.rb
330
+ - spec/lib/generators/full_generator_test_blobs.rb
331
+ - spec/lib/generators/organizer_generator_advanced_spec.rb
332
+ - spec/lib/generators/organizer_generator_simple_spec.rb
261
333
  - spec/localization_adapter_spec.rb
262
334
  - spec/organizer/with_reducer_spec.rb
263
335
  - spec/organizer_key_aliases_spec.rb
@@ -1,8 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activesupport", "~> 3.0"
6
- gem "appraisal", "~> 2.0"
7
-
8
- gemspec :path => "../"