From 50f60f2610c0b8ca01bde28ccbcf80108abb0594 Mon Sep 17 00:00:00 2001 From: Dmitry Babenko Date: Sat, 17 Aug 2019 18:53:47 +0300 Subject: [PATCH] Command Object --- .rubocop.yml | 5 +- Gemfile.lock | 106 ++++++++++++++++++ README.md | 6 +- auxiliary_rails.gemspec | 1 + lib/auxiliary_rails.rb | 1 + lib/auxiliary_rails/abstract_command.rb | 96 ++++++++++++++++ .../install_commands_generator.rb | 12 ++ .../auxiliary_rails/install_generator.rb | 11 ++ .../templates/application_command_template.rb | 2 + spec/auxiliary_rails/abstract_command_spec.rb | 17 +++ spec/sample_commands_spec.rb | 61 ++++++++++ .../support/sample_classes/sample_commands.rb | 31 +++++ 12 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 lib/auxiliary_rails/abstract_command.rb create mode 100644 lib/generators/auxiliary_rails/install_commands_generator.rb create mode 100644 lib/generators/auxiliary_rails/install_generator.rb create mode 100644 lib/generators/auxiliary_rails/templates/application_command_template.rb create mode 100644 spec/auxiliary_rails/abstract_command_spec.rb create mode 100644 spec/sample_commands_spec.rb create mode 100644 spec/support/sample_classes/sample_commands.rb diff --git a/.rubocop.yml b/.rubocop.yml index 2a216d0..c261c4e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,9 +17,8 @@ Layout/AlignArguments: #################### Metrics ############################## Metrics/BlockLength: - Exclude: - - auxiliary_rails.gemspec - - 'spec/**/*_spec.rb' + ExcludedMethods: + - describe #################### Style ############################### diff --git a/Gemfile.lock b/Gemfile.lock index c112ec2..371ae34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,11 +7,76 @@ PATH GEM remote: https://rubygems.org/ specs: + actioncable (5.2.3) + actionpack (= 5.2.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailer (5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.2.3) + actionview (= 5.2.3) + activesupport (= 5.2.3) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.2.3) + activesupport (= 5.2.3) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.2.3) + activesupport (= 5.2.3) + globalid (>= 0.3.6) + activemodel (5.2.3) + activesupport (= 5.2.3) + activerecord (5.2.3) + activemodel (= 5.2.3) + activesupport (= 5.2.3) + arel (>= 9.0) + activestorage (5.2.3) + actionpack (= 5.2.3) + activerecord (= 5.2.3) + marcel (~> 0.3.1) + activesupport (5.2.3) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + arel (9.0.0) ast (2.4.0) + builder (3.2.3) coderay (1.1.2) + concurrent-ruby (1.1.5) + crass (1.0.4) diff-lcs (1.3) + erubi (1.8.0) + globalid (0.4.2) + activesupport (>= 4.2.0) + i18n (1.6.0) + concurrent-ruby (~> 1.0) jaro_winkler (1.5.2) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (0.3.3) + mimemagic (~> 0.3.2) method_source (0.9.2) + ast (~> 2.4.0) + mimemagic (0.3.3) + mini_mime (1.0.2) + mini_portile2 (2.4.0) + minitest (5.11.3) + nio4r (2.4.0) + nokogiri (1.10.3) + mini_portile2 (~> 2.4.0) parallel (1.17.0) parser (2.6.3.0) ast (~> 2.4.0) @@ -19,6 +84,33 @@ GEM coderay (~> 1.1.0) method_source (~> 0.9.0) psych (3.1.0) + rack (2.0.7) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (5.2.3) + actioncable (= 5.2.3) + actionmailer (= 5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) + activemodel (= 5.2.3) + activerecord (= 5.2.3) + activestorage (= 5.2.3) + activesupport (= 5.2.3) + bundler (>= 1.3.0) + railties (= 5.2.3) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + railties (5.2.3) + actionpack (= 5.2.3) + activesupport (= 5.2.3) + method_source + rake (>= 0.8.7) + thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (10.5.0) rspec (3.8.0) @@ -47,8 +139,21 @@ GEM rubocop-rspec (1.32.0) rubocop (>= 0.60.0) ruby-progressbar (1.10.0) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) thor (0.20.3) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) unicode-display_width (1.5.0) + websocket-driver (0.7.1) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.4) PLATFORMS ruby @@ -57,6 +162,7 @@ DEPENDENCIES auxiliary_rails! bundler (~> 2.0) pry + rails (~> 5.2) rake (~> 10.0) rspec (~> 3.0) rubocop diff --git a/README.md b/README.md index 06f2ca9..ebf7e9d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,11 @@ rails new APP_PATH --skip-action-cable --skip-coffee --skip-test --database=post ### Generators ```sh -rails generate auxiliary_rails:api_resource +# install everything +rails generate auxiliary_rails:install + +# install one by one +rails generate auxiliary_rails:install_commands rails generate auxiliary_rails:install_errors rails generate auxiliary_rails:install_rubocop rails generate auxiliary_rails:install_rubocop --no-specify-gems diff --git a/auxiliary_rails.gemspec b/auxiliary_rails.gemspec index 8664c5a..b695dd2 100644 --- a/auxiliary_rails.gemspec +++ b/auxiliary_rails.gemspec @@ -35,6 +35,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 2.0' spec.add_development_dependency 'pry' + spec.add_development_dependency 'rails', '~> 5.2' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'rubocop' diff --git a/lib/auxiliary_rails.rb b/lib/auxiliary_rails.rb index f7c0198..d3cf930 100644 --- a/lib/auxiliary_rails.rb +++ b/lib/auxiliary_rails.rb @@ -1,4 +1,5 @@ require 'auxiliary_rails/abstract_error' +require 'auxiliary_rails/abstract_command' require 'auxiliary_rails/railtie' if defined?(Rails) require 'auxiliary_rails/version' diff --git a/lib/auxiliary_rails/abstract_command.rb b/lib/auxiliary_rails/abstract_command.rb new file mode 100644 index 0000000..be84be4 --- /dev/null +++ b/lib/auxiliary_rails/abstract_command.rb @@ -0,0 +1,96 @@ +require 'active_model' + +module AuxiliaryRails + class AbstractCommand + include ActiveModel::Model + include ActiveModel::Attributes + + def self.call(*args) + new(*args).call + end + + def call + raise NotImplementedError + end + + def errors + @errors ||= ActiveModel::Errors.new(self) + end + + def failure? + status?(:failure) + end + + def status?(value) + ensure_execution! + + status == value&.to_sym + end + + def success? + status?(:success) + end + + def transaction(&block) + ActiveRecord::Base.transaction(&block) if block_given? + end + + # Method for ActiveModel::Errors + def read_attribute_for_validation(attr_name) + if attr_name == :command + self + else + attribute(attr_name) + end + end + + # Method for ActiveModel::Translation + def self.i18n_scope + :commands + end + + protected + + attr_accessor :status + + def ensure_empty_errors! + return if errors.empty? + + error!("`#{self.class}` contains errors.") + end + + def ensure_empty_status! + return if status.nil? + + error!("`#{self.class}` was already executed.") + end + + def ensure_execution! + return if status.present? + + error!("`#{self.class}` was not executed yet.") + end + + def error!(message = nil) + message ||= "`#{self.class}` raised error." + raise ApplicationError, message + end + + def failure!(message = nil) + ensure_empty_status! + + errors.add(:command, :failed, message: message) unless message.nil? + + self.status = :failure + self + end + + def success! + ensure_empty_errors! + ensure_empty_status! + + self.status = :success + self + end + end +end diff --git a/lib/generators/auxiliary_rails/install_commands_generator.rb b/lib/generators/auxiliary_rails/install_commands_generator.rb new file mode 100644 index 0000000..0fa3e6f --- /dev/null +++ b/lib/generators/auxiliary_rails/install_commands_generator.rb @@ -0,0 +1,12 @@ +require 'rails' + +module AuxiliaryRails + class InstallCommandsGenerator < ::Rails::Generators::Base + source_root File.expand_path('templates', __dir__) + + def copy_application_command_file + copy_file 'application_command_template.rb', + 'app/commands/application_command.rb' + end + end +end diff --git a/lib/generators/auxiliary_rails/install_generator.rb b/lib/generators/auxiliary_rails/install_generator.rb new file mode 100644 index 0000000..590de7d --- /dev/null +++ b/lib/generators/auxiliary_rails/install_generator.rb @@ -0,0 +1,11 @@ +require 'rails' + +module AuxiliaryRails + class InstallGenerator < ::Rails::Generators::Base + def install + generate 'auxiliary_rails:install_commands' + generate 'auxiliary_rails:install_errors' + generate 'auxiliary_rails:install_rubocop --no-specify-gems' + end + end +end diff --git a/lib/generators/auxiliary_rails/templates/application_command_template.rb b/lib/generators/auxiliary_rails/templates/application_command_template.rb new file mode 100644 index 0000000..1df1e8b --- /dev/null +++ b/lib/generators/auxiliary_rails/templates/application_command_template.rb @@ -0,0 +1,2 @@ +class ApplicationCommand < AuxiliaryRails::AbstractCommand +end diff --git a/spec/auxiliary_rails/abstract_command_spec.rb b/spec/auxiliary_rails/abstract_command_spec.rb new file mode 100644 index 0000000..12ac62b --- /dev/null +++ b/spec/auxiliary_rails/abstract_command_spec.rb @@ -0,0 +1,17 @@ +RSpec.describe AuxiliaryRails::AbstractCommand do + describe '#call' do + subject(:cmd_class) { described_class } + + it 'raises NotImplementedError exception' do + expect { cmd_class.call }.to raise_error NotImplementedError + end + end + + describe '.call' do + subject(:cmd) { described_class.new } + + it 'raises NotImplementedError exception' do + expect { cmd.call }.to raise_error NotImplementedError + end + end +end diff --git a/spec/sample_commands_spec.rb b/spec/sample_commands_spec.rb new file mode 100644 index 0000000..5c8a5fc --- /dev/null +++ b/spec/sample_commands_spec.rb @@ -0,0 +1,61 @@ +require 'support/sample_classes/sample_commands' + +RSpec.describe SampleCommands do + describe 'SuccessCommand' do + describe '#call' do + subject(:cmd_class) { SampleCommands::SuccessCommand } + + it do + expect(cmd_class.call).to be_a cmd_class + end + end + + describe '.call' do + subject(:cmd) { SampleCommands::SuccessCommand.new } + + it 'returns self as a result' do + expect(cmd.call).to be cmd + end + + it do + expect(cmd.call).to be_success + end + end + end + + describe 'DoubleStatusSetCommand' do + subject(:cmd) { SampleCommands::DoubleStatusSetCommand.new } + + describe '.call' do + it do + expect { cmd.call }.to raise_error ApplicationError, + '`SampleCommands::DoubleStatusSetCommand` was already executed.' + end + end + end + + describe 'SuccessWithErrorsCommand' do + subject(:cmd) { SampleCommands::SuccessWithErrorsCommand.new } + + describe '.call' do + it do + expect { cmd.call }.to raise_error ApplicationError, + '`SampleCommands::SuccessWithErrorsCommand` contains errors.' + end + end + end + + describe 'FailureWithErrorsCommand' do + subject(:cmd) { SampleCommands::FailureWithErrorsCommand.new } + + describe '.call' do + it do + expect { cmd.call }.to change(cmd.errors, :count).from(0).to(1) + end + + it do + puts cmd.call.errors.full_messages + end + end + end +end diff --git a/spec/support/sample_classes/sample_commands.rb b/spec/support/sample_classes/sample_commands.rb new file mode 100644 index 0000000..9244510 --- /dev/null +++ b/spec/support/sample_classes/sample_commands.rb @@ -0,0 +1,31 @@ +class ApplicationError < AuxiliaryRails::AbstractError +end + +module SampleCommands + class SuccessCommand < AuxiliaryRails::AbstractCommand + def call + success! + end + end + + class DoubleStatusSetCommand < AuxiliaryRails::AbstractCommand + def call + failure! + success! + end + end + + class SuccessWithErrorsCommand < AuxiliaryRails::AbstractCommand + def call + errors.add(:command, :error) + success! + end + end + + class FailureWithErrorsCommand < AuxiliaryRails::AbstractCommand + def call + errors.add(:command, :test_failure_message) + failure! + end + end +end