slayer 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
![Slayer](https://raw.githubusercontent.com/apsislabs/slayer/master/slayer_logo.png)
|
2
2
|
|
3
|
-
# Slayer: A Service Layer
|
3
|
+
# Slayer: A Killer Service Layer
|
4
4
|
|
5
|
-
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/slayer.svg)](https://badge.fury.io/rb/slayer) [![Build Status](https://travis-ci.org/apsislabs/slayer.svg?branch=master)](https://travis-ci.org/apsislabs/slayer) [![Code Climate](https://codeclimate.com/github/apsislabs/slayer/badges/gpa.svg)](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
|