commande 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9138bf2b694ca329c484c5ae02d2ee885cf2810e
4
+ data.tar.gz: 608cad9501f9cc6722e513721fced30061bf1bc0
5
+ SHA512:
6
+ metadata.gz: 79731f5f849e680b495a7b38a6d185b719a0985205a38079a6829ec50b3c353299e2be367350be40e37fc1fcaafbcd6af76888d757cbb7f01cdfb98cdc9b8fa9
7
+ data.tar.gz: 8263d5a510b6f5151f9ed895912f29009f5b6e06d79386bea942a61758f857fa28fe4066be0183679fc7ab98dfcb11f26c3e43dd8e7c1a58162462ff1637c711
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ .rakeTasks
11
+ .idea/
12
+ Gemfile.lock
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ Include:
3
+ - '**/Rakefile'
4
+ - 'lib/**/*.rb'
5
+ Exclude:
6
+ - 'Gemfile'
7
+ - 'bin/**/*'
8
+ TargetRubyVersion: 2.3
9
+
10
+ Style/EndOfLine:
11
+ EnforcedStyle: lf
data/.travis.yml ADDED
@@ -0,0 +1,20 @@
1
+ sudo: false
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.3.0
6
+ - 2.4
7
+ - 2.5
8
+ - 2.6
9
+ - rbx-3
10
+ - ruby-head
11
+ matrix:
12
+ allow_failures:
13
+ - rvm: ruby-head
14
+ - rvm: rbx-3
15
+ - rvm: 2.6
16
+ before_install:
17
+ - gem update --system
18
+ - gem --version
19
+ install:
20
+ - bundle install --with development --jobs=3 --retry=3 --path=${BUNDLE_PATH:-vendor/bundle}
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+ - Naming gem as `commande`.
5
+ - Github require continues to work because `command` can be required.
6
+ - Aliases using `command/**` for all `commande/**`
7
+
8
+ ## 0.2.1
9
+
10
+ - `assert_valid` and `refute_valid` take `*args`, to match `Command#call` (and thus `Command#valid?`)
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at derk-jan+github@karrenbeld.info. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in command.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Derk-Jan Karrenbeld
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # Command
2
+ [![Build Status: master](https://travis-ci.com/SleeplessByte/command.svg?branch=master)](https://travis-ci.com/SleeplessByte/command)
3
+ [![Gem Version](https://badge.fury.io/rb/commande.svg)](https://badge.fury.io/rb/commande)
4
+ [![MIT license](http://img.shields.io/badge/license-MIT-brightgreen.svg)](http://opensource.org/licenses/MIT)
5
+
6
+ Command adds the [Command Design Pattern](https://sourcemaking.com/design_patterns/command) to any `Class`.
7
+
8
+ This was based on `Hanami::Interactor`, and started off as adding a direct `call` on the singleton class, before that
9
+ was added to Hanami's. After working with different interactors and command-style gems, including ways to organize
10
+ units for execution and without depending on other utility classes, `command` was born.
11
+
12
+ Because [`command`](https://rubygems.org/gems/command) has been taken on rubygems (but not updated since 2013), and
13
+ [`commando`](https://rubygems.org/gems/commando) has been taken (but not updated since 2009) and the Dutch `opdracht` is
14
+ probably not pronounceable by most people using this, I've decided to register this on the French
15
+ [`commande`](https://rubygems.org/gems/commande).
16
+
17
+ However, if you are using this directly from GitHub, you can continue using it as is, without renaming, as long as you
18
+ change the Gemfile line to `require: 'command'`.
19
+
20
+ ```Ruby
21
+ # Gemfile
22
+ gem 'commande', require: 'command'
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```Ruby
30
+ gem 'commande'
31
+ ```
32
+
33
+ or alternatively if you would like to refer to commande as `Command`:
34
+
35
+ ```Ruby
36
+ gem 'commande', require: 'command'
37
+ ```
38
+
39
+ And then execute:
40
+
41
+ $ bundle
42
+
43
+ Or install it yourself as:
44
+
45
+ $ gem install commande
46
+
47
+ ## Usage
48
+
49
+ There are examples in the code and the tests. Here is a crude and basic example:
50
+
51
+ ```Ruby
52
+ class FetchSecondInput
53
+ include Commande
54
+
55
+ output :fetched
56
+
57
+ def call(*args)
58
+ # always define call
59
+ self.fetched = args.second
60
+ end
61
+
62
+ def valid?(*args)
63
+ args.length == 2
64
+ end
65
+
66
+ private
67
+
68
+ attr_accessor :fetched
69
+ end
70
+
71
+ result = FetchSecondInput.call(42, 'gem')
72
+ result.successful? # => true
73
+ result.fetched # => 'gem'
74
+
75
+ result = FetchSecondInput.call(42, 'gem', 'three is a crowd')
76
+ result.successful? # => false
77
+ result.fetched # => nil
78
+ ```
79
+
80
+ ## Testing
81
+
82
+ There are some `Minitest` assertions included in this library.
83
+
84
+ ```Ruby
85
+ require 'commande/minitest'
86
+ ```
87
+ | Assert | Refute | |
88
+ |:---:|:---:|:---:|
89
+ | `assert_successful(command_result)` | `refute_successful` | passes if the command is successful?
90
+ | `assert_valid(command, *args_for_valid)` | `refute_valid` | passes if the command is valid
91
+ | `assert_with_error(expected, actual)` | `refute_with_error` | passes if the command has a certain error message
92
+
93
+ ## Development
94
+
95
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
96
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
97
+
98
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
99
+ version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
100
+ push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
101
+
102
+ ## Contributing
103
+
104
+ Bug reports and pull requests are welcome on GitHub at [SleeplessByte/commmand](https://github.com/SleeplessByte/command).
105
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to
106
+ the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
111
+
112
+ ## Code of Conduct
113
+
114
+ Everyone interacting in the Shrine::ConfigurableStorage project’s codebases, issue trackers, chat rooms and mailing
115
+ lists is expected to follow the [code of conduct](https://github.com/SleeplessByte/command/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
10
+ end
11
+
12
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'commande'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/commande.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'commande/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'commande'
9
+ spec.version = Commande::VERSION
10
+ spec.authors = ['Derk-Jan Karrenbeld']
11
+ spec.email = ['derk-jan+github@karrenbeld.info']
12
+
13
+ spec.summary = 'Allows for Command pattern style POROs'
14
+ spec.license = 'MIT'
15
+
16
+ spec.metadata = {
17
+ 'bug_tracker_uri' => 'https://github.com/SleeplessByte/command/issues',
18
+ 'changelog_uri' => 'https://github.com/SleeplessByte/command/CHANGELOG.md',
19
+ 'homepage_uri' => 'https://github.com/SleeplessByte/command',
20
+ 'source_code_uri' => 'https://github.com/SleeplessByte/command'
21
+ }
22
+
23
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
24
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
25
+ if spec.respond_to?(:metadata)
26
+ # spec.metadata['allowed_push_host'] = 'https://gems.sleeplessbyte.technology'
27
+ else
28
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
29
+ 'public gem pushes.'
30
+ end
31
+
32
+ # Specify which files should be added to the gem when it is released.
33
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
34
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
35
+ f.match(%r{^(test|spec|features)/})
36
+ end
37
+ spec.bindir = 'exe'
38
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
+ spec.require_paths = ['lib']
40
+
41
+ spec.add_development_dependency 'bundler', '~> 1.16'
42
+ spec.add_development_dependency 'minitest', '~> 5.0'
43
+ spec.add_development_dependency 'rake', '~> 10.0'
44
+ end
data/lib/command.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commande'
4
+
5
+ # noinspection RubyConstantNamingConvention
6
+ Command = Commande
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'command/minitest/assertions/assert_successful'
4
+ require 'command/minitest/assertions/assert_valid'
5
+ require 'command/minitest/assertions/assert_with_error'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commande/minitest/assertions/assert_successful'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commande/minitest/assertions/assert_valid'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commande/minitest/assertions/assert_with_error'
4
+
data/lib/commande.rb ADDED
@@ -0,0 +1,438 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commande/version'
4
+
5
+ module Commande
6
+ class Result < BasicObject
7
+ # Concrete methods
8
+ #
9
+ # @api private
10
+ #
11
+ # @see Commande::Result#respond_to_missing?
12
+ METHODS = ::Hash[initialize: true,
13
+ successful?: true,
14
+ failure?: true,
15
+ fail!: true,
16
+ prepare!: true,
17
+ errors: true,
18
+ error: true,
19
+ logs: true,
20
+ payload: true].freeze
21
+
22
+ # Initialize a new result
23
+ #
24
+ # @param payload [Hash] a payload to carry on
25
+ #
26
+ # @return [Commande::Result]
27
+ #
28
+ # @api private
29
+ def initialize(payload = {})
30
+ @payload = payload
31
+ @errors = []
32
+ @logs = []
33
+ @success = true
34
+ end
35
+
36
+ # Check if the current status is successful
37
+ #
38
+ # @return [TrueClass,FalseClass] the result of the check
39
+ def successful?
40
+ @success && errors.empty?
41
+ end
42
+
43
+ # Check if the current status is not successful
44
+ #
45
+ # @return [TrueClass,FalseClass] the result of the check
46
+ def failure?
47
+ !successful?
48
+ end
49
+
50
+ # Force the status to be a failure
51
+ def fail!
52
+ @success = false
53
+ end
54
+
55
+ # Returns all the errors collected during an operation
56
+ #
57
+ # @return [Array] the errors
58
+ #
59
+ # @see Commande::Result#error
60
+ # @see Commande#call
61
+ # @see Commande#error
62
+ # @see Commande#error!
63
+ def errors
64
+ @errors.dup
65
+ end
66
+
67
+ # @api private
68
+ def add_error(*errors)
69
+ @errors << errors
70
+ @errors.flatten!
71
+ nil
72
+ end
73
+
74
+ # Returns the first errors collected during an operation
75
+ #
76
+ # @return [nil,String] the error, if present
77
+ #
78
+ #
79
+ # @see Commande::Result#errors
80
+ # @see Commande#call
81
+ # @see Commande#error
82
+ # @see Commande#error!
83
+ def error
84
+ errors.first
85
+ end
86
+
87
+ # Returns all the logs collected during an operation
88
+ #
89
+ # @return [Array] the errors
90
+ #
91
+ # @see Commande::Result#log
92
+ # @see Commande#call
93
+ def logs
94
+ @logs.dup
95
+ end
96
+
97
+ # @api private
98
+ def add_log(*logs)
99
+ @logs << logs
100
+ @logs.flatten!
101
+ nil
102
+ end
103
+
104
+ # Prepare the result before to be returned
105
+ #
106
+ # @param payload [Hash] an updated payload
107
+ #
108
+ # @api private
109
+ def prepare!(payload)
110
+ @payload.merge!(payload)
111
+ self
112
+ end
113
+
114
+ def payload
115
+ @payload.dup
116
+ end
117
+
118
+ # Return the class for debugging purposes.
119
+ #
120
+ # @see http://ruby-doc.org/core/Object.html#method-i-class
121
+ def class
122
+ (class << self; self; end).superclass
123
+ end
124
+
125
+ # Bare minimum inspect for debugging purposes.
126
+ #
127
+ # @return [String] the inspect string
128
+ #
129
+ #
130
+ # @see http://ruby-doc.org/core/Object.html#method-i-inspect
131
+ #
132
+ def inspect
133
+ "#<#{self.class}:#{::Kernel.format('0x0000%<id>x', id: (__id__ << 1))}#{__inspect}>"
134
+ end
135
+
136
+ # Alias for __id__
137
+ #
138
+ # @return [Fixnum] the object id
139
+ #
140
+ # @see http://ruby-doc.org/core/Object.html#method-i-object_id
141
+ def object_id
142
+ __id__
143
+ end
144
+
145
+ # Returns true if responds to the given method.
146
+ #
147
+ # @return [TrueClass,FalseClass] the result of the check
148
+ #
149
+ # @see http://ruby-doc.org/core-2.2.1/Object.html#method-i-respond_to-3F
150
+ def respond_to?(method_name, include_all = false)
151
+ respond_to_missing?(method_name, include_all)
152
+ end
153
+
154
+ protected
155
+
156
+ # @api private
157
+ def method_missing(method_name, *)
158
+ @payload.fetch(method_name) { super }
159
+ end
160
+
161
+ # @api private
162
+ def respond_to_missing?(method_name, _include_all)
163
+ method_name = method_name.to_sym
164
+ METHODS[method_name] || @payload.key?(method_name)
165
+ end
166
+
167
+ # @api private
168
+ def __inspect
169
+ " @success=#{@success} @payload=#{@payload.inspect}"
170
+ end
171
+ end
172
+
173
+ # Override for <tt>Module#included</tt>.
174
+ #
175
+ # @api private
176
+ def self.included(base)
177
+ super
178
+
179
+ base.class_eval do
180
+ extend ClassMethods
181
+ end
182
+ end
183
+
184
+ # Commande interface
185
+ # @since 1.1.0
186
+ module Interface
187
+ # Triggers the operation and return a result.
188
+ #
189
+ # All the instance variables marked as output will be available in the result.
190
+ #
191
+ # @return [Commande::Result] the result of the operation
192
+ #
193
+ # @raise [NoMethodError] if this isn't implemented by the including class.
194
+ #
195
+ # @example Expose instance variables in result payload as output
196
+ #
197
+ # class Purchase
198
+ # include Commande
199
+ # output :buyer, :product, :transaction
200
+ #
201
+ # def call(buyer:, product_code:)
202
+ # @product = Product.find_by(product_code: product_code)
203
+ # @buyer = Buyer.find_by(email: buyer)
204
+ # @transaction = Transaction.create(buyer: @buyer, product: @product)
205
+ # end
206
+ # end
207
+ #
208
+ # result = Purchase.new.call(buyer: 'john@smith.com', product_code: 'i23af')
209
+ # result.failure? # => false
210
+ # result.successful? # => true
211
+ #
212
+ # result.product # => #<Product product_code: i23af>
213
+ # result.buyer # => #<Buyer email: john@smith.com>
214
+ # result.foo # => raises NoMethodError
215
+ #
216
+ def call(*args, &block)
217
+ @__result = ::Commande::Result.new
218
+ _call(*args) { super(*args, &block) }
219
+ end
220
+
221
+ private
222
+
223
+ # @api private
224
+ def _call(*args)
225
+ catch :end do
226
+ catch :fail do
227
+ validate!(*args)
228
+ yield
229
+ end
230
+ end
231
+
232
+ _prepare!
233
+ end
234
+
235
+ # @since 1.1.0
236
+ def validate!(*args)
237
+ fail! unless valid?(*args)
238
+ end
239
+ end
240
+
241
+ private
242
+
243
+ # Check if proceed with <tt>#call</tt> invocation.
244
+ # By default it returns <tt>true</tt>.
245
+ #
246
+ # Developers can override it.
247
+ #
248
+ # @return [TrueClass,FalseClass] the result of the check
249
+ #
250
+ def valid?(*)
251
+ true
252
+ end
253
+
254
+ # Fail and interrupt the current flow.
255
+ #
256
+ def fail!
257
+ @__result.fail!
258
+ throw :fail
259
+ end
260
+
261
+ # Interrupt the current flow without failure
262
+ #
263
+ def end!
264
+ throw :end
265
+ end
266
+
267
+ # Log an error without interrupting the flow.
268
+ #
269
+ # When used, the returned result won't be successful.
270
+ #
271
+ # @param message [String] the error message
272
+ #
273
+ # @return false
274
+ #
275
+ #
276
+ # @see Commande#error!
277
+ #
278
+ def error(message)
279
+ @__result.add_error message
280
+ false
281
+ end
282
+
283
+ # Log an error AND interrupting the flow.
284
+ #
285
+ # When used, the returned result won't be successful.
286
+ #
287
+ # @param message [String] the error message
288
+ #
289
+ # @see Commande#error
290
+ #
291
+ def error!(message)
292
+ error(message)
293
+ fail!
294
+ end
295
+
296
+ # Persist a log message
297
+ #
298
+ # @param message [String] the log message
299
+ #
300
+ # @return true
301
+ #
302
+ # @see Commande#error
303
+ #
304
+ def log(message)
305
+ @__result.add_log(message)
306
+ true
307
+ end
308
+
309
+ protected
310
+
311
+ ##
312
+ # Copies errors and logs from sources and prefixes with a header:
313
+ #
314
+ # @param [Commande::Result, ApplicationRecord] source
315
+ # @param [String] header
316
+ # @return [TrueClass, FalseClass] true if successful, false otherwise
317
+ #
318
+ def transfer(source, header: nil)
319
+ transfer_logs(source, header: header)
320
+ transfer_outputs(source)
321
+ transfer_errors(source, header: header)
322
+
323
+ transfer_success?(source)
324
+ end
325
+
326
+ def transfer_logs(source, header: nil)
327
+ return unless source.respond_to?(:logs)
328
+ Array(source.logs).each do |l|
329
+ log header ? ::Kernel.format('%<header>s: %<log>s', header: header, log: l) : l
330
+ end
331
+ end
332
+
333
+ def transfer_outputs(source)
334
+ return unless source.respond_to?(:payload)
335
+ # Copy into current output
336
+ @__result.prepare!(source.payload)
337
+
338
+ # Copy into current commande
339
+ source.payload.each do |name, value|
340
+ setter = :"#{name}="
341
+ if respond_to?(setter, true)
342
+ __send__(setter, value)
343
+ next
344
+ end
345
+
346
+ ivar = :"@#{name}"
347
+ next unless instance_variable_defined?(ivar)
348
+ instance_variable_set(ivar, value)
349
+ end
350
+ end
351
+
352
+ ##
353
+ # Checks the success of source and returns it
354
+ #
355
+ # @param [Commande::Result, ActiveRecord::Base] source
356
+ # @return [TrueClass, FalseClass]
357
+ #
358
+ def transfer_success?(source)
359
+ return source.successful? if source.respond_to?(:successful?)
360
+ source.valid? && source.persisted?
361
+ end
362
+
363
+ def transfer_errors(source, header:)
364
+ errors = source.errors
365
+ errors = source.errors.full_messages if errors.respond_to?(:full_messages)
366
+ Array(errors).each do |e|
367
+ error header ? ::Kernel.format('%<header>s: %<error>s', header: header, error: e) : e
368
+ end
369
+
370
+ errors&.length&.positive?
371
+ end
372
+
373
+ ##
374
+ # Copies the status of an interactor or active record object, #fail! if not successful
375
+ #
376
+ # ATTENTION: your setter needs to be PUBLIC to be copied to.
377
+ #
378
+ # @param [Commande::Result, ApplicationRecord] source
379
+ # @see #transfer
380
+ # @see Commande
381
+ #
382
+ def transfer!(source, header: nil)
383
+ return if transfer(source, header: header)
384
+ fail!
385
+ end
386
+
387
+ # @api private
388
+ def _prepare!
389
+ @__result.prepare!(_outputs)
390
+ end
391
+
392
+ # @api private
393
+ def _outputs
394
+ Hash[].tap do |result|
395
+ self.class.outputs.each do |name, ivar|
396
+ result[name] = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : nil
397
+ end
398
+ end
399
+ end
400
+
401
+ # @api private
402
+ module ClassMethods
403
+ def call(*args, &block)
404
+ new.call(*args, &block)
405
+ end
406
+
407
+ # @api private
408
+ def self.extended(interactor)
409
+ interactor.class_eval do
410
+ singleton_class.class_eval do
411
+ attr_accessor(:outputs)
412
+ end
413
+
414
+ self.outputs = {}
415
+ end
416
+ end
417
+
418
+ def method_added(method_name)
419
+ super
420
+ return unless method_name == :call
421
+
422
+ prepend Commande::Interface
423
+ end
424
+
425
+ # Expose local instance variables into the returning value of <tt>#call</tt>
426
+ #
427
+ # @param instance_variable_names [Symbol,Array<Symbol>] one or more instance
428
+ # variable names
429
+ #
430
+ # @see Commande::Result
431
+ #
432
+ def output(*instance_variable_names)
433
+ instance_variable_names.each do |name|
434
+ outputs[name.to_sym] = "@#{name}"
435
+ end
436
+ end
437
+ end
438
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commande/minitest/assertions/assert_successful'
4
+ require 'commande/minitest/assertions/assert_valid'
5
+ require 'commande/minitest/assertions/assert_with_error'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Assertions
5
+ def assert_successful(actual)
6
+ assert successful?(actual),
7
+ "Expected #{actual.inspect} to be successful?. Actual got these errors: #{actual.errors || ''}"
8
+ end
9
+
10
+ def refute_successful(actual)
11
+ refute successful?(actual),
12
+ "Expected #{actual.inspect} to not be successful?"
13
+ end
14
+
15
+ private
16
+
17
+ def successful?(actual)
18
+ actual.successful?
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Assertions
5
+ def assert_valid(actual, *args)
6
+ assert valid?(actual, *args),
7
+ "Expected #{actual.inspect} to be valid?"
8
+ end
9
+
10
+ def refute_valid(actual, *args)
11
+ refute valid?(actual, *args),
12
+ "Expected #{actual.inspect} to not be valid?"
13
+ end
14
+
15
+ private
16
+
17
+ def valid?(actual, *args)
18
+ actual.valid?(*args)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Assertions
5
+ def assert_with_error(expected, actual)
6
+ assert with_error(expected, actual),
7
+ "Expected #{actual.errors} to have an error '#{expected}'."
8
+ end
9
+
10
+ def refute_with_error(expected, actual)
11
+ refute with_error(expected, actual),
12
+ "Expected #{actual.errors} to not have an error '#{expected}'."
13
+ end
14
+
15
+ private
16
+
17
+ def with_error(expected, actual)
18
+ actual.errors.any? do |error|
19
+ expected == error
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Commande
4
+ VERSION = '0.3.0'
5
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: commande
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Derk-Jan Karrenbeld
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description:
56
+ email:
57
+ - derk-jan+github@karrenbeld.info
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rubocop.yml"
64
+ - ".travis.yml"
65
+ - CHANGELOG.md
66
+ - CODE_OF_CONDUCT.md
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - bin/console
72
+ - bin/setup
73
+ - commande.gemspec
74
+ - lib/command.rb
75
+ - lib/command/minitest.rb
76
+ - lib/command/minitest/assertions/assert_successful.rb
77
+ - lib/command/minitest/assertions/assert_valid.rb
78
+ - lib/command/minitest/assertions/assert_with_error.rb
79
+ - lib/commande.rb
80
+ - lib/commande/minitest.rb
81
+ - lib/commande/minitest/assertions/assert_successful.rb
82
+ - lib/commande/minitest/assertions/assert_valid.rb
83
+ - lib/commande/minitest/assertions/assert_with_error.rb
84
+ - lib/commande/version.rb
85
+ homepage:
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ bug_tracker_uri: https://github.com/SleeplessByte/command/issues
90
+ changelog_uri: https://github.com/SleeplessByte/command/CHANGELOG.md
91
+ homepage_uri: https://github.com/SleeplessByte/command
92
+ source_code_uri: https://github.com/SleeplessByte/command
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.6.14.1
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Allows for Command pattern style POROs
113
+ test_files: []