Coder Social home page Coder Social logo

simple_command's Introduction

Code Climate CI

SimpleCommand

A simple, standardized way to build and use Service Objects (aka Commands) in Ruby

Requirements

  • Ruby 2.6+

Installation

Add this line to your application's Gemfile:

gem 'simple_command'

And then execute:

$ bundle

Or install it yourself as:

$ gem install simple_command

Usage

Here's a basic example of a command that authenticates a user

# define a command class
class AuthenticateUser
  # put SimpleCommand before the class' ancestors chain
  prepend SimpleCommand
  include ActiveModel::Validations

  # optional, initialize the command with some arguments
  def initialize(email, password)
    @email = email
    @password = password
  end

  # mandatory: define a #call method. its return value will be available
  #            through #result
  def call
    if user = User.find_by(email: @email)&.authenticate(@password)
      return user
    else
      errors.add(:base, :failure)
    end
    nil
  end
end

in your locale file

# config/locales/en.yml
en:
  activemodel:
    errors:
      models:
        authenticate_user:
          failure: Wrong email or password

Then, in your controller:

class SessionsController < ApplicationController
  def create
    # initialize and execute the command
    # NOTE: `.call` is a shortcut for `.new(args).call`
    command = AuthenticateUser.call(session_params[:email], session_params[:password])

    # check command outcome
    if command.success?
      # command#result will contain the user instance, if found
      session[:user_token] = command.result.secret_token
      redirect_to root_path
    else
      flash.now[:alert] = t(command.errors.full_messages.to_sentence)
      render :new
    end
  end

  private

  def session_params
    params.require(:session).permit(:email, :password)
  end
end

Test with Rspec

Make the spec file spec/commands/authenticate_user_spec.rb like:

describe AuthenticateUser do
  subject(:context) { described_class.call(username, password) }

  describe '.call' do
    context 'when the context is successful' do
      let(:username) { 'correct_user' }
      let(:password) { 'correct_password' }
      
      it 'succeeds' do
        expect(context).to be_success
      end
    end

    context 'when the context is not successful' do
      let(:username) { 'wrong_user' }
      let(:password) { 'wrong_password' }

      it 'fails' do
        expect(context).to be_failure
      end
    end
  end
end

Contributing

  1. Fork it ( https://github.com/nebulab/simple_command/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

simple_command's People

Contributors

andreapavoni avatar bartboy011 avatar bguban avatar bmorrall avatar guilpejon avatar kennyadsl avatar maryshirl avatar mtylty avatar petergoldstein avatar rgraff avatar rschooley avatar sirwolfgang avatar thg303 avatar vanboom avatar zudochkin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

simple_command's Issues

Make SimpleCommand::Errors compatible with ActiveModel::Errors

After update to v0.1.0 from v0.0.9 tests started to fail with

expect(
  get_users(page: '-2')
).to eq(errors: { page: ["must be a positive number"] })
expected: {:errors=>{:page=>["must be a positive number"]}}
     got: {:errors=>{:page=>"must be a positive number"}}

In the controller errors are rendered the following way:

load_users = LoadUsers.(params)
if load_users.success?
  render json: load_users.result
else
  render json: { errors: load_users.errors }
end

The problem is that SimpleCommand::Errors#to_json format differs from ActiveModel::Errors#to_json

# SimpleCommand::Errors
load_users = LoadUsers.new
load_users.errors.add(:page, 'must be a positive number')
load_users.errors.add(:page, 'cannot be blank')
load_users.errors.as_json
=> {"page"=>"cannot be blank"}

# ActiveModel::Errors
user = User.new
user.errors.add(:email, 'is invalid')
user.errors.add(:email, 'cannot be blank')
user.errors.as_json
=> {:email=>["is invalid", "cannot be blank"]}

It would be good to have SimpleCommand::Errors#as_json to be the same format as ActiveModel::Errors#as_json

LoadError: Unable to autoload constant [command]

I've followed this tutorial for JSON Web Tokens and things Simple Command work beautifully until I hit the tests. In the tests the commands each time AuthorizeApiRequest is called I get this error: LoadError: Unable to autoload constant AuthorizeApiRequest.

# app/commands/authorize_api_request.rb

class AuthorizeApiRequest
  prepend SimpleCommand

  def initialize(headers = {})
    @headers = headers
  end

  def call
    user
  end

  private

  attr_reader :headers

  def user
    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
    @user || errors.add(:token, 'Invalid token') && nil
  end

  def decoded_auth_token
    @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
  end

  def http_auth_header
    if headers['Authorization'].present?
      return headers['Authorization'].split(' ').last
    else
      errors.add(:token, 'Missing token')
    end
    nil
  end
end
# app/controllers/admin/authenticated_controller.rb

module Admin
  class AuthenticatedController < ApplicationController

    prepend SimpleCommand
    
    before_action :authenticate_request

    attr_reader :current_user

    private

    def authenticate_request
      @current_user = AuthorizeApiRequest.call(request.headers).result
      render json: { error: 'Not Authorized' }, status: 401 unless @current_user
    end

  end
end

Am I missing something here?
Does one need to do something extra to use Simple Commands in tests?

Release the gem onto https://rubygems.org

Hey guys, I found that the gem was lastly released 4 years ago. Since those times there were done a lot of changes in the gem. Could you do a new release with the latest changes?

How to export command errors in ams ErrorSerializer?

JSON API Errors Document in AMS
I want to render the error message by using AMS jsonapi serializer.
But there is something wrong if I render a simple_command result.
I can render the errors to json but can not to jsonapi.

#simple_command
Class AuthenticateUser
*
*
  errors.add :user_authentication, 'invalid credentials'
*
*
end
#controller
command = AuthenticateUser.call(params[:email], params[:password])

render json: command, status: 401, adapter: :json_api, serializer: ActiveModel::Serializer::ErrorSerializer

log:

NoMethodError (undefined method `messages' for {:user_authentication=>["invalid credentials"]}:SimpleCommand::Errors):

app/controllers/authentication_controller.rb:10:in `authenticate'

What should I do?

Using Ruby 3 style keyword arguments raises warning in Ruby 2.7

class ExampleCommand
  prepend SimpleCommand
  def initialize(foo:)
    @foo = foo
  end
  def call
    @foo
  end
end

irb(main):010:0> ExampleCommand.call(foo: "bar")
/usr/local/bundle/gems/simple_command-0.1.0/lib/simple_command.rb:9: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
(irb):3: warning: The called method `initialize' is defined here
=> #<ExampleCommand:0x0000560f7af7f988 @foo="bar", @called=true, @result="bar">

The issue is describe here: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/

The warning is in 2.7 and will error in 3.0

How mock with rspec with prepended SimpleCommand

before do
  allow_any_instance_of(Class).to receive(:call).and_return(double)
end

Failure/Error: allow_any_instance_of(Class).to receive(:call).and_return(double)
Using any_instance to stub a method (call) that has been defined on a prepended module (SimpleCommand) is not supported.

undefined method `full_messages` for SimpleCommand::Errors

I'm not sure if this is a real issue but I'm getting an error trying to use this method.
According to the spec here

expect(errors.full_messages).to eq ["Attr1 has an error", "Attr2 has an error", "Attr2 has two errors"]
, I'm doing:

irb(main):012:0> errors = SimpleCommand::Errors.new
=> {}
irb(main):013:0> errors.add(:foo, 'wha')
=> nil
irb(main):014:0> errors
=> {:foo=>["wha"]}
irb(main):015:0> errors.full_messages
Traceback (most recent call last):
        1: from (irb):15
NoMethodError (undefined method `full_messages' for {:foo=>["wha"]}:SimpleCommand::Errors)

how to test with rspec?

I have written a couple of commands using this amazing gem, I wonder what is the best approach to test them with rspec.

version 0.1.0 brakes add_multiple_errors

When calling add_multiple_errors with an error hash i get NoMethodError: undefined method each' for #String:0x00007ff1cd1a8710`

I have an error object from another command object, when i call add_multiple_errors it works with version 0.9.0 and raises a NoMethodError error with 0.1.0:

>command.errors
:error=>["21004 The shared secret you provided does not match the shared secret on file for your account."]}
>errors.add_multiple_errors(command.errors)
NoMethodError: undefined method `each' for #<String:0x00007ff1cd1a8710>

Failing Build?

Hello! I noticed your build badge reads 'failing' so I forked the repo and ran the specs on my machine, but they all pass.

Might be something to look into.

Thanks :)

How to modify default token expiration time

Hi, I'm using SimpleCommand in a Rails app with an Angular 7 front end. It all works beautifully and the auth is smooth and great.

I'm doing a very simple implementation similar to the one you have here in Github example.

My question is: how do I adjust the expiration date of the token ? It seems to default to 24 hours ( I think )

Let me know if you can, or point me in the right direction... thanks !

uninitialized constant

I followed this tutorial.

But it looks like my command is not "autoloaded??"

"exception":"#<NameError: uninitialized constant Api::V1::AuthenticationController::AuthenticateUser>"

These are my files

app/controllers/api/v1/authentication_controller.rb

module Api
    module V1
        class AuthenticationController < ApplicationController
            skip_before_action :authenticate_request

            def authenticate
                command = AuthenticateUser.call(params[:username], params[:password])

                if command.success?
                    render json: {auth_token: command.result}
                else
                    render json: {error: command.errors }, status: :unauthorized
                end
            end
        end
    end
end

app/commands/authorize_api_request.rb

class AuthorizeApiRequest 
    prepend SimpleCommand 

    def initialize(headers = {}) 
        @headers = headers 
    end 

    def call 
        user 
    end 

    private 

    attr_reader :headers 

    def user 
        @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token 
        @user || errors.add(:token, 'Invalid token') && nil 
    end 

    def decoded_auth_token 
        @decoded_auth_token ||= JsonWebToken.decode(http_auth_header) 
    end 

    def http_auth_header 
        if headers['Authorization'].present? 
            return headers['Authorization'].split(' ').last 
        else 
            errors.add(:token, 'Missing token') 
        end 
        nil 
    end 
end

Cut a new release now that Ruby 3.0 is out

Hi -

Do you plan on releasing a version compatible with Ruby 3, now that it's the latest stable version of Ruby?

I see that the keyword argument issue was already fixed in #28.

Thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.