simple_operation 1.0.0 → 1.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: 32d773cfd369dfe0f15ab8ee90b2f73b66dec5e3
4
- data.tar.gz: a20f16a09077c99747b61f99961e937815e637e4
3
+ metadata.gz: e4d62e3d9a9b251bc5134553b1fed1ed60252528
4
+ data.tar.gz: 275f05866d4c3cdeb98edf14485258e42e979045
5
5
  SHA512:
6
- metadata.gz: e2559b6cae03f2b3de34361c604e4cce3b769b20836473471266528be4503462a4f3078284e6eaf2d68fe24396a1332c45dc36ba0c22b34071e99fc245232e40
7
- data.tar.gz: 427d88c82e640db9c389249e00be447d844ac525039c971213420b30f2b20324897ce8aacc2a872e2e08339a5f9eb39e39249cd864d6736727a1453966d0a015
6
+ metadata.gz: 89a3030ad2ed50c1ff4fe4debc67e4638592bb092b5e50ba704ae6f42a6bcea0fa487d64764cb55163052f2f5232944e9be122300fa761e5cec4877b0baf5629
7
+ data.tar.gz: 177cab278536edc7c62449cbd0a977160cb1d0ecf049ab41b015f5c57561153271f94d8f25e30e98284f2197dce3822947644a0713cf8674b84fe7eb16e75aa5
data/README.md CHANGED
@@ -18,76 +18,182 @@ Or install it yourself as:
18
18
 
19
19
  $ gem install simple_operation
20
20
 
21
+ ## Background
22
+
23
+ Working with large business applications means working with a big business logic layer.
24
+ There are many Ruby gems that help building business logic layer, notably:
25
+
26
+ * Trailblazer (http://trailblazer.to)
27
+ * dry-transaction (http://dry-rb.org/gems/dry-transaction/)
28
+
29
+ Why another gem then? I wanted something very simple and easy to use
30
+ and I wanted to be able to call my services like labmdas, without having to create new objects outside of the service.
31
+ I wanted to achieve syntax like `CreateOrder.(articles, user)` with the flexibility of memoizing objects inside the service.
32
+
21
33
  ## Usage
22
34
 
23
- Example usage for SimpleOperation is creating user:
35
+ ### Basic service
24
36
 
25
- ```ruby
26
- class CreateUser < SimpleOperation.new(:login, :password)
37
+ To define a new service, you have to do 2 steps:
38
+
39
+ 1. Create a new class by calling `SimpleOperation.new` with parameters you will be providing to the service
40
+ 2. Put your business logic into `call` method.
27
41
 
28
- InvalidUser = Class.new(RuntimeError)
42
+ Example:
43
+
44
+ ```
45
+ CreateOrder = SimpleOperation.new(:articles, :user) do
29
46
 
30
47
  def call
31
- user = User.new(login, password)
32
- raise InvalidUser unless valid_user?(user)
33
- UserRepository.persist(user)
34
- user
48
+ order = Order.new(articles_ids: articles.map(&:id), user_is: user.id)
49
+ OrderRepository.persist(order)
50
+ order
35
51
  end
52
+ end
53
+ ```
36
54
 
37
- private
38
- def valid_user?(user)
39
- !UserRepository.fetch_all_logins.include?(user.login)
40
- end
55
+ or in a more common class definition style:
56
+
57
+ ```
58
+ class CreateOrder < SimpleOperation.new(:articles, :user)
59
+
60
+ def call
61
+ # your business logic here, methods articles and user are available
62
+ end
41
63
 
42
64
  end
65
+ ```
43
66
 
67
+ Then you can call your service in one of two ways. A more functional way:
44
68
 
45
- CreateUser.('Grzegorz', 'arnvald.to@gmail.com')
69
+ ```
70
+ CreateOrder.(some_articles, some_user)
71
+ ```
46
72
 
47
- # the same effect as line above
48
- CreateUser.new('Grzegorz', 'arnvald.to@gmail.com').()
73
+ or by instantiating the object first
49
74
 
50
- # `perform` is an alias for `call`
51
- CreateUser.new('Grzegorz', 'arnvald.to@gmail.com').perform
75
+ ```
76
+ CreateOrder.new(some_articles, some_user).()
52
77
  ```
53
78
 
54
- If you need output that consists of more than one field, instead of returning multiple values,
55
- you can use `result` methods:
79
+ ### Returning result
56
80
 
57
- ```ruby
58
- class CreateCompanyAndUser < SimpleOperation(:name, :login, :password)
59
- # result on class-level defines output structure
60
- result :company, :user
81
+ SimpleOperation allows defining results by using success/failure definitions
82
+
83
+ ```
84
+ class CreateUser < SimpleOperation.new(:login, :password)
85
+ success :user
86
+ failure :error
61
87
 
62
88
  def call
63
- # result in call method returns defined structure
64
- result create_company(name), create_user(login, password)
89
+ if unique_login?
90
+ user = User.new(login, hashed_password)
91
+ UserRepository.persist(user)
92
+ Success(user)
93
+ else
94
+ Failure("login taken")
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def hashed_password
101
+ ...
102
+ end
103
+
104
+ def unique_login?
105
+ ...
65
106
  end
107
+
66
108
  end
67
109
  ```
68
110
 
69
- There's a sugar syntax for creating classes:
111
+ Using result definitions allows to easily check for status of the transaction and to define callbacks:
112
+
113
+ ```
114
+ result1 = CreateUser.("grzegorz", "witek")
115
+
116
+ result1.success? # => true
117
+ result1.user # => instance of User object
118
+ result1.failure? # => false
119
+
120
+ result2 = CreateUser.("grzegorz", "witek")
121
+ result2.success? # => false
122
+ result2.error # => "login taken"
123
+ result2.failure? # => true
124
+ ```
125
+
126
+ ### Result callbacks
127
+
128
+ Returning success and failure allows using callbacks:
129
+
130
+ ```
131
+ result1
132
+ .on_success { |s| puts "user created with login #{s.user.login}" }
133
+ .on_failure { |f| puts "creation failed because #{f.error}" }
134
+
135
+ # the code above will print "user created with login grzegorz"
136
+
137
+ result2
138
+ .on_success { |s| puts "user created with login #{s.user.login}" }
139
+ .on_failure { |f| puts "creation failed because #{f.error}" }
140
+
141
+ # the code above will print "creation failed because login taken"
142
+ ```
143
+
144
+ Failure callbacks can be assigned to specific reasons, which requires adding `:reason`
145
+ field to the failed response:
146
+
147
+ ```
148
+ class Authenticate < SimpleOperation.new(:login, :password)
149
+ success :user
150
+ failure :reason, :message
70
151
 
71
- ```ruby
72
- CreateUser = SimpleOperation.new(:login, :password) do
73
152
  def call
74
- ...
153
+ return Failure("NO_USER", "user does not exist") unless login_exists?
154
+ return Failure("WRONG_PASS", "wrong password") unless valid_password?
155
+ return Success(user)
75
156
  end
76
157
  end
77
158
  ```
78
159
 
79
- or
160
+ Then each error can be easily handled in a different way:
80
161
 
81
- ```ruby
82
- require 'simple_operation/ext'
162
+ ```
163
+ result = Authenticate.(login, password)
164
+
165
+ result
166
+ .on_failure("NO_USER") { create_new_user(login, password) }
167
+ .on_failure("WRONG_PASS") { increase_invalid_attempt_count }
168
+ .on_failure() { raise DontKnowWhatToDo } # this will be called only if the previous reasons don't match
169
+ .on_success { |s| s.user }
170
+ ```
171
+
172
+ ### Generic results
173
+
174
+ If you don't need a distinction between success and failure, you can define generic result
175
+
176
+ ```
177
+ class AggregateSalesData < SimpleOperation.new(:orders)
178
+
179
+ result :average_transaction, :number_of_transactions, :maximum_transaction
83
180
 
84
- CreateUser = SimpleOperation(:login, :password)
85
181
  def call
86
- ...
182
+ Result(
183
+ find_average_transaction,
184
+ transactions.size,
185
+ find_max_transaction
186
+ )
87
187
  end
188
+
88
189
  end
89
- ```
90
190
 
191
+ result = AggregateSalesData.(orders)
192
+
193
+ result.average_transaction
194
+ result.number_of_transactions
195
+ result.maximum_transaction
196
+ ```
91
197
 
92
198
  ## Contributing
93
199
 
@@ -1,4 +1,6 @@
1
1
  require_relative './simple_operation/version'
2
+ require_relative './simple_operation/success'
3
+ require_relative './simple_operation/failure'
2
4
 
3
5
  class SimpleOperation
4
6
 
@@ -28,9 +30,29 @@ class SimpleOperation
28
30
  @result_class = Struct.new(*args)
29
31
  end
30
32
 
33
+ def self.success(*args)
34
+ @success_class = Success.generate(*args)
35
+ end
36
+
37
+ def self.failure(*args)
38
+ @failure_class = Failure.generate(*args)
39
+ end
40
+
31
41
  def result(*args)
32
42
  self.class.instance_variable_get(:@result_class).new(*args)
33
43
  end
44
+
45
+ def success(*args)
46
+ self.class.instance_variable_get(:@success_class).new(*args)
47
+ end
48
+
49
+ def failure(*args)
50
+ self.class.instance_variable_get(:@failure_class).new(*args)
51
+ end
52
+
53
+ alias_method :Result, :result
54
+ alias_method :Success, :success
55
+ alias_method :Failure, :failure
34
56
  end
35
57
  end
36
58
 
@@ -0,0 +1,41 @@
1
+ require_relative './wrapped_value'
2
+
3
+ class SimpleOperation
4
+ module Failure
5
+
6
+ def self.generate(*args)
7
+ Struct.new(*args) do
8
+ include InstanceMethods
9
+ end
10
+ end
11
+
12
+ module InstanceMethods
13
+
14
+ def on_success
15
+ self
16
+ end
17
+
18
+ def on_failure(match_reason = nil)
19
+ if match_reason.nil? || (respond_to?(:reason) && reason == match_reason)
20
+ WrappedValue.new(yield self)
21
+ else
22
+ self
23
+ end
24
+ end
25
+
26
+ def success?
27
+ false
28
+ end
29
+
30
+ def failure?
31
+ true
32
+ end
33
+
34
+ def unwrap
35
+ self
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ require_relative './wrapped_value'
2
+
3
+ class SimpleOperation
4
+ module Success
5
+
6
+ def self.generate(*args)
7
+ Struct.new(*args) do
8
+ include InstanceMethods
9
+ end
10
+ end
11
+
12
+ module InstanceMethods
13
+
14
+ def on_success
15
+ WrappedValue.new(yield self)
16
+ end
17
+
18
+ def on_failure(reason = nil)
19
+ self
20
+ end
21
+
22
+ def success?
23
+ true
24
+ end
25
+
26
+ def failure?
27
+ false
28
+ end
29
+
30
+ def unwrap
31
+ self
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  class SimpleOperation
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -0,0 +1,17 @@
1
+ class SimpleOperation
2
+ class WrappedValue < SimpleDelegator
3
+
4
+ attr_reader :delegate_sd_obj
5
+
6
+ def on_success
7
+ self
8
+ end
9
+
10
+ def on_failure(reason = nil)
11
+ self
12
+ end
13
+
14
+ alias_method :unwrap, :delegate_sd_obj
15
+
16
+ end
17
+ end
@@ -20,4 +20,5 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.7"
22
22
  spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest"
23
24
  end
@@ -0,0 +1,63 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'simple_operation'
3
+ require 'minitest/autorun'
4
+
5
+ class FailureTest < Minitest::Test
6
+
7
+ FakeFailure = SimpleOperation::Failure.generate(:value)
8
+ FakeReasonFailure = SimpleOperation::Failure.generate(:reason)
9
+
10
+ def test_runs_correctly
11
+ assert SimpleOperation::Failure.generate(:value)
12
+ end
13
+
14
+ def test_has_correct_attributes
15
+ klass = SimpleOperation::Failure.generate(:name, :email)
16
+ instance = klass.new("Tom", "tom@tomtomtomtom.com")
17
+
18
+ assert_equal "Tom", instance.name
19
+ assert_equal "tom@tomtomtomtom.com", instance.email
20
+ end
21
+
22
+ def test_is_not_successful
23
+ refute FakeFailure.new("123").success?
24
+ end
25
+
26
+ def test_is_failure
27
+ assert FakeFailure.new("123").failure?
28
+ end
29
+
30
+ def test_returns_wrapped_value_on_failure
31
+ result = FakeFailure.new("123").on_failure { |s| s.value * 3 }
32
+
33
+ assert_equal SimpleOperation::WrappedValue.new("123123123"), result
34
+ end
35
+
36
+ def test_returns_wrapped_value_on_matching_reason
37
+ failure = FakeReasonFailure.new(:wrong_id)
38
+ result = failure.on_failure(:wrong_id) { |s| "this is #{s.reason}" }
39
+
40
+ assert_equal SimpleOperation::WrappedValue.new("this is wrong_id"), result
41
+ end
42
+
43
+ def test_returns_self_on_non_matching_reason
44
+ failure = FakeReasonFailure.new(:wrong_name)
45
+ result = failure.on_failure(:wrong_id) { |s| "this is #{s.reason}" }
46
+
47
+ assert_equal result, failure
48
+ end
49
+
50
+ def test_returns_self_on_success
51
+ failure = FakeFailure.new("123")
52
+ result = failure.on_success { |s| s.value * 4 }
53
+
54
+ assert_equal failure, result
55
+ end
56
+
57
+ def test_unwraps_self
58
+ failure = FakeFailure.new("123")
59
+
60
+ assert_equal failure, failure.unwrap
61
+ end
62
+ end
63
+
@@ -25,7 +25,7 @@ class SimpleOperationTest < Minitest::Test
25
25
  end
26
26
 
27
27
  def test_reader_is_assigned_default_value
28
- assert_equal klass.new.send(:login), nil
28
+ assert_nil klass.new.send(:login)
29
29
  end
30
30
 
31
31
  def test_instance_has_call_method
@@ -58,6 +58,15 @@ class SimpleOperationTest < Minitest::Test
58
58
  refute result_klass.('Grzegorz').found
59
59
  end
60
60
 
61
+ def test_returns_success
62
+ assert success_failure_klass.('Arnvald').success?
63
+ assert success_failure_klass.('Arnvald').user == {name: 'Grzegorz'}
64
+ end
65
+
66
+ def test_returns_failure
67
+ refute success_failure_klass.('').success?
68
+ assert success_failure_klass.('').error = 'invalid login'
69
+ end
61
70
 
62
71
  def object
63
72
  klass.new('Arnvald')
@@ -71,6 +80,10 @@ class SimpleOperationTest < Minitest::Test
71
80
  FindUserResult
72
81
  end
73
82
 
83
+ def success_failure_klass
84
+ FindUserSuccessFailure
85
+ end
86
+
74
87
  FindUser = SimpleOperation.new(:login) do
75
88
  def call
76
89
  login == 'Arnvald'
@@ -85,4 +98,17 @@ class SimpleOperationTest < Minitest::Test
85
98
  end
86
99
  end
87
100
 
101
+ class FindUserSuccessFailure < SimpleOperation.new(:login)
102
+ success :user
103
+ failure :error
104
+
105
+ def call
106
+ if login == 'Arnvald'
107
+ Success({name: "Grzegorz"})
108
+ else
109
+ Failure("invalid login")
110
+ end
111
+ end
112
+ end
113
+
88
114
  end
@@ -0,0 +1,47 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'simple_operation'
3
+ require 'minitest/autorun'
4
+
5
+ class SuccessTest < Minitest::Test
6
+
7
+ FakeSuccess = SimpleOperation::Success.generate(:value)
8
+
9
+ def test_runs_correctly
10
+ assert SimpleOperation::Success.generate(:value)
11
+ end
12
+
13
+ def test_has_correct_attributes
14
+ klass = SimpleOperation::Success.generate(:name, :email)
15
+ instance = klass.new("Tom", "tom@tomtomtomtom.com")
16
+
17
+ assert_equal "Tom", instance.name
18
+ assert_equal "tom@tomtomtomtom.com", instance.email
19
+ end
20
+
21
+ def test_is_successful
22
+ assert FakeSuccess.new("123").success?
23
+ end
24
+
25
+ def test_is_not_failure
26
+ refute FakeSuccess.new("123").failure?
27
+ end
28
+
29
+ def test_returns_wrapped_value_on_success
30
+ result = FakeSuccess.new("123").on_success { |s| s.value * 3 }
31
+
32
+ assert_equal SimpleOperation::WrappedValue.new("123123123"), result
33
+ end
34
+
35
+ def test_returns_self_on_failure
36
+ success = FakeSuccess.new("123")
37
+ result = success.on_failure { |s| s.value * 4 }
38
+
39
+ assert_equal success, result
40
+ end
41
+
42
+ def test_unwraps_self
43
+ success = FakeSuccess.new("123")
44
+
45
+ assert_equal success, success.unwrap
46
+ end
47
+ end
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'simple_operation'
3
+ require 'minitest/autorun'
4
+
5
+ class WrappedValueTest < Minitest::Test
6
+
7
+ def test_runs_correctly
8
+ assert SimpleOperation::WrappedValue.new("A")
9
+ end
10
+
11
+ def test_returns_self_on_success
12
+ value = SimpleOperation::WrappedValue.new("A")
13
+ assert_equal value, (value.on_success { |v| nil })
14
+ end
15
+
16
+ def test_returns_self_on_failure
17
+ value = SimpleOperation::WrappedValue.new("A")
18
+ assert_equal value, (value.on_failure { |v| nil })
19
+ end
20
+
21
+ def test_unwraps_value
22
+ value = SimpleOperation::WrappedValue.new("A")
23
+ assert_equal "A", value.unwrap
24
+ end
25
+
26
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_operation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Grzegorz Witek
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-02-22 00:00:00.000000000 Z
11
+ date: 2018-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  description: SimpleOperation is a very simple library that facilitates creating service
42
56
  objects
43
57
  email:
@@ -53,10 +67,16 @@ files:
53
67
  - Rakefile
54
68
  - lib/simple_operation.rb
55
69
  - lib/simple_operation/ext.rb
70
+ - lib/simple_operation/failure.rb
71
+ - lib/simple_operation/success.rb
56
72
  - lib/simple_operation/version.rb
73
+ - lib/simple_operation/wrapped_value.rb
57
74
  - simple_operation.gemspec
75
+ - test/failure_test.rb
58
76
  - test/simple_operation_ext_test.rb
59
77
  - test/simple_operation_test.rb
78
+ - test/success_test.rb
79
+ - test/wrapped_value_test.rb
60
80
  homepage: ''
61
81
  licenses:
62
82
  - MIT
@@ -77,10 +97,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
97
  version: '0'
78
98
  requirements: []
79
99
  rubyforge_project:
80
- rubygems_version: 2.5.1
100
+ rubygems_version: 2.5.2
81
101
  signing_key:
82
102
  specification_version: 4
83
103
  summary: Create simple service object
84
104
  test_files:
105
+ - test/failure_test.rb
85
106
  - test/simple_operation_ext_test.rb
86
107
  - test/simple_operation_test.rb
108
+ - test/success_test.rb
109
+ - test/wrapped_value_test.rb