slayer 0.1.0 → 0.2.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/.githooks/pre-push +23 -0
- data/.gitignore +1 -0
- data/.hound.yml +2 -0
- data/.rubocop.yml +61 -0
- data/.rubocop_todo.yml +48 -0
- data/.travis.yml +2 -1
- data/README.md +62 -35
- data/Rakefile +4 -4
- data/bin/install-githooks +7 -0
- data/bin/setup +1 -0
- data/lib/ext/string_ext.rb +11 -0
- data/lib/slayer.rb +9 -6
- data/lib/slayer/command.rb +74 -0
- data/lib/slayer/errors.rb +16 -15
- data/lib/slayer/form.rb +15 -0
- data/lib/slayer/result.rb +17 -16
- data/lib/slayer/result_matcher.rb +217 -0
- data/lib/slayer/service.rb +168 -38
- data/lib/slayer/version.rb +1 -1
- data/slayer.gemspec +19 -11
- data/slayer_logo.png +0 -0
- metadata +113 -6
- data/lib/slayer/composer.rb +0 -80
- data/lib/slayer/string_ext.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14e54657ae9bed79dbb867f2c28a0c72a57d64bb
|
4
|
+
data.tar.gz: ea6a0f28be0e58b77fdde6675f35362bd0f4a57f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3bd6939e5a23675abe05b13506a276bb0cc3905b554e134cd543b800a94e6f33b3ab458b52c1fd394e4b6b4adea4bbc338c292d6419a5d13a05a06518fa3e896
|
7
|
+
data.tar.gz: ff07ca08187a2c6c13ff9f41e2fbad207d4f3607671faa940795d8fc6e612bf74b7be6ccc954075c5f0fed6779b61adde7f2607ef16abfe64d40920eaf6beb53
|
data/.githooks/pre-push
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
echo "Starting unit tests"
|
4
|
+
rake test
|
5
|
+
if [ $? -ne 0 ]; then
|
6
|
+
echo ""
|
7
|
+
echo ""
|
8
|
+
echo "Unit tests failed; push aborted!"
|
9
|
+
exit 1
|
10
|
+
fi
|
11
|
+
|
12
|
+
echo
|
13
|
+
echo "Starting rubocop"
|
14
|
+
rubocop --format worst --format simple --format offenses
|
15
|
+
if [ $? -ne 0 ]; then
|
16
|
+
echo ""
|
17
|
+
echo ""
|
18
|
+
echo "Rubocop failed; push aborted!"
|
19
|
+
exit 1
|
20
|
+
fi
|
21
|
+
|
22
|
+
echo
|
23
|
+
echo "All pre-push checks passed! Pushing to remote"
|
data/.gitignore
CHANGED
data/.hound.yml
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.3
|
5
|
+
Include:
|
6
|
+
- 'lib/**/*.rb'
|
7
|
+
- 'test/**/*.rb'
|
8
|
+
Exclude:
|
9
|
+
- 'bin/**/*'
|
10
|
+
|
11
|
+
|
12
|
+
Style/RedundantSelf:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Style/RedundantReturn:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Style/GuardClause:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Style/ClassAndModuleChildren:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Style/EmptyLinesAroundClassBody:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Style/FrozenStringLiteralComment:
|
28
|
+
Enabled: false
|
29
|
+
|
30
|
+
Style/CommentIndentation:
|
31
|
+
Enabled: false
|
32
|
+
|
33
|
+
Style/BracesAroundHashParameters:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Style/IndentationConsistency:
|
37
|
+
EnforcedStyle: rails
|
38
|
+
|
39
|
+
Metrics/LineLength:
|
40
|
+
Max: 120
|
41
|
+
|
42
|
+
Metrics/ClassLength:
|
43
|
+
Max: 120
|
44
|
+
|
45
|
+
Style/EmptyLineBetweenDefs:
|
46
|
+
AllowAdjacentOneLineDefs: true
|
47
|
+
|
48
|
+
# Temporarily disabled until this can be resolved in the todo file
|
49
|
+
# Style/Documentation:
|
50
|
+
# Exclude:
|
51
|
+
# - 'spec/**/*'
|
52
|
+
# - 'test/**/*'
|
53
|
+
# - 'lib/ext/**/*'
|
54
|
+
|
55
|
+
Style/ClassVars:
|
56
|
+
Exclude:
|
57
|
+
- 'lib/slayer/service.rb'
|
58
|
+
|
59
|
+
Style/MutableConstant:
|
60
|
+
Exclude:
|
61
|
+
- 'lib/slayer/version.rb'
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2017-02-13 09:02:04 -0800 using RuboCop version 0.47.1.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 2
|
10
|
+
Lint/HandleExceptions:
|
11
|
+
Exclude:
|
12
|
+
- 'lib/slayer/command.rb'
|
13
|
+
- 'test/lib/result_matcher_test.rb'
|
14
|
+
|
15
|
+
# Offense count: 2
|
16
|
+
# Cop supports --auto-correct.
|
17
|
+
# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
|
18
|
+
Lint/UnusedMethodArgument:
|
19
|
+
Exclude:
|
20
|
+
- 'lib/slayer/form.rb'
|
21
|
+
|
22
|
+
# Offense count: 1
|
23
|
+
Metrics/AbcSize:
|
24
|
+
Max: 19
|
25
|
+
|
26
|
+
# Offense count: 1
|
27
|
+
Metrics/CyclomaticComplexity:
|
28
|
+
Max: 9
|
29
|
+
|
30
|
+
# Offense count: 5
|
31
|
+
# Configuration parameters: CountComments.
|
32
|
+
Metrics/MethodLength:
|
33
|
+
Max: 18
|
34
|
+
|
35
|
+
# Offense count: 1
|
36
|
+
Metrics/PerceivedComplexity:
|
37
|
+
Max: 10
|
38
|
+
|
39
|
+
# Offense count: 6
|
40
|
+
Style/Documentation:
|
41
|
+
Exclude:
|
42
|
+
- 'spec/**/*'
|
43
|
+
- 'test/**/*'
|
44
|
+
- 'lib/ext/**/*'
|
45
|
+
- 'lib/slayer/command.rb'
|
46
|
+
- 'lib/slayer/errors.rb'
|
47
|
+
- 'lib/slayer/form.rb'
|
48
|
+
- 'lib/slayer/result.rb'
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,28 @@
|
|
1
1
|

|
2
2
|
|
3
|
-
# Slayer: A Service Layer
|
3
|
+
# Slayer: A Killer Service Layer
|
4
4
|
|
5
|
-
|
5
|
+
[](https://badge.fury.io/rb/slayer) [](https://travis-ci.org/apsislabs/slayer) [](https://codeclimate.com/github/apsislabs/slayer)
|
6
6
|
|
7
|
-
Slayer
|
7
|
+
Slayer is intended to operate as a minimal service layer for your ruby application. To achieve this, Slayer provides base classes for business logic.
|
8
8
|
|
9
|
-
|
9
|
+
## Application Structure
|
10
10
|
|
11
|
-
|
11
|
+
Slayer provides 3 base classes for organizing your business logic: `Forms`, `Commands`, and `Services`. Each of these has a distinct role in your application's structure.
|
12
|
+
|
13
|
+
### Forms
|
14
|
+
|
15
|
+
`Slayer::Forms` are objects for wrapping a set of data, usually to be passed as a parameter to a `Command` or `Service`.
|
16
|
+
|
17
|
+
### Commands
|
18
|
+
|
19
|
+
`Slayer::Commands` are the bread and butter of your application's business logic. `Commands` are where you compose services, and perform one-off business logic tasks. In our applications, we usually create a single `Command` per `Controller` endpoint.
|
20
|
+
|
21
|
+
`Commands` should call `Services`, but `Services` should never call `Commands`.
|
22
|
+
|
23
|
+
### Services
|
24
|
+
|
25
|
+
`Services` are the building blocks of `Commands`, and encapsulate re-usable chunks of application logic.
|
12
26
|
|
13
27
|
## Installation
|
14
28
|
|
@@ -20,55 +34,66 @@ gem 'slayer'
|
|
20
34
|
|
21
35
|
And then execute:
|
22
36
|
|
23
|
-
|
37
|
+
```sh
|
38
|
+
$ bundle
|
39
|
+
```
|
24
40
|
|
25
41
|
Or install it yourself as:
|
26
42
|
|
27
|
-
|
43
|
+
```sh
|
44
|
+
$ gem install slayer
|
45
|
+
```
|
28
46
|
|
29
47
|
## Usage
|
30
48
|
|
49
|
+
### Commands
|
50
|
+
|
51
|
+
Slayer Commands should implement `call`, which will `pass` or `fail` the service based on input. Commands return a `Slayer::Result` which has a predictable interface for determining `success?` or `failure?`, a `message`, and a `result` payload object.
|
52
|
+
|
31
53
|
```ruby
|
32
|
-
# A
|
54
|
+
# A Command that passes when given the string "foo"
|
33
55
|
# and fails if given anything else.
|
34
|
-
class
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
end
|
56
|
+
class FooCommand < Slayer::Command
|
57
|
+
def call(foo:)
|
58
|
+
if foo == "foo"
|
59
|
+
pass! result: foo, message: "Passing FooCommand"
|
60
|
+
else
|
61
|
+
fail! result: foo, message: "Failing FooCommand"
|
41
62
|
end
|
63
|
+
end
|
42
64
|
end
|
43
65
|
|
44
|
-
|
45
|
-
#
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
66
|
+
result = FooCommand.call(foo: "foo")
|
67
|
+
result.success? # => true
|
68
|
+
|
69
|
+
result = FooCommand.call(foo: "bar")
|
70
|
+
result.success? # => false
|
71
|
+
```
|
51
72
|
|
52
|
-
|
53
|
-
class FooBarComposer < Slayer::Composer
|
54
|
-
compose FooService, BarService
|
73
|
+
### Forms
|
55
74
|
|
56
|
-
|
57
|
-
pass! result: @results, message: "Yay!"
|
58
|
-
end
|
75
|
+
### Services
|
59
76
|
|
60
|
-
|
61
|
-
return { foo: @composer_params[:foo] }
|
62
|
-
end
|
77
|
+
Slayer Services are objects that should implement re-usable pieces of application logic or common tasks. To prevent circular dependencies Services are required to declare which other Service classes they depend on. If a circular dependency is detected an error is raised.
|
63
78
|
|
64
|
-
|
65
|
-
|
79
|
+
In order to enforce the lack of circular dependencies, Service objects can only call other Services that are declared in their dependencies.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class NetworkService < Slayer::Service
|
83
|
+
def self.post()
|
84
|
+
...
|
66
85
|
end
|
67
86
|
end
|
68
87
|
|
88
|
+
class StripeService < Slayer::Service
|
89
|
+
dependencies NetworkService
|
69
90
|
|
70
|
-
|
71
|
-
|
91
|
+
def self.pay()
|
92
|
+
...
|
93
|
+
NetworkService.post(url: "stripe.com", body: my_payload)
|
94
|
+
...
|
95
|
+
end
|
96
|
+
end
|
72
97
|
```
|
73
98
|
|
74
99
|
## Development
|
@@ -77,6 +102,8 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
77
102
|
|
78
103
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
79
104
|
|
105
|
+
To generate documentation run `yard`. To view undocumented files run `yard stats --list-undoc`.
|
106
|
+
|
80
107
|
## Contributing
|
81
108
|
|
82
109
|
Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/slayer.
|
data/Rakefile
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
3
|
|
4
4
|
Rake::TestTask.new(:test) do |t|
|
5
|
-
t.libs <<
|
6
|
-
t.libs <<
|
5
|
+
t.libs << 'test'
|
6
|
+
t.libs << 'lib'
|
7
7
|
t.test_files = FileList['test/**/*_test.rb']
|
8
8
|
end
|
9
9
|
|
data/bin/setup
CHANGED
data/lib/slayer.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
1
|
+
require 'ext/string_ext' unless defined?(Rails)
|
2
|
+
|
3
|
+
require 'slayer/version'
|
4
|
+
require 'slayer/errors'
|
5
|
+
require 'slayer/result'
|
6
|
+
require 'slayer/result_matcher'
|
7
|
+
require 'slayer/service'
|
8
|
+
require 'slayer/command'
|
9
|
+
require 'slayer/form'
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Slayer
|
2
|
+
class Command
|
3
|
+
attr_accessor :result
|
4
|
+
|
5
|
+
# Internal: Command Class Methods
|
6
|
+
class << self
|
7
|
+
def call(*args, &block)
|
8
|
+
execute_call(block, *args) { |c, *a| c.run(*a) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def call!(*args, &block)
|
12
|
+
execute_call(block, *args) { |c, *a| c.run!(*a) }
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def execute_call(command_block, *args)
|
18
|
+
# Run the Command and capture the result
|
19
|
+
command = self.new
|
20
|
+
result = command.tap { yield(command, *args) }.result
|
21
|
+
|
22
|
+
# Throw an exception if we don't return a result
|
23
|
+
raise CommandNotImplementedError unless result.is_a? Result
|
24
|
+
|
25
|
+
# Run the command block if one was provided
|
26
|
+
unless command_block.nil?
|
27
|
+
matcher = Slayer::ResultMatcher.new(result, command)
|
28
|
+
|
29
|
+
command_block.call(matcher)
|
30
|
+
|
31
|
+
# raise error if not all defaults were handled
|
32
|
+
unless matcher.handled_defaults?
|
33
|
+
raise(CommandResultNotHandledError, 'The pass or fail condition of a result was not handled')
|
34
|
+
end
|
35
|
+
|
36
|
+
begin
|
37
|
+
matcher.execute_matching_block
|
38
|
+
ensure
|
39
|
+
matcher.execute_ensure_block
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
return result
|
44
|
+
end
|
45
|
+
end # << self
|
46
|
+
|
47
|
+
def run(*args)
|
48
|
+
call(*args)
|
49
|
+
rescue CommandFailureError
|
50
|
+
# Swallow the Command Failure
|
51
|
+
end
|
52
|
+
|
53
|
+
# Run the Command
|
54
|
+
def run!(*args)
|
55
|
+
call(*args)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fail the Command
|
59
|
+
def fail!(result:, status: :default, message: nil)
|
60
|
+
@result = Result.new(result, status, message)
|
61
|
+
@result.fail!
|
62
|
+
end
|
63
|
+
|
64
|
+
# Pass the Command
|
65
|
+
def pass!(result:, status: :default, message: nil)
|
66
|
+
@result = Result.new(result, status, message)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Call the command
|
70
|
+
def call
|
71
|
+
raise NotImplementedError, 'Commands must define method `#call`.'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/slayer/errors.rb
CHANGED
@@ -1,21 +1,22 @@
|
|
1
1
|
module Slayer
|
2
|
-
|
3
|
-
|
2
|
+
class CommandFailureError < StandardError
|
3
|
+
attr_reader :result
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
5
|
+
def initialize(result)
|
6
|
+
@result = result
|
7
|
+
super
|
9
8
|
end
|
9
|
+
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
)
|
17
|
-
|
18
|
-
super message
|
19
|
-
end
|
11
|
+
class CommandNotImplementedError < StandardError
|
12
|
+
def initialize(message = nil)
|
13
|
+
message ||= 'Command implementation must call `fail!` or `pass!`, or '\
|
14
|
+
'return a <Slayer::Result> object'
|
15
|
+
super message
|
20
16
|
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class CommandResultNotHandledError < StandardError; end
|
20
|
+
class FormValidationError < StandardError; end
|
21
|
+
class ServiceDependencyError < StandardError; end
|
21
22
|
end
|
data/lib/slayer/form.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
module Slayer
|
4
|
+
class Form
|
5
|
+
include Virtus.model
|
6
|
+
include ActiveModel::Validations if defined?(Rails)
|
7
|
+
|
8
|
+
def validate!
|
9
|
+
validatable = respond_to?(:valid?) && respond_to?(:errors)
|
10
|
+
|
11
|
+
raise NotImplementedError unless validatable
|
12
|
+
raise FormValidationError, errors unless valid?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/slayer/result.rb
CHANGED
@@ -1,23 +1,24 @@
|
|
1
1
|
module Slayer
|
2
|
-
|
3
|
-
|
2
|
+
class Result
|
3
|
+
attr_reader :value, :status, :message
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
def initialize(value, status, message)
|
6
|
+
@value = value
|
7
|
+
@status = status
|
8
|
+
@message = message
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
def success?
|
12
|
+
!failure?
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
15
|
+
def failure?
|
16
|
+
@failure || false
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
19
|
+
def fail!
|
20
|
+
@failure = true
|
21
|
+
raise CommandFailureError, self
|
22
22
|
end
|
23
|
+
end
|
23
24
|
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
module Slayer
|
2
|
+
# ResultMatcher is the object passed to the block of a {Command.call}. The ResultMatcher
|
3
|
+
# allows the block-author to specify which piece of logic they would like to invoke
|
4
|
+
# based on the state of the {Result} object.
|
5
|
+
#
|
6
|
+
# In the event that multiple blocks match the {Result}, only the most specific
|
7
|
+
# matching block will be invoked. Status matches take precedence over default matches.
|
8
|
+
# If there are two blocks with a matching status, the pass/fail block takes precedence
|
9
|
+
# over the all block.
|
10
|
+
#
|
11
|
+
# == Matching based on success or failure
|
12
|
+
#
|
13
|
+
# The ResultMatcher matches calls to {#pass} to a {Result} that returns +true+
|
14
|
+
# for {Result#success?}, calls to {#fail} to a {Result} that returns +true+
|
15
|
+
# for {Result#failure?}, and calls to {#all} to a {Result} in either state.
|
16
|
+
#
|
17
|
+
# A matching call to {#pass} or {#fail} takes precedence over matching calls to {#all}
|
18
|
+
#
|
19
|
+
# == Matching based on status
|
20
|
+
#
|
21
|
+
# Additionally, the ResultMatcher can also match by the {Result#status}. If a status
|
22
|
+
# or statuses is passed to {#pass}, {#fail}, or {#all}, these will only be invoked if the
|
23
|
+
# status of the {Result} matches the passed in status.
|
24
|
+
#
|
25
|
+
# If the default block is the same as the block for one of the statuses the status +:default+
|
26
|
+
# can be used to indicate which block should be used as the default. Successful status matches
|
27
|
+
# take precedence over default matchers.
|
28
|
+
#
|
29
|
+
# == Both pass and fail must be handled
|
30
|
+
#
|
31
|
+
# If the block form of a {Command.call} is invoked, both the block must handle the default
|
32
|
+
# status for both a {Result#success?} and a {Result#failure?}. If both are not handled,
|
33
|
+
# the matching block will not be invoked and a {CommandResultNotHandledError} will be
|
34
|
+
# raised.
|
35
|
+
#
|
36
|
+
# @example Matcher invokes the matching pass block, with precedence given to {#pass} and {#fail}
|
37
|
+
# # Call produces a successful Result
|
38
|
+
# SuccessCommand.call do |m|
|
39
|
+
# m.pass { puts "Pass!" }
|
40
|
+
# m.fail { puts "Fail!" }
|
41
|
+
# m.all { puts "All!" } # will never be invoked, due to both a pass and fail response existing
|
42
|
+
# end
|
43
|
+
# # => prints "Pass!"
|
44
|
+
#
|
45
|
+
# @example Matcher invokes the matching status of the result object, or the default
|
46
|
+
# # Call produces a successful Result with status :ok
|
47
|
+
# SuccessCommand.call do |m|
|
48
|
+
# m.pass(:ok) { puts "Pass, OK!" }
|
49
|
+
# m.pass { puts "Pass, default!" }
|
50
|
+
# m.fail { puts "Fail!" }
|
51
|
+
# end
|
52
|
+
# # => prints "Pass, OK!"
|
53
|
+
#
|
54
|
+
# # Call produces a successful Result with status :created
|
55
|
+
# SuccessCommand.call do |m|
|
56
|
+
# m.pass(:ok) { puts "Pass, OK!" }
|
57
|
+
# m.pass { puts "Pass, default!" }
|
58
|
+
# m.fail { puts "Fail!" }
|
59
|
+
# end
|
60
|
+
# # => prints "Pass, default!"
|
61
|
+
#
|
62
|
+
# @example Matcher invokes the explicitly indicated default block
|
63
|
+
# # Call produces a successful Result with status :created
|
64
|
+
# SuccessCommand.call do |m|
|
65
|
+
# m.pass(:ok, :default) { puts "Pass, OK!" }
|
66
|
+
# m.pass(:great) { puts "Pass, default!" }
|
67
|
+
# m.fail { puts "Fail!" }
|
68
|
+
# end
|
69
|
+
# # => prints "Pass, OK!"
|
70
|
+
#
|
71
|
+
# @example Matcher must handle both pass and fail defaults.
|
72
|
+
# # Call produces a successful Result with status :ok
|
73
|
+
# SuccessCommand.call do |m|
|
74
|
+
# m.pass(:ok) { puts "Pass, OK!"}
|
75
|
+
# m.fail { puts "Fail!" }
|
76
|
+
# end
|
77
|
+
# # => raises CommandResultNotHandledError (because no default pass was provided)
|
78
|
+
#
|
79
|
+
# # Call produces a successful Result with status :ok
|
80
|
+
# SuccessCommand.call do |m|
|
81
|
+
# m.pass(:ok, :default) { puts "Pass, OK!"}
|
82
|
+
# m.fail { puts "Fail!" }
|
83
|
+
# end
|
84
|
+
# # => prints "Pass, OK!"
|
85
|
+
#
|
86
|
+
# # Call produces a successful Result with status :ok
|
87
|
+
# SuccessCommand.call do |m|
|
88
|
+
# m.pass(:ok) { puts "Pass, OK!"}
|
89
|
+
# m.all { puts "All!" }
|
90
|
+
# end
|
91
|
+
# # => prints "Pass, OK!"
|
92
|
+
#
|
93
|
+
# # Call produces a successful Result with status :ok
|
94
|
+
# SuccessCommand.call do |m|
|
95
|
+
# m.pass(:ok, :default) { puts "Pass, OK!"}
|
96
|
+
# end
|
97
|
+
# # => raises CommandResultNotHandledError (because no default fail was provided)
|
98
|
+
class ResultMatcher
|
99
|
+
attr_reader :result, :command
|
100
|
+
|
101
|
+
# @api private
|
102
|
+
def initialize(result, command)
|
103
|
+
@result = result
|
104
|
+
@command = command
|
105
|
+
|
106
|
+
@status = result.status || :default
|
107
|
+
|
108
|
+
@handled_default_pass = false
|
109
|
+
@handled_default_fail = false
|
110
|
+
|
111
|
+
# These are set to false if they are never set. If they are set to `nil` that
|
112
|
+
# means the block intentionally passed `nil` as the block to be executed.
|
113
|
+
@matching_block = false
|
114
|
+
@matching_all = false
|
115
|
+
@default_block = false
|
116
|
+
@default_all = false
|
117
|
+
@ensure_block = false
|
118
|
+
end
|
119
|
+
|
120
|
+
# Provide a block that should be invoked if the {Result} is a success.
|
121
|
+
#
|
122
|
+
# @param statuses [Array<status>] Statuses that should be compared to the {Result}. If
|
123
|
+
# any of provided statuses match the {Result} this block will be considered a match.
|
124
|
+
# The symbol +:default+ can also be used to indicate that this should match any {Result}
|
125
|
+
# not matched by other matchers.
|
126
|
+
#
|
127
|
+
# If no value is provided for statuses it defaults to +:default+.
|
128
|
+
def pass(*statuses, &block)
|
129
|
+
statuses << :default if statuses.empty?
|
130
|
+
@handled_default_pass ||= statuses.include?(:default)
|
131
|
+
|
132
|
+
block_is_match = @result.success? && statuses.include?(@status)
|
133
|
+
block_is_default = @result.success? && statuses.include?(:default)
|
134
|
+
|
135
|
+
@matching_block = block if block_is_match
|
136
|
+
@default_block = block if block_is_default
|
137
|
+
end
|
138
|
+
|
139
|
+
# Provide a block that should be invoked if the {Result} is a failure.
|
140
|
+
#
|
141
|
+
# @param statuses [Array<status>] Statuses that should be compared to the {Result}. If
|
142
|
+
# any of provided statuses match the {Result} this block will be considered a match.
|
143
|
+
# The symbol +:default+ can also be used to indicate that this should match any {Result}
|
144
|
+
# not matched by other matchers.
|
145
|
+
#
|
146
|
+
# If no value is provided for statuses it defaults to +:default+.
|
147
|
+
def fail(*statuses, &block)
|
148
|
+
statuses << :default if statuses.empty?
|
149
|
+
@handled_default_fail ||= statuses.include?(:default)
|
150
|
+
|
151
|
+
block_is_match = @result.failure? && statuses.include?(@status)
|
152
|
+
block_is_default = @result.failure? && statuses.include?(:default)
|
153
|
+
|
154
|
+
@matching_block = block if block_is_match
|
155
|
+
@default_block = block if block_is_default
|
156
|
+
end
|
157
|
+
|
158
|
+
# Provide a block that should be invoked for any {Result}. This has a lower precedence that
|
159
|
+
# either {#pass} or {#fail}.
|
160
|
+
#
|
161
|
+
# @param statuses [Array<status>] Statuses that should be compared to the {Result}. If
|
162
|
+
# any of provided statuses match the {Result} this block will be considered a match.
|
163
|
+
# The symbol +:default+ can also be used to indicate that this should match any {Result}
|
164
|
+
# not matched by other matchers.
|
165
|
+
#
|
166
|
+
# If no value is provided for statuses it defaults to +:default+.
|
167
|
+
def all(*statuses, &block)
|
168
|
+
statuses << :default if statuses.empty?
|
169
|
+
@handled_default_pass ||= statuses.include?(:default)
|
170
|
+
@handled_default_fail ||= statuses.include?(:default)
|
171
|
+
|
172
|
+
block_is_match = statuses.include?(@status)
|
173
|
+
block_is_default = statuses.include?(:default)
|
174
|
+
|
175
|
+
@matching_all = block if block_is_match
|
176
|
+
@default_all = block if block_is_default
|
177
|
+
end
|
178
|
+
|
179
|
+
# Provide a block that should be always be invoked after other blocks have executed. This block
|
180
|
+
# will be invoked even if the other block raises an error.
|
181
|
+
def ensure(&block)
|
182
|
+
@ensure_block = block
|
183
|
+
end
|
184
|
+
|
185
|
+
# @return Whether both the pass and the fail defaults have been handled.
|
186
|
+
#
|
187
|
+
# @api private
|
188
|
+
def handled_defaults?
|
189
|
+
return @handled_default_pass && @handled_default_fail
|
190
|
+
end
|
191
|
+
|
192
|
+
# Executes the provided block that best matched the {Result}. If no block matched
|
193
|
+
# nothing is executed
|
194
|
+
#
|
195
|
+
# @api private
|
196
|
+
def execute_matching_block
|
197
|
+
if @matching_block != false # nil should pass this test
|
198
|
+
@matching_block&.call(@result, @command) # explicit nil will not get called with
|
199
|
+
# safe navigation (&.)
|
200
|
+
elsif @matching_all != false
|
201
|
+
@matching_all&.call(@result, @command)
|
202
|
+
elsif @default_block != false
|
203
|
+
@default_block&.call(@result, @command)
|
204
|
+
elsif @default_all
|
205
|
+
@default_all&.call(@result, @command)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def execute_ensure_block
|
210
|
+
# rubocop:disable Style/IfUnlessModifier
|
211
|
+
if @ensure_block != false # nil should pass this test
|
212
|
+
@ensure_block.call(@result, @command)
|
213
|
+
end
|
214
|
+
# rubocop:enable Style/IfUnlessModifier
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
data/lib/slayer/service.rb
CHANGED
@@ -1,52 +1,182 @@
|
|
1
1
|
module Slayer
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
2
|
+
# Slayer Services are objects that should implement re-usable pieces of
|
3
|
+
# application logic or common tasks. To prevent circular dependencies Services
|
4
|
+
# are required to declare which other Service classes they depend on. If a
|
5
|
+
# circular dependency is detected an error is raised.
|
6
|
+
#
|
7
|
+
# In order to enforce the lack of circular dependencies, Service objects can
|
8
|
+
# only call other Services that are declared in their dependencies.
|
9
|
+
class Service
|
10
|
+
# List the other Service class that this service class depends on. Only
|
11
|
+
# dependencies that are included in this call my be invoked from class
|
12
|
+
# or instances methods of this service class.
|
13
|
+
#
|
14
|
+
# If no dependencies are provided, no other Service classes may be used by
|
15
|
+
# this Service class.
|
16
|
+
#
|
17
|
+
# @param deps [Array<Class>] An array of the other Slayer::Service classes that are used as dependencies
|
18
|
+
#
|
19
|
+
# @example Service calls with dependency declared
|
20
|
+
# class StripeService < Slayer::Service
|
21
|
+
# dependencies NetworkService
|
22
|
+
#
|
23
|
+
# def self.pay()
|
24
|
+
# ...
|
25
|
+
# NetworkService.post(url: "stripe.com", body: my_payload) # OK
|
26
|
+
# ...
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @example Service calls without a dependency declared
|
31
|
+
# class JiraApiService < Slayer::Service
|
32
|
+
#
|
33
|
+
# def self.create_issue()
|
34
|
+
# ...
|
35
|
+
# NetworkService.post(url: "stripe.com", body: my_payload) # Raises Slayer::ServiceDependencyError
|
36
|
+
# ...
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @return [Array<Class>] The transitive closure of dependencies for this object.
|
41
|
+
def self.dependencies(*deps)
|
42
|
+
raise(ServiceDependencyError, "There were multiple dependencies calls of #{self}") if @deps
|
43
|
+
|
44
|
+
deps.each do |dep|
|
45
|
+
unless dep.is_a?(Class)
|
46
|
+
raise(ServiceDependencyError, "The object #{dep} passed to dependencies service was not a class")
|
17
47
|
end
|
18
48
|
|
19
|
-
|
20
|
-
|
21
|
-
begin
|
22
|
-
run!(*args, &block)
|
23
|
-
rescue ServiceFailure
|
24
|
-
end
|
49
|
+
unless dep < Slayer::Service
|
50
|
+
raise(ServiceDependencyError, "The object #{dep} passed to dependencies was not a subclass of #{self}")
|
25
51
|
end
|
52
|
+
end
|
53
|
+
|
54
|
+
unless deps.uniq.length == deps.length
|
55
|
+
raise(ServiceDependencyError, "There were duplicate dependencies in #{self}")
|
56
|
+
end
|
57
|
+
|
58
|
+
@deps = deps
|
59
|
+
|
60
|
+
# Calculate the transitive dependencies and raise an error if there are circular dependencies
|
61
|
+
transitive_dependencies
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
|
66
|
+
attr_reader :deps
|
67
|
+
|
68
|
+
def transitive_dependencies(dependency_hash = {}, visited = [])
|
69
|
+
return @transitive_dependencies if @transitive_dependencies
|
26
70
|
|
27
|
-
|
28
|
-
|
29
|
-
|
71
|
+
@deps ||= []
|
72
|
+
|
73
|
+
# If we've already visited ourself, bail out. This is necessary to halt
|
74
|
+
# execution for a circular chain of dependencies. #halting-problem-solved
|
75
|
+
return dependency_hash[self] if visited.include?(self)
|
76
|
+
|
77
|
+
visited << self
|
78
|
+
dependency_hash[self] ||= []
|
79
|
+
|
80
|
+
# Add each of our dependencies (and it's transitive dependency chain) to our
|
81
|
+
# own dependencies.
|
82
|
+
|
83
|
+
@deps.each do |dep|
|
84
|
+
dependency_hash[self] << dep
|
85
|
+
|
86
|
+
unless visited.include?(dep)
|
87
|
+
child_transitive_dependencies = dep.transitive_dependencies(dependency_hash, visited)
|
88
|
+
dependency_hash[self].concat(child_transitive_dependencies)
|
89
|
+
end
|
90
|
+
|
91
|
+
dependency_hash[self].uniq
|
30
92
|
end
|
31
93
|
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
@result.fail!
|
94
|
+
# NO CIRCULAR DEPENDENCIES!
|
95
|
+
if dependency_hash[self].include? self
|
96
|
+
raise(ServiceDependencyError, "#{self} had a circular dependency")
|
36
97
|
end
|
37
98
|
|
38
|
-
#
|
39
|
-
|
40
|
-
|
99
|
+
# Store these now, so next time we can short-circuit.
|
100
|
+
@transitive_dependencies = dependency_hash[self]
|
101
|
+
|
102
|
+
return @transitive_dependencies
|
103
|
+
end
|
104
|
+
|
105
|
+
def before_each_method(*)
|
106
|
+
@deps ||= []
|
107
|
+
@@allowed_services ||= nil
|
108
|
+
|
109
|
+
# Confirm that this method call is allowed
|
110
|
+
raise_if_not_allowed
|
111
|
+
|
112
|
+
@@allowed_services ||= []
|
113
|
+
@@allowed_services << @deps
|
114
|
+
end
|
115
|
+
|
116
|
+
def raise_if_not_allowed
|
117
|
+
if @@allowed_services
|
118
|
+
allowed = @@allowed_services.last
|
119
|
+
if !allowed || !allowed.include?(self)
|
120
|
+
raise(ServiceDependencyError, "Attempted to call #{self} from another #{Slayer::Service}"\
|
121
|
+
' which did not declare it as a dependency')
|
122
|
+
end
|
41
123
|
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def after_each_method(*)
|
127
|
+
@@allowed_services.pop
|
128
|
+
@@allowed_services = nil if @@allowed_services.empty?
|
129
|
+
end
|
130
|
+
|
131
|
+
def singleton_method_added(name)
|
132
|
+
return if self == Slayer::Service
|
133
|
+
return if @__last_methods_added && @__last_methods_added.include?(name)
|
134
|
+
|
135
|
+
with = :"#{name}_with_before_each_method"
|
136
|
+
without = :"#{name}_without_before_each_method"
|
42
137
|
|
43
|
-
|
44
|
-
|
45
|
-
|
138
|
+
@__last_methods_added = [name, with, without]
|
139
|
+
define_singleton_method with do |*args, &block|
|
140
|
+
before_each_method name
|
141
|
+
begin
|
142
|
+
send without, *args, &block
|
143
|
+
rescue
|
144
|
+
raise
|
145
|
+
ensure
|
146
|
+
after_each_method name
|
147
|
+
end
|
46
148
|
end
|
47
149
|
|
48
|
-
|
49
|
-
|
150
|
+
singleton_class.send(:alias_method, without, name)
|
151
|
+
singleton_class.send(:alias_method, name, with)
|
152
|
+
|
153
|
+
@__last_methods_added = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def method_added(name)
|
157
|
+
return if self == Slayer::Service
|
158
|
+
return if @__last_methods_added && @__last_methods_added.include?(name)
|
159
|
+
|
160
|
+
with = :"#{name}_with_before_each_method"
|
161
|
+
without = :"#{name}_without_before_each_method"
|
162
|
+
|
163
|
+
@__last_methods_added = [name, with, without]
|
164
|
+
define_method with do |*args, &block|
|
165
|
+
self.class.before_each_method name
|
166
|
+
begin
|
167
|
+
send without, *args, &block
|
168
|
+
rescue
|
169
|
+
raise
|
170
|
+
ensure
|
171
|
+
self.class.after_each_method name
|
172
|
+
end
|
50
173
|
end
|
51
|
-
|
52
|
-
|
174
|
+
|
175
|
+
alias_method without, name
|
176
|
+
alias_method name, with
|
177
|
+
|
178
|
+
@__last_methods_added = nil
|
179
|
+
end
|
180
|
+
end # << self
|
181
|
+
end # class Service
|
182
|
+
end # module Slayer
|
data/lib/slayer/version.rb
CHANGED
data/slayer.gemspec
CHANGED
@@ -4,21 +4,29 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'slayer/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'slayer'
|
8
8
|
spec.version = Slayer::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
9
|
+
spec.authors = ['Wyatt Kirby', 'Noah Callaway']
|
10
|
+
spec.email = ['wyatt@apsis.io', 'noah@apsis.io']
|
11
11
|
|
12
|
-
spec.summary = %q{A service layer}
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
12
|
+
spec.summary = %q{A killer service layer}
|
13
|
+
spec.homepage = 'http://www.apsis.io'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
-
spec.bindir =
|
17
|
+
spec.bindir = 'exe'
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
-
spec.require_paths = [
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
spec.
|
22
|
-
spec.
|
23
|
-
|
21
|
+
spec.add_dependency 'virtus', '~> 1.0'
|
22
|
+
spec.add_dependency 'dry-validation', '~> 0.10'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.12'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
27
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.1'
|
28
|
+
spec.add_development_dependency 'mocha', '~> 1.2'
|
29
|
+
spec.add_development_dependency 'byebug', '~> 9.0'
|
30
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
31
|
+
spec.add_development_dependency 'rubocop', '~> 0.47.1'
|
24
32
|
end
|
data/slayer_logo.png
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,15 +1,44 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slayer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wyatt Kirby
|
8
|
+
- Noah Callaway
|
8
9
|
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
12
|
+
date: 2017-02-13 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: virtus
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.0'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: dry-validation
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0.10'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0.10'
|
13
42
|
- !ruby/object:Gem::Dependency
|
14
43
|
name: bundler
|
15
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,27 +81,105 @@ dependencies:
|
|
52
81
|
- - "~>"
|
53
82
|
- !ruby/object:Gem::Version
|
54
83
|
version: '5.0'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: minitest-reporters
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '1.1'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '1.1'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: mocha
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '1.2'
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '1.2'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: byebug
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '9.0'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '9.0'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: yard
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - "~>"
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0.9'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - "~>"
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0.9'
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: rubocop
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - "~>"
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: 0.47.1
|
147
|
+
type: :development
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - "~>"
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: 0.47.1
|
55
154
|
description:
|
56
155
|
email:
|
57
|
-
-
|
156
|
+
- wyatt@apsis.io
|
157
|
+
- noah@apsis.io
|
58
158
|
executables: []
|
59
159
|
extensions: []
|
60
160
|
extra_rdoc_files: []
|
61
161
|
files:
|
162
|
+
- ".githooks/pre-push"
|
62
163
|
- ".gitignore"
|
164
|
+
- ".hound.yml"
|
165
|
+
- ".rubocop.yml"
|
166
|
+
- ".rubocop_todo.yml"
|
63
167
|
- ".travis.yml"
|
64
168
|
- Gemfile
|
65
169
|
- LICENSE.txt
|
66
170
|
- README.md
|
67
171
|
- Rakefile
|
68
172
|
- bin/console
|
173
|
+
- bin/install-githooks
|
69
174
|
- bin/setup
|
175
|
+
- lib/ext/string_ext.rb
|
70
176
|
- lib/slayer.rb
|
71
|
-
- lib/slayer/
|
177
|
+
- lib/slayer/command.rb
|
72
178
|
- lib/slayer/errors.rb
|
179
|
+
- lib/slayer/form.rb
|
73
180
|
- lib/slayer/result.rb
|
181
|
+
- lib/slayer/result_matcher.rb
|
74
182
|
- lib/slayer/service.rb
|
75
|
-
- lib/slayer/string_ext.rb
|
76
183
|
- lib/slayer/version.rb
|
77
184
|
- slayer.gemspec
|
78
185
|
- slayer_logo.png
|
@@ -99,5 +206,5 @@ rubyforge_project:
|
|
99
206
|
rubygems_version: 2.6.8
|
100
207
|
signing_key:
|
101
208
|
specification_version: 4
|
102
|
-
summary: A service layer
|
209
|
+
summary: A killer service layer
|
103
210
|
test_files: []
|
data/lib/slayer/composer.rb
DELETED
@@ -1,80 +0,0 @@
|
|
1
|
-
module Slayer
|
2
|
-
class Composer < Service
|
3
|
-
attr_accessor :called
|
4
|
-
attr_accessor :results
|
5
|
-
attr_accessor :called_services
|
6
|
-
attr_accessor :composer_params
|
7
|
-
|
8
|
-
class << self
|
9
|
-
def compose(*services)
|
10
|
-
@services = services.flatten
|
11
|
-
end
|
12
|
-
|
13
|
-
def services
|
14
|
-
@services ||= []
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
# Locate Results from Magic Methods
|
19
|
-
def method_missing(method_sym, *arguments, &block)
|
20
|
-
if method_sym.to_s =~ /^(.*)_results$/
|
21
|
-
results = @results[$1.to_sym]
|
22
|
-
return results unless results.nil?
|
23
|
-
end
|
24
|
-
|
25
|
-
super
|
26
|
-
end
|
27
|
-
|
28
|
-
def run!(**args, &block)
|
29
|
-
@composer_params = args
|
30
|
-
@called_services = []
|
31
|
-
@results = {}
|
32
|
-
|
33
|
-
# Attempt to run each Service, if any fail,
|
34
|
-
# call rollback on all those already run in
|
35
|
-
# reverse order.
|
36
|
-
begin
|
37
|
-
self.class.services.each do |service|
|
38
|
-
service_sym = service_to_sym(service)
|
39
|
-
service_args = service_to_args(service)
|
40
|
-
|
41
|
-
# Run the service then add it to called_services
|
42
|
-
@results[service_sym] = service.new.run!(service_args)
|
43
|
-
@called_services << service
|
44
|
-
end
|
45
|
-
rescue ServiceFailure
|
46
|
-
@called_services.reverse_each do |service|
|
47
|
-
service_args = service_to_args(service)
|
48
|
-
service_result = service_results(service)
|
49
|
-
|
50
|
-
# Pass the original args and result object
|
51
|
-
service.rollback(service_args, service_result)
|
52
|
-
end
|
53
|
-
|
54
|
-
raise
|
55
|
-
end
|
56
|
-
|
57
|
-
call
|
58
|
-
@called = true
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
# Convert a Service to an underscored symbol
|
64
|
-
def service_to_sym(service)
|
65
|
-
service.name.underscore.to_sym
|
66
|
-
end
|
67
|
-
|
68
|
-
# Convert a Service to a call to an args method
|
69
|
-
def service_to_args(service)
|
70
|
-
service_sym = service_to_sym(service)
|
71
|
-
self.method("#{service_sym}_args").call
|
72
|
-
end
|
73
|
-
|
74
|
-
# Convert a Service to its result object
|
75
|
-
def service_results(service)
|
76
|
-
service_sym = service_to_sym(service)
|
77
|
-
return @results[service_sym]
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|