Rails 5 JSON API con RethinkDB

Con Rails 5 fuera y el nuevo modo API incluido en el framework, veamos que se necesita para configurar una nueva aplicación para que funja como una JSON API.

En esta guía se explica cómo crear y configurar una aplicación Rails con RethinkDB, de tal manera que funcione como una JSON API (puro back-end).

Una aplicación de este tipo se da muy bien para después emplearla con múltiples clientes, ya sean SPAs (Single Page Applications) hechas en Angular o aplicaciones móviles construidas con Ionic 2, wink-wink.

Se asume familiaridad con Rails, git y los códigos de estado HTTP.

Preliminares

Commit inicial

Para generar el proyecto y hacer el primer commit:

rails new json_api --api --skip-active-record
cd json_api
git init
git add .
git ci -m "Commit inicial."

La opción de –skip-active-record desactiva ActiveRecord porque usaremos RethinkDB con NoBrainer en lugar de una base de datos relacional como PostgreSQL.

Configuración inicial

Agrega estas gemas a tu Gemfile:

group :development, :test do
  #...
  # Framework para pruebas
  gem 'minitest-rails'
  # Para generar objetos de prueba
  gem 'fabrication'
  # Para generar datos aleatorios
  gem 'faker'
end
 
#...
# Para usar RethinkDB a través de NoBrainer
gem 'nobrainer'
# Para construir JSON APIs con facilidad
gem 'jbuilder', '~> 2.5'
# Para usar ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

Después en la terminal:

bundle
rails g minitest:install
rails g nobrainer:install

Con esto se instalan las gemas que hagan falta, después se generan los archivos de configuración para Minitest y NoBrainer.
Al ejecutar el generador de Minitest, te va a preguntar si debe sobreescribir el archivo test_helper.rb, contesta sí.

Agrega este contenido al archivo config/application.rb:

config.generators do |g|
  # Framework de pruebas estilo BDD
  g.test_framework :minitest, spec: true, fixture_replacement: :fabrication
  g.fixture_replacement :fabrication, dir: 'test/fabricators'
end

En el archivo test/test_helper.rb, comenta la siguiente línea:

# fixtures :all

No tenemos disponible ActiveRecord, si no se comentan esas líneas al tratar de ejecutar las pruebas se lanzará una excepción.

En el archivo config/initializers/inflections.rb agrega:

ActiveSupport::Inflector.inflections( :en ) do |inflect|
  inflect.acronym 'API'
end

Esto permite que al generar los controladores se utilice API como nombre del módulo contenedor en lugar de Api.

Con la configuración inicial terminada, agrega los cambios al repo:

git add .
git ci -m "Configuración inicial."

Testing

TDD vs BDD

La diferencia entre los dos es el cómo te gusta pensar cuando modelas tu aplicación.

El TDD utiliza el es (assert), el BDD usa el debe ser (must). Básicamente es una cuestión de ser vs debe ser al modelar.

Behavior-driven development

Si lo tuyo es el TDD, pasa a la siguiente sección.

Genera tu primer controlador

rails g controller API::Users

Ejecuta y observa:

rake test

Edita test/controllers/api/users_controller_test.rb y cambia el test de ejemplo por:

it "#index debe ser exitoso" do
  get api_users_url
  response.status.must_equal 200
end

Ejecuta y observa:

rake test

Edita config/routes.rb para agregar el recurso:

namespace :api do
  resources :users
end

Ejecuta y observa:

rake test

Edita app/controllers/api/users_controller.rb y agrega:

def index
  render json: {}
end

Ejecuta y observa:

rake test

😀

Esta fue una muestra de un proceso muy básico de BDD para un controlador.

Genera tu primer modelo

rails g model User name:string email:string password_digest:string

Ejecuta y observa:

rake test

Abre el archivo test/models/user_test.rb y verás:

require "test_helper"
 
describe User do
  let(:user) { User.new }
 
  it "must be valid" do
    value(user).must_be :valid?
  end
end

Si analizamos un poco la situación y contemplamos nuestro modelo User, veremos que el usuario forzosamente necesita al menos un correo. Así, un usuario recién creado debería ser inválido.

Cambia el contenido de ese archivo por este:

require 'test_helper'
 
describe User do
  let(:user) { User.new }
 
  it 'no debe ser válido' do
    value(user).wont_be :valid?
  end
end

Ejecuta y observa:

rake test

Abre el archivo app/models/user.rb y agrega:

validates :email, presence: true

Ejecuta y observa:

rake test

😀

Esto fue una muestra de un proceso muy básico de aplicación de BDD para crear un modelo.

A continuación veremos cómo se haría con TDD.

Test-driven development

Para que los archivos de las pruebas se generen al estilo TDD cambia el valor de spec a false en el archivo config/application.rb:

config.generators do |g|
  # Framework de pruebas estilo TDD
  g.test_framework :minitest, spec: false, fixture_replacement: :fabrication
  g.fixture_replacement :fabrication, dir: 'test/fabricators'
end

Genera tu primer controlador

rails g controller API::Users

Ejecuta y observa:

rake test

Edita test/controllers/api/users_controller_test.rb y cambia el test de ejemplo por:

def test_index_action
  get api_users_url
  assert_response :success
end

Nota: El nombre de los métodos debe comenzar necesariamente con test_ para que sean ejecutados durante el proceso de pruebas.

El método anterior es equivalente a este:

test 'index action' do
  get api_users_url
  assert_response :success
end

Ejecuta y observa:

rake test

Edita config/routes.rb para agregar el recurso:

namespace :api do
  resources :users
end

Ejecuta y observa:

rake test

Edita app/controllers/api/users_controller.rb y agrega:

def index
  render json: {}
end

Ejecuta y observa:

rake test

😀

Esta fue una muestra de un proceso muy básico de TDD para un controlador.

Genera tu primer modelo

rails g model User name:string email:string password_digest:string

Ejecuta y observa:

rake test

Abre el archivo test/models/user_test.rb y verás:

require "test_helper"
 
class UserTest < ActiveSupport::TestCase
  def user
    @user ||= User.new
  end
 
  def test_valid
    assert user.valid?
  end
end

Si analizamos un poco la situación y contemplamos nuestro modelo User, veremos que el usuario forzosamente necesita al menos un correo. Así, un usuario recién creado debería ser inválido.

Cambia el único test que existe en ese archivo por este otro:

def test_invalid
  assert_not user.valid?
end

Ejecuta y observa:

rake test

Abre el archivo app/models/user.rb y agrega:

validates :email, presence: true

Ejecuta y observa:

rake test

😀

Y esta fue una muestra de un proceso muy básico de TDD para un modelo.

TDD o BDD es una cuestión de gustos. En lo personal prefiero el BDD.

Espero esta guía te sea útil.

Repositorio en github

lobo-tuerto/rails_rethinkdb

Mejorando el $log de AngularJS con decorators

La idea de este artículo es contar con un servicio de logging que nos permita contextualizar fácilmente la información que aparece en la consola del navegador.

A continuación pongo unos ejemplos para ilustrar su uso:

Uso básico:

$log.debug( 'línea debug' );
$log.info( 'línea info' );

Salida:

[0] -- 12/3/2016 15:43:38 línea debug
[1] -- 12/3/2016 15:43:38 línea info

Uso con contextos:

var logger = $log.getLogger( 'MainController' );
logger.debug( 'línea 1' );
logger.debug( 'línea 2' );
logger.debug( 'línea 3' );
 
var logger2 = $log.getLogger( 'App' );
logger2.debug( 'otra línea' );
logger2.debug( 'una línea más' );

Salida:

[0] -- 12/3/2016 15:58:05 -- [MainController]:0>  línea 1
[1] -- 12/3/2016 15:58:05 -- [MainController]:1>  línea 2
[2] -- 12/3/2016 15:58:05 -- [MainController]:2>  línea 3
[3] -- 12/3/2016 15:58:05 -- [App]:0>  otra línea
[4] -- 12/3/2016 15:58:05 -- [App]:1>  una línea más

Con desactivación de contextos:

var logger = $log.getLogger( 'MainController' );
logger.enable( false );
logger.debug( 'línea 1' );
logger.debug( 'línea 2' );
logger.enable( true );
logger.debug( 'línea 3' );
 
var logger2 = $log.getLogger( 'App' );
logger2.debug( 'otra línea' );
logger2.debug( 'una línea más' );

Salida:

[0] -- 12/3/2016 16:02:00 -- [MainController]:0>  línea 3
[1] -- 12/3/2016 16:02:00 -- [App]:0>  otra línea
[2] -- 12/3/2016 16:02:00 -- [App]:1>  una línea más

Los servicios en AngularJS se pueden mejorar de dos maneras: Usando decorators o providers.

La ventaja de usar un decorator es que sólo necesitas declararlo dentro del módulo de tu aplicación o incluir el módulo dónde está declarado como dependencia, para que entonces se decore el servicio deseado.

El código presentado a continuación utiliza un decorator:

( function() {
  'use strict';
 
  // Asume que el módulo de tu aplicación es llamado "myApp"
  // En este módulo se define el decorador
  angular.module( 'myApp' ).decorator( '$log', logDecorator );
 
  // La función "logDecorator" recibirá el servicio a decorar como
  // el parámetro $delegate, en este caso $delegate == $log
  function logDecorator( $delegate ) {
 
    // Contador general
    var n = 0;
 
    // El nuevo logger que imprime información básica
    var basicLogger = {
      log: basicDecoration( $delegate.log ),
      debug: basicDecoration( $delegate.debug ),
      info: basicDecoration( $delegate.info ),
      warn:basicDecoration( $delegate.warn ),
      error: basicDecoration( $delegate.error ),
      getLogger: getLogger,
      enabledContexts: {},
      counter: {}
    };
 
    // Esta función regresa el servicio de log básico mejorado
    return basicLogger;
 
 
    // La decoración básica incluye un contador, fecha y hora al inicio
    function basicDecoration( loggingFunction ) {
      return function() {
        var date = new Date();
        var args = Array.prototype.slice.call( arguments );
        args.unshift( '[' + n++ + '] -- ' + date.toLocaleString() );
        loggingFunction.apply( null, args );
      }
    }
 
    // La decoración contextual incluye info del contexto y un contador
    function contextDecoration( loggingFunction, context ) {
      return function() {
        var enabled = basicLogger.enabledContexts[context];
        if( enabled || enabled === undefined ) {
          var args = Array.prototype.slice.call( arguments );
          var contextInfo = '-- [' + context + ':';
          var counterInfo = basicLogger.counter[context]++ + '] >> ';
          args.unshift( contextInfo + counterInfo );
          loggingFunction.apply( null, args );
        }
      }
    }
 
    // Activa o desactiva el logging en un contexto determinado
    function enable( context ) {
      return function( enable ) {
        basicLogger.enabledContexts[context] = enable;
      }
    }
 
    // Regresa un logger para un contexto determinado
    function getLogger( context ) {
 
      // Contador contextual
      basicLogger.counter[context] = 0;
 
      // La nueva instancia de logger con información del contexto
      var contextLogger = {
        log: contextDecoration( basicLogger.log, context ),
        debug: contextDecoration( basicLogger.debug, context ),
        info: contextDecoration( basicLogger.info, context ),
        warn: contextDecoration( basicLogger.warn, context ),
        error: contextDecoration( basicLogger.error, context ),
        enable: enable( context )
      };
 
      // Esta función regresa el servicio de log contextualizado
      return contextLogger;
    }
  }
} )();

Una recomendación es que siempre utilices el servicio de AngularJS $log en vez del console.log, de esta manera siempre podrás desactivar todo el logging desde el bloque de configuración de tu aplicación con algo como esto:

angular.module( 'myApp', [] ).config( config );
 
function config( $logProvider ) {
  $logProvider.debugEnabled( false );
}

Con este nuevo logger también puedes mandar a consola toda actividad en tus controladores, directivas, servicios, etc., y si la información que aparece es demasiada, puedes desactivar los contextos que gustes desde el bloque de ejecución de tu aplicación.

Digamos que tienes un controlador llamado MainController, y en él inicializas un logger con contexto “MainController”. Para desactivar toda la salida a consola generada por ese controlador, puedes hacer algo así:

angular.module( 'myApp', [] ).config( config ).run( run );
 
function config( $logProvider ) {
  $logProvider.debugEnabled( true );
}
 
function run( $log ) {
  var logger = $log.getLogger( 'MainController' );
  logger.enable( false );
}

La idea es que en cada componente de tu aplicación tengas un logger y el contexto sea el nombre del componente.

Referencias

Enhancing AngularJS Logging using Decorators
Enhancing $log in AngularJs the simple way

Error al actualizar Google Chrome en Ubuntu

Si al tratar de hacer un apt-get update te aparece un error de: entrada incorrecta en «sources.list» o fichero mal formado y menciona los sources para Google Chrome:

W: Fallo al obtener http://dl.google.com/linux/chrome/deb/dists/stable/Release  No se pudo encontrar la entrada esperada «main/binary-i386/Packages» en el archivo «Release» (entrada incorrecta en «sources.list» o fichero mal formado)
 
W: Fallo al obtener http://archive.ubuntu.com/ubuntu/dists/wily-security/universe/source/Sources  La suma hash difiere
 
W: Fallo al obtener http://archive.ubuntu.com/ubuntu/dists/wily-security/universe/binary-amd64/Packages  La suma hash difiere
 
W: Fallo al obtener http://archive.ubuntu.com/ubuntu/dists/wily-security/universe/binary-i386/Packages  La suma hash difiere
 
E: No se han podido descargar algunos archivos de índice, se han omitido, o se han utilizado unos antiguos en su lugar.

Debes modificar tu archivo /etc/apt/sources.list.d/google-chrome.list:

sudo gedit /etc/apt/sources.list.d/google-chrome.list

Y cambiar la línea:

deb http://dl.google.com/linux/chrome/deb/ stable main

Por esta:

deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main

Listo, dale:

sudo apt-get update

Referencia

How To Fix The (Annoying) ‘Failed to Fetch’ Chrome apt Error

Cómo arreglar el doble ícono de Google Chrome en elementary OS

Al abrir Google Chrome aparece dos veces el ícono en el dock.

Para corregirlo, abre una terminal y teclea esto:

sudo nano /usr/share/applications/google-chrome.desktop

Inserta StartupWMClass=Google-chrome-stable bajo cada una de las tres secciones siguientes:

[Desktop Entry]
StartupWMClass=Google-chrome-stable
...
 
[NewWindow Shortcut Group]
StartupWMClass=Google-chrome-stable
...
 
[NewIncognito Shortcut Group]
StartupWMClass=Google-chrome-stable
...

Configuración de una aplicación Rails 4.2 con MongoDB para una JSON API

En esta guía se explica cómo crear y configurar una aplicación Rails con MongoDB, de tal manera que funcione como una JSON API, es decir, puro back end.

Una aplicación de este tipo se da muy bien para después conectarla con un cliente SPA (Single Page Application), digamos en AngularJS o para emplearla con una aplicación móvil hecha con Ionic, por ejemplo.

Se asume familiaridad con Rails, git y los códigos de estado HTTP.

Preliminares

Commit inicial

Para generar el proyecto y hacer el primer commit:

rails new json_api --skip-active-record
cd json_api
git init
git add .
git ci -m "Commit inicial."

La opción de –skip-active-record desactiva ActiveRecord porque usaremos MongoDB con Mongoid en lugar de una base de datos relacional como PostgreSQL o MySQL.

Configuración inicial

Agrega estas gemas a tu Gemfile:

group :development, :test do
  ...
  # Framework para pruebas
  gem 'minitest-rails'
  # Para generar objetos de prueba
  gem 'fabrication'
  # Para generar datos de prueba
  gem 'faker'
end
 
# Para usar MongoDB a través de Mongoid
gem 'mongoid'
# Para usar respond_with en los controladores
gem 'responders'
# Para usar ActiveModel#has_secure_password
gem 'bcrypt'

Después en la terminal:

bundle
rails g mongoid:config
rails g minitest:install

Con esto se instalan las gemas que hagan falta, después se generan los archivos de configuración para Mongoid y Minitest. No es necesario correr el generador para los responders.

Al ejecutar el generador de Minitest, te va a preguntar si debe sobreescribir el archivo test_helper.rb, contesta sí.

En el archivo config/mongoid.yml, quita el comentario de la línea que aparece a continuación y cambia el valor a false:

# raise_not_found_error: true
raise_not_found_error: false

No queremos que Mongoid levante una excepción toda vez que no encuentre un documento.

Es deseable tener más control sobre cuándo se lanza una excepción, de esta forma, cuando se busca un usuario para autenticación y no existe, en vez de un código de error 404 (Not Found) podemos mandar un 401 (Unauthorized).

Agrega este contenido al archivo config/application.rb:

config.generators do |g|
  # Framework de pruebas estilo BDD
  g.test_framework :minitest, spec: true, fixture: true, fixture_replacement: :fabrication
 
  # Estamos haciendo una API, no queremos assets, helpers, tampoco views
  g.assets false
  g.helper false
  g.template_engine nil
end
 
# Para manejar la presentación de errores 404 y 500 como JSON
config.exceptions_app = self.routes

En el archivo test/test_helper.rb, comenta las siguientes líneas:

# ActiveRecord::Migration.check_pending!
# fixtures :all

No tenemos disponible ActiveRecord, si no se comentan esas líneas al tratar de ejecutar las pruebas se lanzará una excepción.

En el archivo config/initializers/inflections.rb agrega:

ActiveSupport::Inflector.inflections( :en ) do |inflect|
  inflect.acronym 'API'
end

Esto permite que al generar los controladores se utilice API como nombre del módulo contenedor en lugar de Api.

Configuración de rutas y del controlador principal de la aplicación

En el archivo config/routes.rb agrega:

match '/404' => 'application#not_found', via: [:get, :post, :put, :patch, :delete]
match '/500' => 'application#exception', via: [:get, :post, :put, :patch, :delete]

Esto permite personalizar las respuestas de error cuando no se encuentra alguna ruta o cuando se tiene algún error en la aplicación.

Por defecto, en estos casos lo que se devuelve es HTML (y el código de estado HTTP adecuado), pero estamos interesados en siempre entregar JSON al cliente.

En el archivo app/controllers/application_controller.rb cambia :exception por :null_session y agrega:

protect_from_forgery with: :null_session
 
# Estos métodos se llaman desde las rutas
def not_found
  response = http_error( { status: '404', message: 'Not Found' } )
  render json: response, status: :not_found
end
 
def exception
  response = http_error( { status: '500', message: 'Internal Server Error' } )
  render json: response, status: :internal_server_error
end
 
 
private
# Método de utilería
def http_error( error )
  { error: { http: error } }
end

La línea de protect_from_forgery_with indica el tipo de protección que se utilizará como medida de prevención contra un ataque de tipo CSRF.

Cuando es :exception se lanza una excepción ante cualquier petición POST, PUT, PATCH o DELETE si no viene acompañada de un token especial que Rails normalmente inserta en las vistas. Siendo esta una aplicación que va a ser utilizada como API no cuenta con ellas.

De la documentación: ActionController::RequestForgeryProtection::ClassMethods

Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.

Con :null_session se indica que no habrá estado del lado del servidor, por tanto, no existe una sesión que mantega el estado de conectado dentro del sistema. La autenticación se logra por medio de un token especial que se enviará desde el cliente en la cabecera de la petición. Más adelante veremos cómo.

Otros errores que sería bueno atrapar

Igual, en el archivo app/controllers/application_controller.rb agrega:

# Para cuando lancemos la excepción de documento no encontrado en MongoDB
rescue_from Mongoid::Errors::DocumentNotFound, with: :document_not_found
# Para cuando el tipo de contenido solicitado no sea application/json
rescue_from ActionController::UnknownFormat, with: :not_acceptable
# Para cuando hagan falta parámetros requeridos en algún modelo
rescue_from ActionController::ParameterMissing, with: :unprocessable_entity
 
def document_not_found
  response = http_error( { status: '404', message: 'Document Not Found' } )
  render json: response, status: :not_found
end
 
def not_acceptable
  response = http_error( { status: '406', message: 'Not Acceptable' } )
  render json: response, status: :not_acceptable
end
 
def unprocessable_entity
  response = http_error( { status: '422', message: 'Unprocessable Entity' } )
  render json: response, status: :unprocessable_entity
end
 
def unauthorized
  response = http_error( { status: '401', message: 'Unauthorized' } )
  render json: response, status: :unauthorized
end

Con la configuración inicial terminada, agrega los cambios al repo:

git add .
git ci -m "Configuración inicial."

Testing

TDD vs BDD

La diferencia entre los dos es el cómo te gusta pensar acerca del modelado de la aplicación.

El TDD utiliza el es (assert, afirma), el BDD usa el debe (must, debe). Básicamente es una cuestión de es vs debe ser.

Probemos primero con un poco de BDD

Genera tu primer controlador

rails g controller API::Users

Ejecuta y observa:

rake test

Edita test/controllers/api/users_controller_test.rb y cambia el test de ejemplo por:

it "#index debe ser exitoso" do
  get :index
  response.status.must_equal 200
end

Ejecuta y observa:

rake test

Edita config/routes.rb para agregar el recurso:

namespace :api do
  resources :users
end

Ejecuta y observa:

rake test

Edita app/controllers/api/users_controller.rb y agrega:

def index
  render json: {}
end

Ejecuta y observa:

rake test

😀

Esta fue una muestra de un proceso muy básico de BDD para un controlador.

Genera tu primer modelo

rails g model User name:string email:string password_digest:string

Ejecuta y observa:

rake test

Abre el archivo test/models/user_test.rb y verás:

require 'test_helper'
 
describe User do
  let( :user ) { User.new }
 
  it "must be valid" do
    user.must_be :valid?
  end
end

Si analizamos un poco la situación y contemplamos nuestro modelo User, veremos que el usuario forzosamente necesita al menos un correo. Así, un usuario recién creado debería ser inválido.

Cambia el único test que existe en ese archivo por este otro:

it 'no debe ser válido' do
  user.wont_be :valid?
end

Ejecuta y observa:

rake test

Abre el archivo app/models/user.rb y agrega:

validates :email, presence: true

Ejecuta y observa:

rake test

😀

Y esta fue una muestra de un proceso muy básico de BDD para un modelo.

A continuación veremos cómo se haría con TDD.

Probemos ahora con un poco de TDD

Para que las pruebas se generen al estilo TDD cambia spec a false:

g.test_framework :minitest, spec: false, fixture: false

Genera tu primer controlador

rails g controller API::Users

Ejecuta y observa:

rake test

Edita test/controllers/api/users_controller_test.rb y cambia el test de ejemplo por:

def test_index_action
  get :index
  assert_response :success
end

Nota: El nombre de los métodos debe comenzar necesariamente con test_ para que sean ejecutados durante el proceso de pruebas.

El método anterior es equivalente a este:

test 'index action' do
  get :index
  assert_response :success
end

Ejecuta y observa:

rake test

Edita config/routes.rb para agregar el recurso:

namespace :api do
  resources :users
end

Ejecuta y observa:

rake test

Edita app/controllers/api/users_controller.rb y agrega:

def index
  render json: {}
end

Ejecuta y observa:

rake test

😀

Esta fue una muestra de un proceso muy básico de TDD para un controlador.

Genera tu primer modelo

rails g model User name:string email:string password_digest:string

Ejecuta y observa:

rake test

Abre el archivo test/models/user_test.rb y verás:

require 'test_helper'
 
class UserTest < ActiveSupport::TestCase
 
  def user
    @user ||= User.new
  end
 
  def test_valid
    assert user.valid?
  end
 
end

Si analizamos un poco la situación y contemplamos nuestro modelo User, veremos que el usuario forzosamente necesita al menos un correo. Así, un usuario recién creado debería ser inválido.

Cambia el único test que existe en ese archivo por este otro:

def test_invalid
  assert_not user.valid?
end

Ejecuta y observa:

rake test

Abre el archivo app/models/user.rb y agrega:

validates :email, presence: true

Ejecuta y observa:

rake test

😀

Y esta fue una muestra de un proceso muy básico de TDD para un modelo.

En lo personal prefiero el BDD, siento que en ese estilo se expresa mejor la intención de lo que quieres programar.

Finalmente

Más adelante publicaré una guía acerca de cómo hacer pruebas de integración y cómo utilizar fabricators de manera efectiva en las pruebas de modelos y controladores.

Espero esta guía te sea útil.

Repositorio en github

lobo-tuerto/rails_json_api