simple_service 1.4.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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