menu

Search By Label

The Law of Demeter belongs to the principle that states in a ruby method of an object should invoke only the methods of the following kinds of objects:
  1. itself
  2. its parameters
  3. any objects it creates/instantiates
  4. its direct component objects

The law restricts how deeply a method can reach into another object’s dependency graph, preventing any one method from becoming tightly coupled to another object’s structure.

Multiple Dots

The most obvious violation of the Law of Demeter is “multiple dots,” meaning a chain of methods being invoked on each others’ return values.

class User
  def discounted_plan_price(discount_code)
    coupon = Coupon.new(discount_code)
    coupon.discount(account.plan.price)
  end
end

The call to account.plan.price above violates the Law of Demeter by invoking price on the return value of plan. The price method is not a method on User, its parameter discount_code, its instantiated object coupon or its direct component account.

The quickest way to avoid violations of this nature is to delegate the method:
class User
  def discounted_plan_price(discount_code)
    account.discounted_plan_price(discount_code)
  end
end

class Account
  def discounted_plan_price(discount_code)
    coupon= Coupon.new(discount_code)
    coupon.discount(plan.price)
  end
end

In a Rails application, you can quickly delegate methods using ActiveSupport’s delegate class method:
class User
  delegate :discount_plan_price, to: :account
end

If you find yourself writing lots of delegators, consider changing the consumer class to take a different object

I use polymorphic routes for readability purposes. This is useful to make the lines of code a bit shorter like this:

<%= button_to "Delete", [quote, line_item_date] %>
<%= button_to "Delete", quote_line_item_date_path(quote, line_item_date) %>

It is also possible to use them in controllers. For example, the two following lines for code are equivalent:

redirect_to @quote
redirect_to quote_path(@quote)


If you want to learn more about them, here is a link to the documentation.

What is a JSON Web Token?

A JSON Web Token is an internet standard defined by the Internet Engineering Task Force (IETF) as a: "compact, URL-safe means of representing claims to be transferred between two parties" so, go ahead with the configuration.

Build the Rails app:
rails new jwt_rails_api --api


Add the gems:
gem 'jwt', '~> 2.7'
gem "bcrypt", "~> 3.1.7"

Generate User and Product Models
rails g model User username:string password:string
rails g model Product name:string description:text

After running our migration with rails db:migrate, our setup with models is complete, and our schema, found in db/schema.rb, should now look similar to this:

ActiveRecord::Schema[7.0].define(version: 2024_03_26_224534) do
  create_table "products", force: :cascade do |t|
    t.string "name"
    t.text "description"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "username"
    t.string "password_digest"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
end


Build a jwt Gem Wrapper

Our wrapper class is found in app/lib/json_web_token.rb and looks like this:

class JsonWebToken
  JWT_SECRET = Rails.application.secrets.secret_key_base

  def self.encode(payload, exp = 12.hours.from_now)
    payload[:exp] = exp.to_i

    JWT.encode(payload, JWT_SECRET)
  end

  def self.decode(token)
    body = JWT.decode(token, JWT_SECRET)[0]

    HashWithIndifferentAccess.new(body)
  end
end

At this point, you can already test this class in your Rails console:

data = {"name"=>"Juanequex"}

JsonWebToken.encode(data)
# => "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiQXBwU2lnbmFsIiwiZXhwIjoxNjg1NDI0MjI5fQ.zWJyFHH8Pa6phBOU99XgtRntyfZQSOTX4TdwOxFY9gY"

JsonWebToken.decode(JsonWebToken.encode(data))
# => {"name"=>"Juanequex", "exp"=>1685424262}


Back to the User Model
Now will be a good time to visit our User model at app/models/user.rb. All we need to do here is add the has_secure_password class method:

class User < ApplicationRecord
  has_secure_password
end

Creating a Sample User and Product in Rails is possible through the Rails console or configuring your seeds.
User.create(username: "juanequex", password: "password")
Product.create(name: "Rad Ruby", description: "A book collection of Ruby tips")



Using JWTs in Rails Controllers

 A good place to start is the ApplicationController at app/controllers/application_controller.rb:

class ApplicationController < ActionController::API
  before_action :authenticate

  rescue_from JWT::VerificationError, with: :invalid_token
  rescue_from JWT::DecodeError, with: :decode_error

  private

  def authenticate
    authorization_header = request.headers['Authorization']
    token = authorization_header.split(" ").last if authorization_header
    decoded_token = JsonWebToken.decode(token)

    User.find(decoded_token[:user_id])
  end

  def invalid_token
    render json: { invalid_token: 'invalid token' }
  end

  def decode_error
    render json: { decode_error: 'decode error' }
  end
end

Next, we need an AuthenticationController to which users can send requests and get a signed JSON Web Token from our server. This controller should be placed at app/controllers/authentication_controller.rb and may look like this:

class AuthenticationController < ApplicationController
  skip_before_action :authenticate

  def login
    user = User.find_by(username: params[:username])
    authenticated_user = user&.authenticate(params[:password])

    if authenticated_user
      token = JsonWebToken.encode(user_id: user.id)
      expires_at = JsonWebToken.decode(token)[:exp]

      render json: { token:, expires_at: }, status: :ok
    else
      render json: { error: 'unauthorized' }, status: :unauthorized
    end
  end
end


Testing Our Ruby Application with a Protected Resource
Let's create a controller with rails g controller Product index so we have something like:
class ProductsController < ApplicationController
  before_action :authenticate

  def index
    @products = Product.all

    render json: @products
  end
end

Of course, we need a route to access these controllers. Our config/routes.rb should look something like:
Rails.application.routes.draw do
  post 'login', to: "authentication#login"
  get 'products', to: "products#index"
end


Let's try getting a JWT with a user that doesn't exist:
curl -H "Content-Type: application/json" -X POST -d '{"username":"manny","password":"password"}' http://localhost:3000/login


We should get the following response:
{"error":"unauthorized"}


Now try the same with a user that we created earlier:
curl -H "Content-Type: application/json" -X POST -d '{"username":"juanequex","password":"password"}' http://localhost:3000/login


This should give us a signed JSON web token that could look like this:
{"token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODU0NTEyMTR9.1UEYAbmFOSF93yp9pJqNEzkdHr3rVqutPNZWRIPDYkY","expires_at":1685432077}

Let's keep this token for a second and try accessing a product resource with a bad token (I changed a random character in the token):
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODU0NTEyMTR9.1UEYAbmFOSF93yp9pJqNEzkdHr3rVqutPNZWRIPZYkY" http://localhost:3000/products

output:
{"decode_error":"decode error"}



However, if we make the same request to access the product resource with the valid token we got from the server previously, We're granted access to the product resource:

[{"id":1,"name":"Rad Ruby","description":"A book collection of Ruby tips","created_at":"2024-03-26T19:33:30.826Z","updated_at":"2024-03-26T19:33:30.826Z"}]

That's it! We've successfully secured our Ruby application with a JSON web token!
In Ruby on Rails, the find_or_initialize_by method is a convenient way to find a record in a database based on certain conditions and, if not found, initialize a new instance of the model with those conditions set.

# Example usage
@user = User.find_or_initialize_by(email: 'example@example.com')

In certain instances, when sharing content via Rails view using I18n and incorporating specific HTML instructions, such as the requirement to set a string in bold, we follow this approach:

# config/locales/en.yml
en:
  homepage:
    services:
      title: "This is the <strong>example</strong>"

in this case should use the html_safe method in views like:

 <h1><%= t(:title, scope: %w[homepage services]).html_safe %></h1>

with this implementation, any HTML tag can work effectively.
The :back symbol in Rails is often used to generate a URL that represents the previous page or location the user came from. It's commonly utilized in conjunction with methods like redirect_to redirecting users back to where they were before performing a specific action.


Here's a brief explanation:

- Usage with redirect_to you can use :back with the redirect_to method to send the user back to the previous page:

redirect_to :back

Usage with Links, You can also use :back as the path in a link to create a "Back" button or link:

<%= link_to 'Back', :back, class: 'btn btn-primary' %> 

Switch your rails app database without having to touch a single file. Pass --force to skip prompts.

rails db:system:change --to=postgresql
In your session, it's necessary to set the editor for opening files using this export command:

export EDITOR= 

Afterward, to open the gem files, use the following command:
 
bundle open

This action allows you to open any gem within your Rails project, enabling you to explore its internals and understand the current flow, helping you solve any issues related to it.


In rails 7 the correct way to render templates through the wicked_pdf gem is:

template: 'folder/file', formats: [:html]

is important to know that now is required to set the file without the file extension.
By default, Rails ships with three environments: "development," "test," and "production." To navigate between each environment, you can use the following command: 
 
 
bin/rails db:environment:set RAILS_ENV="YOUR_ENVIRONMENT" 
If you are using the devise gem the initial redirection will be to '/' even though you have another enable route '/welcome'.
So the solution can be achieved with this: 

  • app/controllers/application_controller.rb:

def after_sign_in_path_for(resource) 
 welcome_path 
end