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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f173ba72cf1c5abefb364936804cef31b6ce102c
4
- data.tar.gz: dc9ca5da527f09a06a7b1bfb4167ae0911814e65
3
+ metadata.gz: 0e3f3509cf01d669899e4b014dfd78117a99f0e0
4
+ data.tar.gz: 728f0043ddfc2e9d9c8d031416f900b2c4bba888
5
5
  SHA512:
6
- metadata.gz: ff4e0f4bc3503e57ad6d91c97ecbc4cedc0cd089e5b73a3f0d3c8e6fd5e6155f511e20ac43c85117bd677b7d657187ed8d926e87075be3e1b34494b2c007719e
7
- data.tar.gz: '08fb83a60818d0926f4883b1f43bd18249755616b805450feada31e2b7fe906c8db3a5e684eabc1eedac0dc4e15634c285c4a662aebcaa14fc4f5548bad91e56'
6
+ metadata.gz: 83f503070b874dd3f590e1ef8bcec3c806615a415b3f31a3f1c4c5735a5dc594bff649dc7bd20a031451a8a1b981b2e08aac7e7fc0141e5b9fcd248d1bdea9ef
7
+ data.tar.gz: d0b1f1da0d55f45d8ef5192bc3beb5d9fc924e8a2f03f51ab33141d6973019bf596e341fd5ef4ea06e9ef561e2ac443a4240937a7129e8063fbb17133fe85a0b
data/.travis.yml CHANGED
@@ -1,8 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - jruby-19mode
4
- - 2.0.0
5
4
  - 2.1.0
6
- - 2.2.0
7
5
  - 2.3.0
8
6
  - 2.3.3
7
+ - 2.4.2
data/README.md CHANGED
@@ -6,169 +6,157 @@
6
6
  [![Build Status](https://travis-ci.org/jspillers/simple_service.svg?branch=master)](https://travis-ci.org/jspillers/simple_service)
7
7
  <!--![](http://ruby-gem-downloads-badge.herokuapp.com/jspillers/simple_service)-->
8
8
 
9
- SimpleService provides a way to organize Ruby service objects into highly reusable
10
- and composable classes that are easy to implement and easy to read. Instead of
11
- writing large service objects that perform multiple tasks, SimpleService
12
- helps you breakdown tasks into a set of sequentially performed "Command" objects.
13
- Commands are very small classes that perform exactly one task. When properly designed,
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
- ```ruby
38
- class ProcessSomethingComplex < SimpleService::Organizer
15
+ ## Installation
39
16
 
40
- # optional - ensures the following keys are provided during instantiation
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
- # optional - specifies which keys get returned after #call is executed on
45
- # an organizer instance
46
- returns :modified_thing
19
+ gem 'simple_service'
47
20
 
48
- # what steps comprise this service
49
- # #call will be executed on an instance of each class in sequence
50
- commands DoSomethingImportant, DoAnotherStep
21
+ And then execute:
51
22
 
52
- end
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
- Next, define all command classes that make up the service. Each should inherit
56
- from SimpleService::Command and define similar things to the organizer:
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
- class DoSomethingImportant < SimpleService::Command
60
-
61
- # optional - creates getter/setter for each key specified,
62
- # leave blank to accept arbitrary args
63
- expects :something
64
-
65
- # optional - creates getter/setter for each key specified,
66
- # leave blank to return entire context hash
67
- returns :modified_something, :another_thing
68
-
69
- # required - this is where the work gets done, should only
70
- # do one thing (single responsibility principle)
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
- end
51
+ def do_another_important_thing(message:)
52
+ new_message = "#{message}, we are doing something important!"
87
53
 
88
- class DoSomethingImportant < SimpleService::Command
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
- Within any command you can call ```#failure!``` and then return in order to
94
- abort execution of subsequent commands within the organizer.
63
+ A failure:
95
64
 
96
65
  ```ruby
97
- class DemonstrateFailure < SimpleService::Command
98
-
99
- expects :something
66
+ require 'rubygems'
67
+ require 'simple_service'
100
68
 
101
- returns :good_stuff
69
+ class DoFailingStuff
70
+ include SimpleService
102
71
 
103
- def call
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
- ## Usage
87
+ You can also use ClassNames as commands and to organize them into other files:
115
88
 
116
- Using the service is straight forward - just instantiate it, passing in the
117
- intial context hash, and then call.
89
+ ```ruby
90
+ require 'rubygems'
91
+ require 'simple_service'
118
92
 
119
- ```ruby
120
- starting_context = {
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
- # alternatively, you can call directly from the service class
127
- modified_context = ProcessSomethingComplex.call(starting_context)
96
+ command :add_stuff
128
97
 
129
- modified_context[:modified_thing] # => 2
130
- ```
98
+ def add_stuff(one:, two:)
99
+ success(three: one + two)
100
+ end
101
+ end
131
102
 
132
- If you are using this with a Rails app, placing top level services in
133
- app/services/ and all commands in app/services/commands/ is recommended. If
134
- not using rails, a similar structure would also be recommended.
103
+ class CommandTwo
104
+ include SimpleService
135
105
 
136
- For further examination of usage, here are a few examples:
106
+ command :add_more_stuff
137
107
 
138
- * [hello world example](example/hello_world.rb)
139
- * [nested services example](example/nested_services.rb)
140
- * [override #call on the organizer](example/override_organizer_call_method.rb)
108
+ def add_more_stuff(three:)
109
+ binding.pry
110
+ success(seven: three + 4)
111
+ end
112
+ end
141
113
 
142
- ## Inspiration and Rationale
114
+ class DoNestedStuff
115
+ include SimpleService
143
116
 
144
- This gem is heavily inspired by two very nice gems:
145
- [mutations](https://github.com/cypriss/mutations) and
146
- [light-service](https://github.com/adomokos/light-service).
117
+ commands CommandOne, CommandTwo
118
+ end
147
119
 
148
- Mutations is a great gem, but lacks the concept of a top level organizer.
149
- LightService brings in the notion of the organizer object, but doesn't create
150
- instances of its action objects (what are referred to as commands here). Using
151
- instances rather than class level #call definitions allows the use of private
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
- The other goal of this gem is to do as little as possible above and beyond
155
- just using plain old Ruby objects (PORO's). Things like error handling, logging,
156
- and context status will be left up to the individual to implement in a way that
157
- best suits their use case.
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
- ## Installation
132
+ ```ruby
133
+ require 'rubygems'
134
+ require 'simple_service'
160
135
 
161
- Add this line to your application's Gemfile:
136
+ class LoopingService
137
+ include SimpleService
162
138
 
163
- gem 'simple_service'
139
+ commands :add_one
164
140
 
165
- And then execute:
141
+ def call(kwargs)
142
+ count = kwargs
166
143
 
167
- $ bundle
144
+ 3.times do
145
+ count = super(count)
146
+ end
168
147
 
169
- Or install it yourself as:
148
+ count
149
+ end
170
150
 
171
- $ gem install simple_service
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
 
@@ -1,12 +1,82 @@
1
- require 'simple_service/service_base'
2
- require 'simple_service/command'
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,9 @@
1
+ module SimpleService
2
+ class Configuration
3
+ attr_accessor :verbose_tracking
4
+
5
+ def initialize
6
+ @verbose_tracking = false
7
+ end
8
+ end
9
+ 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