excom 0.4.0 → 1.0.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: 0e2a4c4c2e30de13e0b49be35a9f54a798b902b5
4
- data.tar.gz: c415fda36ef08ab72a157917717ab217a7033ae5
3
+ metadata.gz: 20f76b20a337404d2adaa57942d2939e15c7af7d
4
+ data.tar.gz: 12b63ebcbc418abf3065ac971a94b2c1242411e3
5
5
  SHA512:
6
- metadata.gz: 36cbeb14f6e1c366446787ea8c5b7475197180dd30bd99d8842328cd20071db59cabefb8dd2c9c390addd4040e7588b5a9484509a433eb156b70a10ab5700db4
7
- data.tar.gz: c5d76e81aff3acbda0f7c28059f20ae46f778b465771ab52b2bbc04b46116e218a045a7982a0a36aeb8ab88fdb00d4033ebd9e2ec9ef7acacdf1e216fe20d26d
6
+ metadata.gz: 89f21c5133b8e03630d339465f05dfc90854fef955d0ec71c0bb22c22c626494a2f314e257e961726a622a273ee5117e7253ae4395a7d33d4e8ad684ee95533e
7
+ data.tar.gz: 7e7bb287d15468f03481e4b49cc2a7897ec5b63675e2a918965c758b896f50b3498c04292581c49122433b99b8a008b9d586888d4f3a54de8dd5b751e0dd32ad
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Excom
2
2
 
3
- Flexible and highly extensible Commands (Service Objects) for business logic.
3
+ Flexible and highly extensible Service Objects for business logic organization.
4
4
 
5
5
  [![build status](https://secure.travis-ci.org/akuzko/excom.png)](http://travis-ci.org/akuzko/excom)
6
6
  [![github release](https://img.shields.io/github/release/akuzko/excom.svg)](https://github.com/akuzko/excom/releases)
@@ -21,26 +21,35 @@ Or install it yourself as:
21
21
 
22
22
  $ gem install excom
23
23
 
24
+ ## Preface
25
+
26
+ `Excom` stands for **Ex**excutable **Com**and. Initially, `Excom::Command` was the main
27
+ class, provided by the gem. But it seems that "Service" name become more popular and
28
+ common for describing classes for business logic, so it was renamed in this gem too.
29
+
24
30
  ## Usage
25
31
 
26
- General idea behind every `excom` command is simple: each command can have arguments,
27
- options (named arguments), and should define `run` method that is called during
28
- command execution. Executed command has `status` and `result`.
32
+ General idea behind every `excom` service is simple: each service can have arguments,
33
+ options (named arguments), and should define `execute!` method that is called during
34
+ service execution. Executed service has `status` and `result`.
29
35
 
30
- The **very basic usage** of `Excom` commands can be shown with following example:
36
+ The very basic usage of `Excom` services can be shown with following example:
31
37
 
32
38
  ```rb
33
- # app/commands/todos/update.rb
39
+ # app/services/todos/update.rb
34
40
  module Todos
35
- class Update < Excom::Command
41
+ class Update < Excom::Service
42
+ # `use` class method adds a plugin to a service with specified options
43
+ use :status, success: [:ok], failure: [:unprocessable_entity]
44
+
36
45
  args :todo
37
46
  opts :params
38
47
 
39
- def run
48
+ def execute!
40
49
  if todo.update(params)
41
- result success: todo.as_json
50
+ ok todo.as_json
42
51
  else
43
- result failure: todo.errors
52
+ unprocessable_entity todo.errors
44
53
  end
45
54
  end
46
55
  end
@@ -49,34 +58,30 @@ end
49
58
  # app/controllers/todos/controller
50
59
  class TodosController < ApplicationController
51
60
  def update
52
- command = Todos::Update.new(todo, params: todo_params)
61
+ service = Todos::Update.(todo, params: todo_params)
53
62
 
54
- if command.execute.success?
55
- render json: todo.result
56
- else
57
- render json: todo.result, status: :unprocessable_entity
58
- end
63
+ render json: todo.result, status: service.status
59
64
  end
60
65
  end
61
66
  ```
62
67
 
63
68
  However, even this basic example can be highly optimized by using Excom extensions and helper methods.
64
69
 
65
- ### Command arguments and options
70
+ ### Service arguments and options
66
71
 
67
- Read full version on [wiki](https://github.com/akuzko/excom/wiki#instantiating-command-with-arguments-and-options).
72
+ Read full version on [wiki](https://github.com/akuzko/excom/wiki#instantiating-service-with-arguments-and-options).
68
73
 
69
- Excom commands can be initialized with _arguments_ and _options_ (named arguments). To specify list
74
+ Excom services can be initialized with _arguments_ and _options_ (named arguments). To specify list
70
75
  of available arguments and options, use `args` and `opts` class methods. All arguments and options
71
- are optional during command initialization. However, you cannot pass more arguments to command or
76
+ are optional during service initialization. However, you cannot pass more arguments to service or
72
77
  options that were not declared with `opts` method.
73
78
 
74
79
  ```rb
75
- class MyCommand < Excom::Command
80
+ class MyService < Excom::Service
76
81
  args :foo
77
82
  opts :bar
78
83
 
79
- def run
84
+ def execute!
80
85
  # do something
81
86
  end
82
87
 
@@ -85,84 +90,91 @@ class MyCommand < Excom::Command
85
90
  end
86
91
  end
87
92
 
88
- c1 = MyCommand.new
89
- c1.foo # => 5
90
- c1.bar # => nil
93
+ s1 = MyService.new
94
+ s1.foo # => 5
95
+ s1.bar # => nil
91
96
 
92
- c2 = c1.with_args(1).with_opts(bar: 2)
93
- c2.foo # => 1
94
- c2.bar # => 2
97
+ s2 = s1.with_args(1).with_opts(bar: 2)
98
+ s2.foo # => 1
99
+ s2.bar # => 2
95
100
  ```
96
101
 
97
- ### Command Execution
102
+ ### Service Execution
98
103
 
99
- Read full version on [wiki](https://github.com/akuzko/excom/wiki#command-execution).
104
+ Read full version on [wiki](https://github.com/akuzko/excom/wiki#service-execution).
100
105
 
101
- At the core of each command's execution lies `run` method. You can use `status` and/or
102
- `result` methods to set execution status and result. If none were used, result and status
103
- will be set based on `run` method's return value.
106
+ At the core of each service's execution lies `execute!` method. By default, you can use
107
+ `success`, `failure` and `result` methods to set execution status and result. If none
108
+ were used, result and status will be set based on `execute!` method's return value.
104
109
 
105
110
  Example:
106
111
 
107
112
  ```rb
108
- class MyCommand < Excom::Command
109
- alias_success :ok
113
+ class MyService < Excom::Service
110
114
  args :foo
111
115
 
112
- def run
116
+ def execute!
113
117
  if foo > 2
114
- result ok: foo * 2
118
+ success { foo * 2 }
115
119
  else
116
- result failure: -1
120
+ failure { -1 }
117
121
  end
118
122
  end
119
123
  end
120
124
 
121
- command = MyCommand.new(3)
122
- command.execute.success? # => true
123
- command.status # => :ok
124
- command.result # => 6
125
+ service = MyService.new(3)
126
+ service.execute.success? # => true
127
+ service.result # => 6
125
128
  ```
126
129
 
127
130
  ### Core API
128
131
 
129
132
  Please read about core API and available class and instance methods on [wiki](https://github.com/akuzko/excom/wiki#core-api)
130
133
 
131
- ### Command Extensions (Plugins)
134
+ ### Service Extensions (Plugins)
132
135
 
133
136
  Read full version on [wiki](https://github.com/akuzko/excom/wiki/Plugins).
134
137
 
135
138
  Excom is built with extensions in mind. Even core functionality is organized in plugins that are
136
- used in base `Excom::Command` class. Bellow you can see a list of plugins with some description
139
+ used in base `Excom::Service` class. Bellow you can see a list of plugins with some description
137
140
  and examples that are shipped with `excom`:
138
141
 
139
- - [`:status_helpers`](https://github.com/akuzko/excom/wiki/Plugins#status-helpers) - Allows you to
140
- define status aliases and helper methods named after them to immediately and more explicitly assign
141
- both status and result at the same time:
142
+ - [`:status`](https://github.com/akuzko/excom/wiki/Plugins#status) - Adds `status` execution state
143
+ property to the service, as well as helper methods and behavior to set it. `status` property is not
144
+ bound to the "success" flag of execution state and can have any value depending on your needs. It
145
+ is up to you to setup which statuses correspond to successful execution and which are not. Generated
146
+ status helper methods allow to atomically and more explicitly assign both status and result at
147
+ the same time:
142
148
 
143
149
  ```rb
144
- class Todos::Update
145
- use :status_helpers, success: [:ok], failure: [:unprocessable_entity]
146
- args :todo, :params
147
-
148
- def run
149
- if todo.update(params)
150
- ok todo.as_json
150
+ class Posts::Update < Excom::Service
151
+ use :status,
152
+ success: [:ok],
153
+ failure: [:unprocessable_entity]
154
+
155
+ args :post, :params
156
+
157
+ def execute!
158
+ if post.update(params)
159
+ ok post.as_json
151
160
  else
152
- unprocessable_entity todo.errors
161
+ unprocessable_entity post.errors
153
162
  end
154
163
  end
155
164
  end
156
165
 
157
- command = Todos::Update.(todo, todo_params)
166
+ service = Posts::Update.(post, post_params)
158
167
  # in case params were valid you will have:
159
- command.success? # => true
160
- command.status # => :ok
161
- command.result # => {'id' => 1, ...}
168
+ service.success? # => true
169
+ service.status # => :ok
170
+ service.result # => {'id' => 1, ...}
162
171
  ```
163
172
 
173
+ Note that unlike `success`, `failure`, or `result` methods, status helpers accept result value
174
+ as its argument rather than yield to a block to get it.
175
+
164
176
  - [`:context`](https://github.com/akuzko/excom/wiki/Plugins#context) - Allows you to set an execution
165
- context for a block that will be available to any command that uses this plugin via `context` method.
177
+ context for a block that will be available to any service that uses this plugin via `context` method.
166
178
 
167
179
  ```rb
168
180
  # application_controller.rb
@@ -176,30 +188,30 @@ end
176
188
  ```
177
189
 
178
190
  ```rb
179
- class Posts::Archive < Excom::Command
191
+ class Posts::Archive < Excom::Service
180
192
  use :context
181
193
  args :post
182
194
 
183
- def run
195
+ def execute!
184
196
  post.update(archived: true, archived_by: context[:current_user])
185
197
  end
186
198
  end
187
199
  ```
188
200
 
189
201
  - [`:sentry`](https://github.com/akuzko/excom/wiki/Plugins#sentry) - Allows you to define sentry logic that
190
- will allow or deny command's execution or other related checks. This logic can be defined inline in command
202
+ will allow or deny service's execution or other related checks. This logic can be defined inline in service
191
203
  classes or in dedicated Sentry classes. Much like [pundit](https://github.com/elabs/pundit) Policies, but
192
204
  more. Where pundit governs only authorization logic, Excom's Sentries can deny execution with any reason
193
205
  you find appropriate.
194
206
 
195
207
  ```rb
196
- class Posts::Destroy < Excom::Command
208
+ class Posts::Destroy < Excom::Service
197
209
  use :context
198
210
  use :sentry
199
211
 
200
212
  args :post
201
213
 
202
- def run
214
+ def execute!
203
215
  post.destroy
204
216
  end
205
217
 
@@ -222,16 +234,46 @@ end
222
234
  ```
223
235
 
224
236
  - [`:assertions`](https://github.com/akuzko/excom/wiki/Plugins#assertions) - Provides `assert` method that
225
- can be used for different logic checks during command execution.
237
+ can be used for different logic checks during service execution.
238
+
239
+ - [`:failure_cause`](https://github.com/akuzko/excom/wiki/Plugins#failure_cause) - A small helper plugin
240
+ that can be used to more explicit access to cause of service failure. You can use it if you feel that
241
+ failed service shouldn't have a result, but a cause of the failure instead. Example:
242
+
243
+ ```rb
244
+ class Posts::Create < Excom::Service
245
+ use :status, success: [:ok], failure: [:unprocessable_entity]
246
+ use :failure_cause, cause_method_name: :errors
247
+
248
+ args :params
249
+
250
+ def execute!
251
+ if post.save
252
+ ok post.as_json
253
+ else
254
+ unprocessable_entity post.errors
255
+ end
256
+ end
257
+
258
+ private def post
259
+ @post ||= Post.new(params)
260
+ end
261
+ end
262
+
263
+ service = Posts::Create.(title: 'invalid')
264
+ service.success? # => false
265
+ service.result # => nil
266
+ service.errors # => {title: ["is invalid"]}
267
+ ```
226
268
 
227
269
  - [`:dry_types`](https://github.com/akuzko/excom/wiki/Plugins#dry-types) - Allows you to use
228
270
  [dry-types](http://dry-rb.org/gems/dry-types/) attributes instead of default `args` and `opts`.
229
271
 
230
272
  - [`:caching`](https://github.com/akuzko/excom/wiki/Plugins#caching) - Simple plugin that will prevent
231
- re-execution of command if it already has been executed, and will immediately return result.
273
+ re-execution of service if it already has been executed, and will immediately return result.
232
274
 
233
275
  - [`:rescue`](https://github.com/akuzko/excom/wiki/Plugins#rescue) - Provides `:rescue` execution option.
234
- If set to `true`, any error occurred during command execution will not be raised outside.
276
+ If set to `true`, any error occurred during service execution will not be raised outside.
235
277
 
236
278
  ## Development
237
279
 
@@ -5,14 +5,14 @@ require "excom"
5
5
 
6
6
  Excom::Sentry.deny_with :unauthorized
7
7
 
8
- class Show < Excom::Command
8
+ class Show < Excom::Service
9
9
  use :sentry, class: 'MySentry'
10
10
  use :caching
11
11
  use :dry_types
12
12
 
13
- attribute :foo, Dry::Types['int']
13
+ attribute :foo, Dry::Types['integer']
14
14
 
15
- def run
15
+ def execute!
16
16
  {foo: foo}
17
17
  end
18
18
  end
@@ -27,22 +27,29 @@ class MySentry < Excom::Sentry
27
27
  end
28
28
  end
29
29
 
30
- class Save < Excom::Command
30
+ class MyErrors < Hash
31
+ def add(key, message)
32
+ (self[key] ||= []) << message
33
+ end
34
+ end
35
+
36
+ class Save < Excom::Service
37
+ use :status
31
38
  use :sentry, delegate: [:threshold]
32
39
  use :assertions
40
+ use :errors, errors_class: MyErrors
33
41
 
34
42
  args :foo
35
43
  opts :bar, :baz
36
44
 
37
- alias_success :ok
38
-
39
45
  def foo
40
46
  super || 6
41
47
  end
42
48
 
43
- def run
44
- result ok: foo * 2
49
+ def execute!
50
+ result { foo * 2 }
45
51
  assert { foo > bar }
52
+ errors.add(:foo, 'invalid') if foo > 5
46
53
  end
47
54
 
48
55
  def threshold
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["Artem Kuzko"]
10
10
  spec.email = ["a.kuzko@gmail.com"]
11
11
 
12
- spec.summary = %q{Flexible and highly extensible Commands (Service Objects) for business logic}
13
- spec.description = %q{Flexible and highly extensible Commands (Service Objects) for business logic}
12
+ spec.summary = %q{Flexible and highly extensible Services for business logic organization}
13
+ spec.description = %q{Flexible and highly extensible Services for business logic organization}
14
14
  spec.homepage = "https://github.com/akuzko/excom"
15
15
  spec.license = "MIT"
16
16
 
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_development_dependency "bundler", "~> 1.11"
25
25
  spec.add_development_dependency "rake", "~> 10.0"
26
- spec.add_development_dependency "dry-types", "~> 0.12"
26
+ spec.add_development_dependency "dry-types", "~> 0.13.0"
27
27
  spec.add_development_dependency "dry-struct", "~> 0.4"
28
28
  spec.add_development_dependency "rspec", "~> 3.0"
29
29
  spec.add_development_dependency "rspec-its", "~> 1.2"
@@ -2,7 +2,7 @@ require "excom/version"
2
2
 
3
3
  module Excom
4
4
  autoload :Plugins, 'excom/plugins'
5
- autoload :Command, 'excom/command'
5
+ autoload :Service, 'excom/service'
6
6
 
7
7
  extend Plugins::Context::ExcomMethods
8
8
 
@@ -10,12 +10,12 @@ module Excom
10
10
  def fetch(name)
11
11
  require("excom/plugins/#{name}") unless plugins.key?(name)
12
12
 
13
- plugins[name] || fail("extension `#{name}` is not registered")
13
+ plugins[name] || raise("extension `#{name}` is not registered")
14
14
  end
15
15
 
16
16
  def register(name, extension, options = {})
17
17
  if plugins.key?(name)
18
- fail ArgumentError, "extension `#{name}` is already registered"
18
+ raise ArgumentError, "extension `#{name}` is already registered"
19
19
  end
20
20
  extension.singleton_class.send(:define_method, :excom_options) { options }
21
21
  plugins[name] = extension
@@ -10,6 +10,8 @@ module Excom
10
10
 
11
11
  @args = args
12
12
  @opts = opts
13
+
14
+ super()
13
15
  end
14
16
 
15
17
  def initialize_clone(*)
@@ -54,7 +56,7 @@ module Excom
54
56
  allowed = self.class.args_list.length
55
57
 
56
58
  if actual.length > allowed
57
- fail ArgumentError, "wrong number of args (given #{actual.length}, expected 0..#{allowed})"
59
+ raise ArgumentError, "wrong number of args (given #{actual.length}, expected 0..#{allowed})"
58
60
  end
59
61
  end
60
62
 
@@ -62,16 +64,17 @@ module Excom
62
64
  unexpected = actual.keys - self.class.opts_list
63
65
 
64
66
  if unexpected.any?
65
- fail ArgumentError, "wrong opts #{unexpected} given"
67
+ raise ArgumentError, "wrong opts #{unexpected} given"
66
68
  end
67
69
  end
68
70
 
69
71
  module ClassMethods
70
- def inherited(command_class)
71
- command_class.const_set(:ArgMethods, Module.new)
72
- command_class.send(:include, command_class::ArgMethods)
73
- command_class.args_list.replace args_list.dup
74
- command_class.opts_list.replace opts_list.dup
72
+ def inherited(service_class)
73
+ service_class.const_set(:ArgMethods, Module.new)
74
+ service_class.send(:include, service_class::ArgMethods)
75
+ service_class.args_list.replace args_list.dup
76
+ service_class.opts_list.replace opts_list.dup
77
+ super
75
78
  end
76
79
 
77
80
  def arg_methods
@@ -83,6 +86,8 @@ module Excom
83
86
 
84
87
  argz.each_with_index do |name, i|
85
88
  arg_methods.send(:define_method, name){ @args[i] }
89
+
90
+ arg_methods.send(:define_method, "#{name}?"){ !!@args[i] }
86
91
  end
87
92
  end
88
93
 
@@ -91,6 +96,8 @@ module Excom
91
96
 
92
97
  optz.each do |name|
93
98
  arg_methods.send(:define_method, name){ @opts[name] }
99
+
100
+ arg_methods.send(:define_method, "#{name}?"){ !!@opts[name] }
94
101
  end
95
102
  end
96
103
 
@@ -2,11 +2,11 @@ module Excom
2
2
  module Plugins::Assertions
3
3
  Plugins.register :assertions, self
4
4
 
5
- def assert(fail_with: self.fail_with)
5
+ def assert
6
6
  if yield
7
- status :success unless defined?(@status)
7
+ success! unless state.has_success?
8
8
  else
9
- failure!(fail_with)
9
+ failure!
10
10
  end
11
11
  end
12
12
  end
@@ -1,11 +1,18 @@
1
1
  module Excom
2
2
  module Plugins::Caching
3
- Plugins.register :caching, self, use_with: :prepend
3
+ Plugins.register :caching, self
4
4
 
5
- def execute(*)
6
- return super if block_given? || !executed?
5
+ def initialize(*)
6
+ super
7
+ extend Extension
8
+ end
9
+
10
+ module Extension
11
+ def execute(*)
12
+ return super if block_given? || !executed?
7
13
 
8
- self
14
+ self
15
+ end
9
16
  end
10
17
  end
11
18
  end
@@ -21,8 +21,10 @@ module Excom
21
21
  end
22
22
  end
23
23
 
24
- def local_context
25
- @local_context
24
+ def execute(*)
25
+ Excom.with_context(context) do
26
+ super
27
+ end
26
28
  end
27
29
 
28
30
  module ExcomMethods
@@ -5,11 +5,11 @@ module Excom
5
5
  attr_accessor :attributes
6
6
  protected :attributes=
7
7
 
8
- def self.used(command_class, *)
8
+ def self.used(service_class, *)
9
9
  require 'dry-types'
10
10
  require 'dry-struct'
11
11
 
12
- command_class.const_set(:Attributes, Class.new(Dry::Struct))
12
+ service_class.const_set(:Attributes, Class.new(Dry::Struct))
13
13
  end
14
14
 
15
15
  def initialize(attrs)
@@ -41,30 +41,26 @@ module Excom
41
41
  end
42
42
 
43
43
  def with_args(*)
44
- fail "`with_args' method is not available with :dry_types plugin. use `with_attributes' method instead"
44
+ raise("`with_args' method is not available with :dry_types plugin. use `with_attributes' method instead")
45
45
  end
46
46
 
47
47
  def with_opts(*)
48
- fail "`with_opts' method is not available with :dry_types plugin. use `with_attributes' method instead"
48
+ raise("`with_opts' method is not available with :dry_types plugin. use `with_attributes' method instead")
49
49
  end
50
50
 
51
51
  module ClassMethods
52
52
  def args(*)
53
- fail "`args' method is not available with :dry_types plugin. use `attribute' method instead"
53
+ raise("`args' method is not available with :dry_types plugin. use `attribute' method instead")
54
54
  end
55
55
 
56
56
  def opts(*)
57
- fail "`args' method is not available with :dry_types plugin. use `attribute' method instead"
57
+ raise("`args' method is not available with :dry_types plugin. use `attribute' method instead")
58
58
  end
59
59
 
60
60
  def attribute(name, *args)
61
61
  const_get(:Attributes).send(:attribute, name, *args)
62
62
  arg_methods.send(:define_method, name){ @attributes.send(name) }
63
63
  end
64
-
65
- def constructor_type(*args)
66
- const_get(:Attributes).send(:constructor_type, *args)
67
- end
68
64
  end
69
65
  end
70
66
  end
@@ -0,0 +1,38 @@
1
+ module Excom
2
+ module Plugins::Errors
3
+ Plugins.register :errors, self,
4
+ default_options: {errors_class: Hash, fail_if_present: true}
5
+
6
+ def self.used(service_class, *)
7
+ service_class.add_execution_prop(:errors)
8
+ end
9
+
10
+ def execute(*)
11
+ super
12
+
13
+ if self.class.plugins[:errors].options[:fail_if_present] && !errors.empty?
14
+ failure!
15
+ end
16
+
17
+ self
18
+ end
19
+
20
+ private def initialize(*)
21
+ super
22
+ state.errors = errors_class.new
23
+ end
24
+
25
+ def errors
26
+ state.errors
27
+ end
28
+
29
+ private def errors_class
30
+ self.class.plugins[:errors].options[:errors_class]
31
+ end
32
+
33
+ private def clear_execution_state!
34
+ super
35
+ state.errors = errors_class.new
36
+ end
37
+ end
38
+ end
@@ -1,12 +1,59 @@
1
+ require 'ostruct'
2
+
1
3
  module Excom
2
4
  module Plugins::Executable
3
5
  Plugins.register :executable, self
4
6
 
5
- Result = Struct.new(:status, :result)
7
+ class State
8
+ def self.prop_names
9
+ @prop_names ||= []
10
+ end
11
+
12
+ def self.add_prop(*props)
13
+ prop_names.push(*props)
14
+ props.each{ |prop| def_prop_accessor(prop) }
15
+ end
16
+
17
+ def self.def_prop_accessor(name)
18
+ define_method(name) { @values[name] }
19
+ define_method("#{name}=") { |value| @values[name] = value }
20
+ define_method("has_#{name}?") { @values.key?(name) }
21
+ end
22
+
23
+ def initialize(values = {})
24
+ @values = values
25
+ end
26
+
27
+ def clear!
28
+ @values.clear
29
+ end
30
+
31
+ def prop_names
32
+ self.class.prop_names
33
+ end
34
+
35
+ def replace(other)
36
+ missing_props = prop_names - other.prop_names
37
+
38
+ unless missing_props.empty?
39
+ raise ArgumentError, "cannot accept execution state #{other} due to missing props: #{missing_props}"
40
+ end
41
+
42
+ prop_names.each do |prop|
43
+ @values[prop] = other.public_send(prop)
44
+ end
45
+ end
46
+ end
47
+
48
+ def self.used(service_class, *)
49
+ service_class.const_set(:State, Class.new(State))
50
+ service_class.add_execution_prop(:executed, :success, :result)
51
+ end
52
+
53
+ attr_reader :state
6
54
 
7
55
  def initialize(*)
8
- @executed = false
9
- super
56
+ @state = self.class::State.new(executed: false)
10
57
  end
11
58
 
12
59
  def initialize_clone(*)
@@ -15,108 +62,114 @@ module Excom
15
62
 
16
63
  def execute(*, &block)
17
64
  clear_execution_state!
18
- result = run(&block)
19
- result_with(result) unless defined? @result
20
- @executed = true
65
+ result = execute!(&block)
66
+ result_with(result) unless state.has_result?
67
+ state.executed = true
21
68
 
22
69
  self
23
70
  end
24
71
 
25
72
  def executed?
26
- @executed
73
+ state.executed
27
74
  end
28
75
 
29
76
  def ~@
30
- Result.new(status, result)
77
+ state
31
78
  end
32
79
 
33
- private def run
80
+ private def execute!
34
81
  success!
35
82
  end
36
83
 
37
84
  private def clear_execution_state!
38
- @executed = false
39
- remove_instance_variable('@result'.freeze) if defined?(@result)
40
- remove_instance_variable('@status'.freeze) if defined?(@status)
85
+ state.clear!
86
+ state.executed = false
41
87
  end
42
88
 
43
- def result(obj = UNDEFINED)
44
- return @result if obj == UNDEFINED
89
+ private def success
90
+ assign_successful_state
91
+ assign_successful_result(yield)
92
+ end
45
93
 
46
- case obj
47
- when Hash
48
- if obj.length != 1
49
- fail ArgumentError, "expected 1-item status-result pair, got: #{obj.inspect}"
50
- end
94
+ private def failure
95
+ assign_failed_state
96
+ assign_failed_result(yield)
97
+ end
51
98
 
52
- @status, @result = obj.first
53
- else
54
- result_with(obj)
55
- end
99
+ private def success!
100
+ assign_successful_state
56
101
  end
57
102
 
58
- private def result_with(obj)
59
- if Result === obj
60
- @status, @result = obj.status, obj.result
61
- return @result
62
- end
103
+ private def failure!
104
+ assign_failed_state
105
+ end
63
106
 
64
- @status = obj ? :success : fail_with unless defined?(@status)
65
- @result = obj
107
+ private def assign_successful_state
108
+ state.success = true
109
+ state.result = nil
66
110
  end
67
111
 
68
- def status(status = UNDEFINED)
69
- return @status = status unless status == UNDEFINED
112
+ private def assign_failed_state
113
+ state.success = false
114
+ state.result = nil
115
+ end
70
116
 
71
- @status
117
+ private def assign_successful_result(value)
118
+ state.result = value
72
119
  end
73
120
 
74
- def success?
75
- status == :success || self.class.success_aliases.include?(status)
121
+ private def assign_failed_result(value)
122
+ state.result = value
76
123
  end
77
124
 
78
- def failure?
79
- !success?
125
+ def result
126
+ return state.result unless block_given?
127
+
128
+ result_with(yield)
80
129
  end
81
130
 
82
- private def success!
83
- @status = :success
84
- @result = true
131
+ private def result_with(obj)
132
+ if State === obj
133
+ return state.replace(obj)
134
+ end
135
+
136
+ state.success = !!obj
137
+ if state.success
138
+ assign_successful_result(obj)
139
+ else
140
+ assign_failed_result(obj)
141
+ end
85
142
  end
86
143
 
87
- private def failure!(status = fail_with)
88
- @status = status
144
+ def success?
145
+ state.success == true
89
146
  end
90
147
 
91
- protected def fail_with
92
- self.class.fail_with
148
+ def failure?
149
+ !success?
93
150
  end
94
151
 
95
152
  module ClassMethods
96
- def call(*args)
97
- new(*args).execute
98
- end
99
-
100
- def [](*args)
101
- call(*args).result
153
+ def inherited(klass)
154
+ klass.const_set(:State, Class.new(self::State))
155
+ klass::State.prop_names.replace(self::State.prop_names.dup)
102
156
  end
103
157
 
104
- def fail_with(status = nil)
105
- return @fail_with || :failure if status.nil?
106
-
107
- @fail_with = status
158
+ def add_execution_prop(*props)
159
+ self::State.add_prop(*props)
108
160
  end
109
161
 
110
- def success_aliases
111
- []
162
+ def call(*args)
163
+ new(*args).execute
112
164
  end
165
+ alias_method :execute, :call
113
166
 
114
- def alias_success(*aliases)
115
- singleton_class.send(:define_method, :success_aliases) { super() + aliases }
167
+ def [](*args)
168
+ call(*args).result
116
169
  end
117
170
 
118
171
  def method_added(name)
119
- private :run if name == :run
172
+ private :execute! if name == :execute!
120
173
  super if defined? super
121
174
  end
122
175
  end
@@ -0,0 +1,16 @@
1
+ module Excom
2
+ module Plugins::FailureCause
3
+ Plugins.register :failure_cause, self,
4
+ default_options: {cause_method_name: :cause}
5
+
6
+ def self.used(service_class, cause_method_name:)
7
+ service_class.add_execution_prop(:cause)
8
+ service_class.send(:define_method, cause_method_name) { state.cause }
9
+ end
10
+
11
+ private def assign_failed_result(value)
12
+ state.result = nil
13
+ state.cause = value
14
+ end
15
+ end
16
+ end
@@ -6,6 +6,9 @@ module Excom
6
6
  method = extension.excom_options[:use_with] || :include
7
7
  send(method, extension)
8
8
 
9
+ defaults = extension.excom_options[:default_options]
10
+ opts = defaults.merge(opts) unless defaults.nil?
11
+
9
12
  if extension.const_defined?('ClassMethods')
10
13
  extend extension::ClassMethods
11
14
  end
@@ -19,10 +22,14 @@ module Excom
19
22
  extension
20
23
  end
21
24
 
25
+ def using?(name)
26
+ plugins.key?(name)
27
+ end
28
+
22
29
  def plugins
23
30
  @plugins ||= {}
24
31
  end
25
- alias :extensions :plugins
32
+ alias extensions plugins
26
33
 
27
34
  Reflection = Struct.new(:extension, :options)
28
35
  end
@@ -2,11 +2,9 @@ module Excom
2
2
  module Plugins::Rescue
3
3
  Plugins.register :rescue, self
4
4
 
5
- attr_reader :error
6
-
7
- def initialize_clone(*)
8
- remove_instance_variable('@error') if defined?(@error)
9
- super
5
+ def self.used(service_class, *)
6
+ service_class.use(:status) unless service_class.using?(:status)
7
+ service_class.add_execution_prop :error
10
8
  end
11
9
 
12
10
  def execute(**opts)
@@ -14,12 +12,16 @@ module Excom
14
12
  super
15
13
  rescue StandardError => error
16
14
  clear_execution_state!
17
- @error = error
18
- @status = :error
15
+ failure!(:error)
16
+ state.error = error
19
17
  raise error unless rezcue
20
18
  self
21
19
  end
22
20
 
21
+ def error
22
+ state.error
23
+ end
24
+
23
25
  def error?
24
26
  status == :error
25
27
  end
@@ -4,14 +4,15 @@ module Excom
4
4
 
5
5
  Plugins.register :sentry, self
6
6
 
7
- def self.used(command_class, **opts)
7
+ def self.used(service_class, **opts)
8
8
  klass = opts[:class]
9
9
 
10
- command_class._sentry_class = klass if klass
10
+ service_class.use(:status) unless service_class.using?(:status)
11
+ service_class._sentry_class = klass if klass
11
12
  end
12
13
 
13
14
  def execute(*)
14
- reason = why_cant(:execute)
15
+ reason = why_cant?(:execute)
15
16
 
16
17
  return super if reason.nil?
17
18
 
@@ -21,10 +22,10 @@ module Excom
21
22
  end
22
23
 
23
24
  def can?(action)
24
- why_cant(action).nil?
25
+ why_cant?(action).nil?
25
26
  end
26
27
 
27
- def why_cant(action)
28
+ def why_cant?(action)
28
29
  sentry.denial_reason(action)
29
30
  end
30
31
 
@@ -39,9 +40,9 @@ module Excom
39
40
  module ClassMethods
40
41
  attr_writer :_sentry_class
41
42
 
42
- def inherited(command_class)
43
+ def inherited(service_class)
43
44
  super
44
- command_class.sentry_class(_sentry_class)
45
+ service_class.sentry_class(_sentry_class)
45
46
  end
46
47
 
47
48
  def sentry_class(klass = UNDEFINED)
@@ -59,7 +60,7 @@ module Excom
59
60
  _sentry_class
60
61
  end
61
62
 
62
- @sentry_class.command_class = self
63
+ @sentry_class.service_class = self
63
64
  @sentry_class
64
65
  end
65
66
 
@@ -74,7 +75,7 @@ module Excom
74
75
  const_get(:Sentry).class_eval(&block)
75
76
  else
76
77
  @_sentry_class = @sentry_class = Class.new(Sentry, &block)
77
- @sentry_class.command_class = self
78
+ @sentry_class.service_class = self
78
79
  const_set(:Sentry, @_sentry_class)
79
80
  end
80
81
  end
@@ -9,13 +9,13 @@ module Excom
9
9
  sentry_class.send(:include, sentry_class::Delegations)
10
10
  end
11
11
 
12
- def self.command_class=(klass)
13
- sentinels.each{ |s| s.command_class = klass }
14
- @command_class = klass
12
+ def self.service_class=(klass)
13
+ sentinels.each{ |s| s.service_class = klass }
14
+ @service_class = klass
15
15
  end
16
16
 
17
- def self.command_class
18
- @command_class
17
+ def self.service_class
18
+ @service_class
19
19
  end
20
20
 
21
21
  def self.deny_with(reason)
@@ -56,10 +56,10 @@ module Excom
56
56
  const_get(:Delegations)
57
57
  end
58
58
 
59
- attr_reader :command
59
+ attr_reader :service
60
60
 
61
- def initialize(command)
62
- @command = command
61
+ def initialize(service)
62
+ @service = service
63
63
  end
64
64
 
65
65
  def denial_reason(action)
@@ -74,7 +74,7 @@ module Excom
74
74
 
75
75
  def sentry(klass)
76
76
  klass = derive_sentry_class(klass) unless Class === klass
77
- klass.new(command)
77
+ klass.new(service)
78
78
  end
79
79
 
80
80
  def to_hash
@@ -93,7 +93,7 @@ module Excom
93
93
 
94
94
  private def sentinels
95
95
  @sentinels ||= self.class.sentinels.map do |klass|
96
- klass.new(command)
96
+ klass.new(service)
97
97
  end
98
98
  end
99
99
 
@@ -104,7 +104,7 @@ module Excom
104
104
  end
105
105
 
106
106
  private def constantize(klass, sentry_name)
107
- module_prefix = (inline? ? self.class.command_class.name : self.class.name).sub(/[^:]+\Z/, ''.freeze)
107
+ module_prefix = (inline? ? self.class.service_class.name : self.class.name).sub(/[^:]+\Z/, ''.freeze)
108
108
 
109
109
  klass_name = module_prefix + "_#{klass}".gsub!(/(_([a-z]))/){ $2.upcase } + sentry_name
110
110
 
@@ -114,15 +114,15 @@ module Excom
114
114
  end
115
115
 
116
116
  private def inline?
117
- self.class.command_class.const_defined?(:Sentry) && self.class.command_class::Sentry == self.class
117
+ self.class.service_class.const_defined?(:Sentry) && self.class.service_class::Sentry == self.class
118
118
  end
119
119
 
120
120
  private def define_delegations!
121
- delegated_methods = self.class.command_class.arg_methods.instance_methods +
122
- Array(self.class.command_class.plugins[:sentry].options[:delegate])
121
+ delegated_methods = self.class.service_class.arg_methods.instance_methods +
122
+ Array(self.class.service_class.plugins[:sentry].options[:delegate])
123
123
 
124
124
  delegated_methods.each do |name|
125
- self.class.delegations.send(:define_method, name) { command.public_send(name) }
125
+ self.class.delegations.send(:define_method, name) { service.public_send(name) }
126
126
  end
127
127
 
128
128
  self.class.instance_variable_set('@delegations_defined'.freeze, true)
@@ -0,0 +1,57 @@
1
+ module Excom
2
+ module Plugins::Status
3
+ Plugins.register :status, self,
4
+ default_options: {success: [], failure: []}
5
+
6
+ def self.used(service_class, success:, failure:)
7
+ service_class.add_execution_prop(:status)
8
+
9
+ helpers = Module.new do
10
+ success.each do |name|
11
+ define_method(name) do |result = nil|
12
+ success(name) { result }
13
+ end
14
+ end
15
+
16
+ failure.each do |name|
17
+ define_method(name) do |result = nil|
18
+ failure(name) { result }
19
+ end
20
+ end
21
+ end
22
+
23
+ service_class.const_set('StatusHelpers', helpers)
24
+ service_class.send(:include, helpers)
25
+ end
26
+
27
+ def status
28
+ state.status
29
+ end
30
+
31
+ private def success!(status = :success)
32
+ state.status = status
33
+ super()
34
+ end
35
+
36
+ private def success(status = :success, &block)
37
+ state.status = status
38
+ super(&block)
39
+ end
40
+
41
+ private def failure!(status = :failure)
42
+ state.status = status
43
+ super()
44
+ end
45
+
46
+ private def failure(status = :failure, &block)
47
+ super(&block).tap do
48
+ state.status = status
49
+ end
50
+ end
51
+
52
+ private def result_with(*)
53
+ super
54
+ state.status ||= state.success ? :success : :failure
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,5 @@
1
1
  module Excom
2
- class Command
2
+ class Service
3
3
  extend Excom::Plugins::Pluggable
4
4
 
5
5
  use :executable
@@ -1,3 +1,3 @@
1
1
  module Excom
2
- VERSION = "0.4.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excom
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Kuzko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-02-11 00:00:00.000000000 Z
11
+ date: 2019-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.12'
47
+ version: 0.13.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.12'
54
+ version: 0.13.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: dry-struct
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -122,8 +122,7 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
- description: Flexible and highly extensible Commands (Service Objects) for business
126
- logic
125
+ description: Flexible and highly extensible Services for business logic organization
127
126
  email:
128
127
  - a.kuzko@gmail.com
129
128
  executables: []
@@ -141,19 +140,21 @@ files:
141
140
  - bin/setup
142
141
  - excom.gemspec
143
142
  - lib/excom.rb
144
- - lib/excom/command.rb
145
143
  - lib/excom/plugins.rb
146
144
  - lib/excom/plugins/args.rb
147
145
  - lib/excom/plugins/assertions.rb
148
146
  - lib/excom/plugins/caching.rb
149
147
  - lib/excom/plugins/context.rb
150
148
  - lib/excom/plugins/dry_types.rb
149
+ - lib/excom/plugins/errors.rb
151
150
  - lib/excom/plugins/executable.rb
151
+ - lib/excom/plugins/failure_cause.rb
152
152
  - lib/excom/plugins/pluggable.rb
153
153
  - lib/excom/plugins/rescue.rb
154
154
  - lib/excom/plugins/sentry.rb
155
155
  - lib/excom/plugins/sentry/sentinel.rb
156
- - lib/excom/plugins/status_helpers.rb
156
+ - lib/excom/plugins/status.rb
157
+ - lib/excom/service.rb
157
158
  - lib/excom/version.rb
158
159
  homepage: https://github.com/akuzko/excom
159
160
  licenses:
@@ -178,5 +179,5 @@ rubyforge_project:
178
179
  rubygems_version: 2.6.8
179
180
  signing_key:
180
181
  specification_version: 4
181
- summary: Flexible and highly extensible Commands (Service Objects) for business logic
182
+ summary: Flexible and highly extensible Services for business logic organization
182
183
  test_files: []
@@ -1,25 +0,0 @@
1
- module Excom
2
- module Plugins::StatusHelpers
3
- Plugins.register :status_helpers, self
4
-
5
- def self.used(klass, success: [], failure: [])
6
- klass.alias_success(*success)
7
-
8
- helpers = Module.new do
9
- (success + failure).each do |name|
10
- define_method(name) do |result = nil|
11
- @status = name
12
- @result = result
13
- end
14
- end
15
- end
16
-
17
- klass.const_set('StatusHelpers', helpers)
18
- klass.send(:include, helpers)
19
- end
20
-
21
- def success?
22
- super || self.class.success_aliases.include?(status)
23
- end
24
- end
25
- end