simple_service 1.4.1 → 2.1.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/.travis.yml +1 -2
- data/README.md +111 -123
- data/lib/simple_service.rb +78 -8
- data/lib/simple_service/configuration.rb +9 -0
- data/lib/simple_service/result.rb +56 -0
- data/lib/simple_service/version.rb +1 -1
- data/spec/simple_service_spec.rb +176 -1
- data/spec/support/basic_service.rb +19 -0
- data/spec/support/command_one.rb +21 -0
- data/spec/support/command_two.rb +15 -0
- data/spec/support/looping_service.rb +20 -0
- metadata +12 -28
- data/example/hello_world.rb +0 -29
- data/example/nested_organizer.rb +0 -29
- data/example/nested_services.rb +0 -29
- data/example/override_organizer_call_method.rb +0 -39
- data/lib/simple_service/command.rb +0 -28
- data/lib/simple_service/commands/validates_commands_not_empty.rb +0 -16
- data/lib/simple_service/commands/validates_commands_properly_inherit.rb +0 -24
- data/lib/simple_service/commands/validates_expected_keys.rb +0 -19
- data/lib/simple_service/context.rb +0 -32
- data/lib/simple_service/ensure_organizer_is_valid.rb +0 -22
- data/lib/simple_service/exceptions.rb +0 -10
- data/lib/simple_service/organizer.rb +0 -113
- data/lib/simple_service/service_base.rb +0 -169
- data/spec/lib/command_spec.rb +0 -111
- data/spec/lib/commands/validates_commands_not_empty_spec.rb +0 -28
- data/spec/lib/commands/validates_commands_properly_inherit_spec.rb +0 -34
- data/spec/lib/commands/validates_expected_keys_spec.rb +0 -60
- data/spec/lib/ensure_organizer_is_valid_spec.rb +0 -17
- data/spec/lib/organizer_spec.rb +0 -216
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e3f3509cf01d669899e4b014dfd78117a99f0e0
|
4
|
+
data.tar.gz: 728f0043ddfc2e9d9c8d031416f900b2c4bba888
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83f503070b874dd3f590e1ef8bcec3c806615a415b3f31a3f1c4c5735a5dc594bff649dc7bd20a031451a8a1b981b2e08aac7e7fc0141e5b9fcd248d1bdea9ef
|
7
|
+
data.tar.gz: d0b1f1da0d55f45d8ef5192bc3beb5d9fc924e8a2f03f51ab33141d6973019bf596e341fd5ef4ea06e9ef561e2ac443a4240937a7129e8063fbb17133fe85a0b
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -6,169 +6,157 @@
|
|
6
6
|
[](https://travis-ci.org/jspillers/simple_service)
|
7
7
|
<!---->
|
8
8
|
|
9
|
-
SimpleService
|
10
|
-
and composable
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
these command objects can be reused in multiple organizers minimizing code duplication.
|
15
|
-
|
16
|
-
When an organizer is instantiated a hash is passed in containing initial arguments.
|
17
|
-
This hash is referred to as the context. The context hash is carried along throughout
|
18
|
-
the sequence of command executions and modified by each command. After a
|
19
|
-
successful run, the keys specified are returned. If keys are not specified, the
|
20
|
-
entire context hash is returned.
|
21
|
-
|
22
|
-
# Setup
|
23
|
-
|
24
|
-
First, setup an Organizer class. An Organizer needs the following things defined:
|
25
|
-
|
26
|
-
* expects: keys that are required to be passed into initialize when an
|
27
|
-
instance of organizer is created. If not defined the organizer will
|
28
|
-
accept arbitrary arguments.
|
29
|
-
* returns: keys that will be returned when the organizer has called all of
|
30
|
-
its commands
|
31
|
-
* commands: classes that define all the steps that the organizer will call.
|
32
|
-
The organizer will execute #call on each command in order and the context
|
33
|
-
hash is passed to each of these commands. Any keys within the context that
|
34
|
-
are modified will be merged back into the organizer and passed along to the
|
35
|
-
next command.
|
9
|
+
SimpleService facilitates the creation of Ruby service objects into highly discreet, reusable,
|
10
|
+
and composable units of business logic. The core concept of SimpleService is the definition of
|
11
|
+
"Command" objects/methods. Commands are very small classes or methods that perform exactly one task.
|
12
|
+
When properly designed, these command objects can be composited together or even nested to create
|
13
|
+
complex flows.
|
36
14
|
|
37
|
-
|
38
|
-
class ProcessSomethingComplex < SimpleService::Organizer
|
15
|
+
## Installation
|
39
16
|
|
40
|
-
|
41
|
-
# leave out to accept any arguments/keys
|
42
|
-
expects :something, :another_thing
|
17
|
+
Add this line to your application's Gemfile:
|
43
18
|
|
44
|
-
|
45
|
-
# an organizer instance
|
46
|
-
returns :modified_thing
|
19
|
+
gem 'simple_service'
|
47
20
|
|
48
|
-
|
49
|
-
# #call will be executed on an instance of each class in sequence
|
50
|
-
commands DoSomethingImportant, DoAnotherStep
|
21
|
+
And then execute:
|
51
22
|
|
52
|
-
|
53
|
-
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install simple_service
|
28
|
+
|
29
|
+
# Setup and Basic Usage:
|
54
30
|
|
55
|
-
|
56
|
-
|
31
|
+
* load the gem
|
32
|
+
* include the SimpleService module into your service object class
|
33
|
+
* define one or more comamnds that it will perform, must accept either keyword arguments or a hash argument
|
34
|
+
* call `#success` or `#failure` with any values you wish to pass along to the next command (or wish to return if it is the last command)
|
57
35
|
|
58
36
|
```ruby
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
# getters and setters are available for each key specified
|
72
|
-
# in expects and returns. If not using expects and returns
|
73
|
-
# simply interact with the context hash directly
|
74
|
-
def call
|
75
|
-
# uses getters and setters to modify the context
|
76
|
-
self.modified_something = self.something.to_i + 1
|
77
|
-
|
78
|
-
# or act directly on the context hash
|
79
|
-
context[:modified_something] = context[:something].to_i + 1
|
80
|
-
|
81
|
-
# no need to return anything specific, either the keys
|
82
|
-
# specified in returns will be returned or the entire
|
83
|
-
# context if no returns are defined
|
37
|
+
require 'rubygems'
|
38
|
+
require 'simple_service'
|
39
|
+
|
40
|
+
class DoStuff
|
41
|
+
include SimpleService
|
42
|
+
|
43
|
+
commands :do_something_important, :do_another_important_thing
|
44
|
+
|
45
|
+
def do_something_important(name:)
|
46
|
+
message = "hey #{name}"
|
47
|
+
|
48
|
+
success(message: message)
|
84
49
|
end
|
85
50
|
|
86
|
-
|
51
|
+
def do_another_important_thing(message:)
|
52
|
+
new_message = "#{message}, we are doing something important!"
|
87
53
|
|
88
|
-
|
89
|
-
|
54
|
+
success(the_final_result: new_message)
|
55
|
+
end
|
90
56
|
end
|
57
|
+
|
58
|
+
result = DoStuff.call(name: 'Alice')
|
59
|
+
result.success? #=> true
|
60
|
+
result.value #=> {:the_final_result=>"hey Alice, we are doing something important!"}
|
91
61
|
```
|
92
62
|
|
93
|
-
|
94
|
-
abort execution of subsequent commands within the organizer.
|
63
|
+
A failure:
|
95
64
|
|
96
65
|
```ruby
|
97
|
-
|
98
|
-
|
99
|
-
expects :something
|
66
|
+
require 'rubygems'
|
67
|
+
require 'simple_service'
|
100
68
|
|
101
|
-
|
69
|
+
class DoFailingStuff
|
70
|
+
include SimpleService
|
102
71
|
|
103
|
-
|
104
|
-
if good_stuff_happens
|
105
|
-
self.good_stuff = 'yeah, success!'
|
106
|
-
else
|
107
|
-
failure!('something not so good happened; no more commands should be called')
|
108
|
-
end
|
109
|
-
end
|
72
|
+
commands :fail_at_something
|
110
73
|
|
74
|
+
def fail_at_something(name:)
|
75
|
+
message = "hey #{name}, things went wrong."
|
76
|
+
|
77
|
+
failure(message: message)
|
78
|
+
end
|
111
79
|
end
|
80
|
+
|
81
|
+
result = DoStuff.call(name: 'Bob')
|
82
|
+
result.success? #=> false
|
83
|
+
result.failure? #=> true
|
84
|
+
result.value #=> {:message=>"hey Bob, things went wrong."}
|
112
85
|
```
|
113
86
|
|
114
|
-
|
87
|
+
You can also use ClassNames as commands and to organize them into other files:
|
115
88
|
|
116
|
-
|
117
|
-
|
89
|
+
```ruby
|
90
|
+
require 'rubygems'
|
91
|
+
require 'simple_service'
|
118
92
|
|
119
|
-
|
120
|
-
|
121
|
-
something: '1',
|
122
|
-
:another_thing: AnotherThing.new
|
123
|
-
}
|
124
|
-
modified_context = ProcessSomethingComplex.new(starting_context).call
|
93
|
+
class CommandOne
|
94
|
+
include SimpleService
|
125
95
|
|
126
|
-
|
127
|
-
modified_context = ProcessSomethingComplex.call(starting_context)
|
96
|
+
command :add_stuff
|
128
97
|
|
129
|
-
|
130
|
-
|
98
|
+
def add_stuff(one:, two:)
|
99
|
+
success(three: one + two)
|
100
|
+
end
|
101
|
+
end
|
131
102
|
|
132
|
-
|
133
|
-
|
134
|
-
not using rails, a similar structure would also be recommended.
|
103
|
+
class CommandTwo
|
104
|
+
include SimpleService
|
135
105
|
|
136
|
-
|
106
|
+
command :add_more_stuff
|
137
107
|
|
138
|
-
|
139
|
-
|
140
|
-
|
108
|
+
def add_more_stuff(three:)
|
109
|
+
binding.pry
|
110
|
+
success(seven: three + 4)
|
111
|
+
end
|
112
|
+
end
|
141
113
|
|
142
|
-
|
114
|
+
class DoNestedStuff
|
115
|
+
include SimpleService
|
143
116
|
|
144
|
-
|
145
|
-
|
146
|
-
[light-service](https://github.com/adomokos/light-service).
|
117
|
+
commands CommandOne, CommandTwo
|
118
|
+
end
|
147
119
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
methods within the command for more complex commands that still do a single thing.
|
120
|
+
result = DoNestedStuff.call(one: 1, two: 2)
|
121
|
+
result.success? #=> true
|
122
|
+
result.value #=> {:seven=>7}
|
123
|
+
```
|
153
124
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
125
|
+
If you would like your service to process an enumerable you can override `#call`
|
126
|
+
on your service object. Invoking `#super` in your definition and passing along
|
127
|
+
the appropriate arguments will allow your command chain to proceed as normal, but
|
128
|
+
called multiple times via a loop. The Result object returned from each call to `#super`
|
129
|
+
can be passed in as an argument to the next iteration or you can collect the result objects
|
130
|
+
yourself and then do any post processing required.
|
158
131
|
|
159
|
-
|
132
|
+
```ruby
|
133
|
+
require 'rubygems'
|
134
|
+
require 'simple_service'
|
160
135
|
|
161
|
-
|
136
|
+
class LoopingService
|
137
|
+
include SimpleService
|
162
138
|
|
163
|
-
|
139
|
+
commands :add_one
|
164
140
|
|
165
|
-
|
141
|
+
def call(kwargs)
|
142
|
+
count = kwargs
|
166
143
|
|
167
|
-
|
144
|
+
3.times do
|
145
|
+
count = super(count)
|
146
|
+
end
|
168
147
|
|
169
|
-
|
148
|
+
count
|
149
|
+
end
|
170
150
|
|
171
|
-
|
151
|
+
def add_one(count:)
|
152
|
+
success(count: count + 1)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
If you are using this with a Rails app, placing top level services in
|
158
|
+
`app/services/` and all nested commands in `app/services/commands/` is
|
159
|
+
recommended. Even if not using rails, a similar structure also works well.
|
172
160
|
|
173
161
|
## Contributing
|
174
162
|
|
data/lib/simple_service.rb
CHANGED
@@ -1,12 +1,82 @@
|
|
1
|
-
require 'simple_service/
|
2
|
-
require 'simple_service/
|
3
|
-
require 'simple_service/organizer'
|
4
|
-
require 'simple_service/exceptions'
|
5
|
-
require 'simple_service/commands/validates_commands_not_empty'
|
6
|
-
require 'simple_service/commands/validates_commands_properly_inherit'
|
7
|
-
require 'simple_service/commands/validates_expected_keys'
|
8
|
-
require 'simple_service/ensure_organizer_is_valid'
|
1
|
+
require 'simple_service/result'
|
2
|
+
require 'simple_service/configuration'
|
9
3
|
require 'simple_service/version'
|
10
4
|
|
11
5
|
module SimpleService
|
6
|
+
def self.included(klass)
|
7
|
+
klass.extend ClassMethods
|
8
|
+
klass.include InstanceMethods
|
9
|
+
self.configure
|
10
|
+
end
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_accessor :configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.configure
|
17
|
+
self.configuration ||= Configuration.new
|
18
|
+
yield(configuration) if block_given?
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def call(**kwargs)
|
23
|
+
service = self.new
|
24
|
+
|
25
|
+
if service.method(:call).arity.zero?
|
26
|
+
service.call
|
27
|
+
else
|
28
|
+
service.call(kwargs)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def command(command_name)
|
33
|
+
@commands ||= []
|
34
|
+
@commands << command_name
|
35
|
+
end
|
36
|
+
|
37
|
+
def commands(*args)
|
38
|
+
@commands ||= []
|
39
|
+
@commands += args
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module InstanceMethods
|
44
|
+
def result
|
45
|
+
@result ||= Result.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def commands
|
49
|
+
self.class.instance_variable_get('@commands')
|
50
|
+
end
|
51
|
+
|
52
|
+
def call(kwargs)
|
53
|
+
result.value = kwargs.is_a?(Result) ? kwargs.value : kwargs
|
54
|
+
|
55
|
+
commands.each do |command|
|
56
|
+
@current_command = command
|
57
|
+
|
58
|
+
command_output = if command.is_a?(Class)
|
59
|
+
command.new.call(result.value)
|
60
|
+
elsif command.is_a?(Symbol)
|
61
|
+
method(command).call(result.value)
|
62
|
+
end
|
63
|
+
|
64
|
+
if command_output.is_a?(Result)
|
65
|
+
result.append_result(command_output)
|
66
|
+
end
|
67
|
+
|
68
|
+
break if result.failure?
|
69
|
+
end
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
def success(result_value)
|
75
|
+
result.success!(self.class, @current_command, result_value)
|
76
|
+
end
|
77
|
+
|
78
|
+
def failure(result_value)
|
79
|
+
result.failure!(self.class, @current_command, result_value)
|
80
|
+
end
|
81
|
+
end
|
12
82
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module SimpleService
|
2
|
+
class Result
|
3
|
+
attr_accessor :value, :recorded_commands
|
4
|
+
|
5
|
+
def initialize()
|
6
|
+
@recorded_commands = []
|
7
|
+
@verbose_tracking = SimpleService.configuration.verbose_tracking
|
8
|
+
end
|
9
|
+
|
10
|
+
def success!(klass, command_name, result_value)
|
11
|
+
record_command(klass, command_name, result_value, :success)
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure!(klass, command_name, result_value)
|
15
|
+
record_command(klass, command_name, result_value, :failure)
|
16
|
+
end
|
17
|
+
|
18
|
+
def append_result(other_result)
|
19
|
+
self.value = other_result.value
|
20
|
+
self.recorded_commands += other_result.recorded_commands
|
21
|
+
end
|
22
|
+
|
23
|
+
def commands
|
24
|
+
self.recorded_commands.map {|rc| rc[:command_name] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def successes
|
28
|
+
self.recorded_commands.map {|rc| rc.has_key?(:success) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def success?
|
32
|
+
successes.all?
|
33
|
+
end
|
34
|
+
|
35
|
+
def failure?
|
36
|
+
!success?
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :verbose_tracking
|
42
|
+
|
43
|
+
def record_command(klass, command_name, result_value, success_or_failure)
|
44
|
+
command_attrs = {
|
45
|
+
class_name: klass.to_s,
|
46
|
+
command_name: command_name,
|
47
|
+
}
|
48
|
+
|
49
|
+
command_attrs[:received_args] = self.value if verbose_tracking
|
50
|
+
command_attrs[success_or_failure] = verbose_tracking ? result_value : true
|
51
|
+
|
52
|
+
self.recorded_commands << command_attrs
|
53
|
+
self.value = result_value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|