commande 0.3.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 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: []