simpler_command 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 667240bb9109486b654cb65b71cec68f795d260883567c696e8fef2051dfe8f0
4
+ data.tar.gz: 58899bb01b7775769a386432010de4044120a8b2ee2e6c84f5cff55783c2083a
5
+ SHA512:
6
+ metadata.gz: a6561d0f75b6dbcde6a8246c32d5943f29fcd988eb81760c29d00f6f190b5ba121a371af2f75d2d3b6ff3b2d86d721aa9e2152694f105ab76a690d8b19251153
7
+ data.tar.gz: 566d35c4e9c1bea2ace1f2f9f1cf9d47693013005ea4956143da4d424498a99e6a9a51d31fadf9976dc48b5cdff848e73e206285e03080bf93a11f31cea52b0c
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,18 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Layout/LineLength:
5
+ Max: 100 # Set line width to more sensible default
6
+
7
+ Metrics/BlockLength:
8
+ Exclude:
9
+ - spec/**/*
10
+
11
+ Style/StringLiterals:
12
+ EnforcedStyle: double_quotes # prefer double quotes
13
+
14
+ Style/FormatStringToken:
15
+ Enabled: false
16
+
17
+ Style/FrozenStringLiteralComment:
18
+ EnforcedStyle: always_true # immutable code where possible
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.0
6
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - Initial commit
8
+
9
+ ### Changed
10
+
11
+ ### Removed
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in simpler_command.gemspec
6
+ gemspec
7
+
8
+ gem "activemodel", "~> 6.0.3"
9
+ gem "activesupport", "~> 6.0.3"
10
+
11
+ gem "rake", "~> 12.0"
12
+ gem "rspec", "~> 3.0"
13
+ gem "rubocop", "~> 0.90.0"
14
+ gem "simplecov", "~> 0.19.0"
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simpler_command (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activemodel (6.0.3.3)
10
+ activesupport (= 6.0.3.3)
11
+ activesupport (6.0.3.3)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 0.7, < 2)
14
+ minitest (~> 5.1)
15
+ tzinfo (~> 1.1)
16
+ zeitwerk (~> 2.2, >= 2.2.2)
17
+ ast (2.4.1)
18
+ concurrent-ruby (1.1.7)
19
+ diff-lcs (1.4.4)
20
+ docile (1.3.2)
21
+ i18n (1.8.5)
22
+ concurrent-ruby (~> 1.0)
23
+ minitest (5.14.2)
24
+ parallel (1.19.2)
25
+ parser (2.7.1.4)
26
+ ast (~> 2.4.1)
27
+ rainbow (3.0.0)
28
+ rake (12.3.3)
29
+ regexp_parser (1.7.1)
30
+ rexml (3.2.4)
31
+ rspec (3.9.0)
32
+ rspec-core (~> 3.9.0)
33
+ rspec-expectations (~> 3.9.0)
34
+ rspec-mocks (~> 3.9.0)
35
+ rspec-core (3.9.2)
36
+ rspec-support (~> 3.9.3)
37
+ rspec-expectations (3.9.2)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.9.0)
40
+ rspec-mocks (3.9.1)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.9.0)
43
+ rspec-support (3.9.3)
44
+ rubocop (0.90.0)
45
+ parallel (~> 1.10)
46
+ parser (>= 2.7.1.1)
47
+ rainbow (>= 2.2.2, < 4.0)
48
+ regexp_parser (>= 1.7)
49
+ rexml
50
+ rubocop-ast (>= 0.3.0, < 1.0)
51
+ ruby-progressbar (~> 1.7)
52
+ unicode-display_width (>= 1.4.0, < 2.0)
53
+ rubocop-ast (0.3.0)
54
+ parser (>= 2.7.1.4)
55
+ ruby-progressbar (1.10.1)
56
+ simplecov (0.19.0)
57
+ docile (~> 1.1)
58
+ simplecov-html (~> 0.11)
59
+ simplecov-html (0.12.2)
60
+ thread_safe (0.3.6)
61
+ tzinfo (1.2.7)
62
+ thread_safe (~> 0.1)
63
+ unicode-display_width (1.7.0)
64
+ zeitwerk (2.4.0)
65
+
66
+ PLATFORMS
67
+ ruby
68
+
69
+ DEPENDENCIES
70
+ activemodel (~> 6.0.3)
71
+ activesupport (~> 6.0.3)
72
+ rake (~> 12.0)
73
+ rspec (~> 3.0)
74
+ rubocop (~> 0.90.0)
75
+ simpler_command!
76
+ simplecov (~> 0.19.0)
77
+
78
+ BUNDLED WITH
79
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Ben Morrall
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.
@@ -0,0 +1,192 @@
1
+ # SimplerCommand
2
+
3
+ Yet another simple and standardized way to build and use Commands (aka Service Objects).
4
+
5
+ Strongly inspired by [simple_command](https://github.com/nebulab/simple_command).
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'simpler_command'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install simpler_command
22
+
23
+ ## Usage
24
+
25
+ Here's a basic example of a Command that updates an Album's description, from data collected from an external API.
26
+
27
+ ```ruby
28
+ class UpdateAlbumDescription
29
+ prepend SimplerCommand
30
+
31
+ def initialize(album, lastfm_client)
32
+ @album = album
33
+ @lastfm_client = @lastfm_client
34
+ end
35
+
36
+ def call
37
+ album_info = @lastfm_client.album.get_info(album: album.name, artist: album.artist_name)
38
+ description = album_info.dig("wiki", "contents")
39
+ if description.blank?
40
+ errors.add(:description, "was not found on Last.fm")
41
+ return
42
+ end
43
+
44
+ @album.update(description: description)
45
+ nil
46
+ end
47
+ end
48
+ ```
49
+
50
+ The Command can be invoked by calling `.call` on the class.
51
+
52
+ ```ruby
53
+ class Albums::UpdateDescriptionController < ApplicationController
54
+ def create
55
+ album = Album.find(params[:album_id])
56
+ lastfm_client = Lastfm.new(ENV.fetch("LASTFM_API_KEY"), ENV.fetch("LASTFM_API_SECRET"))
57
+
58
+ command = UpdateAlbumDescription.call(album, lastfm_client)
59
+ if command.success?
60
+ flash[:notice] = "Description updated successfully"
61
+ redirect_to album
62
+ else
63
+ flash[:alert] = alert.errors.full_messages.to_sentence
64
+ redirect_to edit_album_path(album)
65
+ end
66
+ end
67
+ end
68
+ ```
69
+
70
+ ### The result object
71
+
72
+ Commands are Service Objects, built with the intent of following the principles of Command-query separation: every method should either be a command that performs an action, or a query that returns data to the caller, but not both.
73
+
74
+ Occassionally, there are instances where some data would assist with control flow or for logging. In those cases, the returned object from your call is availale from the `result` method in the returned object.
75
+
76
+ ```ruby
77
+ PublishPost = Struct.new(:post) do
78
+ prepend SimplerCommand
79
+
80
+ def call
81
+ published_date = Time.zone.now
82
+ unless post.update(published_date: published_date)
83
+ errors.add(:base, "Unable to update the Post")
84
+ errors.add_all(post.errors)
85
+ end
86
+ published_date
87
+ end
88
+ end
89
+
90
+ # ...
91
+
92
+ post = Post.find(123)
93
+ command = PublishPost.call(post)
94
+ if command.success?
95
+ logger.info("Post published on: " + command.result.strftime("%Y-%m-%d"))
96
+ end
97
+ ```
98
+
99
+ Attempting to call the `result` method for a failed command will result in a `SimplerCommand::Failure` being raised.
100
+
101
+ Additionally, you can invoke the `call` method by passing a block, which will yeild the result for a successful operation, or raise `SimplerCommand::Failure` in the advent of an error.
102
+
103
+ ```ruby
104
+ class PublishPostJob < ApplicationJob
105
+ retry_on SimplerCommand::Failure
106
+
107
+ def perform(post)
108
+ PublishPost.call(post) do |published_date|
109
+ Rollbar.info("Post published on: " + published_date.strftime("%Y-%m-%d"))
110
+ end
111
+ end
112
+ end
113
+ ```
114
+
115
+ You can also use the `.call!` method instead of `.call`. If there aren't any errors, it will return the result, otherwise it will raise an exception.
116
+
117
+ ```ruby
118
+ published_date = PublishPost.call!
119
+ puts "Post published on: " + published_date.strftime("%Y-%m-%d")
120
+ ```
121
+
122
+ ### Using ActiveModel::Validations with I18n
123
+
124
+ String translations for errors can be provided by using ActiveModel::Validations within your Command.
125
+
126
+ ```ruby
127
+ class ExampleCommand
128
+ prepend SimplerCommand
129
+ include ActiveModel::Validations
130
+
131
+ def call
132
+ errors.add(:base, :failure)
133
+ nil
134
+ end
135
+ end
136
+ ```
137
+
138
+ in your locale file
139
+
140
+ ```yaml
141
+ # config/locales/en.yml
142
+ en:
143
+ activemodel:
144
+ errors:
145
+ models:
146
+ example_command:
147
+ failure: Everything is wrong!
148
+ ```
149
+
150
+ ### Testing with RSpec
151
+
152
+ Make the spec file `spec/commands/authenticate_user_spec.rb` like:
153
+
154
+ ```ruby
155
+ describe AuthenticateUser do
156
+ subject(:context) { described_class.call(username, password) }
157
+
158
+ describe '.call' do
159
+ context 'when the call is successful' do
160
+ let(:username) { 'correct_user' }
161
+ let(:password) { 'correct_password' }
162
+
163
+ it 'succeeds' do
164
+ expect(context).to be_success
165
+ end
166
+ end
167
+
168
+ context 'when the call is not successful' do
169
+ let(:username) { 'wrong_user' }
170
+ let(:password) { 'wrong_password' }
171
+
172
+ it 'fails' do
173
+ expect(context).to be_failure
174
+ end
175
+ end
176
+ end
177
+ end
178
+ ```
179
+
180
+ ## Development
181
+
182
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
183
+
184
+ To install this gem onto your local machine, run `bundle exec rake install`.
185
+
186
+ ## Contributing
187
+
188
+ Bug reports and pull requests are welcome on GitHub at https://github.com/bmorrall/simpler_command.
189
+
190
+ ## License
191
+
192
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "simpler_command"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
@@ -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
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simpler_command/version"
4
+ require "simpler_command/string_utils"
5
+ require "simpler_command/errors"
6
+
7
+ # Provides a simple structure for Commands (Services).
8
+ #
9
+ # Prepend SimplerCommand to your Command (Service) objects and implemente a call methods.
10
+ #
11
+ # In the advent of a failure, Log any errors to an errors object.
12
+ module SimplerCommand
13
+ # Indicates the implementing class has not defined a #call method
14
+ class NotImplementedError < StandardError; end
15
+
16
+ # Indicates the #call function did not succeed
17
+ class Failure < StandardError; end
18
+
19
+ # Provided class methods to each implementing class
20
+ module ClassMethods
21
+ def call(*args, &block)
22
+ new(*args).call(&block)
23
+ end
24
+
25
+ def call!(*args)
26
+ call(*args).result
27
+ end
28
+ end
29
+
30
+ def self.prepended(base)
31
+ base.extend ClassMethods
32
+ end
33
+
34
+ def call(&block)
35
+ raise NotImplementedError unless defined?(super)
36
+
37
+ unless called?
38
+ @called = true
39
+ @result = super
40
+ end
41
+
42
+ yield result if block_given?
43
+
44
+ self
45
+ end
46
+
47
+ def success?
48
+ called? && !failure?
49
+ end
50
+ alias successful? success?
51
+
52
+ def result
53
+ raise Failure, StringUtils.to_sentence(errors.full_messages) if failure?
54
+
55
+ @result
56
+ end
57
+
58
+ def failure?
59
+ called? && errors.any?
60
+ end
61
+
62
+ def errors
63
+ return super if defined?(super)
64
+
65
+ @errors ||= Errors.new
66
+ end
67
+
68
+ private
69
+
70
+ def called?
71
+ @called ||= false
72
+ end
73
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimplerCommand
4
+ # Provides an Errors implementation similar to ActiveModel::Errors
5
+ class Errors < Hash
6
+ def add(key, value, _opts = {})
7
+ self[key] ||= []
8
+ self[key] << value
9
+ self[key].uniq!
10
+ end
11
+
12
+ def add_all(errors_hash)
13
+ errors_hash.each do |key, values|
14
+ Array(values).each do |value|
15
+ add key, value
16
+ end
17
+ end
18
+ end
19
+
20
+ def each
21
+ each_key do |field|
22
+ self[field].each { |message| yield field, message }
23
+ end
24
+ end
25
+
26
+ def full_messages
27
+ map { |attribute, message| full_message(attribute, message) }
28
+ end
29
+
30
+ # Allow ActiveSupport to render errors similar to ActiveModel::Errors
31
+ def as_json(options = nil)
32
+ {}.tap do |output|
33
+ each do |field, value|
34
+ output[field] ||= []
35
+ output[field] << value
36
+ end
37
+ end.as_json(options)
38
+ end
39
+
40
+ private
41
+
42
+ def full_message(attribute, message)
43
+ return message if attribute == :base
44
+
45
+ attr_name = StringUtils.humanize(attribute.to_s)
46
+ [attr_name, message].join(" ")
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimplerCommand
4
+ # Simple Utilities for either using ActiveSupport methods, or falling back to equivalents.
5
+ #
6
+ # Used only for generating Human-readable Strings, which should not be used for logic
7
+ module StringUtils
8
+ module_function
9
+
10
+ # Converts a string to a human-readable string
11
+ def humanize(string)
12
+ attribute = string.tr(".", "_")
13
+ if attribute.respond_to?(:humanize)
14
+ attribute.humanize
15
+ else
16
+ attribute.tr("_", " ").capitalize
17
+ end
18
+ end
19
+
20
+ # Attempt Array#to_sentence provided by ActiveSupport, or fall back to join
21
+ def to_sentence(array)
22
+ if array.respond_to?(:to_sentence)
23
+ array.to_sentence
24
+ else
25
+ array.join(", ")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimplerCommand
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/simpler_command/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "simpler_command"
7
+ spec.version = SimplerCommand::VERSION
8
+ spec.authors = ["Ben Morrall"]
9
+ spec.email = ["bemo56@hotmail.com"]
10
+
11
+ spec.summary = "Yet another simple and standardized way to build and use Commands (aka Service Objects)"
12
+ spec.homepage = "https://github.com/bmorrall/simpler_command"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/bmorrall/simpler_command"
18
+ spec.metadata["changelog_uri"] = "https://github.com/bmorrall/simpler_command/blob/master/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "exe"
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simpler_command
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Morrall
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-09-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - bemo56@hotmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".gitignore"
21
+ - ".rspec"
22
+ - ".rubocop.yml"
23
+ - ".travis.yml"
24
+ - CHANGELOG.md
25
+ - Gemfile
26
+ - Gemfile.lock
27
+ - LICENSE.txt
28
+ - README.md
29
+ - Rakefile
30
+ - bin/console
31
+ - bin/setup
32
+ - lib/simpler_command.rb
33
+ - lib/simpler_command/errors.rb
34
+ - lib/simpler_command/string_utils.rb
35
+ - lib/simpler_command/version.rb
36
+ - simpler_command.gemspec
37
+ homepage: https://github.com/bmorrall/simpler_command
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ homepage_uri: https://github.com/bmorrall/simpler_command
42
+ source_code_uri: https://github.com/bmorrall/simpler_command
43
+ changelog_uri: https://github.com/bmorrall/simpler_command/blob/master/CHANGELOG.md
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 2.3.0
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 3.1.2
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: Yet another simple and standardized way to build and use Commands (aka Service
63
+ Objects)
64
+ test_files: []