logicum 0.0.1 → 1.0.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/.gitignore +1 -0
- data/README.md +134 -15
- data/lib/logicum.rb +2 -3
- data/lib/logicum/class_attribute.rb +42 -0
- data/lib/logicum/errors.rb +12 -0
- data/lib/logicum/interactor.rb +75 -0
- data/lib/logicum/result.rb +30 -0
- data/lib/logicum/version.rb +1 -1
- metadata +8 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98f50316e65e143565d4860ded87ea42f6c15710e9366ad3d461de939090ba9c
|
4
|
+
data.tar.gz: ec3ac7c1b95508eabeb1b65a6f893b2b952add91d306ba96701810f90ddeb102
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e193993fec01cbf098e429750d2fe0a102c7742a547cb674520986e34abc5c686781d009dbd832ef105b745cbc2e03db614fe3e829595f306b09373c73af6a93
|
7
|
+
data.tar.gz: ad952edbb3aa6cd4f17ea7f5e8057890e7e22299b53b32866700be6c3f5c793c6ab9c31418511ad4009e9db4cae0ed0d864384616d5dc0702914bb51a58072b3
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,140 @@
|
|
1
1
|
# Logicum
|
2
2
|
|
3
|
-
|
3
|
+
A simple, consistent interface for executing a unit of business logic.
|
4
|
+
|
5
|
+
|
6
|
+
## Usage
|
7
|
+
|
8
|
+
In a nutshell:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
class DoSomething
|
12
|
+
# Turn your object into an interactor.
|
13
|
+
include Logicum::Interactor
|
14
|
+
|
15
|
+
# Declare any values available on the result of call().
|
16
|
+
# You must set these instance variables in your call() method.
|
17
|
+
provides :foo, :bar
|
18
|
+
|
19
|
+
# Encapsulate your logic in a call() method.
|
20
|
+
#
|
21
|
+
# The call() method will not raise an error if your logic raises an error.
|
22
|
+
# Instead the result will be a failure and the error message will be available.
|
23
|
+
#
|
24
|
+
# Returns a result object which responds to :success?, :failure?, and :error.
|
25
|
+
def call(params:)
|
26
|
+
# do stuff
|
27
|
+
|
28
|
+
# Set the variables to provide on the result.
|
29
|
+
@foo = params[:foo] + 3
|
30
|
+
@bar = 153
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# And you use it like this:
|
35
|
+
|
36
|
+
result = DoSomething.new.call params: {foo: 42}
|
37
|
+
result.success? # true
|
38
|
+
result.foo # 45
|
39
|
+
result.bar # 153
|
40
|
+
```
|
41
|
+
|
42
|
+
If you don't need to pass arguments into your initializer, you can send `:call` to the class instead:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
result = DoSomething.call params: {foo: 42}
|
46
|
+
```
|
47
|
+
|
48
|
+
The result is successful unless an exception is raised. You can also explicitly make the result a failure using the `fail!` method, which takes an optional string message.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class DoSomething
|
52
|
+
include Logicum::Interactor
|
53
|
+
|
54
|
+
def call(foo:)
|
55
|
+
fail! 'This went wrong'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
result = DoSomething.call 153
|
60
|
+
result.failure? # true
|
61
|
+
resut.error # 'This went wrong'
|
62
|
+
```
|
63
|
+
|
64
|
+
## Purpose
|
65
|
+
|
66
|
+
The motivation was to move all business logic out of Rails controllers.
|
67
|
+
|
68
|
+
Instead of this:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class UsersController < ApplicationController
|
72
|
+
|
73
|
+
def create
|
74
|
+
@user = User.new user_params
|
75
|
+
|
76
|
+
if @user.save
|
77
|
+
redirect_to @user
|
78
|
+
else
|
79
|
+
render :edit
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
You can write this:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
class AddUser
|
90
|
+
include Logicum::Interactor
|
91
|
+
|
92
|
+
provides :user
|
93
|
+
|
94
|
+
def call(params)
|
95
|
+
@user = User.new params
|
96
|
+
@user.save!
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
class UsersController < ApplicationController
|
102
|
+
|
103
|
+
def create
|
104
|
+
result = AddUser.call user_params
|
105
|
+
|
106
|
+
if result.success?
|
107
|
+
redirect_to result.user
|
108
|
+
else
|
109
|
+
@user = result.user
|
110
|
+
render :edit
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
This is more code, so why bother?
|
118
|
+
|
119
|
+
The controller no longer has any business logic in it. It simply mediates between HTTP and your domain. We have separated concerns, reduced coupling, and increased cohesion.
|
120
|
+
|
121
|
+
It's a consistent interface.
|
122
|
+
|
123
|
+
If you situate all your business operations in a directory, e.g. `app/interactors/` or `app/services/`, you can see at a glance everything your application does.
|
124
|
+
|
125
|
+
The more complicated your logic gets, the more appealing this approach is. The example above is simple and therefore not especially compelling :) However as your application grows and you add business logic – e.g. sending emails, updating analytics, triggering background jobs – you can do it without cluttering up your controllers with details they should not know about.
|
126
|
+
|
127
|
+
|
128
|
+
## Inspiration
|
129
|
+
|
130
|
+
Although the command object / service object pattern has been around for ages I have never felt it was worthwhile for my applications. However recently these three libraries persuaded me otherwise.
|
131
|
+
|
132
|
+
- GoCardless's [Coach](https://github.com/gocardless/coach)
|
133
|
+
- CollectiveIdea's [Interactor](https://github.com/collectiveidea/interactor)
|
134
|
+
- Hanami's [Interactor](https://github.com/hanami/utils/blob/a74304af5bb69f6a561aad2718943388fac30782/lib/hanami/interactor.rb)
|
135
|
+
|
136
|
+
I wanted something even lighter weight, providing just enough structure for the benefits to materialise, so I wrote my own.
|
4
137
|
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
138
|
|
7
139
|
## Installation
|
8
140
|
|
@@ -20,19 +152,6 @@ Or install it yourself as:
|
|
20
152
|
|
21
153
|
$ gem install logicum
|
22
154
|
|
23
|
-
## Usage
|
24
|
-
|
25
|
-
TODO: Write usage instructions here
|
26
|
-
|
27
|
-
## Development
|
28
|
-
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
-
|
31
|
-
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).
|
32
|
-
|
33
|
-
## Contributing
|
34
|
-
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/airblade/logicum.
|
36
155
|
|
37
156
|
## License
|
38
157
|
|
data/lib/logicum.rb
CHANGED
@@ -0,0 +1,42 @@
|
|
1
|
+
# https://github.com/hanami/utils/blob/master/lib/hanami/utils/class_attribute.rb
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Logicum
|
6
|
+
module ClassAttribute
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def class_attribute(*attributes)
|
15
|
+
singleton_class.class_eval do
|
16
|
+
attr_accessor *attributes
|
17
|
+
end
|
18
|
+
|
19
|
+
class_attributes.merge attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def inherited(subclass)
|
25
|
+
class_attributes.each do |attribute|
|
26
|
+
value = send(attribute).dup
|
27
|
+
subclass.class_attribute attribute
|
28
|
+
subclass.send "#{attribute}=", value
|
29
|
+
end
|
30
|
+
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def class_attributes
|
37
|
+
@class_attributes ||= Set.new
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Logicum
|
2
|
+
|
3
|
+
Error = Class.new StandardError
|
4
|
+
|
5
|
+
# The business object does not implement a call() instance method.
|
6
|
+
MissingCallError = Class.new Error
|
7
|
+
|
8
|
+
# The business object declares that it provides a value, but it was
|
9
|
+
# not set in the call() instance method.
|
10
|
+
ProvisionError = Class.new Error
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'logicum/class_attribute'
|
2
|
+
require 'logicum/result'
|
3
|
+
require 'logicum/errors'
|
4
|
+
|
5
|
+
module Logicum
|
6
|
+
module Interactor
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend ClassMethods
|
10
|
+
base.prepend CallInterface
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def self.extended(base)
|
16
|
+
base.class_eval do
|
17
|
+
include ClassAttribute
|
18
|
+
class_attribute :provisions
|
19
|
+
self.provisions = []
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def provides(*instance_variable_names)
|
24
|
+
provisions.concat instance_variable_names
|
25
|
+
end
|
26
|
+
|
27
|
+
# Shortcut for caller if nothing needed in intializer.
|
28
|
+
# For example:
|
29
|
+
#
|
30
|
+
# AddUser.call foo: 'bar'
|
31
|
+
#
|
32
|
+
# is equivalent to:
|
33
|
+
#
|
34
|
+
# AddUser.new.call foo: 'bar'
|
35
|
+
def call(*args, &block)
|
36
|
+
new.call *args, &block
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
module CallInterface
|
42
|
+
def call(*)
|
43
|
+
raise MissingCallError unless defined? super
|
44
|
+
|
45
|
+
@__result__ = Result.new
|
46
|
+
|
47
|
+
begin
|
48
|
+
super
|
49
|
+
rescue StandardError => e
|
50
|
+
@__result__.fail! e.message
|
51
|
+
end
|
52
|
+
|
53
|
+
self.class.provisions.each do |attr|
|
54
|
+
ivar_name = "@#{attr}"
|
55
|
+
if instance_variable_defined? ivar_name
|
56
|
+
val = instance_variable_get ivar_name
|
57
|
+
@__result__.define_singleton_method(attr) { val }
|
58
|
+
else
|
59
|
+
# Calling code must ensure instance variables to provide are
|
60
|
+
# set before any code which could raise an exception.
|
61
|
+
raise ProvisionError, "#{ivar_name} was not set in call() method"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
@__result__
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def fail!(message = '')
|
71
|
+
@__result__.fail! message
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Logicum
|
2
|
+
module Interactor
|
3
|
+
|
4
|
+
class Result
|
5
|
+
def initialize
|
6
|
+
@success = true
|
7
|
+
@error = ''
|
8
|
+
end
|
9
|
+
|
10
|
+
def success?
|
11
|
+
@success
|
12
|
+
end
|
13
|
+
|
14
|
+
def failure?
|
15
|
+
!success?
|
16
|
+
end
|
17
|
+
|
18
|
+
def fail!(message = '')
|
19
|
+
@success = false
|
20
|
+
@error = message
|
21
|
+
end
|
22
|
+
|
23
|
+
def error
|
24
|
+
@error.dup
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
data/lib/logicum/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logicum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andy Stewart
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -67,6 +67,10 @@ files:
|
|
67
67
|
- bin/console
|
68
68
|
- bin/setup
|
69
69
|
- lib/logicum.rb
|
70
|
+
- lib/logicum/class_attribute.rb
|
71
|
+
- lib/logicum/errors.rb
|
72
|
+
- lib/logicum/interactor.rb
|
73
|
+
- lib/logicum/result.rb
|
70
74
|
- lib/logicum/version.rb
|
71
75
|
- logicum.gemspec
|
72
76
|
homepage: https://github.com/airblade/logicum
|
@@ -88,7 +92,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
92
|
- !ruby/object:Gem::Version
|
89
93
|
version: '0'
|
90
94
|
requirements: []
|
91
|
-
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 2.7.3
|
92
97
|
signing_key:
|
93
98
|
specification_version: 4
|
94
99
|
summary: Simplifies writing a unit of business logic.
|