excom 0.4.0 → 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 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