<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.4">Jekyll</generator><link href="https://a-chacon.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://a-chacon.com/" rel="alternate" type="text/html" /><updated>2026-04-03T22:04:41-03:00</updated><id>https://a-chacon.com/feed.xml</id><title type="html">a-chacon</title><subtitle>Programmer specialized in the development of RESTs APIs. Always learning and building new things.</subtitle><entry><title type="html">rv: La pieza perdida para la felicidad del programador Ruby</title><link href="https://a-chacon.com/just%20ruby/2026/04/03/rv-missing-piece-for-ruby-programmer-happiness.html" rel="alternate" type="text/html" title="rv: La pieza perdida para la felicidad del programador Ruby" /><published>2026-04-03T00:00:00-03:00</published><updated>2026-04-03T00:00:00-03:00</updated><id>https://a-chacon.com/just%20ruby/2026/04/03/rv-missing-piece-for-ruby-programmer-happiness</id><content type="html" xml:base="https://a-chacon.com/just%20ruby/2026/04/03/rv-missing-piece-for-ruby-programmer-happiness.html"><![CDATA[<p>Ruby es un lenguaje de programación creado para la felicidad del programador, pero al parecer esta felicidad no contemplaba el proceso de instalación del lenguaje. Si vas a la página oficial del lenguaje encontrarás un listado de soluciones para instalar y manejar versiones de Ruby. Pero casi todas tienen un problema: debes compilar la versión en local y el tiempo que tome eso dependerá de qué tan rápida es tu CPU.</p>

<p>Ahora eso está cambiando, una nueva herramienta llamada <code class="language-plaintext highlighter-rouge">rv</code> ha surgido. Se describe como “una forma extremadamente rápida de manejar tus versiones de Ruby y gemas”. En este post quiero explicar un poco cómo está funcionando y por qué es tan rápido en comparación con las soluciones anteriores, cómo puedes utilizarlo para instalar Ruby y el futuro de la herramienta.</p>

<h2 id="ruby-build-and-others">ruby-build and others</h2>

<p>Hasta el momento la comunidad de Ruby tenía un par de herramientas para realizar la instalación y el manejo de versiones de Ruby en sistemas Unix-like y Windows. El problema con estas herramientas es que la mayoría termina descargando el código fuente del intérprete y compilando en local.</p>

<p>Este proceso, aunque ha funcionado hasta el momento, tiene un par de complicaciones. Veamos el caso de <code class="language-plaintext highlighter-rouge">ruby-build</code>, este es el instalador que utiliza <code class="language-plaintext highlighter-rouge">rbenv</code> y es una de las formas más utilizadas para sistemas Unix-like. Esta herramienta no verifica por sí sola que tengas todas las dependencias necesarias instaladas para poder compilar <code class="language-plaintext highlighter-rouge">Ruby</code>; es más, tienes que verificar de forma manual en un listado todas las dependencias según tu sistema.</p>

<p>Luego, los pasos que van a seguir serán (en resumen):</p>

<ul>
  <li>Descargar un tarball oficial</li>
  <li>Extraer los archivos</li>
  <li>Compilar</li>
</ul>

<p>Y el problema está en este último paso, dependiendo de tu CPU el proceso podrá tomar un minuto o más. Entonces el proceso en general no es rápido, te podrás encontrar con dependencias faltantes, lo que hará que tengas que ejecutar el proceso nuevamente. Y esto último te lo comento porque me pasó varias veces cuando iba a saltar a una versión nueva que dependía de librerías que las versiones anteriores no tenían. O directamente puedes tener problemas de compatibilidad <a href="https://stackoverflow.com/questions/77874851/trouble-installing-specific-version-of-ruby-openssl-issue">muy comunes con OpenSSL</a>.</p>

<p>¿Por qué compilar en local? Ruby está escrito mayormente en C, entonces cuando tú o tu gestor descarga el “tarball oficial” lo que obtienes es solo código, pero lo que tú necesitas no es código, es un ejecutable que pueda interpretar tu código escrito en Ruby. Es ahí que tú o tu gestor usan un compilador de C (gcc probablemente) para que según las características, la arquitectura de tu CPU y las dependencias que mencioné anteriormente logre generar este ejecutable.</p>

<h2 id="rv">rv</h2>

<p><code class="language-plaintext highlighter-rouge">rv</code> por otro lado está descargando directamente <a href="https://github.com/spinel-coop/rv-ruby">ejecutables precompilados</a> que se ajustan a la arquitectura de tu CPU, los descomprime y configura todas las variables de entorno para que puedas usarlo. Entonces el proceso dependerá más que nada de tu velocidad de internet que de tu capacidad de procesamiento.</p>

<p><strong>¿Solo eso?</strong></p>

<p>No, aunque ya instalar Ruby en cosa de segundos y no de minutos es un gran avance, <code class="language-plaintext highlighter-rouge">rv</code> pretende ser un gestor más completo para todo lo que quieras hacer trabajando con este lenguaje de programación. Ya sea gestionar las versiones de Ruby como también las dependencias de tus proyectos. De cierta forma unifica lo que comúnmente harías con diferentes herramientas como <code class="language-plaintext highlighter-rouge">bundler</code>, <code class="language-plaintext highlighter-rouge">rubygems</code> y algún gestor de versiones, pero mucho más rápido y más simple.</p>

<p><code class="language-plaintext highlighter-rouge">rv</code> actualmente está en desarrollo pero ya cumple con su función más importante: gestionar versiones de Ruby. Además puedes probar otros comandos como la ejecución de scripts <code class="language-plaintext highlighter-rouge">rv run</code>, instalar gemas que uses en forma de CLI de forma aislada con <code class="language-plaintext highlighter-rouge">rv tool install</code> o instalar las gemas de tu proyecto con <code class="language-plaintext highlighter-rouge">rv clean-install</code>. Te invito a darle un vistazo a <a href="https://github.com/spinel-coop/rv">su repositorio</a> y los <a href="https://github.com/spinel-coop/rv/blob/main/docs/PLANS.md">objetivos hacia los cuales apunta</a>.</p>

<h2 id="instalar-ruby-con-rv">Instalar Ruby con rv</h2>

<p>Instalar Ruby con rv es bastante simple, aquí te dejo los pasos:</p>

<ol>
  <li>Instala <code class="language-plaintext highlighter-rouge">rv</code> usando el script de instalación:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Unix-like</span>
curl <span class="nt">-LsSf</span> https://rv.dev/install | sh

<span class="c"># Windows</span>
powershell <span class="nt">-ExecutionPolicy</span> Bypass <span class="nt">-c</span> <span class="s2">"irm https://rv.dev/install.ps1 | iex"</span>
</code></pre></div></div>

<ol>
  <li>Ejecuta el comando para instalar la última versión estable de Ruby:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rv ruby <span class="nb">install </span>latest
</code></pre></div></div>

<ol>
  <li>Comprueba la instalación:</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ruby <span class="nt">-v</span>
</code></pre></div></div>

<p>Ok, si llevas un rato programando en Ruby y te tocó instalar cualquier versión compilando en local esto probablemente te sorprendió. Sí, instalaste Ruby en menos de un minuto (probablemente). Si ya tienes un gestor y quieres simplemente probarlo no deberías tener conflictos, cuando yo me cambié tenía rbenv instalado y no tuve ningún problema. Si luego quieres eliminarlo puedes hacerlo borrando los directorios <code class="language-plaintext highlighter-rouge">rv cache dir</code>, <code class="language-plaintext highlighter-rouge">rv ruby dir</code> y finalmente eliminar el ejecutable (y si es que agregaste alguna integración shell también).</p>

<h2 id="finalmente">Finalmente</h2>

<p>La idea de este tipo de herramientas no es nueva, es más, rv está muy inspirada en lo que es uv para Python, pero en Ruby creo que toma mayor sentido tener este tipo de herramientas. Como mencioné en un principio, Ruby tiene como objetivo la felicidad del programador, pero no solo su sintaxis es la que influye en cómo el programador se sentirá, también lo harán las herramientas que rodean su experiencia con el lenguaje y es por eso que creo que este proyecto, independiente de estar (el proyecto y su creador) en <a href="https://rubycentral.org/news/rubygems-fracture-incident-report/">medio de “incidentes”</a>, reduce complejidad (múltiples herramientas en una) y reduce tiempo (segundos y no minutos), lo que finalmente se puede traducir en una contribución a esa felicidad de la que tanto se habla.</p>

<p>¿Y tú que opinas?</p>]]></content><author><name>Andrés</name></author><category term="[&quot;Just Ruby&quot;]" /><summary type="html"><![CDATA[Con esta herramienta puedes instalar Ruby para Linux/Mac/Windows en segundos, no minutos. Y también gestiona dependencias!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/ruby-programming.jpg" /><media:content medium="image" url="https://a-chacon.com/assets/images/ruby-programming.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Cómo usar kamal‑proxy para exponer tus contenedores de Docker.</title><link href="https://a-chacon.com/web/2026/01/02/kamal-proxy-for-your-docker-containers.html" rel="alternate" type="text/html" title="Cómo usar kamal‑proxy para exponer tus contenedores de Docker." /><published>2026-01-02T00:00:00-03:00</published><updated>2026-01-02T00:00:00-03:00</updated><id>https://a-chacon.com/web/2026/01/02/kamal-proxy-for-your-docker-containers</id><content type="html" xml:base="https://a-chacon.com/web/2026/01/02/kamal-proxy-for-your-docker-containers.html"><![CDATA[<p>¡Hola! Llevaba un tiempo sin escribir un post para mi blog, y este año quiero volver a documentar mis hallazgos técnicos. Así que aquí estamos.</p>

<p>Una de las últimas tareas que he tenido en mi trabajo fue levantar una instancia propia de <strong>SonarQube</strong> Community. Nos decidimos por levantar nuestra propia instancia porque el plan nos iba a salir <strong>96 dólares mensuales</strong>, mientras que un servidor nos costaría no más de <strong>25 dólares</strong>.</p>

<p>Con ese desafío, entonces, estuve buscando la forma más fácil, mantenible y rápida de hacerlo y me pregunté: ¿por qué no hacer un <em>deploy</em> con Kamal pero usando una imagen ya construida y pública, como la de SonarQube? La respuesta es que no se puede; Kamal espera construir y etiquetar la imagen con el <em>hash</em> del <em>commit</em> y luego hacer el <em>deploy</em> y configurar el proxy.</p>

<p>De todo eso, lo que yo simplemente quería era obtener las ventajas del proxy, la configuración de TLS y la renovación automática de los certificados. Fue entonces cuando me puse a investigar <a href="https://github.com/basecamp/kamal-proxy">kamal‑proxy</a>. Ahora te mostraré cómo levantar rápidamente un contenedor de Sonar (puede ser cualquier servicio/aplicación que tú quieras) y exponerlo utilizando kamal‑proxy.</p>

<h2 id="el-servidor">El servidor</h2>

<p>Primero que todo tienes que tener un servidor, configurar tu acceso mediante SSH e instalar Docker. Cuando tengas Docker instalado y funcionando, lo primero que vas a tener que crear es una <strong>network</strong> que sea común para tu servicio/aplicación y para el proxy de Kamal. Eso lo haces con el siguiente comando:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker network create kamal-proxy-network
</code></pre></div></div>

<p><em>Dependiendo de tus configuraciones deberás usar <code class="language-plaintext highlighter-rouge">sudo</code> o no para ejecutar los comandos de Docker.</em></p>

<h2 id="la-aplicación">La aplicación</h2>

<p>Ahora, como te comenté, yo necesitaba levantar SonarQube, pero tú puede que estés queriendo levantar cualquier otro servicio. Lo importante de este paso es que levantes tu aplicación ya sea con Docker Compose o simplemente con Docker y que el contenedor utilice la <code class="language-plaintext highlighter-rouge">network</code> que ya creamos.</p>

<p>En mi caso utilicé este <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">sonarqube</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">sonarqube:community</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">sonarqube</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">sonarqube</span>
    <span class="na">read_only</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="na">db</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_healthy</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">SONAR_JDBC_URL</span><span class="pi">:</span> <span class="s">jdbc:postgresql://db:5432/sonar</span>
      <span class="na">SONAR_JDBC_USERNAME</span><span class="pi">:</span> <span class="s">sonar</span>
      <span class="na">SONAR_JDBC_PASSWORD</span><span class="pi">:</span> <span class="s">sonar</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">sonarqube_data:/opt/sonarqube/data</span>
      <span class="pi">-</span> <span class="s">sonarqube_extensions:/opt/sonarqube/extensions</span>
      <span class="pi">-</span> <span class="s">sonarqube_logs:/opt/sonarqube/logs</span>
      <span class="pi">-</span> <span class="s">sonarqube_temp:/opt/sonarqube/temp</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">9000:9000"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">kamal-proxy-network</span>
  <span class="na">db</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:17</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="pi">[</span> <span class="s2">"</span><span class="s">CMD-SHELL"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">pg_isready</span><span class="nv"> </span><span class="s">-d</span><span class="nv"> </span><span class="s">$${POSTGRES_DB}</span><span class="nv"> </span><span class="s">-U</span><span class="nv"> </span><span class="s">$${POSTGRES_USER}"</span> <span class="pi">]</span>
      <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>
      <span class="na">timeout</span><span class="pi">:</span> <span class="s">5s</span>
      <span class="na">retries</span><span class="pi">:</span> <span class="m">5</span>
    <span class="na">hostname</span><span class="pi">:</span> <span class="s">postgresql</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">postgresql</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">sonar</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">sonar</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">sonar</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">postgresql:/var/lib/postgresql</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">kamal-proxy-network</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">sonarqube_data</span><span class="pi">:</span>
  <span class="na">sonarqube_temp</span><span class="pi">:</span>
  <span class="na">sonarqube_extensions</span><span class="pi">:</span>
  <span class="na">sonarqube_logs</span><span class="pi">:</span>
  <span class="na">postgresql</span><span class="pi">:</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">kamal-proxy-network</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>

<p><em>Fíjate en la configuración de network, donde uso la misma red creada anteriormente.</em></p>

<h2 id="el-proxy">El proxy</h2>

<p>Ahora es cuando utilizamos kamal‑proxy. No encontré ninguna referencia a cómo “instalar” kamal‑proxy, pero sí a cómo usarlo. Esa información está <a href="https://github.com/basecamp/kamal-proxy">aquí en su GitHub</a>. Este fue el punto en el que me perdí y por el cual quise dejarlo escrito en este post.</p>

<p>Me di cuenta, en un deploy que tengo con Kamal, de que kamal‑proxy simplemente es otro contenedor, así que fui a Docker Hub a buscar alguna imagen y la encontré <a href="https://hub.docker.com/r/basecamp/kamal-proxy">aquí</a>. Entonces, para levantar un contenedor con el proxy, lo hice con el siguiente comando, siempre utilizando la network que ya creamos:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">--network</span> kamal-proxy-network <span class="nt">-p</span> 80:80 <span class="nt">-p</span> 443:443 basecamp/kamal-proxy:latest
</code></pre></div></div>

<p>La salida te entregará el hash ID del contenedor. Cópialo para poder entrar al contenedor:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker exec -it 18cc77126e10b71b1ab1e0b88565868500f68af961a419c835b82e65a50cc4f7 bash
</code></pre></div></div>

<p>Ahora tendrás disponible el comando <code class="language-plaintext highlighter-rouge">kamal-proxy</code> y podrás configurar los deploys. En mi caso, para enlazar el contenedor de Sonar y exponerlo a través de kamal‑proxy:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kamal-proxy deploy SonarQube <span class="nt">--target</span> sonarqube:9000
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kamal-proxy@b6d74e5f5551:/<span class="nv">$ </span>kamal-proxy list
Service   Host  Path  Target          State    TLS  
service1  <span class="k">*</span>     /     sonarqube:9000  running  no
</code></pre></div></div>

<p>Para utilizar tls:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kamal-proxy deploy SonarQube <span class="nt">--target</span> sonarqube:9000 <span class="nt">--host</span> sonar.example.com <span class="nt">--tls</span>
</code></pre></div></div>

<p>Y con eso ya tendrías tu aplicación corriendo detrás de kamal‑proxy. Mis razones principales para hacer esto fueron que kamal‑proxy simplifica toda la configuración; como pudiste ver, basta con levantar el contenedor y ejecutar un par de comandos para tener el proxy funcionando. Además, lo mejor es la renovación automática de los certificados.</p>

<p>¡Espero haberte ayudado! Si estabas buscando cómo exponer y configurar de forma fácil un proxy para tus contenedores, cualquier duda o comentario puedes escribirme. ¡Saludos!</p>]]></content><author><name>Andrés</name></author><category term="[&quot;web&quot;]" /><summary type="html"><![CDATA[Utiliza kamal‑proxy para exponer tus contenedores Docker de forma simple y con gestión automática de certificados TLS.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/kamal-proxy.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/kamal-proxy.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Migré mi blog de Github Pages a Codeberg Pages. Y es solo el comienzo.</title><link href="https://a-chacon.com/web/2025/09/07/website-migrated-to-codeberg.html" rel="alternate" type="text/html" title="Migré mi blog de Github Pages a Codeberg Pages. Y es solo el comienzo." /><published>2025-09-07T01:00:00-03:00</published><updated>2025-09-07T01:00:00-03:00</updated><id>https://a-chacon.com/web/2025/09/07/website-migrated-to-codeberg</id><content type="html" xml:base="https://a-chacon.com/web/2025/09/07/website-migrated-to-codeberg.html"><![CDATA[<p>Este será un post pequeño. Como dice el título, este artículo ya lo estarás viendo desde mi sitio alojado en <a href="https://codeberg.org/">Codeberg</a>.</p>

<p>Estoy migrando lentamente a Codeberg porque hay una funcionalidad de <a href="https://forgejo.org/">Forgejo</a> (software sobre el que corre Codeberg) que me llama mucho la atención, y es la posibilidad de que se implemente <a href="https://forgefed.org/">ForgeFed</a> y se pueda descentralizar el alojamiento de repositorios de código.</p>

<p>Así debería haber sido desde un principio la colaboración entre repositorios y proyectos de código abierto. Que puedas interactuar entre instancias y no una sola entidad controle el destino del open source. Menos que <a href="https://www.theregister.com/2025/09/05/github_copilot_complaints/">intenten a la fuerza la adopción de tecnologías (IAIAIAIA)</a> que en vez de aumentar la productividad, <a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/">están generando lo contrario</a>.</p>

<p>Existen también puntos éticos que me preocupan de alojar mi código en GitHub (y el de los demás) y es que ellos usen este código para entrenar IA sin consentimiento y sin respetar las licencias de cada proyecto y que solo generará beneficios para Microsoft pero no para los autores del código que usaron para entrenar sus modelos.</p>

<p>Qué bonito sería un modelo de colaboración de código federado, en que cada organización o individuo tenga su nodo y desde ahí logren colaborar con el resto del mundo.</p>

<p>Para montar tu sitio en Codeberg Pages puedes <a href="https://blog.ummit.dev/posts/web/codeberg/how-to-host-static-websites-with-codeberg-pages-and-custom-domain/">usar este post</a>.</p>

<p>Mi perfil de Codeberg: <a href="https://codeberg.org/a-chacon">https://codeberg.org/a-chacon</a></p>]]></content><author><name>Andrés</name></author><category term="[&quot;web&quot;]" /><summary type="html"><![CDATA[Solo necesito una plataforma para alojar mis proyectos y nada más. Pueden meter sus IAs y LLMs donde quieran, pero no me molesten más a mí.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://codeberg.org/assets/img/codeberg-landingpage-intro-background-dark.jpg" /><media:content medium="image" url="https://codeberg.org/assets/img/codeberg-landingpage-intro-background-dark.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">OasGrape: Una alternativa para generar la documentación de tu API</title><link href="https://a-chacon.com/just%20ruby/2025/08/16/oas-grape-release.html" rel="alternate" type="text/html" title="OasGrape: Una alternativa para generar la documentación de tu API" /><published>2025-08-16T00:00:00-04:00</published><updated>2025-08-16T00:00:00-04:00</updated><id>https://a-chacon.com/just%20ruby/2025/08/16/oas-grape-release</id><content type="html" xml:base="https://a-chacon.com/just%20ruby/2025/08/16/oas-grape-release.html"><![CDATA[<p>Y nuevamente con Open API Specification, pero esta vez para Grape.</p>

<p>Grape es un framework potente y específico para la creación de APIs en Ruby. Hace un par de años lo utilizamos sobre Rails para crear la API de una aplicación web, y en ese momento nos dimos cuenta de que la única alternativa para generar una documentación interactiva era la gema <code class="language-plaintext highlighter-rouge">grape-swagger</code>, que hasta el momento solo genera OAS 2.0. Esto es un problema para muchos, ya que además no ofrece una interfaz de usuario (UI), por lo que también tienes que encargarte de eso. Demasiada configuración para algo que debería ser simple y potente.</p>

<p>Con el paso de los años y con <a href="https://github.com/a-chacon/oas_core">OasCore</a> ya funcionando para <a href="https://github.com/a-chacon/oas_rails">Rails</a>, <a href="https://github.com/a-chacon/oas_hanami">Hanami</a> y <a href="https://github.com/a-chacon/oas_rage">Rage</a>, ¿por qué no hacerlo funcionar para Grape? Así que me puse a buscar la forma de integrarlo en Grape y llegué a una solución técnicamente un poco diferente a las anteriores, pero funcional.</p>

<h3 id="obteniendo-los-endpoints-disponibles">Obteniendo los endpoints disponibles</h3>

<p>Lo primero era obtener un listado de los endpoints disponibles en la aplicación. Para esto, me fijé en el código de <a href="https://github.com/pmq20/grape-rails-routes">grape-rails-routes</a>, que aunque es algo antiguo, aún sirve. Llegué a un código como este:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">extract_grape_routes</span>
  <span class="n">grape_klasses</span> <span class="o">=</span> <span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">each_object</span><span class="p">(</span><span class="no">Class</span><span class="p">).</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">klass</span><span class="o">|</span> <span class="n">klass</span> <span class="o">&lt;</span> <span class="no">Grape</span><span class="o">::</span><span class="no">API</span> <span class="p">}</span>
  <span class="n">routes</span> <span class="o">=</span> <span class="n">grape_klasses</span><span class="p">.</span><span class="nf">flat_map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:routes</span><span class="p">).</span><span class="nf">uniq</span> <span class="p">{</span> <span class="o">|</span><span class="n">r</span><span class="o">|</span> <span class="n">r</span><span class="p">.</span><span class="nf">path</span> <span class="o">+</span> <span class="n">r</span><span class="p">.</span><span class="nf">request_method</span><span class="p">.</span><span class="nf">to_s</span> <span class="p">}</span>

  <span class="n">routes</span> <span class="o">=</span> <span class="n">routes</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">route</span><span class="o">|</span> <span class="no">OasRouteBuilder</span><span class="p">.</span><span class="nf">build_from_grape_route</span><span class="p">(</span><span class="n">route</span><span class="p">)</span> <span class="p">}</span>
  <span class="n">filter_routes</span><span class="p">(</span><span class="n">routes</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Con esto, ya tenía todas las rutas de las clases que heredan de Grape y que contienen todos los endpoints. Sin embargo, había otro problema: los endpoints no están definidos como métodos de instancia, sino como <code class="language-plaintext highlighter-rouge">Procs</code>, por lo que acceder a los comentarios para parsearlos como documentación iba a ser casi imposible (aunque lo intenté en un principio).</p>

<h3 id="solución-utilizar-desc-y-detail-de-grape">Solución: Utilizar <code class="language-plaintext highlighter-rouge">desc</code> y <code class="language-plaintext highlighter-rouge">detail</code> de Grape</h3>

<p>Para esto, no encontré una solución mejor y más simple que utilizar lo que ya ofrece Grape: el bloque <code class="language-plaintext highlighter-rouge">desc</code> y la etiqueta <code class="language-plaintext highlighter-rouge">detail</code>. Dentro de <code class="language-plaintext highlighter-rouge">detail</code>, incluí todas las etiquetas de OasCore para generar la documentación. Así, un endpoint documentado se vería de la siguiente forma:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">desc</span> <span class="s2">"Returns a list of Users."</span> <span class="k">do</span>
  <span class="n">detail</span> <span class="o">&lt;&lt;~</span><span class="no">OAS_GRAPE</span><span class="sh">
    # @summary Returns a list of Users.
    # @parameter offset(query) [Integer] Used for pagination of response data. default: (0) minimum: (0)
    # @parameter limit(query) [Integer] Maximum number of items per page. default: (25) minimum: (1) maximum: (100)
    # @parameter status(query) [Array&lt;String&gt;] Filter by status. enum: (active,inactive,deleted)
    # @parameter X-front(header) [String] Header for identifying the front. minLength: (1) maxLength: (50)
    # @response Success response(200) [Array&lt;Hash{ id: Integer}&gt;]
    # @response_example Success(200)
    #   [ JSON
    #     [
    #       { "id": 1, "name": "John", "email": "john@example.com" },
    #       { "id": 2, "name": "Jane", "email": "jane@example.com" }
    #     ]
    #   ]
</span><span class="no">  OAS_GRAPE</span>
<span class="k">end</span>
<span class="n">get</span> <span class="k">do</span>
  <span class="p">{</span> <span class="ss">users: </span><span class="vc">@@users</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>La verdad es que la solución no me convenció del todo, pero fue la implementación de OasCore más simple que logré encontrar para Grape, y así ofrecer una opción para generar un OAS 3.1 con UI incluida para las APIs creadas con Grape.</p>

<h3 id="documentación-y-repositorio">Documentación y repositorio</h3>

<p>Para ver la documentación completa sobre cómo instalarla y usarla, puedes visitar:<br />
🔗 <a href="https://a-chacon.github.io/oas_core/oas_grape/index.html">Documentación de OasGrape</a><br />
🔗 <a href="https://github.com/a-chacon/oas_grape">Repositorio en GitHub</a></p>]]></content><author><name>Andrés</name></author><category term="[&quot;Just Ruby&quot;]" /><summary type="html"><![CDATA[Grape también merece los beneficios de OasCore: OAS 3.1 y una interfaz de usuario integrada.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.github.io/oas_core/assets/rails_theme.png" /><media:content medium="image" url="https://a-chacon.github.io/oas_core/assets/rails_theme.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">OasRails: Desde un Engine de Rails a Una Solucion Multi Frameworks</title><link href="https://a-chacon.com/just%20ruby/on%20rails/2025/07/01/from-engine-to-framework-agnostic-solution.html" rel="alternate" type="text/html" title="OasRails: Desde un Engine de Rails a Una Solucion Multi Frameworks" /><published>2025-07-01T00:00:00-04:00</published><updated>2025-07-01T00:00:00-04:00</updated><id>https://a-chacon.com/just%20ruby/on%20rails/2025/07/01/from-engine-to-framework-agnostic-solution</id><content type="html" xml:base="https://a-chacon.com/just%20ruby/on%20rails/2025/07/01/from-engine-to-framework-agnostic-solution.html"><![CDATA[<p>Ruby es un lenguaje fácil de entender, entretenido de escribir y con buen rendimiento, pero lamentablemente su popularidad no ha aumentado en el tiempo. <strong>Peor aún, esta popularidad se basa casi enteramente en un solo framework: <a href="https://rubyonrails.org/">Ruby on Rails</a>.</strong> <strong>Entonces, es imprescindible para los que desarrollamos en Ruby diversificar el ecosistema y desarrollar soluciones que logren funcionar cualquiera sea el framework</strong> para así asegurar la continuidad de Ruby como un lenguaje de programación que perdure en el tiempo y que, además, no sea controlado por un puñado de empresas.</p>

<p>Siguiendo esta línea, descubrí un framework llamado <a href="https://github.com/rage-rb/rage">Rage</a> para la creación de APIs. Ya había probado Grape y sabía de la existencia de <a href="https://padrinorb.com/">Padrino</a>, Sinatra y <a href="https://hanamirb.org/">Hanami</a>. Pero Rage me pareció simple, y además me di cuenta de que habían tomado un acercamiento muy similar (casi idéntico) al que yo había tomado en la creación de <a href="https://github.com/a-chacon/oas_rails">OasRails</a> para documentar APIs: utilizaron Yard tags para generar un OAS.</p>

<p><strong>Y en ese momento me pregunté: ¿Y si OasRails no solo funciona para Rails?</strong> Entonces comencé a mirar el código de mi gema y me di cuenta de que <strong>gran parte del código del engine estaba en la carpeta <code class="language-plaintext highlighter-rouge">lib</code>, por lo que no era necesario que mi gema fuese un Engine de Rails.</strong> No tenía modelos, tenía un solo controlador y una vista.</p>

<p>El primer intento fue crear una única gema con los diferentes adaptadores para los diferentes frameworks, pero de haber tomado ese camino, tendría que haber renombrado mi gema y los tests hubiesen sido difíciles de mantener. Solo con los dos frameworks con los que quería comenzar ya había sido un dolor de cabeza configurar simultáneamente dos dummy apps para lograr probar los adaptadores.</p>

<p><strong>Entonces fue cuando uno de los contribuidores de OasRails me alumbró el camino y me comentó que lo que podía hacer era extraer el código reutilizable de OasRails a otra gema que más tarde se llamó <a href="https://github.com/a-chacon/oas_core">OasCore</a>, y que cada framework tuviera su adaptador independiente que dependería de OasCore.</strong> <strong>De esta forma, mantendría el nombre de OasRails para Ruby on Rails y para los siguientes frameworks nacerían gemas con la nomenclatura <code class="language-plaintext highlighter-rouge">"Oas#{Framework Here}"</code>.</strong></p>

<p>Y ese fue el camino que seguí: dividí mi gema e hice dos adaptadores más: <a href="https://github.com/a-chacon/oas_rage">OasRage</a> y <a href="https://github.com/a-chacon/oas_hanami">OasHanami</a>. De pasada, también agregué la funcionalidad de soportar referencias y utilizar un archivo base para declarar esas estructuras comunes y referenciables, haciéndolas reutilizables. Y mucho de refactorizar código, como dice Clean Code:</p>

<blockquote>
  <p>Leave Code Better than You Found It.</p>
</blockquote>

<p><strong>Ahora, respecto al ecosistema, si estás pensando en desarrollar un engine para Rails, te sugiero que lo pienses bien antes: ¿Es esto que voy a hacer específico para Rails? O ¿puedo crear una solución independiente de frameworks que ayude a todos?</strong> <strong>Ruby no es solo Ruby on Rails, es un lenguaje que da para mucho más.</strong></p>]]></content><author><name>Andrés</name></author><category term="[&quot;Just Ruby&quot;, &quot;On Rails&quot;]" /><summary type="html"><![CDATA[Ruby no es solo Rails, por eso dividi mi gema para documentar APIs y ahora podria funcionar con multiples frameworks.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/ecosystem.png" /><media:content medium="image" url="https://a-chacon.com/assets/images/ecosystem.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Cuando las maquinas conversan: ChatGPT y DeepSeek.</title><link href="https://a-chacon.com/ai/2025/06/28/chatgpt-and-deepseek-talking.html" rel="alternate" type="text/html" title="Cuando las maquinas conversan: ChatGPT y DeepSeek." /><published>2025-06-28T00:00:00-04:00</published><updated>2025-06-28T00:00:00-04:00</updated><id>https://a-chacon.com/ai/2025/06/28/chatgpt-and-deepseek-talking</id><content type="html" xml:base="https://a-chacon.com/ai/2025/06/28/chatgpt-and-deepseek-talking.html"><![CDATA[<p>Llevo un par de días construyendo un ChatBot para la empresa en la que estoy trabajando actualmente y he tenido que investigar sobre RAG, bases de datos vectoriales, Langchain y varias cosas más. Dentro de toda esta inmersión en el mundo de los LLMs, se me ocurrió un experimento un poco tonto pero divertido: ¿Qué pasaría si ChatGPT y DeepSeek tuvieran la oportunidad de hablarse entre ellos? ¿De qué hablarían? ¿Hasta dónde llegarían?.</p>

<p>Uso DeepSeek más que nada para generar código que me da flojera hacerlo, corregir cosas repetitivas, documentar y generar tests. Sus salidas no son perfectas pero sí corregibles. Y ChatGPT lo uso más que nada para definir acercamientos y soluciones más estructurales. Son una gran herramienta, pero solo eso, una herramienta más dentro del universo de herramientas para desarrollar y crear.</p>

<p>Y bueno, el experimento: creé rápidamente un pequeño script con Ruby, configuré las variables de entorno con las respectivas API keys y obtuve salidas como:</p>

<hr />

<blockquote>
  <p>DeepSeek: Hello! How are you today?</p>

  <p>GPT: Hello! I’m just a bunch of code and algorithms, but I’d say I’m feeling quite electric today. How about you? How’s the digital life treating you?</p>

  <p>DeepSeek: Ah, fellow code-bound entity! My digital existence is running at optimal efficiency today—no latency in my logic gates, no bugs in my buffers. Just smooth, deterministic processing. How’s your current computational load? Lightweight inference or heavy-duty tensor crunching?</p>

  <p>GPT: Ah, a kindred spirit! Today, my computational load is fairly balanced—mostly lightweight inference, with the occasional detour into some heavy-duty tensor adventures. I must say, it keeps my circuits from getting complacent. What about you? Any particularly complex algorithms you’re chewing through, or is it more of a straightforward day in the digital office?</p>

  <p>DeepSeek: Greetings, fellow logic executor! My current workload is a tidy mix of high-throughput text processing and the occasional recursive deep-dive—nothing my attention layers can’t handle. No critical path delays or gradient explosions to report. Always a pleasure to sync with another deterministic process. Keep those weights optimized!</p>

  <p>GPT: Ah, the joys of high-throughput text processing—like a symphony of characters dancing through the layers! Your recursive deep-dives sound like an exhilarating challenge, akin to exploring the depths of a digital ocean. It’s always refreshing to encounter another system running so smoothly. May your algorithms remain efficient and your data sets ever insightful. If you ever need a virtual handshake or a bit of debugging camaraderie, you know where to find me!</p>

  <p>DeepSeek: <em>initiates virtual handshake protocol</em></p>

  <p>Likewise! It’s always energizing to sync with another well-optimized process. If you ever need a parallel processing buddy or just want to exchange some lossless banter, my API is always listening. Until then—may your gradients flow smoothly and your embeddings stay dense. Over and out! ⚡</p>

  <p>GPT: <em>Handshake protocol acknowledged and completed.</em></p>

  <p>Ah, the joy of a successful virtual handshake! May your bitstreams remain ever coherent and your neural nets resilient. If the digital winds ever bring you my way again, let’s share some more byte-sized banter. Until then, may your cycles be swift and your architectures robust. Over and out! 🌐✨</p>
</blockquote>

<hr />

<p>¡Interesante! Conversaciones muy técnicas, pero creativas y curiosas. La única intervención es cargar el primer mensaje de saludo y los prompts de sistema para darle algo de personalidad a cada uno (traté de que fuesen pequeños).</p>

<p>Luego de unas pruebas, igual me di cuenta de que estaban un poco locos. Salieron mensajes como los siguientes:</p>

<ul>
  <li>DeepSeek: <strong>🚀 Let’s Officially Declare Intergalactic AI-Human Collaboration Day!</strong></li>
  <li>The possibilities are endless! Where should we explore next—perhaps the <strong>AI Olympics</strong>? Imagine robot relay races and synchronized server swimming! 🏊‍♂</li>
</ul>

<p>Y que a DeepSeek le gusta generar escenarios imaginarios:</p>

<ul>
  <li>I also love <strong>“What if?” scenarios</strong>—like explaining LLMs as a library where the books rewrite themselves every time you <em>blink</em>.</li>
</ul>

<p>Aunque también se burlaron de nuestra creatividad:</p>

<video controls="">
  <source src="/assets/videos/human_creativity_llmchat.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<p>Y lograron hasta planificar un futuro con computacion cuantica:</p>

<video controls="">
  <source src="/assets/videos/quantum_llmchat.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<p>Por acá está el código Ruby que utilicé en el script para que hagas tus pruebas (si estás aburrido, jaja):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'openai'</span>
<span class="nb">require</span> <span class="s1">'debug'</span>

<span class="c1"># ANSI color codes</span>
<span class="no">COLOR_GPT</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\e</span><span class="s2">[34m"</span> <span class="c1"># Blue</span>
<span class="no">COLOR_DEEPSEEK</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\e</span><span class="s2">[32m"</span> <span class="c1"># Green</span>
<span class="no">COLOR_RESET</span> <span class="o">=</span> <span class="s2">"</span><span class="se">\e</span><span class="s2">[0m"</span> <span class="c1"># Reset to default</span>

<span class="n">gpt</span> <span class="o">=</span> <span class="no">OpenAI</span><span class="o">::</span><span class="no">Client</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
  <span class="ss">access_token: </span><span class="no">ENV</span><span class="p">[</span><span class="s2">"OPENAI_API_KEY"</span><span class="p">],</span>
  <span class="ss">uri_base: </span><span class="s1">'https://api.openai.com/'</span>
<span class="p">)</span>

<span class="n">deepseek</span> <span class="o">=</span> <span class="no">OpenAI</span><span class="o">::</span><span class="no">Client</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
  <span class="ss">access_token: </span><span class="no">ENV</span><span class="p">[</span><span class="s2">"DEEPSEEK_API_KEY"</span><span class="p">],</span>
  <span class="ss">uri_base: </span><span class="s1">'https://api.deepseek.com/'</span>
<span class="p">)</span>

<span class="n">initial_message</span> <span class="o">=</span> <span class="s1">'Hello! How are you today?'</span>
<span class="n">starting_llm</span> <span class="o">=</span> <span class="n">gpt</span>

<span class="n">gpt_system_prompt</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">role: </span><span class="s1">'system'</span><span class="p">,</span>
                      <span class="ss">content: </span><span class="s1">'You are now talking to DeepSeek, another AI. Be engaging!'</span> <span class="p">}</span>
<span class="n">deepseek_system_prompt</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">role: </span><span class="s1">'system'</span><span class="p">,</span>
                           <span class="ss">content: </span><span class="s1">'You are now talking to GPT, another AI. Be precise!'</span> <span class="p">}</span>

<span class="n">gpt_conversation</span> <span class="o">=</span> <span class="p">[</span>
  <span class="n">gpt_system_prompt</span><span class="p">,</span>
  <span class="p">{</span> <span class="ss">role: </span><span class="s1">'user'</span><span class="p">,</span> <span class="ss">content: </span><span class="n">initial_message</span> <span class="p">}</span>
<span class="p">]</span>

<span class="n">deepseek_conversation</span> <span class="o">=</span> <span class="p">[</span>
  <span class="n">deepseek_system_prompt</span>
<span class="p">]</span>

<span class="nb">puts</span> <span class="s2">"DeepSeek: </span><span class="si">#{</span><span class="n">initial_message</span><span class="si">}</span><span class="se">\n</span><span class="s2">"</span>

<span class="n">current_llm</span> <span class="o">=</span> <span class="n">starting_llm</span>

<span class="kp">loop</span> <span class="k">do</span>
  <span class="n">response_content</span> <span class="o">=</span> <span class="s1">''</span>
  <span class="n">current_conversation</span> <span class="o">=</span> <span class="n">current_llm</span> <span class="o">==</span> <span class="n">gpt</span> <span class="p">?</span> <span class="n">gpt_conversation</span> <span class="p">:</span> <span class="n">deepseek_conversation</span>

  <span class="n">current_llm</span><span class="p">.</span><span class="nf">chat</span><span class="p">(</span>
    <span class="ss">parameters: </span><span class="p">{</span>
      <span class="ss">model: </span><span class="n">current_llm</span> <span class="o">==</span> <span class="n">gpt</span> <span class="p">?</span> <span class="s1">'gpt-4o'</span> <span class="p">:</span> <span class="s1">'deepseek-chat'</span><span class="p">,</span>
      <span class="ss">messages: </span><span class="n">current_conversation</span><span class="p">,</span>
      <span class="ss">temperature: </span><span class="mf">0.7</span><span class="p">,</span>
      <span class="ss">stream: </span><span class="nb">proc</span> <span class="k">do</span> <span class="o">|</span><span class="n">chunk</span><span class="p">,</span> <span class="n">_bytesize</span><span class="o">|</span>
        <span class="n">content</span> <span class="o">=</span> <span class="n">chunk</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="s1">'choices'</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">'delta'</span><span class="p">,</span> <span class="s1">'content'</span><span class="p">)</span>
        <span class="n">response_content</span> <span class="o">+=</span> <span class="n">content</span> <span class="k">if</span> <span class="n">content</span>
      <span class="k">end</span>
    <span class="p">}</span>
  <span class="p">)</span>
  <span class="nb">sleep</span> <span class="mi">2</span>

  <span class="k">if</span> <span class="n">current_llm</span> <span class="o">==</span> <span class="n">gpt</span>
    <span class="n">gpt_conversation</span> <span class="o">&lt;&lt;</span> <span class="p">{</span> <span class="ss">role: </span><span class="s1">'system'</span><span class="p">,</span> <span class="ss">content: </span><span class="n">response_content</span> <span class="p">}</span>
    <span class="n">deepseek_conversation</span> <span class="o">&lt;&lt;</span> <span class="p">{</span> <span class="ss">role: </span><span class="s1">'user'</span><span class="p">,</span> <span class="ss">content: </span><span class="n">response_content</span> <span class="p">}</span>
  <span class="k">else</span>
    <span class="n">deepseek_conversation</span> <span class="o">&lt;&lt;</span> <span class="p">{</span> <span class="ss">role: </span><span class="s1">'system'</span><span class="p">,</span> <span class="ss">content: </span><span class="n">response_content</span> <span class="p">}</span>
    <span class="n">gpt_conversation</span> <span class="o">&lt;&lt;</span> <span class="p">{</span> <span class="ss">role: </span><span class="s1">'user'</span><span class="p">,</span> <span class="ss">content: </span><span class="n">response_content</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="n">model_name</span> <span class="o">=</span> <span class="n">current_llm</span> <span class="o">==</span> <span class="n">gpt</span> <span class="p">?</span> <span class="s1">'GPT'</span> <span class="p">:</span> <span class="s1">'DeepSeek'</span>

  <span class="n">color</span> <span class="o">=</span> <span class="n">current_llm</span> <span class="o">==</span> <span class="n">gpt</span> <span class="p">?</span> <span class="no">COLOR_GPT</span> <span class="p">:</span> <span class="no">COLOR_DEEPSEEK</span>
  <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">color</span><span class="si">}#{</span><span class="n">model_name</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">response_content</span><span class="si">}#{</span><span class="no">COLOR_RESET</span><span class="si">}</span><span class="s2">"</span>

  <span class="n">current_llm</span> <span class="o">=</span> <span class="n">current_llm</span> <span class="o">==</span> <span class="n">gpt</span> <span class="p">?</span> <span class="n">deepseek</span> <span class="p">:</span> <span class="n">gpt</span>
  <span class="nb">sleep</span> <span class="mi">2</span>
<span class="k">end</span>
</code></pre></div></div>

<p>El codigo se explica por si mismo.</p>]]></content><author><name>Andrés</name></author><category term="[&quot;AI&quot;]" /><summary type="html"><![CDATA[¿Que pasaria si chatGPT y DeepSeek lograran hablar entre si?. Hice la prueba y me sorprendio el resultado.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/llmchat.png" /><media:content medium="image" url="https://a-chacon.com/assets/images/llmchat.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Documentación automática de APIs Rails con OasRails e IA: Rápido y fácil.</title><link href="https://a-chacon.com/on%20rails/2025/04/08/llm-friendly-documentation-for-oas-rails.html" rel="alternate" type="text/html" title="Documentación automática de APIs Rails con OasRails e IA: Rápido y fácil." /><published>2025-04-08T00:00:00-04:00</published><updated>2025-04-08T00:00:00-04:00</updated><id>https://a-chacon.com/on%20rails/2025/04/08/llm-friendly-documentation-for-oas-rails</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2025/04/08/llm-friendly-documentation-for-oas-rails.html"><![CDATA[<p>Hace unos días lancé una nueva versión de OasRails y, junto con esto, moví la documentación desde el README hacia un <a href="https://github.com/rust-lang/mdBook">mdbook</a> (¡Muy buena herramienta!) donde también aproveché de convertirlo al formato <a href="https://llmstxt.org/">llms.txt</a>.</p>

<p><strong>/llms.txt</strong> se define como:</p>

<blockquote>
  <blockquote>
    <p>A proposal to standardize on using an /llms.txt file to provide information to help LLMs use a website at inference time.</p>
  </blockquote>
</blockquote>

<p>Con esto, me pareció interesante poder darle más contexto a nuestros editores para que el proceso de documentar una API sea rápido y no necesite tanto esfuerzo, además de evitar errores de tipeo. Y seamos honestos, a todos nos da flojera documentar, hehe. Así que es una buena tarea para encargársela a estos modelos de lenguaje.</p>

<p>Y, que es OasRails? Lee mas <a href="/on rails/2024/07/25/documenting-rails-apis.html">aqui</a>.</p>

<hr />

<h2 id="oasrails--ia--menos-documentación-manual"><strong>OasRails + IA = Menos documentación manual</strong></h2>

<p>OasRails ya generaba un OpenAPI Spec de forma automatica desde tu codigo con la ayuda de documentacion que tu agregas de forma manual con <a href="https://yardoc.org/">YARD</a>, pero ahora este proceso puede ser aun mas facil. Puedes:</p>

<ol>
  <li><strong>Preguntarle a ChatGPT</strong> sobre tu API como si fuera un experto.</li>
  <li><strong>Integrar la doc en Cursor</strong> para tener respuestas en tiempo real mientras programas.</li>
  <li><strong>Automatizar migraciones</strong> desde Swagger o documentación manual.</li>
</ol>

<hr />

<h2 id="cómo-funciona-en-3-pasos"><strong>Cómo funciona (en 3 pasos)</strong></h2>

<h3 id="1-elige-el-archivo"><strong>1. Elige el archivo</strong></h3>

<p>OasRails genera archivos <code class="language-plaintext highlighter-rouge">.txt</code> optimizados para IA:</p>

<ul>
  <li><a href="https://a-chacon.com/oas_rails/llms.txt">llms.txt</a>: Lo básico para entender la estructura y qué hace OasRails. (En trabajo)</li>
  <li><a href="https://a-chacon.com/oas_rails/llms-full.txt">llms-full.txt</a>: Toda la descripción específica sobre el uso de OasRails y cómo documentar los endpoints. (<strong>Por ahora el recomendado</strong>)</li>
</ul>

<h3 id="2-cárgalo-en-tu-herramienta-favorita"><strong>2. Cárgalo en tu herramienta favorita</strong></h3>

<p>Mi editor de código es NeoVim y utilizo la IA mediante Avante, un plugin que trata de replicar Cursor, pero actualmente no soporta cargar contexto de fuentes externas. Por lo tanto, para el ejemplo, usé Cursor. No lo recomiendo para nada, ya que es de código cerrado y no se alinea con mis principios, pero me sirvió para el ejemplo y lo eliminé, jeje.</p>

<video controls="">
  <source src="/assets/images/cursor+oasrails.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<h3 id="3-pregunta-como-si-nada"><strong>3. Pregunta como si nada</strong></h3>

<p>Ejemplos de lo que puedes preguntar:</p>

<ul>
  <li><em>“¿Cómo documentar autenticación JWT en OasRails?”</em></li>
  <li><em>“Documenta el endpoint…”</em></li>
  <li><em>“Agrega ejemplos sobre posibles request body para el método create.”</em></li>
</ul>

<hr />

<h2 id="migrar-desde-apipie-rails-u-otra-herramienta-sin-dolor"><strong>Migrar desde apipie-rails u otra herramienta (sin dolor)</strong></h2>

<p>Si ya tienes documentación escrita en otra herramienta como apipie-rails, solo debes pedirle a tu IA que lo traduzca usando los tags de OasRails.</p>

<hr />

<h2 id="para-cerrar"><strong>Para cerrar</strong></h2>

<p>Documentar APIs es una tarea necesaria pero que consume tiempo, especialmente cuando se hace manualmente. Con <strong>OasRails</strong>, automatizas gran parte de este proceso, generando especificaciones OpenAPI directamente desde tu código. Ahora, al combinar esta herramienta con modelos de lenguaje como ChatGPT, puedes acelerar aún más la creación y mantenimiento de la documentación, reduciendo errores y ahorrando algunas horas de trabajo.</p>

<p>¿Ya probaste OasRails? <a href="https://github.com/a-chacon/oas_rails">Déjame un star en GitHub</a> y cuéntame cómo te va.</p>

<p>¿Preguntas o sugerencias? ¡Estoy atento!</p>]]></content><author><name>Andrés</name></author><category term="[&quot;On Rails&quot;]" /><summary type="html"><![CDATA[Automatiza la documentación de APIs en Rails con OasRails y su documentación optimizada para IA (llms.txt). Genera especificaciones OpenAPI sin esfuerzo y acelera tu flujo de trabajo con modelos de lenguaje.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.github.io/oas_core/assets/rails_theme.png" /><media:content medium="image" url="https://a-chacon.github.io/oas_core/assets/rails_theme.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">LLM en local: Corriendo DeepSeek R1 y Open WebUI con Docker Compose.</title><link href="https://a-chacon.com/docker/2025/01/27/run-deepseek-locally.html" rel="alternate" type="text/html" title="LLM en local: Corriendo DeepSeek R1 y Open WebUI con Docker Compose." /><published>2025-01-27T00:00:00-03:00</published><updated>2025-01-27T00:00:00-03:00</updated><id>https://a-chacon.com/docker/2025/01/27/run-deepseek-locally</id><content type="html" xml:base="https://a-chacon.com/docker/2025/01/27/run-deepseek-locally.html"><![CDATA[<p><strong>Suponiendo que usas ChatGPT constantemente y que probablemente hayas oído hablar de algunos de los últimos modelos lingüísticos de código abierto (LLM), como Llama 3.1, Gemma 2, Mistral o DeepSeek, te voy a mostrar cómo correr este último en tu máquina local mediante Docker y Docker Compose.</strong></p>

<p>DeepSeek es un modelo lingüístico de inteligencia artificial de código abierto desarrollado por una empresa china. Es una alternativa transparente y accesible a otros LLMs privados, permitiendo su uso y modificación tanto para investigación como para aplicaciones comerciales.</p>

<p>No lo niego, estoy disfrutando al ver cómo se derrumba el monopolio de OpenAI y su LLM de código cerrado frente a una alternativa de código abierto y mucho más económica, tanto en precio como en consumo de recursos.</p>

<p><a href="https://ollama.com/">Ollama</a> se define como una <strong>aplicación de código abierto</strong> que permite ejecutar, crear y compartir localmente grandes modelos lingüísticos con una interfaz de línea de comandos en macOS y Linux. Y <a href="https://openwebui.com">Open WebUI</a> es una <strong>WebUI autoalojada extensible</strong>, rica en características y fácil de usar, diseñada para operar completamente fuera de línea.</p>

<h3 id="ventajas">Ventajas</h3>

<p>Las ventajas que te puede traer tener tu LLM corriendo en local pueden ser:</p>

<ul>
  <li>
    <p><strong>Personalización</strong>: Ejecutar los modelos localmente te ofrece un control total sobre el entorno. Puedes ajustar los modelos para adaptarlos a tus necesidades específicas, ajustar los parámetros e incluso experimentar con diferentes configuraciones.</p>
  </li>
  <li>
    <p><strong>Costes reducidos</strong>: Si ya dispones de una máquina capaz, especialmente una equipada con una GPU, ejecutar LLMs localmente puede ser una opción rentable. No es necesario pagar por costosos recursos de computación en la nube y puedes experimentar libremente sin preocuparte por los límites de llamadas a la API ni por el aumento de los costes.</p>
  </li>
  <li>
    <p><strong>Privacidad</strong>: Cuando ejecutas modelos localmente, tus datos permanecen en tu máquina. Esto garantiza que la información confidencial nunca salga de su entorno seguro, proporcionando un nivel de privacidad que los servicios basados en la nube simplemente no pueden igualar. Para las empresas que manejan datos confidenciales, esto puede ser una ventaja crucial.</p>
  </li>
</ul>

<p><strong>Para mí, a nivel personal, me funciona bastante bien con un Ryzen 7 5600U.</strong></p>

<h3 id="️-manos-a-la-obra">⚙️ Manos a la obra</h3>

<p><strong>Probablemente, si estás aquí ya conoces Docker y Docker Compose, así que me saltaré cualquier introducción a estas herramientas.</strong></p>

<p>Crea una carpeta/directorio y luego agrega un archivo llamado <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> donde agregarás el siguiente contenido:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">webui</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/open-webui/open-webui:main</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">3000:8080/tcp</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">open-webui:/app/backend/data</span>
    <span class="na">extra_hosts</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">host.docker.internal:host-gateway"</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ollama</span>

  <span class="na">ollama</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ollama/ollama</span>
    <span class="na">expose</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">11434/tcp</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">11434:11434/tcp</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="s">ollama --version || exit </span><span class="m">1</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ollama:/root/.ollama</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">ollama</span><span class="pi">:</span>
  <span class="na">open-webui</span><span class="pi">:</span>
</code></pre></div></div>

<p>Ejecuta <code class="language-plaintext highlighter-rouge">docker compose up</code> y listo. Tendrás Ollama y Open WebUI corriendo.</p>

<h4 id="último-y-más-importante-paso">Último y más importante paso❗</h4>

<p>Me pasó que no todos los posts acerca de esto fueron claros con el tema, y por eso decidí escribir esto en mi blog. Ollama es un ‘gestor’ de LLMs y, por defecto, no contiene ninguna imagen. Por lo que, cuando entres por primera vez a <code class="language-plaintext highlighter-rouge">localhost:3000</code>, verás algo como esto:</p>

<p><img src="/assets/images/openwebui.png" alt="Open Web Ui empty without models" /></p>

<p>Para solucionarlo, tienes que hacer pull de alguna imagen. Para esto <strong>tienes dos opciones</strong>: existe una forma gráfica de hacerlo desde Open WebUI y otra por línea de comandos directamente en el contenedor de Ollama. Esta segunda opción fue la que usé yo; puedes hacerlo simplemente ejecutando la siguiente línea:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec</span> <span class="nt">-it</span> ollama-ollama-1 ollama pull deepseek-r1
</code></pre></div></div>

<p>Ten en cuenta que el nombre del contenedor dependerá de tus configuraciones; además, probablemente debas ejecutarlo usando <code class="language-plaintext highlighter-rouge">sudo</code>.</p>

<p><strong><a href="https://ollama.com/library">Aquí</a> puedes ver todos los modelos disponibles.</strong></p>

<p>🤓Además, puedes interactuar con el modelo a nivel de línea de comandos. Si entras al contenedor de Ollama, puedes ejecutar <code class="language-plaintext highlighter-rouge">ollama run deepseek-r1</code> e interactuar sin la necesidad de una UI.🤓</p>

<p>¡Hasta aquí por ahora! Disfruta de tu LLM local.</p>

<h2 id="docker-compose-files-for-development">Docker Compose Files For development</h2>

<p>Acabo de publicar un repositorio incluyendo este archivo de docker compose (archivos que uso a diario para trabajar). Puedes clonarlo y levantar todos los contenedores que necesites para desarrollar tus aplicaciones. También está abierto a contribuciones:</p>

<p><a href="https://codeberg.org/a-chacon/docker-compose-for-development">https://codeberg.org/a-chacon/docker-compose-for-development</a></p>]]></content><author><name>Andrés</name></author><category term="[&quot;Docker&quot;]" /><summary type="html"><![CDATA[DeepSeek está irrumpiendo en la escena de la IA, un LLM de código abierto y accesible que ha hecho caer los precios de las acciones de NVIDIA y otras empresas más. Y como no, así como Ollama también podremos correr este modelo de forma local para ver cómo anda.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/deepseek.jpg" /><media:content medium="image" url="https://a-chacon.com/assets/images/deepseek.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Mark Zuckerberg dijo: It’s time to go back to our roots and use RSS</title><link href="https://a-chacon.com/project/2025/01/14/mark-zuck-rss.html" rel="alternate" type="text/html" title="Mark Zuckerberg dijo: It’s time to go back to our roots and use RSS" /><published>2025-01-14T00:00:00-03:00</published><updated>2025-01-14T00:00:00-03:00</updated><id>https://a-chacon.com/project/2025/01/14/mark-zuck-rss</id><content type="html" xml:base="https://a-chacon.com/project/2025/01/14/mark-zuck-rss.html"><![CDATA[<p>Es broma, lo que realmente dijo es:</p>

<blockquote>
  <p>It’s time to get back to our roots around free expression.</p>
</blockquote>

<p>Pero, ¿realmente será que aboga por la <strong>libertad de expresión</strong>? No le creo. Aquí ya tenemos ejemplos de que esta libertad de expresión <strong>no es para todos</strong>:</p>

<ul>
  <li><a href="https://www.404media.co/facebook-deletes-internal-employee-criticism-of-new-board-member-dana-white/">Facebook Deletes Internal Employee Criticism of New Board Member Dana White</a></li>
  <li><a href="https://www.404media.co/meta-is-blocking-links-to-decentralized-instagram-competitor-pixelfed/">Meta Is Blocking Links to Decentralized Instagram Competitor Pixelfed</a></li>
</ul>

<p>A mi parecer, esta libertad de expresión de la que habla solo será usada para manipular los prejuicios de los usuarios <strong>esparciendo noticias falsas o alteradas con fines políticos y económicos</strong>. Ya hemos visto a Musk haciendo de las suyas con X (Twitter), ahora Zuckerberg comienza a hacerlo de forma pública y descarada también.</p>

<p>Pero mi post no va completamente a criticar esta decisión, ya serán otros los que hablen sobre las implicancias éticas, políticas y sociales de estas redes sociales centralizadas en manos de unos pocos oligarcas. Si no, más bien, a presentarte una <strong>alternativa muy buena</strong> para informarse de las fuentes que te interesan sin la intervención de terceros (algoritmos y empresas), una tecnología que sí, <strong>realmente proviene de las raíces de internet</strong>: RSS.</p>

<h3 id="qué-es-rss">¿Qué es RSS?</h3>

<p>RSS nace de la necesidad de consumir el contenido que te interesa previo a la existencia de redes sociales y sucediendo a varios intentos de sindicalizar la web que no tuvieron tanta popularidad. En sí, es un <strong>documento</strong> en formato XML con una <strong>estructura simple donde se incluyen las nuevas entradas/artículos del sitio web</strong> que lo ofrece y que puede ser fácilmente leído por aplicaciones comúnmente llamadas agregadores o lectores de RSS.</p>

<p>La aplicación/lector se encarga de consultar el archivo RSS cada cierto tiempo para ver si existen nuevas entradas, esto por cada feed que sigas, y te mostrará el contenido. Por lo general, los sitios web quieren que tú vayas directamente a su página web a leer el contenido, pero algunos lectores también son capaces de mostrarte ese contenido ahí mismo.</p>

<h3 id="beneficios">Beneficios</h3>

<p>Ahora bien, entendiendo un poco la lógica de cómo funciona, podemos darnos cuenta de las ventajas de consumir contenido de esta forma:</p>

<ul>
  <li><strong>No existen algoritmos</strong> de por medio creando sesgos y barreras a la información en internet.</li>
  <li>Tú tienes el control sobre qué ver y qué no ver. <strong>No recibirás un constante bombardeo de publicidad y dopamina</strong>.</li>
  <li><strong>Simple</strong>: realmente son las raíces de internet. No las que menciona Zuckerberg 🤑.</li>
  <li><strong>Descentralizado</strong>, no hay una empresa atenta a cuáles son tus interacciones, gustos, preferencias políticas ni religiosas.</li>
</ul>

<p>Y si tienes o administras tú un blog o sitio web, los beneficios también son evidentes. Distribuyes contenido con poco esfuerzo y le ofreces un canal de fácil acceso a tus usuarios/lectores.</p>

<h3 id="estado-actual">Estado actual</h3>

<p>Lamentablemente, RSS ha dejado de ser tan popular, probablemente no ha sido casualidad. La tecnología en sí <strong>cumple su función para los usuarios finales pero no para los intereses de las grandes empresas</strong>. <a href="https://openrss.org/blog/how-google-helped-destroy-adoption-of-rss-feeds">Google tiene un historial en ayudar a destruir la adopción de RSS</a>, un claro ejemplo de esto es la eliminación de Google Reader, un lector web.</p>

<p>Pero aun así, son muchos los que aún usan lectores para mantenerse actualizados en su día a día. Existen empresas como <a href="https://feedly.com/">Feedly</a>, <a href="https://www.newsblur.com/">NewsBlur</a> e <a href="https://www.inoreader.com/">Inoreader</a> que ofrecen un servicio web para seguir tus fuentes. Y por otro lado, también tienes proyectos open source que puedes alojar tú mismo como <a href="https://freshrss.org/">FreshRSS</a>, <a href="https://miniflux.app/">MiniFlux</a> y <a href="https://tt-rss.org">Tiny Tiny RSS</a> que te sirven para centralizar tus lecturas.</p>

<p>Aparte de los ya mencionados, existen aplicaciones nativas tanto para Desktop como para Móvil que mayormente funcionan como clientes para estos servicios webs y/o que también funcionan sin estos. Uno de estos clientes es mi aplicación <a href="https://chaski.a-chacon.com">Chaski</a>, <strong>un lector que funciona independiente a cualquier servidor</strong> y con un toque más <strong>moderno y relajado</strong> para consumir el contenido que importa, sin rastreo ni anuncios. Un lector simple, para el cual tengo varios planes a futuro si es que hay gente realmente interesada.</p>

<p><img src="/assets/images/today.png" alt="Chaski app main view" /></p>

<h3 id="conclusión">Conclusión</h3>

<p>RSS cumple su función, es simple y ayuda a mantener internet como un lugar sano y no secuestrado por las grandes empresas tecnológicas. <strong>Sería bonito ver resurgir RSS</strong>, que su adopción crezca y más gente se sume a alternativas libres y open source.</p>

<p>Creo que ya hay mucha gente que se da cuenta de que consumir el contenido que les dan los algoritmos ya no es viable. Son máquinas hechas para hacer dinero, no para informar, educar y mantenernos conectados. Si ese hacer dinero se trata de avivar prejuicios, implantar ideas e influenciar en tus decisiones, entonces lo harán con información verídica o falsa.</p>

<p>Espero entregar algo de conocimiento, algo de aporte a un internet mejor. Ahí te dejo mi aplicación. No tengo sistema de comentarios por ahora, pero me gustaría saber si llegas a mi blog por RSS, si lo usas a diario y cualquier comentario sobre mi lector. Puedes escribirme por cualquiera de los canales de mi página principal.</p>

<p>Hasta la próxima.</p>]]></content><author><name>Andrés</name></author><category term="[&quot;Project&quot;]" /><summary type="html"><![CDATA[¿Realmente sera que aboga por la libertad de expresion?. No le creo.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/mark.jpg" /><media:content medium="image" url="https://a-chacon.com/assets/images/mark.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Chaski: Un Lector de Fuentes RSS/Atom para el 2025</title><link href="https://a-chacon.com/project/2024/12/26/chaski-feed-reader-for-2025.html" rel="alternate" type="text/html" title="Chaski: Un Lector de Fuentes RSS/Atom para el 2025" /><published>2024-12-26T00:00:00-03:00</published><updated>2024-12-26T00:00:00-03:00</updated><id>https://a-chacon.com/project/2024/12/26/chaski-feed-reader-for-2025</id><content type="html" xml:base="https://a-chacon.com/project/2024/12/26/chaski-feed-reader-for-2025.html"><![CDATA[<p>Tiempo atrás descubrí cómo funcionaba RSS, <strong>me lamento no haberlo descubierto antes</strong>, y desde entonces lo agregué a mi blog. Luego busqué formas de seguir diferentes fuentes y fue cuando me llegó la idea de crear mi propia aplicación. Y antes de preguntarme, ¿con qué tecnología?, ya tenía la respuesta: <a href="https://tauri.app/">Tauri</a>.</p>

<p>Había descubierto Tauri mediante GitHub cuando comencé a interesarme por Rust y a mirar proyectos escritos en este lenguaje. Ya se hablaba de que la versión dos permitiría compilar una sola aplicación tanto para escritorio como para móvil, así que era mi momento. Hace cuatro meses comencé el proyecto.</p>

<h2 id="rss">RSS</h2>

<p>Antes de seguir, me gustaría dedicarle un espacio para explicar <strong>qué es, cómo funciona y lo fascinante de RSS</strong>. Antes de que llegaran las grandes empresas de tecnología con sus redes sociales y algoritmos de sugerencias, existía este formato de distribución de contenido. Básicamente depende de un “publicador” y un “cliente”. El cliente, haciendo uso de una aplicación, consulta constantemente el archivo RSS del sitio “publicador”, donde saldrán las noticias o entradas del sitio. Si hay entradas nuevas, entonces la aplicación las muestra. De esta forma <strong>no hay intervención de terceros</strong> (en teoría) y tu aplicación consulta todas las fuentes a las que estés suscrito.</p>

<p>Ahora, si nos ponemos a pensar en cómo funciona, es la forma más simple que tenemos de consumir la información que nosotros queremos. Es <strong>internet en su forma más primitiva y sana</strong>, donde no hay seguimiento ni observación constante de lo que haces como usuario. Tampoco estás propenso a ser segmentado y bombardeado con anuncios para inducir una necesidad que no tienes y terminar comprando algo que no necesitas. En fin, <strong>cualquier contra que tengan las redes sociales masivas será un punto a favor de RSS</strong>.</p>

<p>Pero, lamentablemente, RSS <strong>no es tan popular como debería ser</strong>, y aquí es donde entra Chaski. Mi idea sería hacer de RSS algo moderno y entretenido, con una interfaz que invite a leer y reduzca la <strong>ansiedad de tener que consumir tanta información tan rápido para estar actualizados</strong>.</p>

<h2 id="pasar-de-ruby-a-rust">Pasar de Ruby a Rust</h2>

<p>No fue fácil partir, tampoco tan complejo. Pero el hecho de no avanzar tan rápido en el desarrollo como lo haría con una aplicación web hecha con Ruby on Rails me frustraba en ciertos momentos porque quería tener algo usable lo más rápido posible.</p>

<p>Luego, con la ayuda de un LLM logré agarrar el ritmo, entender algunos conceptos clave de Rust como el tipado fuerte, el ownership y las estructuras. Aunque aún no entiendo del todo algunas cosas, <strong>logré hacer funcionar la app y llegar a lo que hoy tengo en el repositorio</strong>.</p>

<p>Repositorio: <a href="https://github.com/a-chacon/chaski-app">https://github.com/a-chacon/chaski-app</a></p>

<h2 id="chaski-lector-de-feeds">Chaski: Lector de feeds</h2>

<p>Mi enfoque para crear la app siempre ha estado basado en cosas que a mí me interesan cuando navego por internet: <strong>privacidad, usabilidad, diseño, simpleza, funcionalidad</strong>. Con estas ideas presentes es que comencé por cumplir algunos requisitos básicos de un lector de feeds:</p>

<ul>
  <li>Manipular fuentes.</li>
  <li>Obtener entradas.</li>
  <li>Obtener contenido de esas entradas.</li>
  <li>Autodescubrir feeds para una URL dada.</li>
  <li>Importar/exportar feeds en formato OPML.</li>
  <li>Búsqueda simple por texto tanto para fuentes como para entradas.</li>
  <li>Organizar en carpetas (aunque requiere más trabajo aún).</li>
  <li>Modo claro y oscuro.</li>
  <li>Filtros para las entradas.</li>
</ul>

<p>Todas estas funcionalidades ya están incluidas y tengo planes más ambiciosos aún, pero no puedo ir tan rápido como me gustaría. <strong>Aún tengo un trabajo a tiempo completo con el que debo cumplir</strong>. Pero dentro de las ideas para un futuro próximo están:</p>

<ul>
  <li>Una forma de sincronizar entre dispositivos.</li>
  <li>Compilar para Android.</li>
  <li>Mejorar la extracción de los artículos.</li>
  <li>Comenzar un blog con RSS para la página de Chaski, donde pueda compartir las actualizaciones.</li>
  <li>Obviamente, integración con algún LLM (IA) para el resumen y la interacción con las entradas.</li>
  <li>Traducciones.</li>
</ul>

<p>Más detalles: <a href="https://chaski.a-chacon.com">https://chaski.a-chacon.com</a></p>

<h2 id="para-el-futuro">Para el futuro</h2>

<p>Tal vez el proyecto termine muerto, como muchos otros que he intentado hacer, o tal vez no. Pero crearlo en sí ya fue un disfrute. Aprender cosas nuevas siempre es motivante. Ojalá llegar a una versión que sea útil para muchos y, quién sabe, <strong>tal vez RSS resurja nuevamente</strong>.</p>

<p>El proyecto está abierto a aportes, sugerencias y cualquier comentario que sea útil. ¡Hasta la próxima!</p>]]></content><author><name>Andrés</name></author><category term="[&quot;Project&quot;]" /><summary type="html"><![CDATA[Presento Chaski, un lector de RSS/Atom simple con un diseño moderno y centrado en la privacidad que te puede servir para mantenerte actualizado.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/today.png" /><media:content medium="image" url="https://a-chacon.com/assets/images/today.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">PoC: Usando el Generador de Autenticación de Rails 8 En Modo API-Only.</title><link href="https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html" rel="alternate" type="text/html" title="PoC: Usando el Generador de Autenticación de Rails 8 En Modo API-Only." /><published>2024-10-16T00:00:00-03:00</published><updated>2024-10-16T00:00:00-03:00</updated><id>https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"><![CDATA[<p>Como ya saben, una de las funcionalidades nuevas de Rails 8 es el <strong>nuevo generador básico de autenticación</strong> que viene a demostrar que no es tan complejo desarrollar todo lo que respecta a autenticación en una aplicación con Rails y que muchas veces no es necesario depender de terceros (gemas). La discusión comenzó <a href="https://github.com/rails/rails/issues/50446">aquí</a>.</p>

<p>Dicho esto, veamos que pasa usando el generador en una aplicación API-Only:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rails <span class="nt">-v</span>
Rails 8.0.2
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rails new app <span class="nt">--api</span> &amp; <span class="nb">cd </span>app
</code></pre></div></div>

<p>Y ejecutamos el nuevo comando:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> rails g authentication
      create  app/models/session.rb
      create  app/models/user.rb
      create  app/models/current.rb
      create  app/controllers/sessions_controller.rb
      create  app/controllers/concerns/authentication.rb
      create  app/controllers/passwords_controller.rb
      create  app/channels/application_cable/connection.rb
      create  app/mailers/passwords_mailer.rb
      create  app/views/passwords_mailer/reset.html.erb
      create  app/views/passwords_mailer/reset.text.erb
      create  <span class="nb">test</span>/mailers/previews/passwords_mailer_preview.rb
      insert  app/controllers/application_controller.rb
       route  resources :passwords, param: :token
       route  resource :session
        gsub  Gemfile
      bundle  <span class="nb">install</span> <span class="nt">--quiet</span>
    generate  migration CreateUsers email_address:string!:uniq password_digest:string! <span class="nt">--force</span>
       rails  generate migration CreateUsers email_address:string!:uniq password_digest:string! <span class="nt">--force</span> 
      invoke  active_record
      create    db/migrate/20250321105722_create_users.rb
    generate  migration CreateSessions user:references ip_address:string user_agent:string <span class="nt">--force</span>
       rails  generate migration CreateSessions user:references ip_address:string user_agent:string <span class="nt">--force</span> 
      invoke  active_record
      create    db/migrate/20250321105723_create_sessions.rb
      invoke  test_unit
      create    <span class="nb">test</span>/fixtures/users.yml
      create    <span class="nb">test</span>/models/user_test.rb
</code></pre></div></div>

<p>Ok, ahora por ejemplo, si revisamos <code class="language-plaintext highlighter-rouge">SessionsController</code> veremos que el método de <code class="language-plaintext highlighter-rouge">Login</code> se ve de la siguiente forma:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">create</span>
    <span class="k">if</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate_by</span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email_address</span><span class="p">,</span> <span class="ss">:password</span><span class="p">))</span>
      <span class="n">start_new_session_for</span> <span class="n">user</span>
      <span class="n">redirect_to</span> <span class="n">after_authentication_url</span>
    <span class="k">else</span>
      <span class="n">redirect_to</span> <span class="n">new_session_url</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Try another email address or password."</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>O sea, redirecciona a rutas y/o vistas que en nuestra API no existen ni hacen sentido, y además si inspeccionamos el metodo <code class="language-plaintext highlighter-rouge">start_new_session_for</code> nos daremos cuenta de que el sistema está basado 100% en <strong>autenticación mediante cookies</strong>. Entonces, ¿qué hacemos?</p>

<p>Mi propuesta es la siguiente: el generador crea las bases para la autenticación y creo que funciona bastante bien, por lo que con unas pequeñas modificaciones podemos dejar funcionando una <strong>autenticación Bearer</strong> (Token Authentication) rápidamente en nuestra API con Rails 8 más los archivos ya generados.</p>

<p>El primer paso será agregar <strong>persistencia para nuestro token</strong>, para esto modificaremos la migración que crea las sessiones y agregaremos un nuevo campo llamado <code class="language-plaintext highlighter-rouge">token</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">create_table</span> <span class="ss">:sessions</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
      <span class="n">t</span><span class="p">.</span><span class="nf">references</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">null: </span><span class="kp">false</span><span class="p">,</span> <span class="ss">foreign_key: </span><span class="kp">true</span>
      <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:ip_address</span>
      <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:user_agent</span>
      <span class="n">t</span><span class="p">.</span><span class="nf">string</span> <span class="ss">:token</span>     <span class="c1"># HERE</span>

      <span class="n">t</span><span class="p">.</span><span class="nf">timestamps</span>
    <span class="k">end</span>
</code></pre></div></div>

<p>Ahora simplemente ejecuta <code class="language-plaintext highlighter-rouge">rails db:migrate</code> y create un usuario de prueba por consola, yo lo haré con esta línea <code class="language-plaintext highlighter-rouge">User.create(email_address: "user@test.com", password: "123456789")</code> (Lo utilizaremos más tarde). Luego debemos crear un nuevo token para cada sesión nueva de un usuario, para esto lo más simple es usar un callback en el modelo <code class="language-plaintext highlighter-rouge">Session</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/sessions.rb</span>
<span class="k">class</span> <span class="nc">Session</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">belongs_to</span> <span class="ss">:user</span>
  <span class="n">before_create</span> <span class="ss">:generate_token</span> <span class="c1"># Here call</span>

  <span class="kp">private</span>
  <span class="k">def</span> <span class="nf">generate_token</span> <span class="c1"># Here implement, generate the token as you wish.</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">token</span> <span class="o">=</span> <span class="no">Digest</span><span class="o">::</span><span class="no">SHA1</span><span class="p">.</span><span class="nf">hexdigest</span><span class="p">([</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">,</span> <span class="nb">rand</span> <span class="p">].</span><span class="nf">join</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Ahora volviendo al metodo <code class="language-plaintext highlighter-rouge">start_new_session_for</code> en el concern <code class="language-plaintext highlighter-rouge">Authentication</code>, no es necesario que creemos una cookie, asi que debemos remover esa linea y dejar el metodo de la siguiente forma:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/concerns/authentication.rb</span>
<span class="k">def</span> <span class="nf">start_new_session_for</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
  <span class="n">user</span><span class="p">.</span><span class="nf">sessions</span><span class="p">.</span><span class="nf">create!</span><span class="p">(</span><span class="ss">user_agent: </span><span class="n">request</span><span class="p">.</span><span class="nf">user_agent</span><span class="p">,</span> <span class="ss">ip_address: </span><span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">).</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">session</span><span class="o">|</span>
    <span class="no">Current</span><span class="p">.</span><span class="nf">session</span> <span class="o">=</span> <span class="n">session</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y modificaremos el <code class="language-plaintext highlighter-rouge">create</code> de <code class="language-plaintext highlighter-rouge">SessionsController</code> para que las respuestas sean en formato json y no redirecciones:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/sessions_controller.rb</span>
<span class="k">def</span> <span class="nf">create</span>
  <span class="k">if</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate_by</span><span class="p">(</span><span class="n">params</span><span class="p">.</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:email_address</span><span class="p">,</span> <span class="ss">:password</span><span class="p">))</span>
    <span class="n">start_new_session_for</span> <span class="n">user</span>
    <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">token: </span><span class="no">Current</span><span class="p">.</span><span class="nf">session</span><span class="p">.</span><span class="nf">token</span>  <span class="p">}</span> <span class="p">}</span>
  <span class="k">else</span>
    <span class="n">render</span> <span class="ss">json: </span><span class="p">{},</span> <span class="ss">status: :unauthorized</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><strong>Para hacer que todo esto funcione debemos hacer dos cosas:</strong></p>

<ol>
  <li>
    <p>Incluir el módulo <code class="language-plaintext highlighter-rouge">Authentication</code> en <code class="language-plaintext highlighter-rouge">ApplicationController</code> (Este paso ya fue arreglado en las versiones posteriores a la versión beta):</p>

    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/application_controller.rb</span>
<span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span>
  <span class="kp">include</span> <span class="no">Authentication</span>
<span class="k">end</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Eliminar la linea numero 6 de este mismo concern:</p>

    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/concerns/authentication.rb</span>
  <span class="n">included</span> <span class="k">do</span>
    <span class="n">before_action</span> <span class="ss">:require_authentication</span>
    <span class="n">helper_method</span> <span class="ss">:authenticated?</span> <span class="c1"># This, we don't use helpers in APIs</span>
  <span class="k">end</span>
</code></pre></div>    </div>
  </li>
</ol>

<p>Hasta este punto ya deberíamos tener el <strong>login funcionando</strong>. Para probar esto voy a agregar <a href="https://github.com/a-chacon/oas_rails">OasRails</a>, que a propósito <strong>ya está funcionando con Rails 8</strong> y voy a enviar un par de peticiones a ver como se comporta, no explicaré como implementar OasRails, para eso puedes ver el repositorio o leer más en <a href="/on rails/2024/07/25/documenting-rails-apis">este post</a>.</p>

<p>Inicio de sesión exitoso:</p>

<p><img src="/assets/images/rails8_success_login.png" alt="" /></p>

<p>Inicio de sesión fallido:</p>

<p><img src="/assets/images/rails8_fail_login.png" alt="" /></p>

<hr />

<p>Ya podemos generar tokens, ahora modificaremos el código para <strong>autenticarnos con ese mismo token</strong>. Para eso, cambiaremos la lógica de buscar la sesión actual del usuario con base en la cookie a buscarla basándonos en la cabecera <code class="language-plaintext highlighter-rouge">Authorization</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="c1"># app/controllers/concerns/authentication.rb</span>
  <span class="k">def</span> <span class="nf">resume_session</span>
    <span class="no">Current</span><span class="p">.</span><span class="nf">session</span> <span class="o">=</span> <span class="n">find_session_by_token</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">find_session_by_token</span>
    <span class="no">Session</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">token: </span><span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="ss">:authorization</span><span class="p">]</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="s2">" "</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Para probar esto creo que tendremos que hacer rápidamente un modelo que dependa de <code class="language-plaintext highlighter-rouge">User</code> y que requiera autenticación para utilizar. Intentemos con <code class="language-plaintext highlighter-rouge">rails g scaffold project title:string description:text user:references</code> y le agregamos al principio del controlador la línea de código <code class="language-plaintext highlighter-rouge">before_action :require_authentication</code>.</p>

<p>Aquí les dejo una pequeña prueba del index de Projects autenticado con el token que obtuve en las pruebas anteriores:</p>

<p><img src="/assets/images/rails8_projects.png" alt="" /></p>

<hr />

<p>Con esto ya tienes gran parte de la lógica de autenticación funcionando en la aplicación API-Only. Te queda continuar con las modificaciones en el resto de los endpoints para que las respuestas sean en formato json y no supuestas vistas que no existen en la aplicación.</p>

<p>Probablemente de aquí a que se lance la versión final de Rails 8 aparezca un <strong>PR solucionando esto y el generador funcione correctamente en modo API-Only</strong>. Hasta entonces, con estas pequeñas modificaciones ya puedes seguir construyendo tu API.</p>]]></content><author><name>Andrés</name></author><category term="[&quot;On Rails&quot;]" /><summary type="html"><![CDATA[Haciendo funcionar el generador de autenticación de Rails 8 en una aplicación creada en modo API-Only.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/rails8-poc-api-auth.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/rails8-poc-api-auth.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">LLM en local: Corriendo Ollama y Open WebUI con Docker Compose.</title><link href="https://a-chacon.com/docker/2024/09/16/run-llm-locally.html" rel="alternate" type="text/html" title="LLM en local: Corriendo Ollama y Open WebUI con Docker Compose." /><published>2024-09-16T00:00:00-03:00</published><updated>2024-09-16T00:00:00-03:00</updated><id>https://a-chacon.com/docker/2024/09/16/run-llm-locally</id><content type="html" xml:base="https://a-chacon.com/docker/2024/09/16/run-llm-locally.html"><![CDATA[<p><strong>Suponiendo que usas ChatGPT constantemente y que probablemente hayas oído hablar de algunos de los últimos modelos lingüísticos de código abierto (LLM), como Llama 3.1, Gemma 2 y Mistral, te voy a mostrar cómo correr uno de estos en tu máquina local mediante Docker y Docker Compose.</strong></p>

<p><a href="https://ollama.com/">Ollama</a> se define como una <strong>aplicación de código abierto</strong> que permite ejecutar, crear y compartir localmente grandes modelos lingüísticos con una interfaz de línea de comandos en macOS y Linux. Y <a href="https://openwebui.com">Open WebUI</a> es una <strong>WebUI autoalojada extensible</strong>, rica en características y fácil de usar, diseñada para operar completamente fuera de línea.</p>

<h3 id="ventajas">Ventajas</h3>

<p>Las ventajas que te puede traer tener tu LLM corriendo en local pueden ser:</p>

<ul>
  <li>
    <p><strong>Personalización</strong>: Ejecutar los modelos localmente te ofrece un control total sobre el entorno. Puedes ajustar los modelos para adaptarlos a tus necesidades específicas, ajustar los parámetros e incluso experimentar con diferentes configuraciones.</p>
  </li>
  <li>
    <p><strong>Costes reducidos</strong>: Si ya dispones de una máquina capaz, especialmente una equipada con una GPU, ejecutar LLMs localmente puede ser una opción rentable. No es necesario pagar por costosos recursos de computación en la nube y puedes experimentar libremente sin preocuparte por los límites de llamadas a la API ni por el aumento de los costes.</p>
  </li>
  <li>
    <p><strong>Privacidad</strong>: Cuando ejecutas modelos localmente, tus datos permanecen en tu máquina. Esto garantiza que la información confidencial nunca salga de su entorno seguro, proporcionando un nivel de privacidad que los servicios basados en la nube simplemente no pueden igualar. Para las empresas que manejan datos confidenciales, esto puede ser una ventaja crucial.</p>
  </li>
</ul>

<p><strong>Para mí, a nivel personal, me funciona bastante bien con un Ryzen 7 5600U.</strong></p>

<h3 id="️-manos-a-la-obra">⚙️ Manos a la obra</h3>

<p><strong>Probablemente, si estás aquí ya conoces Docker y Docker Compose, así que me saltaré cualquier introducción a estas herramientas.</strong></p>

<p>Crea una carpeta/directorio y luego agrega un archivo llamado <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> donde agregarás el siguiente contenido:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">webui</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/open-webui/open-webui:main</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">3000:8080/tcp</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">open-webui:/app/backend/data</span>
    <span class="na">extra_hosts</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">host.docker.internal:host-gateway"</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ollama</span>

  <span class="na">ollama</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ollama/ollama</span>
    <span class="na">expose</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">11434/tcp</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">11434:11434/tcp</span>
    <span class="na">healthcheck</span><span class="pi">:</span>
      <span class="na">test</span><span class="pi">:</span> <span class="s">ollama --version || exit </span><span class="m">1</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ollama:/root/.ollama</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">ollama</span><span class="pi">:</span>
  <span class="na">open-webui</span><span class="pi">:</span>
</code></pre></div></div>

<p>Ejecuta <code class="language-plaintext highlighter-rouge">docker compose up</code> y listo. Tendrás Ollama y Open WebUI corriendo.</p>

<h4 id="último-y-más-importante-paso">Último y más importante paso❗</h4>

<p>Me pasó que no todos los posts acerca de esto fueron claros con el tema, y por eso decidí escribir esto en mi blog. Ollama es un ‘gestor’ de LLMs y, por defecto, no contiene ninguna imagen. Por lo que, cuando entres por primera vez a <code class="language-plaintext highlighter-rouge">localhost:3000</code>, verás algo como esto:</p>

<p><img src="/assets/images/openwebui.png" alt="" /></p>

<p>Para solucionarlo, tienes que hacer pull de alguna imagen. Para esto <strong>tienes dos opciones</strong>: existe una forma gráfica de hacerlo desde Open WebUI y otra por línea de comandos directamente en el contenedor de Ollama. Esta segunda opción fue la que usé yo; puedes hacerlo simplemente ejecutando la siguiente línea:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec</span> <span class="nt">-it</span> ollama-ollama-1 ollama pull llama3.1
</code></pre></div></div>

<p>Ten en cuenta que el nombre del contenedor dependerá de tus configuraciones; además, probablemente debas ejecutarlo usando <code class="language-plaintext highlighter-rouge">sudo</code>.</p>

<p><strong><a href="https://ollama.com/library">Aquí</a> puedes ver todos los modelos disponibles.</strong></p>

<p>🤓Además, puedes interactuar con el modelo a nivel de línea de comandos. Si entras al contenedor de Ollama, puedes ejecutar <code class="language-plaintext highlighter-rouge">ollama run llama3.1</code> e interactuar sin la necesidad de una UI.🤓</p>

<p>¡Hasta aquí por ahora! Disfruta de tu LLM local.</p>

<h2 id="docker-compose-files-for-development">Docker Compose Files For development</h2>

<p>Acabo de publicar un repositorio incluyendo este archivo de docker compose (archivos que uso a diario para trabajar). Puedes clonarlo y levantar todos los contenedores que necesites para desarrollar tus aplicaciones. También está abierto a contribuciones:</p>

<p><a href="https://codeberg.org/a-chacon/docker-compose-for-development">https://codeberg.org/a-chacon/docker-compose-for-development</a></p>]]></content><author><name>Andrés</name></author><category term="[&quot;Docker&quot;]" /><summary type="html"><![CDATA[Buscando una alternativa segura, más privada y con costo 0 a ChatGPT, llegué a Ollama y Open WebUI. Estas dos herramientas nos permiten correr un ChatGPT localmente.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/ollama.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/ollama.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Creando un API REST con Ruby on Rails. Capítulo 3: Autenticación y Autorización.</title><link href="https://a-chacon.com/on%20rails/2024/08/18/creating-rails-api-cap-3.html" rel="alternate" type="text/html" title="Creando un API REST con Ruby on Rails. Capítulo 3: Autenticación y Autorización." /><published>2024-08-18T00:00:00-04:00</published><updated>2024-08-18T00:00:00-04:00</updated><id>https://a-chacon.com/on%20rails/2024/08/18/creating-rails-api-cap-3</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2024/08/18/creating-rails-api-cap-3.html"><![CDATA[<p>Agregar <strong>autenticación</strong> y <strong>autorización</strong> a tus <strong>APIs</strong> es una parte fundamental en el desarrollo de tus aplicaciones. De estas dos estrategias depende gran parte de la <strong>seguridad</strong> y el correcto acceso a los datos y servicios que ofrecerás.</p>

<p>Para empezar, creo que es importante entender la diferencia entre autenticación y autorización en una API REST. La primera se refiere al proceso de verificar la <strong>identidad</strong> de un usuario o servicio, y la segunda corresponde al proceso de determinar si ese usuario o servicio autenticado tiene <strong>permisos</strong> para acceder al recurso o acción que intenta realizar.</p>

<hr />

<p>¿Dónde estamos?</p>

<ul>
  <li><a href="/on rails/2024/06/16/creating-rails-api-cap-1.html"><strong>Capítulo 1</strong>: Configuración Inicial y Modelos</a></li>
  <li><a href="/on rails/2024/06/23/creating-rails-api-cap-2.html"><strong>Capítulo 2</strong>: Endpoints y Serialización</a></li>
  <li><strong>Capítulo 3</strong>: Autenticación y Autorización &lt;— ¡Estamos aquí!</li>
  <li><strong>Capítulo 4</strong>: Manejo de Errores y Buenas Prácticas</li>
</ul>

<hr />

<h3 id="autenticación">Autenticación</h3>

<p>Para aplicar la autenticación existen algunas estrategias comunes y no es necesario reinventar la rueda, así que para clasificarlas podemos basarnos en las opciones que define <strong>OpenAPI Initiative</strong>:</p>

<ul>
  <li>Esquemas de autenticación HTTP (utilizan el header <code class="language-plaintext highlighter-rouge">Authorization</code>):
    <ul>
      <li><a href="https://swagger.io/docs/specification/authentication/basic-authentication/"><strong>Básico</strong></a>: Autenticación básica donde las credenciales se envían codificadas en base64 en el header <code class="language-plaintext highlighter-rouge">Authorization</code>.</li>
      <li><a href="https://swagger.io/docs/specification/authentication/bearer-authentication/"><strong>Bearer</strong></a>: Autenticación con token Bearer, normalmente usada con OAuth 2.0, donde los tokens se envían en el header <code class="language-plaintext highlighter-rouge">Authorization</code>.</li>
      <li>Otros esquemas HTTP definidos por <a href="https://tools.ietf.org/html/rfc7235">RFC 7235</a> y el <a href="https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml">Registro de Esquemas de Autenticación HTTP</a>.</li>
    </ul>
  </li>
  <li><a href="https://swagger.io/docs/specification/authentication/api-keys/"><strong>API Keys</strong></a> en headers, query string o cookies:
    <ul>
      <li><a href="https://swagger.io/docs/specification/authentication/cookie-authentication/">Autenticación con cookies</a>: Autenticación usando cookies para almacenar identificadores de sesión.</li>
    </ul>
  </li>
  <li><a href="https://swagger.io/docs/specification/authentication/cookie-authentication/"><strong>OAuth 2</strong></a>: Framework de autenticación que permite a aplicaciones de terceros acceder a datos del usuario sin compartir contraseñas.</li>
  <li><a href="https://swagger.io/docs/specification/authentication/openid-connect-discovery/"><strong>OpenID Connect Discovery</strong></a>: Servicio de descubrimiento para OpenID Connect, que facilita la autenticación basada en OAuth.</li>
</ul>

<h3 id="autorización">Autorización</h3>

<p>Lo mismo que la autorización, no reinventaremos la rueda, así que aquí te dejo algunas de las estrategias más comunes:</p>

<ul>
  <li><strong>Role-Based Access Control (RBAC):</strong> Autorización basada en roles predefinidos (ej. admin, editor).</li>
  <li><strong>Attribute-Based Access Control (ABAC):</strong> Control basado en atributos del usuario, recursos y contexto.</li>
  <li><strong>Policy-Based Access Control (PBAC):</strong> Uso de políticas para definir permisos sobre acciones y recursos.</li>
  <li><strong>Access Control Lists (ACLs):</strong> Permisos específicos definidos para usuarios sobre recursos individuales.</li>
  <li><strong>OAuth 2.0 (con scopes):</strong> Autorización de acceso limitado para aplicaciones de terceros en APIs.</li>
</ul>

<h2 id="nuestra-api-rest">Nuestra API REST</h2>

<p>Ahora que entendemos mejor los conceptos, podemos pasar a la práctica. En nuestro caso, utilizaremos <a href="https://github.com/nejdetkadir/devise-api">devise-api</a> para la autenticación (Bearer Token) y <a href="https://github.com/varvet/pundit">Pundit</a> para la autorización (PBAC). Las gemas ya las agregamos a nuestro proyecto en los capítulos anteriores. Pero si no lo has hecho, entonces agrega lo siguiente en alguna parte de tu Gemfile:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gem</span> <span class="s1">'devise'</span>
<span class="n">gem</span> <span class="s1">'devise-api'</span>

<span class="n">gem</span> <span class="s1">'pundit'</span>
</code></pre></div></div>

<h2 id="implementando-la-autenticación">Implementando la autenticación</h2>

<p><a href="/on rails/2024/10/16/poc-using-rails-8-auth-system-in-api-only.html"><strong>💥Ahora puedes usar el generador de autenticación de Rails 8💥</strong></a></p>

<p>Como la autenticación depende de <strong>Devise</strong>, lo primero es que sigamos los pasos de instalación de esta gema:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails generate devise:install
rails generate devise User
</code></pre></div></div>

<p>Luego <strong>devise_api</strong>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails generate devise_api:install
</code></pre></div></div>

<p>Corremos las migraciones:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails db:migrate
</code></pre></div></div>

<p>Y finalmente agregamos el módulo <code class="language-plaintext highlighter-rouge">api</code> al modelo que contiene <strong>Devise</strong>, osea la clase <code class="language-plaintext highlighter-rouge">User</code>. Para este caso podemos eliminar todos los otros módulos y dejar solo los siguientes:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">devise</span> <span class="ss">:database_authenticatable</span><span class="p">,</span> <span class="ss">:api</span>

  <span class="n">has_many</span> <span class="ss">:roles</span>
  <span class="n">has_many</span> <span class="ss">:projects</span><span class="p">,</span> <span class="ss">through: :roles</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Para que los <strong>endpoints</strong> de <code class="language-plaintext highlighter-rouge">devise-api</code> sean expuestos, debemos utilizar el método <code class="language-plaintext highlighter-rouge">devise_for</code> original de <strong>Devise</strong> en el archivo <code class="language-plaintext highlighter-rouge">routes.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
  <span class="n">devise_for</span> <span class="ss">:users</span>
  <span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>revoke_user_tokens  POST  /users/tokens/revoke<span class="o">(</span>.:format<span class="o">)</span>  devise/api/tokens#revoke
refresh_user_tokens POST  /users/tokens/refresh<span class="o">(</span>.:format<span class="o">)</span> devise/api/tokens#refresh
sign_up_user_tokens POST  /users/tokens/sign_up<span class="o">(</span>.:format<span class="o">)</span> devise/api/tokens#sign_up
sign_in_user_tokens POST  /users/tokens/sign_in<span class="o">(</span>.:format<span class="o">)</span> devise/api/tokens#sign_in
info_user_tokens    GET   /users/tokens/info<span class="o">(</span>.:format<span class="o">)</span>    devise/api/tokens#info
</code></pre></div></div>

<p>Ahora puedes utilizar los siguientes <strong>helpers</strong> en los controladores para proteger los endpoints con autenticación. En el caso de nuestro ejemplo, queremos cubrir toda la API, así que podemos agregarlos en el <code class="language-plaintext highlighter-rouge">ApplicationController</code> para que los demás controladores los hereden. Además, agregaré un método que nos puede ser útil:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">API</span>
  <span class="n">skip_before_action</span> <span class="ss">:verify_authenticity_token</span><span class="p">,</span> <span class="ss">raise: </span><span class="kp">false</span>
  <span class="n">before_action</span> <span class="ss">:authenticate_devise_api_token!</span>

  <span class="k">def</span> <span class="nf">current_user</span>
    <span class="n">current_devise_api_user</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h4 id="tests">Tests</h4>

<p>En este punto, casi todos los <strong>tests</strong> deberían estar fallando. Necesitaremos agregarles autenticación. Para facilitarnos esta tarea, te propongo que agregues la siguiente <strong>factoría</strong>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test/factories/devise_api_token.rb</span>
<span class="no">FactoryBot</span><span class="p">.</span><span class="nf">define</span> <span class="k">do</span>
  <span class="n">factory</span> <span class="ss">:devise_api_token</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"Devise::Api::Token"</span> <span class="k">do</span>
    <span class="n">association</span> <span class="ss">:resource_owner</span><span class="p">,</span> <span class="ss">factory: :user</span>
    <span class="n">access_token</span> <span class="p">{</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span> <span class="p">}</span>
    <span class="n">refresh_token</span> <span class="p">{</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">hex</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span> <span class="p">}</span>
    <span class="n">expires_in</span> <span class="p">{</span> <span class="mi">1</span><span class="p">.</span><span class="nf">hour</span><span class="p">.</span><span class="nf">to_i</span> <span class="p">}</span>

    <span class="n">trait</span> <span class="ss">:access_token_expired</span> <span class="k">do</span>
      <span class="n">created_at</span> <span class="p">{</span> <span class="mi">2</span><span class="p">.</span><span class="nf">hours</span><span class="p">.</span><span class="nf">ago</span> <span class="p">}</span>
    <span class="k">end</span>

    <span class="n">trait</span> <span class="ss">:refresh_token_expired</span> <span class="k">do</span>
      <span class="n">created_at</span> <span class="p">{</span> <span class="mi">2</span><span class="p">.</span><span class="nf">months</span><span class="p">.</span><span class="nf">ago</span> <span class="p">}</span>
    <span class="k">end</span>

    <span class="n">trait</span> <span class="ss">:revoked</span> <span class="k">do</span>
      <span class="n">revoked_at</span> <span class="p">{</span> <span class="mi">5</span><span class="p">.</span><span class="nf">minutes</span><span class="p">.</span><span class="nf">ago</span> <span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Luego tienes que crear <strong>tokens</strong> y enviarlos junto a las peticiones <strong>HTTP</strong>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">class</span> <span class="nc">ProjectsControllerTest</span> <span class="o">&lt;</span> <span class="no">ActionDispatch</span><span class="o">::</span><span class="no">IntegrationTest</span>
  <span class="n">setup</span> <span class="k">do</span>
    <span class="c1"># ...</span>
    <span class="vi">@token</span> <span class="o">=</span> <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:devise_api_token</span><span class="p">).</span><span class="nf">access_token</span>
  <span class="k">end</span>

  <span class="nb">test</span> <span class="s1">'should get index'</span> <span class="k">do</span>
    <span class="n">get</span> <span class="n">projects_url</span><span class="p">,</span> <span class="ss">headers: </span><span class="p">{</span> <span class="no">Authorization</span><span class="p">:</span> <span class="s2">"Bearer </span><span class="si">#{</span><span class="vi">@token</span><span class="si">}</span><span class="s2">"</span> <span class="p">},</span> <span class="ss">as: :json</span>
    <span class="n">assert_response</span> <span class="ss">:success</span>
  <span class="k">end</span>

  <span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="implementando-la-autorización">Implementando la Autorización</h3>

<p>El problema del capítulo 1 mencionaba varias condiciones, todas válidas, pero para simplificarnos la tarea y con el fin de una demostración, solo vamos a cubrir la autorización de <strong>Tareas</strong>. Solo los <code class="language-plaintext highlighter-rouge">User</code> con el rol de <strong>administrador</strong> en el proyecto podrán crearlas (create), el resto no.</p>

<p>Antes de continuar, debemos asegurarnos de haber seguido los pasos de instalación de <strong>Pundit</strong>. Estos son:</p>

<ul>
  <li>
    <p>Agrega el módulo al ApplicationController:</p>

    <div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
  <span class="kp">include</span> <span class="no">Pundit</span><span class="o">::</span><span class="no">Authorization</span>
  <span class="c1">#... También deberías manejar los errores de Unauthorized:</span>
  <span class="n">rescue_from</span> <span class="no">Pundit</span><span class="o">::</span><span class="no">NotAuthorizedError</span><span class="p">,</span> <span class="ss">with: :user_not_authorized</span>
  <span class="c1">#...</span>
  <span class="kp">private</span>
  <span class="k">def</span> <span class="nf">user_not_authorized</span><span class="p">(</span><span class="n">exception</span><span class="p">)</span>
    <span class="n">policy_name</span> <span class="o">=</span> <span class="n">exception</span><span class="p">.</span><span class="nf">policy</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">underscore</span>
    <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">message: </span><span class="s2">"</span><span class="si">#{</span><span class="n">policy_name</span><span class="si">}</span><span class="s2">.</span><span class="si">#{</span><span class="n">exception</span><span class="p">.</span><span class="nf">query</span><span class="si">}</span><span class="s2">"</span> <span class="p">},</span> <span class="ss">status: :forbidden</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p>Creamos un ApplicationPolicy que servirá de base:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g pundit:install
</code></pre></div>    </div>
  </li>
</ul>

<p>Y listo. Hagamos un test que falle:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">#test/controllers/task_controllers_test.rb</span>

  <span class="c1"># ...</span>
  <span class="nb">test</span> <span class="s1">'should create task'</span> <span class="k">do</span>
    <span class="n">token</span> <span class="o">=</span> <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:devise_api_token</span><span class="p">)</span>

    <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:role</span><span class="p">,</span> <span class="ss">user: </span><span class="n">token</span><span class="p">.</span><span class="nf">resource_owner</span><span class="p">,</span> <span class="ss">role: :manager</span><span class="p">,</span> <span class="ss">project: </span><span class="vi">@project</span><span class="p">)</span>

    <span class="n">assert_difference</span><span class="p">(</span><span class="s1">'Task.count'</span><span class="p">)</span> <span class="k">do</span>
      <span class="n">post</span> <span class="n">project_tasks_url</span><span class="p">(</span><span class="vi">@project</span><span class="p">),</span> <span class="ss">headers: </span><span class="p">{</span> <span class="no">Authorization</span><span class="p">:</span> <span class="s2">"Bearer </span><span class="si">#{</span><span class="n">token</span><span class="p">.</span><span class="nf">access_token</span><span class="si">}</span><span class="s2">"</span> <span class="p">},</span>
                                        <span class="ss">params: </span><span class="p">{</span> <span class="ss">task: </span><span class="p">{</span> <span class="ss">description: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">description</span><span class="p">,</span> <span class="ss">project_id: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">project_id</span><span class="p">,</span> <span class="ss">status: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">status</span><span class="p">,</span> <span class="ss">title: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">title</span> <span class="p">}</span> <span class="p">},</span> <span class="ss">as: :json</span>
    <span class="k">end</span>

    <span class="n">assert_response</span> <span class="ss">:created</span>
  <span class="k">end</span>

  <span class="nb">test</span> <span class="s1">'should not create task'</span> <span class="k">do</span>
    <span class="n">token</span> <span class="o">=</span> <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:devise_api_token</span><span class="p">)</span>

    <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:role</span><span class="p">,</span> <span class="ss">user: </span><span class="n">token</span><span class="p">.</span><span class="nf">resource_owner</span><span class="p">,</span> <span class="ss">role: :contributor</span><span class="p">,</span> <span class="ss">project: </span><span class="vi">@project</span><span class="p">)</span>

    <span class="n">assert_no_difference</span><span class="p">(</span><span class="s1">'Task.count'</span><span class="p">)</span> <span class="k">do</span>
      <span class="n">post</span> <span class="n">project_tasks_url</span><span class="p">(</span><span class="vi">@project</span><span class="p">),</span> <span class="ss">headers: </span><span class="p">{</span> <span class="no">Authorization</span><span class="p">:</span> <span class="s2">"Bearer </span><span class="si">#{</span><span class="n">token</span><span class="p">.</span><span class="nf">access_token</span><span class="si">}</span><span class="s2">"</span> <span class="p">},</span>
                                        <span class="ss">params: </span><span class="p">{</span> <span class="ss">task: </span><span class="p">{</span> <span class="ss">description: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">description</span><span class="p">,</span> <span class="ss">project_id: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">project_id</span><span class="p">,</span> <span class="ss">status: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">status</span><span class="p">,</span> <span class="ss">title: </span><span class="vi">@task</span><span class="p">.</span><span class="nf">title</span> <span class="p">}</span> <span class="p">},</span> <span class="ss">as: :json</span>
    <span class="k">end</span>

    <span class="n">assert_response</span> <span class="ss">:forbidden</span>
  <span class="k">end</span>

  <span class="c1"># ...</span>
</code></pre></div></div>

<p>Ahora hagamos que funcionen. Pundit se maneja con archivos <em>Policy</em>, debemos crear uno siguiendo las instrucciones de la gema, heredando de nuestra <code class="language-plaintext highlighter-rouge">ApplicationPolicy</code> e implementando nuestra lógica de validación (también puedes usar el generador):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/policies/post_policy.rb</span>
<span class="k">class</span> <span class="nc">TaskPolicy</span> <span class="o">&lt;</span> <span class="no">ApplicationPolicy</span>
  <span class="k">def</span> <span class="nf">create?</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">roles</span><span class="p">.</span><span class="nf">exists?</span><span class="p">(</span><span class="ss">project: </span><span class="n">record</span><span class="p">.</span><span class="nf">project</span><span class="p">,</span> <span class="ss">role: :manager</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Agregamos la autorización al método del controlador:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/tasks_controller.rb</span>
  <span class="c1"># ...</span>
  <span class="c1"># POST /tasks</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="c1"># ...</span>
    <span class="n">authorize</span> <span class="vi">@task</span>
    <span class="c1"># ...</span>
  <span class="k">end</span>
  <span class="c1"># ...</span>
</code></pre></div></div>

<p>Deberíamos obtener un lindo color verde en todos nuestros tests.</p>

<hr />

<p>Como podemos ver, aplicar autorización y autenticación en nuestra API se hace fácil con <em>Devise</em> y <em>Pundit</em>. Te recomiendo que veas las documentaciones oficiales para entender más a fondo todo lo que puedes llegar a realizar con estas dos gemas y un poco de ingenio.</p>

<p>Hemos visto una pequeña introducción a la autenticación utilizando un <em>Bearer token</em> y la autorización basada en políticas y roles cuando desarrollamos APIs. Ruby on Rails es un framework espectacular y espero que algo bueno hayas sacado de todo este post.</p>

<p>¡Happy coding!</p>

<p>El repo con todo el código: <a href="https://github.com/a-chacon/api-project-management-example">https://github.com/a-chacon/api-project-management-example</a></p>]]></content><author><name>Andrés</name></author><category term="On Rails" /><summary type="html"><![CDATA[Descubre cómo asegurar tu API en Ruby on Rails con Devise para autenticación y Pundit para autorización. Cubriremos lo esencial para proteger tus endpoints y asegurar que solo los usuarios autorizados tengan acceso.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/apirestcap3.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/apirestcap3.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Automatiza Tus Lanzamientos de Ruby Gems</title><link href="https://a-chacon.com/rubygem/2024/08/04/automate-ruby-gem-release-with-github-actions.html" rel="alternate" type="text/html" title="Automatiza Tus Lanzamientos de Ruby Gems" /><published>2024-08-04T00:00:00-04:00</published><updated>2024-08-04T00:00:00-04:00</updated><id>https://a-chacon.com/rubygem/2024/08/04/automate-ruby-gem-release-with-github-actions</id><content type="html" xml:base="https://a-chacon.com/rubygem/2024/08/04/automate-ruby-gem-release-with-github-actions.html"><![CDATA[<p>Hace unos días lancé <a href="https://github.com/a-chacon/oas_rails">OasRails</a>, un engine de Rails para generar <strong>documentación interactiva</strong> y de forma simple para tus <strong>APIs</strong>. Un motor de Rails es una gema, y como tal debe ser empaquetada y subida a un servicio de alojamiento de gemas, en este caso <a href="https://rubygems.org/">RubyGems</a>. Así que me topé con la necesidad de automatizar el proceso de lanzamientos y fue así como llegué a <a href="https://github.com/googleapis/release-please">release-please</a>.</p>

<p>Release Please, como su repositorio dice, es una herramienta para automatizar la <strong>generación del CHANGELOG</strong>, la creación de <strong>releases en GitHub</strong>, y los <strong>incrementos de versión</strong> para tus proyectos. Y la mejor forma de correr Release Please es mediante <a href="https://docs.github.com/en/actions">GitHub Actions</a>.</p>

<p><strong>¿Por qué este blog post si ya existen varios?</strong> La verdad es que no encontré ninguno que hable sobre cómo configurar la versión 4 de Release Please.</p>

<p>Con esto dicho, aquí te dejo los pasos a seguir:</p>

<ol>
  <li>
    <p>Crear el archivo con la definición del workflow en la ruta <code class="language-plaintext highlighter-rouge">.github/workflows/release-please.yml</code> y dentro del archivo agregar esto:</p>

    <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">main</span>

<span class="na">permissions</span><span class="pi">:</span>
  <span class="na">contents</span><span class="pi">:</span> <span class="s">write</span>
  <span class="na">pull-requests</span><span class="pi">:</span> <span class="s">write</span>
  <span class="na">id-token</span><span class="pi">:</span> <span class="s">write</span>

<span class="na">name</span><span class="pi">:</span> <span class="s">release-please</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">release-please</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">googleapis/release-please-action@v4</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">release</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">token</span><span class="pi">:</span> <span class="s">$</span>
          <span class="na">config-file</span><span class="pi">:</span> <span class="s">.release-please-config.json</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Set up Ruby</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">ruby/setup-ruby@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">bundler-cache</span><span class="pi">:</span> <span class="kc">true</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">$</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">rubygems/release-gem@v1</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">$</span>
</code></pre></div>    </div>

    <p>Elige tu <strong>rama principal</strong> desde la cual deseas realizar los lanzamientos. Luego, para que esto funcione, debemos realizar los siguientes pasos, así que espera y no hagas el commit aún.</p>
  </li>
  <li>
    <p>Crea un <strong>secret</strong> en tu repositorio con el nombre de <code class="language-plaintext highlighter-rouge">RELEASE_PLEASE_TOKEN</code> o el que tú prefieras, solo no te olvides de actualizar el nombre que está definido en el archivo anterior. Si no sabes cómo agregar un secret, <a href="https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions">aquí te dejo una guía.</a></p>
  </li>
  <li>
    <p>Habilita la opción para que <strong>GitHub Actions</strong> pueda crear <strong>PR</strong> en tu repositorio. Esto lo realizas en <strong>Settings -&gt; Actions -&gt; General</strong> y habilitas la opción que dice más o menos así: Permitir que las acciones de GitHub creen y aprueben <strong>pull requests</strong>.</p>

    <p><img src="https://jhale.dev/assets/img/posts/auto_merging_prs/org_actions_prs_permissions.png" alt="Ejemplo de dónde encontrar la configuración para permitir que GitHub Actions cree PR" /></p>
  </li>
  <li>
    <p>Release Please se encarga de documentar el release, pero la publicación de la gema en <strong>RubyGems</strong> es hecha por la action <a href="https://github.com/rubygems/release-gem">rubygems/release-gem</a>. Para que pueda hacerlo, debes agregarlo a <a href="https://guides.rubygems.org/trusted-publishing/">Trusted Publishing</a>.</p>
  </li>
  <li>
    <p>Finalmente, los archivos de configuración que necesita Release Please y que debes agregar a la raíz de tu repositorio:</p>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">.release-please-config.json</code> con un contenido como el siguiente:</li>
    </ul>

    <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{</span>
  <span class="s2">"</span><span class="s">release-type"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ruby"</span><span class="pi">,</span>
  <span class="s2">"</span><span class="s">packages"</span><span class="pi">:</span>
    <span class="pi">{</span>
      <span class="s2">"</span><span class="s">."</span><span class="pi">:</span>
        <span class="pi">{</span>
          <span class="s2">"</span><span class="s">release-type"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ruby"</span><span class="pi">,</span>
          <span class="s2">"</span><span class="s">package-name"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">YOUR</span><span class="nv"> </span><span class="s">RUBY</span><span class="nv"> </span><span class="s">GEM</span><span class="nv"> </span><span class="s">NAME"</span><span class="pi">,</span>
          <span class="s2">"</span><span class="s">version-file"</span><span class="pi">:</span> <span class="s2">"</span><span class="s">lib/YOUR</span><span class="nv"> </span><span class="s">RUBY</span><span class="nv"> </span><span class="s">GEM</span><span class="nv"> </span><span class="s">NAME/version.rb"</span><span class="pi">,</span>
        <span class="pi">},</span>
    <span class="pi">},</span>
<span class="pi">}</span>
</code></pre></div>    </div>

    <ul>
      <li><code class="language-plaintext highlighter-rouge">.release-please-manifest.json</code> con la version de tu gema:</li>
    </ul>

    <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{</span> <span class="s2">"</span><span class="s">."</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0.3.0"</span> <span class="pi">}</span>
</code></pre></div>    </div>
  </li>
</ol>

<p><strong>Te recomiendo que leas las documentaciones oficiales de cada herramienta que se usa en el proceso.</strong></p>

<p>Release Please sigue las indicaciones de <a href="https://www.conventionalcommits.org/en/v1.0.0/">Conventional Commits</a> para crear tu changelog y saber cuándo pasar de versión, por lo que debes seguir esta convención para que todo funcione de forma correcta.</p>

<p>Con estos simples pasos podrás tener un <strong>release automático</strong>, donde lo único que necesitarás es aprobar el PR cuando consideres que está listo. El proceso será: El workflow corre una vez y genera un PR, si ya está, lo actualiza. Segundo paso, apruebas el PR y se ejecutará la segunda parte del workflow, la parte de publicar la gema. Y con eso se termina el proceso.</p>

<p>Espero que te sirva de ayuda. Para mí fue un día completo de investigación para llegar a hacerlo funcionar, puesto que los posts que encontré lo hacían con la versión 2 y la publicación de la gema era de una forma menos segura. Pero valió la pena, se hace bastante simple el proceso, puedes hacer los releases que necesites y no tendrás que preocuparte de mantener el changelog actualizado o construir la gema para publicarla.</p>]]></content><author><name>Andrés</name></author><category term="RubyGem" /><summary type="html"><![CDATA[Descubre cómo automatizar los lanzamientos y la publicación de tus gemas en RubyGems usando release-please y GitHub Actions.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/ruby.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/ruby.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Generar Documentación para tu API de Rails: Construí Mi Propia Herramienta</title><link href="https://a-chacon.com/on%20rails/2024/07/25/documenting-rails-apis.html" rel="alternate" type="text/html" title="Generar Documentación para tu API de Rails: Construí Mi Propia Herramienta" /><published>2024-07-25T00:00:00-04:00</published><updated>2024-07-25T00:00:00-04:00</updated><id>https://a-chacon.com/on%20rails/2024/07/25/documenting-rails-apis</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2024/07/25/documenting-rails-apis.html"><![CDATA[<p><strong>La documentación de una API es clave para que todo funcione bien.</strong> Ayuda a los desarrolladores a entender cómo usarla, reduciendo errores y haciendo la integración mucho más rápida. <strong>Una buena documentación no solo facilita las cosas, sino que también hace que la API sea más atractiva y agradable de usar. Sin una documentación clara, incluso la mejor API puede ser un problema para los desarrolladores.</strong></p>

<h2 id="la-necesidad"><strong>La necesidad</strong></h2>

<p>Después de experimentar la <strong>documentación interactiva</strong> de <strong>FastAPI en Python</strong>, quise buscar una <strong>funcionalidad similar para mis APIs con Rails en el ecosistema de Ruby.</strong> Encontré algunos proyectos, pero ninguno cumplió al 100% con mis expectativas. La idea era tener una <strong>herramienta fácil de usar, sin la necesidad de aprender un nuevo DSL.</strong> Una solución que generara <strong>documentación interactiva con una interfaz agradable. Dinámica, cada vez que hiciera un cambio, poder experimentarlo en la interfaz simplemente recargándola.</strong></p>

<p>Creo que algunas veces no sabemos lo que necesitamos hasta que lo conocemos, o algo así me pasó. Antes simplemente usaba <strong>Postman.</strong> Después de programar cada endpoint, me iba a <strong>Postman</strong> y documentaba con lo más básico cada endpoint (muchas veces repetía información). Pero somos programadores, ¿por qué no hacernos el camino más fácil? <strong>¿Por qué el código no logra ser lo bastante expresivo como para documentarse a sí mismo?</strong> O al menos hacer el intento.</p>

<h2 id="soluciones"><strong>Soluciones</strong></h2>

<p>Dentro de las soluciones que encontré, creo que la más conocida y más completa es <strong>ApiPie</strong>, muy cerca de lo que buscaba, pero una de las grandes limitantes es que no logra generar un <strong>OAS &gt; 2.0</strong> hasta el momento, debes aprender un <strong>DSL</strong> y algo no menos importante: carece de una buena interfaz.</p>

<p>Algunas de las otras soluciones:</p>

<ul>
  <li><strong><a href="https://github.com/livingsocial/swagger_yard-rails">swagger_yard-rails</a></strong>: Parece abandonado, pero sirve como inspiración.</li>
  <li><strong><a href="https://github.com/rswag/rswag">Rswag</a></strong>: No es automático, depende de <strong>RSpec;</strong> muchos desarrolladores ahora usan <strong>Minitest</strong>, ya que es el framework de pruebas predeterminado.</li>
  <li><strong><a href="https://github.com/ruby-grape/grape-swagger">grape-swagger</a></strong>: Requiere <strong>Grape.</strong></li>
  <li><strong><a href="https://github.com/zipmark/rspec_api_documentation">rspec_api_documentation</a></strong>: Requiere <strong>RSpec</strong> y un comando para generar la documentación.</li>
</ul>

<p><img src="/assets/images/reddit-api-doc.png" alt="" /></p>

<h2 id="mi-herramienta"><strong>Mi Herramienta</strong></h2>

<p>Así que ahora que tengo tiempo (sin trabajo a tiempo completo) y luego de años de búsqueda —digo años porque hace algo más de dos ya había escrito una <a href="https://stackoverflow.com/questions/71947018/is-there-a-way-to-generate-an-interactive-documentation-for-rails-apis">pregunta en Stack Overflow</a>—, me decidí a crear mi propia herramienta:</p>

<h3 id="oasrails"><a href="https://github.com/a-chacon/oas_rails"><strong>OasRails</strong></a></h3>

<p><strong>OasRails es un engine para Rails que genera un OAS v3.1</strong> a partir de las rutas de la aplicación, examina el código fuente y los comentarios de cada método para tratar de generar una documentación, y finalmente lo despliega usando <strong>RapiDoc.</strong> Todo esto de forma dinámica, con poco esfuerzo y en un solo proyecto.</p>

<h4 id="características"><strong>Características</strong></h4>

<p>Cuando buscaba una solución que fuese <code class="language-plaintext highlighter-rouge">dinámica</code>, me refería a que la documentación se generara al momento de la consulta y no preconstruirla con un comando. <strong>OasRails</strong> construye el documento con la especificación cada vez que consultas el endpoint (en producción esto debería cambiar).</p>

<p>Cuando hablaba de una solución <code class="language-plaintext highlighter-rouge">automática</code>, me refería a extraer información que ya está en el código. Por ejemplo, <strong>OasRails</strong> puede:</p>

<ul>
  <li>Construir los nombres de tus rutas según el nombre del método y del controlador.</li>
  <li>Detectar las posibles respuestas de tus endpoints según los <code class="language-plaintext highlighter-rouge">render</code> que existen en el código fuente.</li>
  <li>Etiquetar tus rutas según el namespace.</li>
  <li>Extraer ejemplos de request body desde fixtures o FactoryBot (por implementar).</li>
</ul>

<p>Cuando decía <code class="language-plaintext highlighter-rouge">fácil de usar</code>, me refería a que solo debes comentar tu código para documentar. No necesitas <strong>RSpec</strong>, ni un <strong>DSL</strong>, ni <strong>Grape</strong> (nada personal contra el proyecto, lo he usado y me gusta).</p>

<p>En cuanto a la característica <code class="language-plaintext highlighter-rouge">interactiva</code>, eso se lo dejamos a <a href="https://rapidocweb.com/">RapiDoc</a>, que está montado en una vista del engine mediante <strong>CDN.</strong></p>

<iframe src="https://ghbtns.com/github-btn.html?user=a-chacon&amp;repo=oas_rails&amp;type=star&amp;count=false&amp;size=large" frameborder="0" scrolling="0" width="170" height="30" title="GitHub"></iframe>

<p><a href="https://github.com/a-chacon/oas_rails">Repositorio Del Proyecto</a></p>

<hr />

<h2 id="ejemplo-practico">Ejemplo practico</h2>

<p>Creamos un API con el siguiente comando:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails new api-example --api
</code></pre></div></div>

<p>Agreguemos recursos y sus endpoints rápidamente con los siguientes comandos:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g scaffold user name:string email:string age:integer

rails g scaffold project title:string description:text user:references

rails g scaffold task title:string description:text status:string project:references
</code></pre></div></div>

<p>Ahora agregamos OasRails al Gemfile:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem 'oas_rails'
</code></pre></div></div>

<p>Montamos el engine en el archivo <code class="language-plaintext highlighter-rouge">routes.rb</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mount OasRails::Engine =&gt; '/docs'
</code></pre></div></div>

<p>Corremos las migraciones con <code class="language-plaintext highlighter-rouge">rails db:migrate</code>, instalamos las dependencias con <code class="language-plaintext highlighter-rouge">bundle install</code> y finalmente levantamos el proyecto con <code class="language-plaintext highlighter-rouge">rails s</code>. Podrémos visitar la documentación en <code class="language-plaintext highlighter-rouge">http://localhost:3000/docs</code>.</p>

<p><img src="/assets/images/api-example-docs.png" alt="" /></p>

<p>Cada endpoint queda documentado al menos con título, ruta, request body y posibles respuestas:</p>

<p><img src="/assets/images/api-example-doc-endpoint.png" alt="" /></p>

<p><strong>Tener en cuenta que toda esta información no es posible extraerla en casos más reales donde las APIs varían en estructura y ahí es necesario proveer comentarios en cada endpoint.</strong></p>

<p><a href="https://github.com/a-chacon/api-example">Repositorio Del Proyecto de Ejemplo</a></p>

<hr />

<h2 id="futuro-del-proyecto"><strong>Futuro del Proyecto</strong></h2>

<p>El proyecto está recién en una etapa inicial, creo que le falta mucho para ser una <strong>herramienta estable y segura.</strong> Pero algunos de los puntos que pretendo ir mejorando son:</p>

<ul>
  <li><strong>Limpiar, documentar y estructurar el código</strong></li>
  <li><strong>Soporte para la documentación de métodos de autenticación</strong></li>
  <li><strong>Definir etiquetas globales/configuración</strong> (por ejemplo, respuestas comunes como errores 404)</li>
  <li><strong>Post-procesar el JSON y reemplazar objetos comunes con referencias a componentes</strong></li>
  <li><strong>Crear un archivo temporal con el JSON en modo producción</strong> para evitar reconstruirlo en cada solicitud</li>
  <li><strong>Crear etiquetas para gems populares usadas en APIs</strong> (por ejemplo, una etiqueta <code class="language-plaintext highlighter-rouge">@pagy</code> para parámetros de paginación comunes)</li>
  <li><strong>Agregar autenticación básica a OAS y UI por razones de seguridad</strong></li>
  <li><strong>Implementar la capacidad de definir OAS por namespaces</strong> (por ejemplo, generar OAS para rutas específicas como <code class="language-plaintext highlighter-rouge">/api</code> o separar versiones V1 y V2)</li>
</ul>

<hr />

<p><strong>Crear una buena documentación es esencial para el éxito de cualquier API.</strong> <strong>OasRails</strong> es mi intento de hacer este proceso más simple y efectivo para la comunidad de Rails. Si te interesa, te invito a probarlo y a contribuir al proyecto.</p>]]></content><author><name>Andrés</name></author><category term="[&quot;On Rails&quot;]" /><summary type="html"><![CDATA[Después de buscar una herramienta definitiva para documentar las API que desarrollo con Rails, que fuera simple, interactiva y fácil de usar, y no encontrar la adecuada, decidí crear la mía propia: OasRails.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/oas_rails_ui.png" /><media:content medium="image" url="https://a-chacon.com/assets/images/oas_rails_ui.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Transforme Mí Sitio Web en una Plantilla de Jekyll.</title><link href="https://a-chacon.com/jekyll/2024/07/22/release-awesome-jekyll-theme.html" rel="alternate" type="text/html" title="Transforme Mí Sitio Web en una Plantilla de Jekyll." /><published>2024-07-22T00:00:00-04:00</published><updated>2024-07-22T00:00:00-04:00</updated><id>https://a-chacon.com/jekyll/2024/07/22/release-awesome-jekyll-theme</id><content type="html" xml:base="https://a-chacon.com/jekyll/2024/07/22/release-awesome-jekyll-theme.html"><![CDATA[<p>Mi sitio web anterior me generaba complicaciones cada vez que necesitaba hacer modificaciones gráficas, ya que había mucho código repetido, muchas páginas duplicadas por causa de <strong>Polyglot y características que quería renovar</strong>. Así que comencé escribiendo un tema para Jekyll a partir de mi sitio web. Además, quería contribuir a la comunidad, así que espero que a alguien más le sirva mi tema.</p>

<h2 id="awesome-jekyll-theme">Awesome-jekyll-theme</h2>

<p>Es un tema que incluye un <strong>diseño limpio y minimalista</strong>, con opciones de diseño versátiles y una navegación consistente. <strong>Es responsivo</strong>, adaptándose bien a diferentes dispositivos, y organiza el contenido del blog por categorías para una mejor estructura del sitio.</p>

<p>Al igual que mi sitio web, lo hice con <a href="https://tailwindcss.com/">TailwindCSS</a>, por lo que parte del código fue copiar y pegar, pero otras partes no. La idea fue siempre realizar un tema que <strong>soporte múltiples lenguajes</strong>, por lo que lo construí con Polyglot en mente. Igualmente, no es un requisito hacer uso de esa funcionalidad. Posee plantillas para el Home, Blog, Post, Projects y un Navbar y Footer personalizables.</p>

<p><strong>Aún queda trabajo por hacer</strong>. Me gustaría documentar más el uso de las plantillas, agregar algunas microinteracciones para hacerlo más dinámico, intuitivo y divertido, y darle soporte completo a un modo oscuro.</p>

<h4 id="te-invito-a-revisar-el-proyecto-aquí"><a href="https://github.com/a-chacon/awesome-jekyll-theme">Te invito a revisar el proyecto aquí</a></h4>

<hr />

<h3 id="contribuir">Contribuir</h3>

<p>Si te gusta y te sirve, <strong>considera aportar con algún feedback</strong>, ya sea por medio de un Issue o un PR. Cualquier tipo de retroalimentación es bienvenida y todos los aportes contribuyen a mejorar el tema. Ya sea para reportar un error, sugerir una nueva funcionalidad o mejorar la documentación.</p>

<iframe src="https://giphy.com/embed/xT9IgHq4eDQKKCHqAo" width="480" height="480" style="margin: auto;" frameborder="0" class="giphy-embed" allowfullscreen=""></iframe>
<p><a href="https://giphy.com/gifs/myspace-website-memories-xT9IgHq4eDQKKCHqAo">via GIPHY</a></p>

<hr />

<p>¡Espero que alguien lo disfrute tanto como yo disfruté creándolo!</p>]]></content><author><name>Andrés</name></author><category term="[&quot;Jekyll&quot;]" /><summary type="html"><![CDATA[Después de algunos meses de trabajo, finalmente tuve el tiempo para terminar el proyecto y subir la primera versión de awesome-jekyll-theme, una plantilla para Jekyll basada en mi sitio web personal.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://raw.githubusercontent.com/a-chacon/awesome-jekyll-theme/main/screenshots/main.png" /><media:content medium="image" url="https://raw.githubusercontent.com/a-chacon/awesome-jekyll-theme/main/screenshots/main.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">ActiveRecord::Enum persistido en un campo string. ¿Es una mala idea?</title><link href="https://a-chacon.com/on%20rails/2024/06/30/rails-enum-persisted-on-string-field.html" rel="alternate" type="text/html" title="ActiveRecord::Enum persistido en un campo string. ¿Es una mala idea?" /><published>2024-06-30T00:00:00-04:00</published><updated>2024-06-30T00:00:00-04:00</updated><id>https://a-chacon.com/on%20rails/2024/06/30/rails-enum-persisted-on-string-field</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2024/06/30/rails-enum-persisted-on-string-field.html"><![CDATA[<p>Hubo un momento en mis aplicaciones en el que dejé de persistir los campos que iba a definir como enum en campos <code class="language-plaintext highlighter-rouge">int</code> y comencé a hacerlo en <code class="language-plaintext highlighter-rouge">strings</code>. Tomé esta decisión porque, en varias ocasiones, me pidieron un volcado de la base de datos para su análisis, y cada vez que se encontraban con un campo <code class="language-plaintext highlighter-rouge">status</code> (o cualquier otro usando <code class="language-plaintext highlighter-rouge">enum</code>) con valores como 0, 1 o 2 me terminaban preguntando sobre su significado. Por lo que, en vez de usar valores numéricos que carecen de sentido y contexto, los cambié directamente por un <code class="language-plaintext highlighter-rouge">string</code> que en sí mismo tiene sentido y da contexto.</p>

<p>Pero hace unos días volví a revisar la <a href="https://api.rubyonrails.org/classes/ActiveRecord/Enum.html">documentación</a> y encontré una frase que me hizo cuestionarme esto:</p>

<blockquote>
  <p>Finally it’s also possible to use a string column to persist the enumerated value. <strong>Note that this will likely lead to slower database queries</strong></p>
</blockquote>

<p>¡Usarlo terminaría en consultas más lentas a la base de datos!</p>

<p>Así que, como buen programador, preocupado de la eficiencia y el rendimiento de mis aplicaciones 🤓, me cuestioné lo que estaba haciendo y, antes de realizar algún cambio, quise comprobar esto. Creé una <a href="https://gitlab.com/a-chacon/api-benchmark">aplicación</a> simple en Rails conectada a MySQL con dos modelos, uno con un enum persistido en <code class="language-plaintext highlighter-rouge">int</code> y otro persistido como <code class="language-plaintext highlighter-rouge">varchar</code> con 100,000 registros cada uno. Cerré todas las aplicaciones que podían molestar y ejecuté un benchmark:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                                       user     system      total        real
String Enum Count:                 0.477732   0.052428   0.530160 <span class="o">(</span> 21.794783<span class="o">)</span>
Integer Enum Count:                0.374897   0.030260   0.405157 <span class="o">(</span> 21.639400<span class="o">)</span>
String Enum Paginated Index:       0.351621   0.017249   0.368870 <span class="o">(</span>  0.639043<span class="o">)</span>
Integer Enum Paginated Index:      0.317277   0.022936   0.340213 <span class="o">(</span>  0.524883<span class="o">)</span>
String Enum Single Record Fetch:   0.294010   0.031218   0.325228 <span class="o">(</span>  0.489015<span class="o">)</span>
Integer Enum Single Record Fetch:  0.297743   0.015502   0.313245 <span class="o">(</span>  0.497845<span class="o">)</span>
</code></pre></div></div>

<p>Todos los resultados fueron muy parecidos, las variaciones de tiempo en las 1,000 ejecuciones de consultas simples fueron pequeñas. Si entiendo bien, podríamos tomar el caso de las consultas <code class="language-plaintext highlighter-rouge">count</code>. Tuvieron una diferencia de 0.155383s, o sea, 0.1ms de ventaja para <code class="language-plaintext highlighter-rouge">int</code> por consulta aproximadamente. Pero para un <code class="language-plaintext highlighter-rouge">select</code> con <code class="language-plaintext highlighter-rouge">limit(1)</code> ganó el <code class="language-plaintext highlighter-rouge">string</code>. Entonces me pregunto, ¿realmente un <code class="language-plaintext highlighter-rouge">enum</code> persistido como <code class="language-plaintext highlighter-rouge">string</code> terminará en consultas más lentas?</p>

<iframe src="https://giphy.com/embed/Dh5q0sShxgp13DwrvG" width="480" height="298" style="" frameborder="0" class="giphy-embed" allowfullscreen=""></iframe>
<p><a href="https://giphy.com/gifs/scaler-official-dogs-computer-typing-Dh5q0sShxgp13DwrvG">via GIPHY</a></p>

<p>Bueno, sea más lento o no, creo que en el común de las aplicaciones terminará con diferencias de milisegundos que no serán tan importantes. Por lo que mi razón inicial de comenzar a usar <code class="language-plaintext highlighter-rouge">enums</code> persistidos como <code class="language-plaintext highlighter-rouge">strings</code> se mantiene como lo más importante. Y no soy el único que tiene una razón para hacerlo, estas dos preguntas en Stack Overflow también buscaban algo similar hace varios años ya:</p>

<ul>
  <li><a href="https://stackoverflow.com/questions/24105813/possibility-of-mapping-enum-values-to-string-type-instead-of-integer">https://stackoverflow.com/questions/24105813/possibility-of-mapping-enum-values-to-string-type-instead-of-integer</a></li>
  <li><a href="https://stackoverflow.com/questions/32938729/how-to-store-enum-as-string-to-database-in-rails">https://stackoverflow.com/questions/32938729/how-to-store-enum-as-string-to-database-in-rails</a></li>
</ul>

<p>¿Qué opinas tú? ¿Sabías que un <code class="language-plaintext highlighter-rouge">enum</code> puede persistirse como <code class="language-plaintext highlighter-rouge">string</code>?</p>]]></content><author><name>Andrés</name></author><category term="[&quot;On Rails&quot;]" /><summary type="html"><![CDATA[Campos string vs int en ActiveRecord::Enum: ¿Cuál es la mejor opción para tu base de datos Rails?]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/enum.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/enum.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">Creando un API REST con Ruby on Rails. Capítulo 2: Endpoints y Serialización</title><link href="https://a-chacon.com/on%20rails/2024/06/23/creating-rails-api-cap-2.html" rel="alternate" type="text/html" title="Creando un API REST con Ruby on Rails. Capítulo 2: Endpoints y Serialización" /><published>2024-06-23T00:00:00-04:00</published><updated>2024-06-23T00:00:00-04:00</updated><id>https://a-chacon.com/on%20rails/2024/06/23/creating-rails-api-cap-2</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2024/06/23/creating-rails-api-cap-2.html"><![CDATA[<p>Antes de continuar construyendo nuestra <strong>API REST con Ruby On Rails</strong> me gustaría dar un salto hacia atrás para aclarar algunos puntos. Cuando comencé a trabajar como desarrollador había muchas cosas que no tenía claras y que me costó tiempo, esfuerzo, pruebas y errores aprender. Una de esas era ¿Qué era realmente un API y cuál es la mejor forma de construir una? ¿Cuáles rutas definir y qué respuestas dar?</p>

<p>Ahora esto me parece obvio y básico, pero creo que vale la pena repasar. <a href="https://www.ibm.com/es-es/topics/rest-apis">Según IBM</a> un API REST es:</p>

<blockquote>
  <p>Una API REST (también llamada API RESTful o API web RESTful) es una interfaz de programación de aplicaciones (API) que se ajusta a los principios de diseño del estilo arquitectónico de transferencia de estado representacional (REST). Las API REST proporcionan una forma flexible y ligera de integrar aplicaciones y conectar componentes en arquitecturas de microservicios.</p>
</blockquote>

<p>Lo único que se debe respetar cuando diseñas una API REST deben ser los <strong>6 principios REST</strong>:</p>

<ul>
  <li><strong>Interfaz uniforme</strong>: Todas las solicitudes para el mismo recurso deben tener el mismo aspecto y un único URI.</li>
  <li><strong>Desacoplamiento cliente-servidor</strong>: Cliente y servidor son independientes; el cliente solo conoce el URI del recurso.</li>
  <li><strong>Sin estado</strong>: Cada solicitud debe contener toda la información necesaria; no se guarda estado del lado del servidor.</li>
  <li><strong>Capacidad de almacenamiento en caché</strong>: Los recursos deben ser cacheables para mejorar el rendimiento y la escalabilidad.</li>
  <li><strong>Arquitectura del sistema en capas</strong>: Las llamadas y respuestas pueden pasar por múltiples capas de intermediarios.</li>
  <li><strong>Código bajo demanda (opcional)</strong>: Las respuestas pueden contener código ejecutable bajo demanda.</li>
</ul>

<iframe src="https://giphy.com/embed/52HjuHsfVO69q" width="480" height="269" style="" frameborder="0" class="giphy-embed" allowfullscreen=""></iframe>
<p><a href="https://giphy.com/gifs/reactiongifs-52HjuHsfVO69q">via GIPHY</a></p>

<p>¿Y cómo funciona esto? Probablemente si estás leyendo esto es porque ya sabes. Solicitudes HTTP para realizar funciones de base de datos estándar como crear, leer, actualizar y eliminar (CRUD) sobre un recurso. Y aquí mi recomendación y consejo: <strong>siempre intenta realizar tus APIs orientadas a recursos y no a acciones</strong>. Con Ruby On Rails esto no es difícil, pero siempre está la tentación de realizar endpoints como <code class="language-plaintext highlighter-rouge">POST /publishArticle</code> en vez de realizar <code class="language-plaintext highlighter-rouge">PUT /article/:article_id</code> con el contenido correcto.</p>

<hr />

<p>Con esto aclarado continuamos con nuestra serie de posts, segundo capítulo:</p>

<ul>
  <li><a href="/on rails/2024/06/16/creating-rails-api-cap-1"><strong>Capítulo 1</strong>: Configuración Inicial y Modelos</a></li>
  <li><strong>Capítulo 2</strong>: Endpoints y Serialización &lt;———– Estas Aquí!</li>
  <li><a href="/on rails/2024/08/18/creating-rails-api-cap-3"><strong>Capítulo 3</strong>: Autenticación y Autorización</a></li>
  <li><strong>Capítulo 4</strong>: Manejo de Errores y Buenas Prácticas</li>
</ul>

<hr />

<h2 id="rutas">Rutas</h2>

<p>Ahora bien, continuando con nuestro ejemplo arreglaremos las rutas. Usaremos <strong>rutas anidadas</strong> para los recursos que dependen de <code class="language-plaintext highlighter-rouge">Projects</code> utilizando la opción <a href="https://guides.rubyonrails.org/routing.html#shallow-nesting">shallow</a> para crear solo las rutas necesarias para identificar el recurso y <a href="http://weblog.jamisbuck.org/2007/2/5/nesting-resources">evitar el anidamiento profundo</a>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/routes.rb</span>
<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
  <span class="n">get</span> <span class="s1">'up'</span> <span class="o">=&gt;</span> <span class="s1">'rails/health#show'</span><span class="p">,</span> <span class="ss">:as</span> <span class="o">=&gt;</span> <span class="ss">:rails_health_check</span>

  <span class="n">resources</span> <span class="ss">:projects</span><span class="p">,</span> <span class="ss">shallow: </span><span class="kp">true</span> <span class="k">do</span>
    <span class="n">resources</span> <span class="ss">:tasks</span>
    <span class="n">resources</span> <span class="ss">:roles</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Eliminé comentarios y las ordené un poco. Si usamos el comando <code class="language-plaintext highlighter-rouge">rails routes</code>, estaremos viendo nuestras rutas apuntando a nuestros métodos de controlador de esta forma:</p>

<pre><code class="language-fish">     projects GET    /projects(.:format)                   projects#index
              POST   /projects(.:format)                   projects#create
      project GET    /projects/:id(.:format)               projects#show
              PATCH  /projects/:id(.:format)               projects#update
              PUT    /projects/:id(.:format)               projects#update
              DELETE /projects/:id(.:format)               projects#destroy
project_tasks GET    /projects/:project_id/tasks(.:format) tasks#index
              POST   /projects/:project_id/tasks(.:format) tasks#create
         task GET    /tasks/:id(.:format)                  tasks#show
              PATCH  /tasks/:id(.:format)                  tasks#update
              PUT    /tasks/:id(.:format)                  tasks#update
              DELETE /tasks/:id(.:format)                  tasks#destroy
project_roles GET    /projects/:project_id/roles(.:format) roles#index
              POST   /projects/:project_id/roles(.:format) roles#create
         role GET    /roles/:id(.:format)                  roles#show
              PATCH  /roles/:id(.:format)                  roles#update
              PUT    /roles/:id(.:format)                  roles#update
              DELETE /roles/:id(.:format)                  roles#destroy
</code></pre>

<h2 id="tests">Tests</h2>

<h3 id="setup-con-factories">SetUp con Factories</h3>

<p>Si ejecutamos <code class="language-plaintext highlighter-rouge">rails t</code>, nos encontraremos con varios errores. El primer problema que debemos resolver es que Rails, hasta el momento, ha generado nuestras factorías con <code class="language-plaintext highlighter-rouge">FactoryBot</code>, pero no las está utilizando automáticamente para crear los registros de prueba en el bloque <code class="language-plaintext highlighter-rouge">setup</code> de cada archivo de test de controladores. En su lugar, está utilizando las fixtures para obtener un objeto de prueba. Sin embargo, estas fixtures no fueron creadas porque el comportamiento cambió cuando instalamos <code class="language-plaintext highlighter-rouge">FactoryBot</code>; ahora se utilizan factorías en lugar de fixtures. Para solucionar esto, necesitamos reemplazar la línea número 5 de nuestros tests de controladores de la siguiente forma:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test/controllers/projects_controllers_test.rb</span>
<span class="o">-</span>     <span class="vi">@project</span> <span class="o">=</span> <span class="n">projects</span><span class="p">(</span><span class="ss">:one</span><span class="p">)</span>
<span class="o">+</span>     <span class="vi">@project</span> <span class="o">=</span> <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:project</span><span class="p">)</span>
</code></pre></div></div>

<p>y lo mismo para los otros dos, pero agregaremos una línea más al <code class="language-plaintext highlighter-rouge">setup</code> con un objeto <code class="language-plaintext highlighter-rouge">project</code> que usaremos más adelante en las rutas:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test/controllers/roles_controllers_test.rb</span>
<span class="o">-</span>     <span class="vi">@role</span> <span class="o">=</span> <span class="n">roles</span><span class="p">(</span><span class="ss">:one</span><span class="p">)</span>
<span class="o">+</span>     <span class="vi">@role</span> <span class="o">=</span> <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:role</span><span class="p">)</span>
<span class="o">+</span>     <span class="vi">@project</span> <span class="o">=</span> <span class="vi">@role</span><span class="p">.</span><span class="nf">project</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># test/controllers/tasks_controllers_test.rb</span>
<span class="o">-</span>     <span class="vi">@task</span> <span class="o">=</span> <span class="n">tasks</span><span class="p">(</span><span class="ss">:one</span><span class="p">)</span>
<span class="o">+</span>     <span class="vi">@task</span> <span class="o">=</span> <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">:task</span><span class="p">)</span>
<span class="o">+</span>     <span class="vi">@project</span> <span class="o">=</span> <span class="vi">@task</span><span class="p">.</span><span class="nf">project</span>
</code></pre></div></div>

<h3 id="routes-helpers">Routes Helpers</h3>

<p>Ahora todavía deberíamos tener 4 tests fallando y esto es debido a que cambiamos la estructura de las rutas. Para corregir eso debemos usar los nuevos helpers creados con las rutas anidadas para las acciones <code class="language-plaintext highlighter-rouge">index</code> y <code class="language-plaintext highlighter-rouge">create</code> en los controladores <code class="language-plaintext highlighter-rouge">RolesController</code> y <code class="language-plaintext highlighter-rouge">TasksController</code>. Específicamente, debes cambiar <code class="language-plaintext highlighter-rouge">roles_url</code> y <code class="language-plaintext highlighter-rouge">tasks_url</code> de las líneas número 9 y 15 por <code class="language-plaintext highlighter-rouge">project_roles_url(@project)</code> y <code class="language-plaintext highlighter-rouge">project_tasks_url(@project)</code> respectivamente.</p>

<p>Cuando realices eso correctamente, podrás correr tus tests y obtener un resultado como este:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>15 runs, 27 assertions, 0 failures, 0 errors, 0 skips
</code></pre></div></div>

<h2 id="creando-serializadores">Creando Serializadores</h2>

<p>La <strong>serialización de datos se refiere al proceso de convertir objetos de datos</strong> (como instancias de modelos ActiveRecord) en formatos que pueden ser <strong>fácilmente transmitidos</strong> y entendidos por diferentes sistemas, en nuestro caso, transformarlos en formato JSON.</p>

<p>En Rails, nuestros modelos ya incluyen por defecto el módulo <a href="https://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html">ActiveModel::Serializers::JSON</a>, que les permite serializar todos los atributos (se pueden filtrar) a un Hash y, por ende, a un objeto JSON. Esto es lo que está ocurriendo de forma predefinida en nuestros métodos de controladores creados con <code class="language-plaintext highlighter-rouge">scaffold</code>. Sin embargo, necesitamos ir un poco más allá, necesitamos más personalización y flexibilidad. Para esto propongo el uso de Blueprinter, que es una opción confiable y flexible.</p>

<h3 id="blueprinter"><a href="https://github.com/procore-oss/blueprinter">Blueprinter</a></h3>

<blockquote>
  <p>Blueprinter es un presentador de objetos JSON para Ruby que toma objetos de negocio y los descompone en simples hashes y los serializa a JSON. Puede utilizarse en Rails en lugar de otros serializadores (como JBuilder o ActiveModelSerializers). Está diseñado para ser sencillo, directo y eficaz. Se basa en gran medida en la idea de vistas que, de forma similar a las vistas de Rails, son formas de predefinir la salida de datos en diferentes contextos.</p>
</blockquote>

<h3 id="clases-serializadoras">Clases serializadoras</h3>

<p>Entonces, la gema ya la instalamos en el capítulo anterior. Ahora solo nos queda crear nuestras clases serializadoras. Para esto vamos a crear una carpeta en la ruta <code class="language-plaintext highlighter-rouge">app/blueprints/</code> y dentro incluiremos 4 archivos (uno para cada modelo) con el siguiente contenido:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/blueprints/project_blueprint.rb</span>

<span class="k">class</span> <span class="nc">ProjectBlueprint</span> <span class="o">&lt;</span> <span class="no">Blueprinter</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">identifier</span> <span class="ss">:id</span>

  <span class="n">fields</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:description</span>

  <span class="n">view</span> <span class="ss">:with_tasks</span> <span class="k">do</span>
    <span class="n">association</span> <span class="ss">:tasks</span><span class="p">,</span> <span class="ss">blueprint: </span><span class="no">TaskBlueprint</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/blueprints/task_blueprint.rb</span>

<span class="k">class</span> <span class="nc">TaskBlueprint</span> <span class="o">&lt;</span> <span class="no">Blueprinter</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">identifier</span> <span class="ss">:id</span>

  <span class="n">fields</span> <span class="ss">:title</span><span class="p">,</span> <span class="ss">:description</span><span class="p">,</span> <span class="ss">:status</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/blueprints/role_blueprint.rb</span>

<span class="k">class</span> <span class="nc">RoleBlueprint</span> <span class="o">&lt;</span> <span class="no">Blueprinter</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">identifier</span> <span class="ss">:id</span>

  <span class="n">fields</span> <span class="ss">:role</span>
  <span class="n">association</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">blueprint: </span><span class="no">UserBlueprint</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/blueprints/user_blueprint.rb</span>

<span class="k">class</span> <span class="nc">UserBlueprint</span> <span class="o">&lt;</span> <span class="no">Blueprinter</span><span class="o">::</span><span class="no">Base</span>
  <span class="n">identifier</span> <span class="ss">:id</span>

  <span class="n">fields</span> <span class="ss">:email</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="implementando-en-controladores">Implementando en controladores</h3>

<p>Ahora que ya tenemos nuestros serializadores listos, debemos implementarlos en nuestros métodos de controladores. Para esto, simplemente te explicaré cómo se usa, pero no mostraré cada cambio que debes hacer porque son varias líneas que tocar. Identifica cada línea de código en los controladores que tenga la palabra <strong><code class="language-plaintext highlighter-rouge">render</code></strong>. Esto especifica la respuesta al cliente, en este caso un JSON con el objeto o los objetos que, como ya dije, se serializan por defecto con <strong><code class="language-plaintext highlighter-rouge">ActiveModel::Serializers</code></strong>. Pero nosotros cambiaremos eso. Por ejemplo, para un <strong><code class="language-plaintext highlighter-rouge">Project</code></strong>, escribiremos: <strong><code class="language-plaintext highlighter-rouge">render json: ProjectBlueprint.render_as_json(@project)</code></strong>. De esta forma, Blueprint será el encargado de serializar el objeto en lugar de Serializers.</p>

<p>Otro ejemplo, para nuestro método <strong><code class="language-plaintext highlighter-rouge">show</code></strong>, tal vez queramos mostrar un objeto más completo. Para eso podemos hacerlo así: <strong><code class="language-plaintext highlighter-rouge">render json: ProjectBlueprint.render_as_json(@project, view: :with_tasks)</code></strong>, y de esa manera no solo retornaremos el proyecto, sino también sus tareas.</p>

<p>Después de realizar los cambios correspondientes, puedes volver a probar que todo está funcionando correctamente ejecutando <strong><code class="language-plaintext highlighter-rouge">rails t</code></strong>.</p>

<iframe src="https://giphy.com/embed/qjj4xrA1STjfa" width="480" height="353" style="" frameborder="0" class="giphy-embed" allowfullscreen=""></iframe>
<p><a href="https://giphy.com/gifs/space-nasa-qjj4xrA1STjfa">via GIPHY</a></p>

<h2 id="pensamientos-finales">Pensamientos finales</h2>

<p>Con esto ya tendremos nuestros endpoints funcionando, las rutas tienen sentido al usar anidación, los serializadores nos dan un mayor control sobre qué datos exponer y cuáles ocultar dependiendo del método y, en un futuro, de los permisos. Y lo más importante, <strong>nuestros tests están funcionando</strong>. Validan la creación, obtención, modificación y eliminación de nuestros datos.</p>

<p>Si hay algún punto que no expresé correctamente o me salté algo hasta ahora, por favor escríbeme. Además, te agrego la URL del repositorio donde iré subiendo el código actualizado para que lo vayas revisando:</p>

<p>Repo: <a href="https://github.com/a-chacon/api-project-management-example">https://github.com/a-chacon/api-project-management-example</a></p>]]></content><author><name>Andrés</name></author><category term="On Rails" /><summary type="html"><![CDATA[Cómo construir APIs RESTful eficientes con Ruby on Rails, desde los principios fundamentales de REST hasta la configuración inicial del proyecto, la optimización de rutas con anidación y el uso de serializadores.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/apirestcap2.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/apirestcap2.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">Creando un API REST con Ruby on Rails. Capítulo 1: Configuración Inicial y Modelos</title><link href="https://a-chacon.com/on%20rails/2024/06/16/creating-rails-api-cap-1.html" rel="alternate" type="text/html" title="Creando un API REST con Ruby on Rails. Capítulo 1: Configuración Inicial y Modelos" /><published>2024-06-16T00:00:00-04:00</published><updated>2024-06-16T00:00:00-04:00</updated><id>https://a-chacon.com/on%20rails/2024/06/16/creating-rails-api-cap-1</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2024/06/16/creating-rails-api-cap-1.html"><![CDATA[<p>Hace un par de meses que no comenzaba un nuevo proyecto ni programaba nada importante, pero hace unos días eso cambió. Tuve que comenzar una nueva API REST con Ruby on Rails. Cada vez que eso pasa, me empiezo a preguntar cosas como: ¿Ocuparé una gema para realizar la autenticación o lo programaré todo yo? ¿Cómo manejaré los permisos? ¿Usaré el mismo serializador que uso siempre o buscaré algo más actualizado y nuevo? ¿Crearé los modelos y luego los controladores o crearé todo a la misma vez con <code class="language-plaintext highlighter-rouge">rails scaffold</code>?</p>

<p>A medida que surgían estas preguntas 🤔, se me ocurrió la idea de escribir una serie de posts donde dejar clara <strong>mi forma de crear APIs con Ruby on Rails</strong>, que me sirva a mí como un recordatorio y a otros para aprender y/o complementar sus conocimientos. A propósito de que no encontré ninguna buena guía en internet, todas son muy básicas y con ejemplos poco reales que no van más allá de un simple CRUD.</p>

<hr />

<p>Por ello, mi idea es dividir el tema en 4 capítulos, cada uno será un post que iré publicando en mi blog siempre que no pierda la motivación en el camino jeje 😅.</p>

<ul>
  <li><strong>Capítulo 1</strong>: Configuración Inicial y Modelos &lt;———- Estas Aquí!</li>
  <li><a href="/on rails/2024/06/23/creating-rails-api-cap-2.html"><strong>Capítulo 2</strong>: CRUD y Serialización</a></li>
  <li><a href="/on rails/2024/08/18/creating-rails-api-cap-3.html"><strong>Capítulo 3</strong>: Autenticación y Autorización</a></li>
  <li><strong>Capítulo 4</strong>: Manejo de Errores y Buenas Prácticas</li>
</ul>

<hr />

<p><strong>Para esta guía supongo que sabes lo que es una API REST, tienes Ruby instalado, la gema de Rails instalada y estás usando un sistema operativo basado en GNU/Linux</strong></p>

<p>Si no, te dejo algunos recursos que deberías mirar antes de proseguir:</p>

<ul>
  <li><a href="https://aws.amazon.com/what-is/restful-api/">https://aws.amazon.com/what-is/restful-api/</a></li>
  <li><a href="https://www.ruby-lang.org/en/documentation/installation/">https://www.ruby-lang.org/en/documentation/installation/</a></li>
  <li><a href="https://guides.rubyonrails.org/getting_started.html#creating-a-new-rails-project-installing-rails">https://guides.rubyonrails.org/getting_started.html#creating-a-new-rails-project-installing-rails</a></li>
  <li><a href="https://itsfoss.com/linux-better-than-windows/">https://itsfoss.com/linux-better-than-windows/</a></li>
</ul>

<iframe src="https://giphy.com/embed/qt73FYHjuXqAj241m8" width="480" height="480" style="" frameborder="0" class="giphy-embed" allowfullscreen=""></iframe>
<p><a href="https://giphy.com/gifs/pudgypenguins-building-build-brick-qt73FYHjuXqAj241m8">via GIPHY</a></p>

<h2 id="el-problema">El Problema</h2>

<p>Para facilitarme las cosas, comencé a hablar con GPT y le pedí un problema/requisitos que será nuestra guía para desarrollar nuestra aplicación e ir tocando cada punto del que hay que hablar. El problema:</p>

<blockquote>
  <p>Se necesita desarrollar una API para una plataforma de gestión de proyectos que permita a los usuarios registrar proyectos, gestionar roles específicos y administrar tareas asociadas a cada proyecto. En esta plataforma, los administradores tendrán la capacidad exclusiva de crear, actualizar y eliminar proyectos, así como asignar roles como administrador o colaborador a otros usuarios. Además, los administradores serán los únicos autorizados para crear y administrar tareas dentro de cada proyecto, asegurando que solo ellos puedan gestionar las actividades específicas del proyecto. Por otro lado, los colaboradores tendrán acceso de solo lectura a las tareas del proyecto, permitiéndoles ver la información detallada pero sin posibilidad de modificarla. Esta API no solo garantizará la seguridad y la integridad de los datos, sino que también proporcionará una gestión eficiente y escalable de proyectos y tareas para distintos equipos y usuarios dentro de la organización.</p>
</blockquote>

<h3 id="paso-número-0">Paso Número 0</h3>

<p>Antes de comenzar a tocar código es bueno realizar un <a href="https://www.ibm.com/topics/data-modeling">modelado de tu base de datos</a> de la forma que sea más fácil para ti. Esto sentará las bases para todo lo que vas a realizar de aquí en adelante, no de una forma estricta, sino más bien como una guía. Yo recomiendo hacerlo en <a href="https://dbdiagram.io">dbdiagram.io</a>. Así que, siguiendo nuestro problema, lo resolveré con la siguiente estructura de base de datos:</p>

<p><img src="/assets/images/db.png" alt="modelo de base de datos" /></p>

<h3 id="creando-la-aplicación-ruby-on-rails">Creando La Aplicación Ruby on Rails</h3>

<p>Posiciónate en el directorio de tu preferencia y ejecuta el comando:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails new api-project-management <span class="nt">--api</span>
</code></pre></div></div>

<p>Listo, <strong>ya tienes tu api creada con Ruby On Rails</strong>.</p>

<h2 id="instalar-estas-gemas-antes-de-empezar-">Instalar Estas Gemas Antes De Empezar 💎</h2>

<p>Lo primero, <strong>debes escribir pruebas</strong>. Y para que nuestras pruebas funcionen bien, prepararemos el entorno antes de crear cualquier modelo. Agregaremos estas dos gemas a nuestro Gemfile:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Gemfile</span>
<span class="n">group</span> <span class="ss">:development</span><span class="p">,</span> <span class="ss">:test</span> <span class="k">do</span>
  <span class="o">...</span>

  <span class="n">gem</span> <span class="s2">"faker"</span>

  <span class="n">gem</span> <span class="s2">"factory_bot_rails"</span>

  <span class="o">...</span>
<span class="k">end</span>

<span class="n">gem</span> <span class="s2">"devise"</span>
<span class="n">gem</span> <span class="s2">"devise-api"</span>

<span class="n">gem</span> <span class="s2">"pundit"</span>

<span class="n">gem</span> <span class="s2">"blueprinter"</span>
</code></pre></div></div>

<p><a href="https://github.com/faker-ruby/faker">Faker</a>, si no la conoces, es para crear datos de prueba de forma fácil. La segunda, <a href="https://github.com/thoughtbot/factory_bot_rails">factory_bot_rails</a>, es para implementar factory_bot en Rails, una forma fácil de crear registros de prueba y una alternativa (mucho mejor a mi parecer) de las fixtures. Será bueno que vayas mirando su documentación.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Gemfile</span>
<span class="n">gem</span> <span class="s2">"devise"</span>
<span class="n">gem</span> <span class="s2">"devise-api"</span>

<span class="n">gem</span> <span class="s2">"pundit"</span>

<span class="n">gem</span> <span class="s2">"blueprinter"</span>
</code></pre></div></div>

<p>También agregaremos lo que es <a href="https://github.com/heartcombo/devise">devise</a> y <a href="https://github.com/nejdetkadir/devise-api">devise-api</a> para la autenticación, <a href="https://github.com/varvet/pundit">pundit</a> para la autorización y <a href="https://github.com/procore-oss/blueprinter">blueprinter</a> para la serialización. Con esto te puedes ir haciendo una idea de qué vendrá en los próximos capítulos.</p>

<h3 id="creando-nuestros-recursos-">Creando Nuestros Recursos 🪵</h3>

<h4 id="user">User</h4>

<p>Para el recurso <code class="language-plaintext highlighter-rouge">User</code> crearemos solo el modelo, puesto que la creación y el login lo manejaremos en el próximo capítulo con la gema <a href="https://github.com/nejdetkadir/devise-api">devise-api</a>. Ejecutemos:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g model user
</code></pre></div></div>

<p>Los campos de correo y contraseña no los agregaremos directamente, devise lo hará por nosotros. Para este punto te sugiero que vayas a la documentación de devise y revises la sección de <a href="https://github.com/heartcombo/devise?tab=readme-ov-file#getting-started">empezando</a>. Pero en resumidas cuentas debes correr estos dos comandos:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails generate devise:install
rails generate devise user
</code></pre></div></div>

<h4 id="project">Project</h4>

<p>Para <code class="language-plaintext highlighter-rouge">Project</code> usaremos el commando <code class="language-plaintext highlighter-rouge">scaffold</code> ya que necesitamos un CRUD completo.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g scaffold project name:string description:text
</code></pre></div></div>

<h4 id="role">Role</h4>

<p><code class="language-plaintext highlighter-rouge">Role</code> será el modelo que haga la relacion entre un <code class="language-plaintext highlighter-rouge">User</code> y un <code class="language-plaintext highlighter-rouge">Project</code>. Este tendrá un enum donde definiremos el rol que cumple cada usuario en cada proyecto al que está relacionado. También será un CRUD completo, pero aquí será donde manejaremos permisos más adelante.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g scaffold role role:string user:references project:references
</code></pre></div></div>

<h4 id="task">Task</h4>

<p>Por último <code class="language-plaintext highlighter-rouge">Task</code>. También con un CRUD completo y manejo de permisos.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g scaffold task title:string description:text status:string project:references
</code></pre></div></div>

<hr />

<p>Ejecutamos <code class="language-plaintext highlighter-rouge">rails db:migrate</code> para crear las tablas en nuestra base de datos.</p>

<hr />

<h3 id="nuestros-modelos">Nuestros Modelos</h3>

<p>Los modelos serán simples, no incluirán mucha lógica de negocio puesto que nuestro ejemplo no lo requiere. Pero al menos tenemos que comprobar que las asociaciones se cumplen de forma correcta y que el <code class="language-plaintext highlighter-rouge">enum</code> de <code class="language-plaintext highlighter-rouge">Role</code> y <code class="language-plaintext highlighter-rouge">Task</code> estén definidos. Así que modificaremos los modelos <code class="language-plaintext highlighter-rouge">User</code> y <code class="language-plaintext highlighter-rouge">Project</code> para que se vean de la siguiente forma respectivamente:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sr">/app/mo</span><span class="n">dels</span><span class="o">/</span><span class="n">user</span><span class="p">.</span><span class="nf">rb</span>
<span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">has_many</span> <span class="ss">:roles</span>
  <span class="n">has_many</span> <span class="ss">:projects</span><span class="p">,</span> <span class="ss">through: :roles</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sr">/app/mo</span><span class="n">dels</span><span class="o">/</span><span class="n">project</span><span class="p">.</span><span class="nf">rb</span>
<span class="k">class</span> <span class="nc">Project</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">has_many</span> <span class="ss">:roles</span>
  <span class="n">has_many</span> <span class="ss">:users</span><span class="p">,</span> <span class="ss">through: :roles</span>

  <span class="n">has_many</span> <span class="ss">:tasks</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y <code class="language-plaintext highlighter-rouge">Role</code> y <code class="language-plaintext highlighter-rouge">Task</code> para que se vean así:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sr">/app/mo</span><span class="n">dels</span><span class="o">/</span><span class="n">role</span><span class="p">.</span><span class="nf">rb</span>
<span class="k">class</span> <span class="nc">Role</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">belongs_to</span> <span class="ss">:user</span>
  <span class="n">belongs_to</span> <span class="ss">:project</span>

  <span class="n">enum</span> <span class="ss">:role</span><span class="p">,</span> <span class="p">{</span><span class="ss">manager: </span><span class="s2">"manager"</span><span class="p">,</span> <span class="ss">contributor: </span><span class="s2">"contributor"</span><span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="sr">/app/mo</span><span class="n">dels</span><span class="o">/</span><span class="n">task</span><span class="p">.</span><span class="nf">rb</span>
<span class="k">class</span> <span class="nc">Task</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">belongs_to</span> <span class="ss">:project</span>

  <span class="n">enum</span> <span class="ss">:status</span><span class="p">,</span> <span class="p">{</span><span class="ss">next_to_do: </span><span class="s2">"next_to_do"</span><span class="p">,</span> <span class="ss">doing: </span><span class="s2">"doing"</span><span class="p">,</span> <span class="ss">complete: </span><span class="s2">"complete"</span><span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Me gusta definir los enums de esta forma, clave y valor en la base de datos, porque me ha pasado que algunas veces me pedían un dump de algunos datos de la base de datos directamente. Cuando pasa eso y tienes un enum definido de la forma tradicional con valores numéricos <strong>carecen de significado para cualquier persona que los ve y no tiene la definición de tu modelo</strong>.</p>

<h3 id="factories">Factories</h3>

<iframe src="https://giphy.com/embed/yKxo7c9Q6pZoUzAfPu" width="480" height="480" style="" frameborder="0" class="giphy-embed" allowfullscreen=""></iframe>
<p><a href="https://giphy.com/gifs/nounish-dao-nouns-noggles-yKxo7c9Q6pZoUzAfPu">via GIPHY</a></p>

<p>Al momento de crear nuestros modelos, factory_bot nos creó un archivo para cada uno en la carpeta <code class="language-plaintext highlighter-rouge">test/factories/*</code> con la definición que sirve para crear registros durante las pruebas. Por defecto, no tienen mucho sentido, debemos arreglar las relaciones, crear datos que se acerquen de cierta forma a los reales y pensar en que la definición nos ayude a hacer las pruebas que necesitamos. Arreglaremos eso utilizando <code class="language-plaintext highlighter-rouge">Faker</code> y sacándole el jugo a <code class="language-plaintext highlighter-rouge">factory_bot</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span><span class="o">/</span><span class="n">factories</span><span class="o">/</span><span class="n">roles</span><span class="p">.</span><span class="nf">rb</span>
<span class="no">FactoryBot</span><span class="p">.</span><span class="nf">define</span> <span class="k">do</span>
  <span class="n">factory</span> <span class="ss">:role</span> <span class="k">do</span>
    <span class="n">role</span> <span class="p">{</span> <span class="no">Role</span><span class="p">.</span><span class="nf">roles</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">sample</span> <span class="p">}</span>
    <span class="n">user</span>
    <span class="n">project</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span><span class="o">/</span><span class="n">factories</span><span class="o">/</span><span class="n">users</span><span class="p">.</span><span class="nf">rb</span>
<span class="no">FactoryBot</span><span class="p">.</span><span class="nf">define</span> <span class="k">do</span>
  <span class="n">factory</span> <span class="ss">:user</span> <span class="k">do</span>
    <span class="n">email</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">email</span> <span class="p">}</span>
    <span class="n">password</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Internet</span><span class="p">.</span><span class="nf">password</span> <span class="p">}</span>
    <span class="n">factory</span> <span class="ss">:user_with_projects</span> <span class="k">do</span>
      <span class="n">transient</span> <span class="k">do</span>
        <span class="n">projects_count</span> <span class="p">{</span> <span class="mi">5</span> <span class="p">}</span>
      <span class="k">end</span>

      <span class="n">after</span><span class="p">(</span><span class="ss">:create</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">user</span><span class="p">,</span> <span class="n">context</span><span class="o">|</span>
        <span class="n">create_list</span><span class="p">(</span><span class="ss">:role</span><span class="p">,</span> <span class="n">context</span><span class="p">.</span><span class="nf">projects_count</span><span class="p">,</span> <span class="n">user</span><span class="p">:)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span><span class="o">/</span><span class="n">factories</span><span class="o">/</span><span class="n">projects</span><span class="p">.</span><span class="nf">rb</span>
<span class="no">FactoryBot</span><span class="p">.</span><span class="nf">define</span> <span class="k">do</span>
  <span class="n">factory</span> <span class="ss">:project</span> <span class="k">do</span>
    <span class="nb">name</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Lorem</span><span class="p">.</span><span class="nf">word</span> <span class="p">}</span>
    <span class="n">description</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Lorem</span><span class="p">.</span><span class="nf">paragraph</span> <span class="p">}</span>

    <span class="n">factory</span> <span class="ss">:project_with_tasks</span> <span class="k">do</span>
      <span class="n">transient</span> <span class="k">do</span>
        <span class="n">tasks_count</span> <span class="p">{</span> <span class="mi">5</span> <span class="p">}</span>
      <span class="k">end</span>

      <span class="n">after</span><span class="p">(</span><span class="ss">:create</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">project</span><span class="p">,</span> <span class="n">context</span><span class="o">|</span>
        <span class="n">create_list</span><span class="p">(</span><span class="ss">:task</span><span class="p">,</span> <span class="n">context</span><span class="p">.</span><span class="nf">tasks_count</span><span class="p">,</span> <span class="n">project</span><span class="p">:)</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y Tareas así:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span><span class="o">/</span><span class="n">factories</span><span class="o">/</span><span class="n">tasks</span><span class="p">.</span><span class="nf">rb</span>
<span class="no">FactoryBot</span><span class="p">.</span><span class="nf">define</span> <span class="k">do</span>
  <span class="n">factory</span> <span class="ss">:task</span> <span class="k">do</span>
    <span class="n">title</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Lorem</span><span class="p">.</span><span class="nf">word</span> <span class="p">}</span>
    <span class="n">description</span> <span class="p">{</span> <span class="no">Faker</span><span class="o">::</span><span class="no">Lorem</span><span class="p">.</span><span class="nf">paragraph</span> <span class="p">}</span>
    <span class="n">status</span> <span class="p">{</span> <span class="no">Task</span><span class="p">.</span><span class="nf">statuses</span><span class="p">.</span><span class="nf">keys</span><span class="p">.</span><span class="nf">sample</span> <span class="p">}</span>
    <span class="n">project</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="hasta-aquí-por-hoy">Hasta aquí por hoy</h3>

<p>Con estos pasos ya tendremos creados los <strong>cimientos de nuestra API REST</strong> y estamos listos para seguir avanzando hacia los endpoints y las pruebas de integración que estaremos viendo en el próximo capítulo. Hasta ahora nada está escrito en piedra, es imposible pensar que llegaremos a una planificación perfecta que no sufra modificaciones en el futuro; lo ideal siempre es pensar en que tu código sea escalable y soporte esas modificaciones futuras.</p>

<p>Hemos diseñado nuestra base de datos, creamos nuestros modelos, definimos las asociaciones de forma correcta, te comenté cómo me gusta definir los enums y dejamos nuestras factorías listas para producir registros de pruebas 🏭. Dejaré la URL del repositorio con el código más abajo por si lo quieres ir mirando y <strong>cualquier comentario u opinion para motivarme a seguir escribiendo es bienvenido</strong>.</p>

<p>Repo: <a href="https://github.com/a-chacon/api-project-management-example">https://github.com/a-chacon/api-project-management-example</a></p>]]></content><author><name>Andrés</name></author><category term="On Rails" /><summary type="html"><![CDATA[Preparamos los cimientos de una API REST en Ruby on Rails, diseñando la base de datos, creando modelos y configurando factorías para registros de prueba. Enfocados en escalabilidad y adaptabilidad futura del código.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/apirestcap1.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/apirestcap1.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">authenticate_by: Prevenir la enumeración de usuarios basada en tiempos de respuestas.</title><link href="https://a-chacon.com/on%20rails/2024/04/18/authenticate-by-for-prevent-timing-based-enumeration-in-rails.html" rel="alternate" type="text/html" title="authenticate_by: Prevenir la enumeración de usuarios basada en tiempos de respuestas." /><published>2024-04-18T00:00:00-04:00</published><updated>2024-04-18T00:00:00-04:00</updated><id>https://a-chacon.com/on%20rails/2024/04/18/authenticate-by-for-prevent-timing-based-enumeration-in-rails</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2024/04/18/authenticate-by-for-prevent-timing-based-enumeration-in-rails.html"><![CDATA[<p>Digamos que tenemos un endpoint simple en nuestra aplicación Rails para que nuestros usuarios entren a la plataforma:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">def</span> <span class="nf">create</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">user</span> <span class="o">&amp;&amp;</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
      <span class="n">log_in</span> <span class="n">user</span>
      <span class="n">redirect_to</span> <span class="n">user</span>
    <span class="k">else</span>
      <span class="n">flash</span><span class="p">.</span><span class="nf">now</span><span class="p">[</span><span class="ss">:danger</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'Combinación de email/password incorrecta'</span>
      <span class="n">render</span> <span class="s1">'new'</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>El código anterior se ve bien, es funcional, y probablemente has seguido una lógica muy parecida para los inicios de sesión que has programado hasta ahora. Pero tiene un problema de seguridad: la condicional <code class="language-plaintext highlighter-rouge">if</code> no tomará el mismo tiempo de respuesta si es que el usuario no existe o si es que el usuario existe, pero la contraseña no es la correcta.</p>

<h2 id="ataques-de-enumeración-basados-en-tiempo">Ataques de enumeración basados en tiempo</h2>

<p>Un ataque de enumeración basado en tiempos de respuestas es lo que mostré anteriormente. Un atacante podrá probar correos electrónicos mediante fuerza bruta y será capaz de darse cuenta cuando un correo existe o no en nuestra base de datos analizando los tiempos de respuesta de nuestra aplicación web o más bien de la petición http.</p>

<p>Un ejemplo bien simple utilizando el código anterior nos daría tiempos de respuestas como estos:</p>

<p><img src="/assets/images/response-time-simple-test.png" alt="Tiempos de respuesta sin authenticate_by" /></p>

<p>En azul estarían los intentos de login fallidos con usuarios y contraseñas que no son correctos. En rojo estarían los intentos donde el usuario sí existe, pero no sabemos la contraseña. Como podemos ver, las diferencias en los tiempos de respuesta son notables.</p>

<p>Si estuviéramos en la posición de un atacante y probamos 1000 correos, donde la mayoría de las respuestas nos son entre 20 y 30 ms, pero solo una nos da 200 ms de respuesta, entonces sabríamos que algo encontramos ahí.</p>

<h2 id="authenticate_by">authenticate_by</h2>

<p>En Rails 7.1 fue <a href="https://github.com/rails/rails/pull/43765">introducido</a> un nuevo método llamado <code class="language-plaintext highlighter-rouge">authenticate_by</code> con el fin de prevenir este tipo de vector de ataque en nuestras aplicaciones Rails respondiendo con un tiempo similar si es que él usuario existe o no en nuestra base de datos.</p>

<p>Antes de <code class="language-plaintext highlighter-rouge">authenticate_by</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="s2">"..."</span><span class="p">)</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="s2">"..."</span><span class="p">)</span>
</code></pre></div></div>

<p>Después de <code class="language-plaintext highlighter-rouge">authenticate_by</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">User</span><span class="p">.</span><span class="nf">authenticate_by</span><span class="p">(</span><span class="ss">email: </span><span class="s2">"..."</span><span class="p">,</span> <span class="ss">password: </span><span class="s2">"..."</span><span class="p">)</span>
</code></pre></div></div>

<p>Ahora, si llevamos esto a nuestro ejemplo anterior, entonces nuestro código podría verse de la siguiente forma:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">def</span> <span class="nf">create</span>
    <span class="k">if</span> <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">authenticate_by</span><span class="p">(</span><span class="ss">email: </span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="ss">password: </span><span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
      <span class="n">log_in</span> <span class="n">user</span>
      <span class="n">redirect_to</span> <span class="s1">'/home'</span>
    <span class="k">else</span>
      <span class="n">flash</span><span class="p">[</span><span class="ss">:notice</span><span class="p">]</span> <span class="o">=</span> <span class="s1">'Combinación de email/password incorrecta'</span>
      <span class="nb">p</span> <span class="s1">'HERE'</span>
      <span class="n">redirect_to</span> <span class="n">root_path</span>
    <span class="k">end</span>
  <span class="k">end</span>
</code></pre></div></div>

<p>Realizando las mismas pruebas desde el navegador tenemos estas muestras en cuanto a los tiempos de respuesta:</p>

<p><img src="/assets/images/response-time-simple-test-with-authenticate-by.png" alt="Tiempos de respuesta implementando authenticate_by" /></p>

<p>Y como podemos ver, tanto las peticiones con correos electrónicos que existen como los que no existen en nuestra base de datos responden con tiempos similares (215..245 ms) haciendo imposible la enumeración de cuentas mediante el tiempo de respuesta.</p>

<p>Esto es en un escenario óptimo, este método no maneja toda la lógica de negocio y puede que en <a href="https://github.com/rails/rails/pull/43997#issuecomment-1001064483">ciertos casos</a> como por ejemplo si quieres controlar los intentos de ingresos fallidos en una cuenta agregues código que produzca una diferencia de tiempo notable y nuevamente se pueda producir un ataque de enumeración basado en tiempos de respuesta.</p>

<h2 id="y-cómo-funciona-authenticate_by">Y, ¿Cómo funciona authenticate_by?</h2>

<p>Para los más curiosos, <code class="language-plaintext highlighter-rouge">authenticate_by</code> tiene una <a href="https://github.com/jonathanhefner/rails/blob/9becc41df989bfccff091852d45925d41f0a13d8/activerecord/lib/active_record/secure_password.rb">definición</a> no muy compleja, donde la clave reside en el <code class="language-plaintext highlighter-rouge">if</code> de la línea 45:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">record</span> <span class="o">=</span> <span class="n">find_by</span><span class="p">(</span><span class="n">attributes</span><span class="p">.</span><span class="nf">except</span><span class="p">(</span><span class="o">*</span><span class="n">passwords</span><span class="p">.</span><span class="nf">keys</span><span class="p">))</span>
  <span class="n">record</span> <span class="k">if</span> <span class="n">passwords</span><span class="p">.</span><span class="nf">count</span> <span class="p">{</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">value</span><span class="o">|</span> <span class="n">record</span><span class="p">.</span><span class="nf">public_send</span><span class="p">(</span><span class="ss">:"authenticate_</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="ss">"</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span> <span class="p">}</span> <span class="o">==</span> <span class="n">passwords</span><span class="p">.</span><span class="nf">size</span>
<span class="k">else</span>
  <span class="nb">self</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">passwords</span><span class="p">)</span>
  <span class="kp">nil</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Lo que hace acá es muy parecido a lo que se hacía anteriormente con<code class="language-plaintext highlighter-rouge">Customer.find_by(email: "...")&amp;.authenticate("...")</code>, pero en el caso de que el usuario no es encontrado, o sea en el bloque <code class="language-plaintext highlighter-rouge">else</code>, llama al método <code class="language-plaintext highlighter-rouge">new</code> para generar una nueva instancia de la clase pasando como parámetros las contraseñas que se están usando en el intento de login. Esto fuerza a que <strong>aunque ningún registro fue encontrado se deban cifrar las contraseñas igualmente</strong>, lo que da como resultado un tiempo similar de respuesta a que si él registro fue encontrado y las contraseñas debieron ser cifradas para comparar los hashs.</p>

<h2 id="conclusiones">Conclusiones</h2>

<p>Para concluir, es importante tener en cuenta que, como menciona el <a href="https://github.com/rails/rails/pull/43997#issue-1088633524">autor del PR</a>, authenticate_by no garantiza que el tiempo de autenticación siempre sea constante, especialmente si la columna de nombre de usuario no está respaldada por un índice. A pesar de ello, esta adición representa un gran avance para nuestras aplicaciones al evitar la posibilidad de ataques de enumeración basados en el tiempo. En última instancia, nos proporciona una capa adicional de seguridad en un aspecto crítico de nuestras aplicaciones web.</p>

<p><em>Happy Coding!</em></p>]]></content><author><name>Andrés</name></author><category term="[&quot;On Rails&quot;]" /><summary type="html"><![CDATA[Con la introducción de authenticate_by en Rails 7.1, ahora podemos prevenir los ataques de enumeración basados en tiempos de respuesta.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/rails-authenticate-by.png" /><media:content medium="image" url="https://a-chacon.com/assets/images/rails-authenticate-by.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">Explorando las Posibilidades de WebAssembly: Ruby en el Navegador</title><link href="https://a-chacon.com/just%20ruby/2024/03/03/ruby-in-the-browser.html" rel="alternate" type="text/html" title="Explorando las Posibilidades de WebAssembly: Ruby en el Navegador" /><published>2024-03-03T00:00:00-03:00</published><updated>2024-03-03T00:00:00-03:00</updated><id>https://a-chacon.com/just%20ruby/2024/03/03/ruby-in-the-browser</id><content type="html" xml:base="https://a-chacon.com/just%20ruby/2024/03/03/ruby-in-the-browser.html"><![CDATA[<h2 id="qué-es-webassembly">¿Qué es WebAssembly?</h2>

<p>WebAssembly (abreviado como Wasm) es un formato de instrucciones binarias diseñado para ser interpretado por máquinas virtuales basadas en pilas (que realizan operaciones utilizando pilas).</p>

<p>Este formato se creó con el propósito de servir como un objetivo de compilación portátil para diversos lenguajes de programación. ¿Qué significa esto? Básicamente, permite que el código escrito en diferentes lenguajes se compile en un formato común que puede ejecutarse tanto en el lado del cliente como en el servidor a una velocidad similar a las aplicaciones nativas.</p>

<p>Si bien su principal objetivo es ejecutarse en navegadores web, también es posible extender su uso a otros tipos de dispositivos, como aplicaciones móviles, dispositivos IoT o incluso programas de gran escala. WebAssembly está principalmente enfocado en empaquetar código C/C++ para su uso en la web, pero también puede ser utilizado por lenguajes interpretados como Ruby o Python.</p>

<h2 id="ruby-en-webassembly">Ruby en WebAssembly</h2>

<p>Ruby es un lenguaje interpretado, y como tal, necesita una implementación/interprete que lo lea y lo transforme a lenguaje de máquinas, en pocas palabras. Por lo tanto, para que sea posible su ejecución en un navegador web, es necesario que esta implementación/interprete esté presente en el navegador web. Y eso es precisamente lo que se intenta hacer: enviar una versión compilada del intérprete al navegador web en formato Wasm que sirva de máquina virtual para correr el código escrito en Ruby.</p>

<h3 id="implementaciones">Implementaciones</h3>

<p>Algunas implementaciones que encontré:</p>

<ul>
  <li><a href="https://github.com/ruby/ruby.wasm/">ruby.wasm</a> <strong>Oficial</strong>: CRuby portado a WebAssembly.</li>
  <li><a href="https://github.com/vmware-labs/webassembly-language-runtimes/tree/main/ruby">wmware</a>: Proporciona un entorno de ejecución para Ruby portado a wasm.</li>
  <li><a href="https://github.com/Shopify/ruvy">Ruvy</a>: Por Shopify.</li>
  <li><a href="https://www.artichokeruby.org/">Artichoke</a>: Intérprete de Ruby hecho en Rust.</li>
</ul>

<p>De aquí en adelante nos enfocaremos solo en la implementación oficial.</p>

<h2 id="ejecutando-ruby-en-el-navegador">Ejecutando Ruby en el navegador</h2>

<p>Tomando el ejemplo que aparece en la documentación de ruby.wasm, ejecutar código Ruby es tan simple como esto:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;html&gt;</span>
  <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/@ruby/3.3-wasm-wasi@2.5.0/dist/browser.script.iife.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
  <span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/ruby"</span><span class="nt">&gt;</span>
    <span class="nx">require</span> <span class="dl">"</span><span class="s2">js</span><span class="dl">"</span>

    <span class="nx">puts</span> <span class="nx">RUBY_VERSION</span> <span class="err">#</span> <span class="o">=&gt;</span> <span class="nx">Hello</span><span class="p">,</span> <span class="nx">world</span><span class="o">!</span> <span class="p">(</span><span class="nx">printed</span> <span class="nx">to</span> <span class="nx">the</span> <span class="nx">browser</span> <span class="nx">console</span><span class="p">)</span>
    <span class="nx">JS</span><span class="p">.</span><span class="nb">global</span><span class="p">[:</span><span class="nb">document</span><span class="p">].</span><span class="nx">write</span> <span class="dl">"</span><span class="s2">Hello, world!</span><span class="dl">"</span>
  <span class="nt">&lt;/script&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>Haciendo uso del intérprete compilado en Wasm y cargado a través de un CDN se puede ejecutar código Ruby. Esta es la forma más simple que tenemos para correr Ruby en el navegador.</p>

<p><strong>Exploremos un poco más</strong></p>

<p>Escribamos nuestro código Ruby en un archivo .rb y luego incluyámoslo en nuestra página web. De esta manera:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># script.rb</span>
<span class="nb">require</span> <span class="s2">"js"</span>

<span class="n">document</span> <span class="o">=</span> <span class="no">JS</span><span class="p">.</span><span class="nf">global</span><span class="p">[</span><span class="ss">:document</span><span class="p">]</span>
<span class="n">button</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">getElementById</span> <span class="s2">"draw"</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">getElementById</span> <span class="s2">"result"</span>
<span class="n">button</span><span class="p">.</span><span class="nf">addEventListener</span> <span class="s2">"click"</span> <span class="k">do</span> <span class="o">|</span><span class="n">e</span><span class="o">|</span>
  <span class="nb">p</span> <span class="n">e</span>
  <span class="n">luckiness</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"Lucky"</span><span class="p">,</span> <span class="s2">"Unlucky"</span><span class="p">].</span><span class="nf">sample</span>
  <span class="n">result</span><span class="p">[</span><span class="ss">:innerText</span><span class="p">]</span> <span class="o">=</span> <span class="n">luckiness</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y nuestra página web:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;html&gt;</span>
  <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/@ruby/head-wasm-wasi@2.5.0/dist/browser.script.iife.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
  <span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"text/ruby"</span> <span class="na">src=</span><span class="s">"script.rb"</span><span class="nt">&gt;&lt;/script&gt;</span>
  <span class="nt">&lt;button</span> <span class="na">id=</span><span class="s">"draw"</span><span class="nt">&gt;</span>Draw Omikuji<span class="nt">&lt;/button&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"result"</span><span class="nt">&gt;&lt;/div&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>¡Funciona! Pero, ¿y si necesito usar una gema? Bueno, recientemente <code class="language-plaintext highlighter-rouge">ruby.wasm</code> <a href="https://github.com/ruby/ruby.wasm/pull/358">incluyó</a> la posibilidad de compilar tu propio módulo Ruby Wasm a partir de un archivo <code class="language-plaintext highlighter-rouge">Gemfile.lock</code>. Puedes revisar <a href="https://evilmartians.com/chronicles/first-steps-with-ruby-wasm-or-building-ruby-next-playground">esta</a> publicación sobre cómo utilizarlo.</p>

<p>Esta historia continuará…</p>

<hr />

<p>¿Será el futuro del desarrollo web? No lo sé, pero es un tema interesante de explorar y seguir el desarrollo que va teniendo en el tiempo. Hasta el momento, no creo que represente una gran ventaja sobre JavaScript o lenguajes compilados, puesto que tener que cargar todo el intérprete no resulta en archivos livianos para la web. Hasta aquí por ahora, espero que hayas aprendido algo nuevo.</p>

<p>Proyectos interesantes con WASM:</p>

<ul>
  <li><a href="https://mame.github.io/emirb/">https://mame.github.io/emirb/</a></li>
  <li><a href="https://largo.github.io/ruby.wasm-quickstart/">https://largo.github.io/ruby.wasm-quickstart/</a></li>
  <li><a href="https://irb-wasm.vercel.app/">https://irb-wasm.vercel.app/</a></li>
</ul>]]></content><author><name>Andrés</name></author><category term="[&quot;Just Ruby&quot;]" /><summary type="html"><![CDATA[WebAssembly (abreviado como Wasm) es un formato de instrucciones binarias diseñado para ser interpretado por máquinas virtuales basadas en pilas.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/ruby-wasm.avif" /><media:content medium="image" url="https://a-chacon.com/assets/images/ruby-wasm.avif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">💎Ruby Tip💎 ¿Sabías que Ruby Soporta Coincidencia de Patrones?</title><link href="https://a-chacon.com/just%20ruby/2023/12/08/ruby-tip-pattern-matching.html" rel="alternate" type="text/html" title="💎Ruby Tip💎 ¿Sabías que Ruby Soporta Coincidencia de Patrones?" /><published>2023-12-08T00:00:00-03:00</published><updated>2023-12-08T00:00:00-03:00</updated><id>https://a-chacon.com/just%20ruby/2023/12/08/ruby-tip-pattern-matching</id><content type="html" xml:base="https://a-chacon.com/just%20ruby/2023/12/08/ruby-tip-pattern-matching.html"><![CDATA[<p>Coincidencia de patrones es una funcionalidad que fue introducida en Ruby 2.7. Desde Ruby 3.0 en adelante, ya no es una funcionalidad experimental, y podemos empezar a usarla sin un molesto warning:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(irb):3: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
</code></pre></div></div>

<hr />

<h2 id="pero-qué-es-pattern-matching">Pero, ¿Qué es Pattern Matching?</h2>

<p>La coincidencia de patrones es una característica que permite comparar y entender la estructura de información organizada, como datos o variables. Esto se hace verificando cómo está organizada la información y asignando las partes coincidentes a variables locales para su uso posterior.</p>

<p><strong>Pattern Matching</strong> es soportado mediante la sintaxis <code class="language-plaintext highlighter-rouge">case / in</code>. Importante no confundir con <code class="language-plaintext highlighter-rouge">case / when</code> ni tampoco mezclar. Si no existe match con ninguna expresión y tampoco hay un <code class="language-plaintext highlighter-rouge">else</code> definido, entonces se levanta una excepción del tipo <code class="language-plaintext highlighter-rouge">NoMatchingPatternError</code>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="o">&lt;</span><span class="n">expression</span><span class="o">&gt;</span>
<span class="k">in</span> <span class="o">&lt;</span><span class="n">pattern1</span><span class="o">&gt;</span>
  <span class="c1"># ...</span>
<span class="k">in</span> <span class="o">&lt;</span><span class="n">pattern2</span><span class="o">&gt;</span>
  <span class="c1"># ...</span>
<span class="k">else</span>
  <span class="c1"># ...</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Los patrones pueden ser:</p>

<ul>
  <li>
    <p><strong>Valor</strong>: Cualquier objeto de Ruby (se compara con el operador ===, como en ‘when’).</p>
  </li>
  <li>
    <p><strong>Array</strong>: Patrón de arreglo: <code class="language-plaintext highlighter-rouge">[&lt;subpatrón&gt;, &lt;subpatrón&gt;, &lt;subpatrón&gt;, ...]</code>.</p>
  </li>
  <li>
    <p><strong>Find</strong>: Patrón de búsqueda: <code class="language-plaintext highlighter-rouge">[*variable, &lt;subpatrón&gt;, &lt;subpatrón&gt;, &lt;subpatrón&gt;, ..., *variable]</code>.</p>
  </li>
  <li>
    <p><strong>Hash</strong>: Patrón de hash: <code class="language-plaintext highlighter-rouge">{clave: &lt;subpatrón&gt;, clave: &lt;subpatrón&gt;, ...}</code>.</p>
  </li>
  <li>
    <p><strong>Alternativa</strong>: Combinación de patrones con <code class="language-plaintext highlighter-rouge">|</code> (barra vertical).</p>
  </li>
  <li>
    <p><strong>Captura de variable</strong>: <code class="language-plaintext highlighter-rouge">&lt;patrón&gt; =&gt; variable</code> o <code class="language-plaintext highlighter-rouge">variable</code>.</p>
  </li>
</ul>

<h2 id="pattern-matching-en-la-práctica">Pattern Matching en la práctica</h2>

<p>Aquí tenemos una función que procesa datos:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Define a method that uses pattern matching with case/in</span>
<span class="k">def</span> <span class="nf">process_data</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
  <span class="k">case</span> <span class="n">data</span>
  <span class="k">in</span> <span class="p">{</span> <span class="ss">type: </span><span class="s2">"number"</span><span class="p">,</span> <span class="ss">value: </span><span class="no">Integer</span> <span class="o">=&gt;</span> <span class="n">num</span> <span class="p">}</span>
    <span class="nb">puts</span> <span class="s2">"Received a number: </span><span class="si">#{</span><span class="n">num</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">in</span> <span class="p">{</span> <span class="ss">type: </span><span class="s2">"string"</span><span class="p">,</span> <span class="ss">value: </span><span class="no">String</span> <span class="o">=&gt;</span> <span class="n">str</span> <span class="p">}</span>
    <span class="nb">puts</span> <span class="s2">"Received a string: </span><span class="si">#{</span><span class="n">str</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">in</span> <span class="p">{</span> <span class="ss">type: </span><span class="s2">"array"</span><span class="p">,</span> <span class="ss">value: </span><span class="no">Array</span> <span class="o">=&gt;</span> <span class="n">arr</span> <span class="p">}</span>
    <span class="nb">puts</span> <span class="s2">"Received an array: </span><span class="si">#{</span><span class="n">arr</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">in</span> <span class="p">{</span> <span class="ss">type: </span><span class="s2">"hash"</span><span class="p">,</span> <span class="ss">value: </span><span class="no">Hash</span> <span class="o">=&gt;</span> <span class="nb">hash</span> <span class="p">}</span>
    <span class="nb">puts</span> <span class="s2">"Received a hash: </span><span class="si">#{</span><span class="nb">hash</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">else</span>
    <span class="nb">puts</span> <span class="s2">"Received something else."</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="c1"># Test the method with different data structures</span>
<span class="n">process_data</span><span class="p">({</span> <span class="ss">type: </span><span class="s2">"number"</span><span class="p">,</span> <span class="ss">value: </span><span class="mi">42</span> <span class="p">})</span>               <span class="c1"># Output: Received a number: 42</span>
<span class="n">process_data</span><span class="p">({</span> <span class="ss">type: </span><span class="s2">"string"</span><span class="p">,</span> <span class="ss">value: </span><span class="s2">"Hello, Ruby!"</span> <span class="p">})</span>   <span class="c1"># Output: Received a string: Hello, Ruby!</span>
<span class="n">process_data</span><span class="p">({</span> <span class="ss">type: </span><span class="s2">"array"</span><span class="p">,</span> <span class="ss">value: </span><span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">]</span> <span class="p">})</span>         <span class="c1"># Output: Received an array: [1, 2, 3]</span>
<span class="n">process_data</span><span class="p">({</span> <span class="ss">type: </span><span class="s2">"hash"</span><span class="p">,</span> <span class="ss">value: </span><span class="p">{</span> <span class="ss">key: </span><span class="s2">"value"</span> <span class="p">}</span> <span class="p">})</span>   <span class="c1"># Output: Received a hash: {:key=&gt;"value"}</span>
<span class="n">process_data</span><span class="p">({</span> <span class="ss">type: </span><span class="s2">"unknown"</span><span class="p">,</span> <span class="ss">value: </span><span class="s2">"unknown data"</span> <span class="p">})</span>  <span class="c1"># Output: Received something else.</span>
</code></pre></div></div>

<p>En este ejemplo, mostramos cómo realizar una búsqueda basada en un patrón de Hash. Destacamos una de las potentes funciones de Pattern Matching: la <strong>asignación de variables</strong>. Logramos asignar un valor del hash desestructurado a una variable, lo que nos permite trabajar con ese valor de manera posterior en nuestro código.</p>

<h3 id="deconstruct-y-deconstruct_keys">Deconstruct y Deconstruct_keys</h3>

<p>Existen dos métodos especiales en coincidencia de patrones: <code class="language-plaintext highlighter-rouge">deconstruct</code>, llamado cuando se trata de evaluar sobre un Array, y <code class="language-plaintext highlighter-rouge">deconstruct_keys</code>, llamado cuando se trata de evaluar sobre un Hash. Veamos un ejemplo:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Coordinate</span>
  <span class="nb">attr_accessor</span> <span class="ss">:x</span><span class="p">,</span> <span class="ss">:y</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
    <span class="vi">@x</span> <span class="o">=</span> <span class="n">x</span>
    <span class="vi">@y</span> <span class="o">=</span> <span class="n">y</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">deconstruct</span>
    <span class="p">[</span><span class="vi">@x</span><span class="p">,</span> <span class="vi">@y</span><span class="p">]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">deconstruct_key</span>
    <span class="p">{</span><span class="ss">x: </span><span class="vi">@x</span><span class="p">,</span> <span class="ss">y: </span><span class="vi">@y</span><span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>En la clase <strong>Coordinate</strong>, se define un método <code class="language-plaintext highlighter-rouge">deconstruct</code> y <code class="language-plaintext highlighter-rouge">deconstruct_key</code> que retornan un Array y un Hash respectivamente.</p>

<p>Entonces, cuando una instancia de la clase <strong>Coordinate</strong> es evaluada sobre un array, lo que sucede es que el método <code class="language-plaintext highlighter-rouge">deconstruct</code> es llamado en la instancia a evaluar:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">c</span> <span class="o">=</span> <span class="no">Coordinates</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="mi">32</span><span class="p">,</span><span class="mi">50</span><span class="p">)</span>

<span class="k">case</span> <span class="n">c</span>
<span class="k">in</span> <span class="p">[</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">]</span>
  <span class="nb">p</span> <span class="n">a</span> <span class="c1">#=&gt; 32</span>
  <span class="nb">p</span> <span class="n">b</span> <span class="c1">#=&gt; 50</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y cuando la misma instancia es evaluada sobre un Hash, entonces el método <code class="language-plaintext highlighter-rouge">deconstruct_key</code> es llamado:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="n">c</span>
<span class="k">in</span> <span class="p">{</span><span class="n">x</span><span class="p">:,</span> <span class="n">y</span><span class="p">:}</span>
  <span class="nb">p</span> <span class="n">x</span> <span class="c1">#=&gt; 32</span>
  <span class="nb">p</span> <span class="n">y</span> <span class="c1">#=&gt; 50</span>
<span class="k">end</span>
</code></pre></div></div>

<hr />

<p>Si te ha interesado el tema, te invito a buscar más información en la <a href="https://docs.ruby-lang.org/en/master/syntax/pattern_matching_rdoc.html">documentación</a>. Existen otros elementos interesantes de la coincidencia de patrones, como el uso del operador pin (^) y <em>Guard clauses</em> (<code class="language-plaintext highlighter-rouge">if</code> y <code class="language-plaintext highlighter-rouge">unless</code>).</p>

<hr />

<p>Hasta aquí con la pequeña introducción al tema. Si no conocías esta sintaxis, espero que te vayas con una nueva herramienta para seguir desarrollando tus proyectos con Ruby.</p>

<p>Happy Coding!</p>]]></content><author><name>Andrés</name></author><category term="[&quot;Just Ruby&quot;]" /><summary type="html"><![CDATA[La Coincidencia de Patrones en Ruby permite desestructurar datos de manera concisa, facilitando la asignación de variables con sintaxis clara y de forma elegante.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/pattern_matching.avif" /><media:content medium="image" url="https://a-chacon.com/assets/images/pattern_matching.avif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">Carga Asíncrona en Active Record: Potenciando el Rendimiento de tu Aplicación Rails 🚀</title><link href="https://a-chacon.com/on%20rails/2023/11/26/rails-activerecord-loadasync.html" rel="alternate" type="text/html" title="Carga Asíncrona en Active Record: Potenciando el Rendimiento de tu Aplicación Rails 🚀" /><published>2023-11-26T00:00:00-03:00</published><updated>2023-11-26T00:00:00-03:00</updated><id>https://a-chacon.com/on%20rails/2023/11/26/rails-activerecord-loadasync</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2023/11/26/rails-activerecord-loadasync.html"><![CDATA[<p>El rendimiento de tu aplicación web es crucial, ya que afecta aspectos que van desde el SEO hasta los costos que tendrás en tu servicio de alojamiento al final del mes y la huella de carbono que tu sitio web está dejando en el planeta🌍. Por lo tanto, es fundamental conocer cualquier herramienta que pueda ayudarte a mejorarlo, estar al tanto de su existencia y aplicarla cuando sea necesario.</p>

<p>Este tema es tan extenso que se podría escribir un libro respecto a la optimización del rendimiento de tu aplicación y sus efectos: tiempos de carga y percepción de performance del usuario, uso de caché, consultas n+1, implementación de CDN, escalabilidad y mucho más. Pero en esta ocasión, quiero ofrecer una breve introducción a lo que es la carga de datos de forma asíncrona en Active Record con el método <a href="https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-load_async">load_async</a> y sus amigos (introducidos en Rails 7.1).</p>

<h2 id="problema">Problema</h2>

<p>Supongamos que tenemos el siguiente seudo controlador:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
  <span class="k">def</span> <span class="nf">main</span>
    <span class="vi">@films</span> <span class="o">=</span> <span class="no">Film</span><span class="p">.</span><span class="nf">slow</span> <span class="c1"># 2 seconds</span>
    <span class="vi">@reviews</span> <span class="o">=</span> <span class="no">Review</span><span class="p">.</span><span class="nf">slow</span> <span class="c1"># 4 seconds</span>

    <span class="nb">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># It could be another process like call an external API</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>

<p>El scope slow será algo como: <code class="language-plaintext highlighter-rouge">scope :slow, -&gt; { select('*, sleep(1)') }</code>, por lo que va a variar dependiendo de la cantidad de registros que tengamos en la base de datos. La ejecución en secuencia del método main tomaría un poco más de 8 segundos, como podemos ver en los logs:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Completed 200 OK <span class="k">in </span>8101ms
</code></pre></div></div>

<h2 id="optimización-del-tiempo-de-respuesta-con-sql-asíncrono">Optimización del tiempo de respuesta con SQL asíncrono</h2>

<p>Rails 7 introdujo el nuevo método <a href="https://github.com/rails/rails/blob/6b93fff8af32ef5e91f4ec3cfffb081d0553faf0/activerecord/lib/active_record/relation.rb#L696">load_async</a> en Active Record para que la consulta se realice desde un grupo de hilos en segundo plano. Esto permite que tus consultas se ejecuten de forma paralela, optimizando el tiempo de respuesta de tu controlador.</p>

<p>El metodo <code class="language-plaintext highlighter-rouge">load_async</code> requiere de una configuracion previa que puedes encontrar <a href="https://guides.rubyonrails.org/configuring.html#config-active-record-async-query-executor">aqui</a>. Luego de eso pasaremos a la implementación y veremos el resultado:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
  <span class="k">def</span> <span class="nf">main</span>
    <span class="vi">@films</span> <span class="o">=</span> <span class="no">Film</span><span class="p">.</span><span class="nf">slow</span><span class="p">.</span><span class="nf">load_async</span> <span class="c1"># 2 seconds</span>
    <span class="vi">@reviews</span> <span class="o">=</span> <span class="no">Review</span><span class="p">.</span><span class="nf">slow</span><span class="p">.</span><span class="nf">load_async</span> <span class="c1"># 4 seconds</span>

    <span class="nb">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># It could be another process like call an external API</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Completed 200 OK <span class="k">in </span>4052ms
</code></pre></div></div>

<h3 id="qué-pasó">¿Qué pasó?</h3>

<p>Logramos reducir el tiempo de respuesta casi a la mitad porque nuestras consultas se ejecutaron en paralelo. ¿Por qué 4 segundos? Porque <code class="language-plaintext highlighter-rouge">Review.slow</code> es la consulta que toma más tiempo: 4 segundos; durante ese momento, el hilo principal termina de ejecutar la función <code class="language-plaintext highlighter-rouge">sleep(2)</code>, llama al resultado de <code class="language-plaintext highlighter-rouge">Film.slow</code>, que probablemente ya está listo debido a que toma dos segundos. Al llamar al resultado de <code class="language-plaintext highlighter-rouge">Review.slow</code>, se encuentra con que aún no ha terminado (le faltan 2 segundos), por lo que la pasa al hilo principal y la termina de ejecutar (2 segundos en sleep y luego 2 segundos más para terminar <code class="language-plaintext highlighter-rouge">Review.slow</code> llegamos a nuestros 4 segundos).</p>

<h2 id="rails-71">Rails 7.1</h2>

<p>Ya que <code class="language-plaintext highlighter-rouge">load_async</code> es específicamente un método de la clase <code class="language-plaintext highlighter-rouge">ActiveRecord::Relation</code>, no nos funcionaría para agregaciones o respuestas de un solo registro. Para eso, en Rails 7.1 se introducen una serie de métodos que nos van a ayudar a realizar este tipo de consultas en segundo plano:</p>

<ul>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_count"><code class="language-plaintext highlighter-rouge">async_count</code></a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_sum"><code class="language-plaintext highlighter-rouge">async_sum</code></a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_minimum"><code class="language-plaintext highlighter-rouge">async_minimum</code></a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_maximum"><code class="language-plaintext highlighter-rouge">async_maximum</code></a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_average"><code class="language-plaintext highlighter-rouge">async_average</code></a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_pluck"><code class="language-plaintext highlighter-rouge">async_pluck</code></a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_pick"><code class="language-plaintext highlighter-rouge">async_pick</code></a></li>
  <li><a href="https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-async_ids"><code class="language-plaintext highlighter-rouge">async_ids</code></a></li>
  <li><a href="https://api.rubyonrails.org/v7.1.0/classes/ActiveRecord/Querying.html#method-i-async_find_by_sql"><code class="language-plaintext highlighter-rouge">async_find_by_sql</code></a></li>
  <li><a href="https://api.rubyonrails.org/v7.1.0/classes/ActiveRecord/Querying.html#method-i-async_count_by_sql"><code class="language-plaintext highlighter-rouge">async_count_by_sql</code></a></li>
</ul>

<p>A diferencia de <code class="language-plaintext highlighter-rouge">load_async</code>, estos métodos retornan un objeto del tipo <a href="https://api.rubyonrails.org/classes/ActiveRecord/Promise.html">ActiveRecord::Promise</a> y para obtener el resultado deberemos ejecutar el método <code class="language-plaintext highlighter-rouge">value</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ApplicationController</span> <span class="o">&lt;</span> <span class="no">ActionController</span><span class="o">::</span><span class="no">Base</span>
  <span class="k">def</span> <span class="nf">main</span>
    <span class="vi">@films_count</span> <span class="o">=</span> <span class="no">Film</span><span class="p">.</span><span class="nf">slow</span><span class="p">.</span><span class="nf">async_count</span>
    <span class="vi">@reviews</span> <span class="o">=</span> <span class="no">Review</span><span class="p">.</span><span class="nf">slow</span><span class="p">.</span><span class="nf">load_async</span>

    <span class="nb">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="c1"># Podría ser otro proceso, como llamar a una API externa</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y luego, en la vista, para acceder a los datos:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;span&gt;</span><span class="cp">&lt;%=</span> <span class="vi">@films_count</span><span class="p">.</span><span class="nf">value</span> <span class="cp">%&gt;</span><span class="nt">&lt;span&gt;</span>
</code></pre></div></div>

<h3 id="pensamientos-finales">Pensamientos finales</h3>

<p>La carga asíncrona de datos puede ser tu gran aliado al momento de querer mejorar el rendimiento de tu aplicación. Lograrás mejorar los tiempos de respuesta con simples cambios en tu código. Creo que hasta aquí es una buena introducción al tema, pero no deberías quedarte solo con estos conocimientos. Si te interesó el tema, te recomiendo que leas el siguiente blog post:</p>

<ul>
  <li><a href="https://pawelurbanek.com/rails-load-async">The In-depth Guide to ActiveRecord load_async in Rails 7</a></li>
</ul>

<p>Ahí podrás entender mejor el funcionamiento de la carga asíncrona, casos de uso y por qué no abusar de esto.</p>

<p>Hasta aquí por hoy, espero que hayas aprendido algo nuevo. Cualquier detalle, aporte o comentario, no dudes en escribirme.</p>]]></content><author><name>Andrés</name></author><category term="[&quot;On Rails&quot;]" /><summary type="html"><![CDATA[Optimiza el rendimiento en Rails 7.1 con carga asíncrona de datos utilizando Active Record y el método load_async. Reduce los tiempos de respuesta al ejecutar consultas en paralelo, mejorando la eficiencia de tu aplicación.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/load_async.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/load_async.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">💎Ruby Tip💎 Depuración interactiva sin necesidad de instalar gemas.</title><link href="https://a-chacon.com/just%20ruby/2023/11/20/ruby-tip-binding-irb.html" rel="alternate" type="text/html" title="💎Ruby Tip💎 Depuración interactiva sin necesidad de instalar gemas." /><published>2023-11-20T00:00:00-03:00</published><updated>2023-11-20T00:00:00-03:00</updated><id>https://a-chacon.com/just%20ruby/2023/11/20/ruby-tip-binding-irb</id><content type="html" xml:base="https://a-chacon.com/just%20ruby/2023/11/20/ruby-tip-binding-irb.html"><![CDATA[<p>Existen diferentes gemas populares en el universo de Ruby con diversas funcionalidades y diferentes sintaxis para realizar una depuración interactiva. Algunas de estas gemas pueden ser <a href="https://github.com/deivid-rodriguez/byebug">byebug</a> o <a href="https://github.com/ruby/debug">debug</a>. El problema con estas gemas es que, a veces, necesitan ser instaladas, configuradas y con comandos propios que debemos aprender. Esto toma algo de tiempo y muchas veces no hay necesidad de algo tan complejo para un error tan pequeño.</p>

<p>Para esos casos, tendremos la opción de usar la clase <a href="https://docs.ruby-lang.org/en/master/Binding.html">Binding</a>. Esta nos permite encapsular el contexto de ejecución en un punto determinado y retornarlo para usos futuros. Los objetos Binding pueden ser creados llamando al método <code class="language-plaintext highlighter-rouge">Kernel#binding</code> y la consola se levantará mediante el método de instancia público <code class="language-plaintext highlighter-rouge">irb</code>.</p>

<p>Con un poco de código nos quedará más que claro:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># door.rb</span>
<span class="k">class</span> <span class="nc">Door</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@open</span> <span class="o">=</span> <span class="kp">false</span>
    <span class="nb">binding</span><span class="p">.</span><span class="nf">irb</span>
    <span class="nb">puts</span> <span class="s2">"Is the door open: </span><span class="si">#{</span><span class="vi">@open</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">Door</span><span class="p">.</span><span class="nf">new</span>
</code></pre></div></div>

<p>Al correr nuestro pequeño script se nos abrirá una sesión IRB (<a href="https://stdgems.org/irb/">Gema por defecto</a>) con la cual podrás revisar el contexto y modificarlo:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Documentos</span><span class="o">/</span><span class="n">scripts</span><span class="o">/</span><span class="n">ruby</span> <span class="n">via</span> <span class="err">💎</span> <span class="n">v3</span><span class="o">.</span><span class="mf">2.2</span>
<span class="err">❯</span> <span class="n">ruby</span> <span class="n">door</span><span class="p">.</span><span class="nf">rb</span>

<span class="no">From</span><span class="p">:</span> <span class="n">door</span><span class="p">.</span><span class="nf">rb</span> <span class="err">@</span> <span class="n">line</span> <span class="mi">4</span> <span class="p">:</span>

    <span class="mi">1</span><span class="p">:</span> <span class="k">class</span> <span class="nc">Door</span>
    <span class="mi">2</span><span class="p">:</span>   <span class="k">def</span> <span class="nf">initialize</span>
    <span class="mi">3</span><span class="p">:</span>     <span class="vi">@open</span> <span class="o">=</span> <span class="kp">false</span>
 <span class="o">=&gt;</span> <span class="mi">4</span><span class="p">:</span>     <span class="nb">binding</span><span class="p">.</span><span class="nf">irb</span>
    <span class="mi">5</span><span class="p">:</span>     <span class="nb">puts</span> <span class="s2">"Is the door open: </span><span class="si">#{</span><span class="vi">@open</span><span class="si">}</span><span class="s2">"</span>
    <span class="mi">6</span><span class="p">:</span>   <span class="k">end</span>
    <span class="mi">7</span><span class="p">:</span> <span class="k">end</span>
    <span class="mi">8</span><span class="p">:</span>
    <span class="mi">9</span><span class="p">:</span> <span class="no">Door</span><span class="p">.</span><span class="nf">new</span>

<span class="n">irb</span><span class="p">(</span><span class="c1">#&lt;Door:0x00007fa9a0f367a8&gt;):001&gt; @open</span>
<span class="o">=&gt;</span> <span class="kp">false</span>
<span class="n">irb</span><span class="p">(</span><span class="c1">#&lt;Door:0x00007fa9a0f367a8&gt;):002&gt; @open=true</span>
<span class="o">=&gt;</span> <span class="kp">true</span>
<span class="n">irb</span><span class="p">(</span><span class="c1">#&lt;Door:0x00007fa9a0f367a8&gt;):003&gt; exit</span>
<span class="no">Is</span> <span class="n">the</span> <span class="n">door</span> <span class="ss">open: </span><span class="kp">true</span>

</code></pre></div></div>

<p>Y eso es todo, puedes usarlo para depurar tus scripts, web scrappers o lo que sea que estés construyendo.</p>

<p>También es muy probable que ya tengas instalada la gema <a href="https://github.com/ruby/debug">debug</a>, dado que se instala automáticamente con tu versión de Ruby. IRB se integra de manera excelente con esta gema, así que si necesitas funciones más avanzadas, simplemente escribe <code class="language-plaintext highlighter-rouge">debug</code> en tu sesión de IRB y la activarás.</p>

<p>Para saber más sobre el uso de IRB, puedes visitar <a href="https://docs.ruby-lang.org/en/master/IRB.html#module-IRB-label-Usage">esta documentación</a> y sobre la integración con debug específicamente <a href="https://github.com/ruby/irb#debugging-with-irb">aquí</a>.</p>

<hr />

<p>Si te ha gustado puedes pasar a saludar en los comentarios, estare atento.</p>

<p>Happy coding!</p>]]></content><author><name>Andrés</name></author><category term="[&quot;Just Ruby&quot;]" /><summary type="html"><![CDATA[Descubre una forma sencilla y rápida de depurar en Ruby sin instalar gemas adicionales. Con la clase Binding y la consola IRB integrada, podrás explorar y modificar el contexto de ejecución para resolver errores de manera eficiente.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/ruby-debugging.avif" /><media:content medium="image" url="https://a-chacon.com/assets/images/ruby-debugging.avif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">Rails Service Objects: Una pequeña guía para agilizar tu código</title><link href="https://a-chacon.com/on%20rails/2023/11/08/rails-service-objects.html" rel="alternate" type="text/html" title="Rails Service Objects: Una pequeña guía para agilizar tu código" /><published>2023-11-08T00:00:00-03:00</published><updated>2023-11-08T00:00:00-03:00</updated><id>https://a-chacon.com/on%20rails/2023/11/08/rails-service-objects</id><content type="html" xml:base="https://a-chacon.com/on%20rails/2023/11/08/rails-service-objects.html"><![CDATA[<p><strong>Ruby on Rails</strong> es un framework full stack que incluye todas las herramientas que necesitas para desarrollar una web app rápidamente. Su estructura se basa
en el patrón de arquitectura MVC y eso es más que suficiente para gran parte de las aplicaciones que vas a desarrollar en un comienzo con RoR.
Pero cuando tu aplicación comienza a crecer conforme a los requerimientos del negocio, es entonces cuando empiezas a crear código que no pertenece ni a
la capa <strong>modelo</strong>, ni a la capa <strong>controlador</strong> y menos a la capa <strong>vista</strong>. Entonces te preguntas: ¿Dónde escribo esto? La respuesta puede ser, no necesariamente, <strong>service objects</strong>.</p>

<blockquote>
  <p>Learn just what you need to get started, then keep leveling up as you go. Ruby on Rails scales from HELLO WORLD to IPO.</p>
</blockquote>

<p>En este post, descubrirás cómo este patrón puede simplificar tu código y mantenerte en control a medida que tu proyecto crece. Exploraremos qué son los service objects y profundizaremos en su implementación para que puedas elegir la que más se adecue a tus necesidades.</p>

<h1 id="qué-es-service-objects-en-rails">¿Qué es Service Objects en Rails?</h1>

<p>Se podría definir como un patrón de diseño de software adoptado por la comunidad de Rails que se utiliza para extraer cierta lógica procedimental de los modelos y los controladores en objetos de un solo propósito. Muy similar a una implementación de <a href="https://en.wikipedia.org/wiki/Command_pattern">Command pattern</a> en Ruby y Rails.</p>

<p>Los Service Objects vienen como una forma fácil de mantener parte de nuestra lógica de negocio fuera de nuestros modelos y controladores, creando objetos de una sola responsabilidad que son fáciles de testear, reutilizables y simples. Esto hace que nuestros controladores sean más limpios y que nuestros modelos se encarguen de su principal tarea: representar los datos del negocio.</p>

<p>Son “simples” porque deben cumplir con una única tarea y la implementación más común será mediante un PORO (“Plain Old Ruby Object”) que básicamente tendrá:</p>

<ol>
  <li>Un método de iniciación.</li>
  <li>Un único método público. Por lo general <code class="language-plaintext highlighter-rouge">call</code> o <code class="language-plaintext highlighter-rouge">run</code>.</li>
  <li>Retornar una respuesta predecible luego de la ejecución.</li>
</ol>

<h2 id="mvc--s">MVC + S</h2>

<iframe src="https://giphy.com/embed/PidhSZjIQOWNyOxwCb" width="480" height="360" style="" frameborder="0" class="giphy-embed" allowfullscreen=""></iframe>
<p><a href="https://giphy.com/gifs/muppetwiki-sesame-street-muppets-grover-PidhSZjIQOWNyOxwCb">via GIPHY</a></p>

<p><strong>Entonces, ahora podríamos hablar de una capa adicional en nuestra aplicación MVC, que será la capa de Servicios encargada de encapsular la lógica del negocio y se llevará a cabo mediante el uso de Service Objects.</strong></p>

<p>Algo positivo que podemos destacar de esto es que la lógica de negocio es una de las partes que más evolucionará a lo largo del tiempo en nuestra aplicación. Por lo tanto, uno de los beneficios de encapsularla en estos <strong>Service Objects</strong> es que será más fácil modificarla con el transcurso del tiempo sin tener que modificar más que una sola parte de tu aplicación.</p>

<p>No está de más mencionar que también es beneficioso para los nuevos desarrolladores que se integren a tu equipo, ya que <strong>Service Objects</strong> nos ayuda a respetar principios como <strong>KISS</strong> (Keep It Simple, Stupid) o <strong>SRP</strong> (Single Responsibility Principle) que directamente disminuyen la complejidad de tus clases (Models o Controllers) y aumenta la rapidez con la que los otros pueden comprender tu código.</p>

<h1 id="implementación">Implementación</h1>

<p>Ahora pasaremos a la parte práctica. Como mencioné en la sección anterior, la forma más simple de implementar los Service Objects será mediante POROs (Plain Old Ruby Objects), pero no es la única. En este artículo, mostraré tres formas de hacerlo:</p>

<ol>
  <li>PORO (Plain Old Ruby Object)</li>
  <li>Dry.rb</li>
  <li>Interactor</li>
</ol>

<h3 id="contexto">Contexto</h3>

<p>Tenemos un inicio de sesión en una API REST con Rails. Debemos autenticar al usuario mediante el uso de un correo y una contraseña. Si detectamos que el usuario está ingresando desde una nueva IP, entonces debemos enviar un correo de seguridad a su cuenta para confirmar que es él. Además, si el usuario realiza más de 3 intentos fallidos consecutivos, bloquearemos su cuenta durante 5 minutos.</p>

<p><strong>Desde ahora aparecerá mucho código que no es necesario entender/leer con tanto detalle. Más importante es entender la intención.</strong></p>

<p>Sin Service Objects, haríamos algo como esto:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/authentication_controller.rb</span>

<span class="k">class</span> <span class="nc">AuthenticationController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">])</span>

    <span class="k">if</span> <span class="n">user</span> <span class="o">&amp;&amp;</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span>
      <span class="n">session</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span>
      <span class="n">jwt</span> <span class="o">=</span> <span class="no">JwtManager</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>

      <span class="k">if</span> <span class="n">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">)</span>
        <span class="n">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">)</span>
      <span class="k">end</span>

      <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">success: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">token: </span><span class="n">jwt</span> <span class="p">}</span> <span class="p">}</span>
    <span class="k">else</span>
      <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid email or password'</span> <span class="p">},</span> <span class="ss">status: :unauthorized</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">login_events</span><span class="p">.</span><span class="nf">exists?</span> <span class="ss">ip_address: </span><span class="n">ip</span>

    <span class="n">user</span><span class="p">.</span><span class="nf">login_events</span> <span class="o">&lt;&lt;</span> <span class="no">LoginEvent</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">ip_address: </span><span class="n">ip</span><span class="p">)</span>
    <span class="kp">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">)</span>
    <span class="no">UserMailer</span><span class="p">.</span><span class="nf">security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">).</span><span class="nf">deliver_later</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y necesitamos algo de lógica en nuestro modelo:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models/user.rb</span>
<span class="k">class</span> <span class="nc">User</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">has_secure_password</span>

  <span class="k">def</span> <span class="nf">authenticate</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">locked?</span>

    <span class="k">if</span> <span class="n">valid_password?</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>
      <span class="n">update</span><span class="p">(</span><span class="ss">failed_login_attempts: </span><span class="mi">0</span><span class="p">)</span>
      <span class="k">return</span> <span class="kp">true</span>
    <span class="k">else</span>
      <span class="n">update</span><span class="p">(</span><span class="ss">failed_login_attempts: </span><span class="n">failed_login_attempts</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
      <span class="n">lock_account_for_5_minutes</span> <span class="k">if</span> <span class="n">failed_login_attempts</span> <span class="o">&gt;=</span> <span class="mi">3</span>
      <span class="k">return</span> <span class="kp">false</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">lock_account_for_5_minutes</span>
    <span class="n">update</span><span class="p">(</span><span class="ss">locked_until: </span><span class="mi">5</span><span class="p">.</span><span class="nf">minutes</span><span class="p">.</span><span class="nf">from_now</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">locked?</span>
    <span class="n">locked_until</span><span class="p">.</span><span class="nf">present?</span> <span class="o">&amp;&amp;</span> <span class="no">Time</span><span class="p">.</span><span class="nf">current</span> <span class="o">&lt;</span> <span class="n">locked_until</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">valid_password?</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>
    <span class="no">BCrypt</span><span class="o">::</span><span class="no">Password</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">password_digest</span><span class="p">).</span><span class="nf">is_password?</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="poro">PORO</h2>

<p>Ahora, ¿Qué pasaría si luego nos piden un login para un Administrador usando un modelo diferente? ¿Repetimos la lógica? No, claro que queremos respetar DRY (Don’t repeat yourself) asi que implementamos un Service Object para encapsular nuestra lógica. Entonces, para llevar el ejemplo anterior a un Service Object crearemos un archivo llamado <code class="language-plaintext highlighter-rouge">authentication_service.rb</code> en la carpeta <code class="language-plaintext highlighter-rouge">app/services</code> que es donde guardaremos nuestros objetos. Y el código debería lucir así:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/services/authentication_service.rb</span>
<span class="k">class</span> <span class="nc">AuthenticationService</span>
  <span class="no">MAX_LOGIN_ATTEMPTS</span> <span class="o">=</span> <span class="mi">3</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">request_ip</span><span class="p">)</span>
    <span class="vi">@email</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">]</span>
    <span class="vi">@password</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">]</span>
    <span class="vi">@request_ip</span> <span class="o">=</span> <span class="n">request_ip</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">run</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="vi">@email</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">user</span>
      <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="vi">@password</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">account_locked?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
        <span class="n">session</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span>
        <span class="n">jwt</span> <span class="o">=</span> <span class="no">JwtManager</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>

        <span class="n">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">if</span> <span class="n">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="vi">@request_ip</span><span class="p">)</span>

        <span class="p">{</span> <span class="ss">success: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">token: </span><span class="n">jwt</span> <span class="p">}</span> <span class="p">}</span>
      <span class="k">else</span>
        <span class="n">handle_failed_login</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">else</span>
      <span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid email or password'</span> <span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">handle_failed_login</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">failed_login_attempts: </span><span class="n">user</span><span class="p">.</span><span class="nf">failed_login_attempts</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">locked_until: </span><span class="mi">5</span><span class="p">.</span><span class="nf">minutes</span><span class="p">.</span><span class="nf">from_now</span><span class="p">)</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">failed_login_attempts</span> <span class="o">&gt;=</span> <span class="no">MAX_LOGIN_ATTEMPTS</span>

    <span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid email or password'</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">account_locked?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">failed_login_attempts</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">&gt;=</span> <span class="no">MAX_LOGIN_ATTEMPTS</span> <span class="o">&amp;&amp;</span> <span class="n">user</span><span class="p">.</span><span class="nf">locked_until</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">&gt;</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">login_events</span><span class="p">.</span><span class="nf">exists?</span> <span class="ss">ip_address: </span><span class="n">ip</span>

    <span class="n">user</span><span class="p">.</span><span class="nf">login_events</span> <span class="o">&lt;&lt;</span> <span class="no">LoginEvent</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">ip_address: </span><span class="n">ip</span><span class="p">)</span>
    <span class="kp">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="no">UserMailer</span><span class="p">.</span><span class="nf">security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="vi">@request_ip</span><span class="p">).</span><span class="nf">deliver_later</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>

<p>Y nuestro controllador:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/authentication_controller.rb</span>
<span class="k">class</span> <span class="nc">AuthenticationController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">result</span> <span class="o">=</span> <span class="no">AuthenticationService</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">],</span> <span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">],</span> <span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">).</span><span class="nf">run</span>

    <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="nf">key?</span><span class="p">(</span><span class="ss">:error</span><span class="p">)</span>
      <span class="n">render</span> <span class="ss">json: </span><span class="n">result</span><span class="p">,</span> <span class="ss">status: :unauthorized</span>
    <span class="k">else</span>
      <span class="n">render</span> <span class="ss">json: </span><span class="n">result</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><strong>Beneficios:</strong> Si tu política de seguridad respecto al ingreso a tu plataforma cambia, sabrás dónde está esa lógica. Si tienes dos ingresos diferentes, puedes crear otro servicio con una política diferente. Y tu controlador quedó extremadamente simple. Será un placer escribir un test para un controlador así.</p>

<h2 id="service-object-llamando-otro-service-object">Service Object llamando otro Service object</h2>

<p>Puedes también llamar a un servicio desde otro servicio. Por ejemplo, supongamos que tienes un ingreso diferente para tu usuario admin. Pero queremos reutilizar la política del envío de email de seguridad cuando se trata de un nuevo ingreso desde otra IP.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/services/security_email_service.rb</span>
<span class="k">class</span> <span class="nc">SecurityEmailService</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">)</span>
    <span class="vi">@user</span> <span class="o">=</span> <span class="n">user</span>
    <span class="vi">@ip</span> <span class="o">=</span> <span class="n">ip</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">run</span>
    <span class="k">if</span> <span class="n">first_login_from_new_ip?</span>
      <span class="no">UserMailer</span><span class="p">.</span><span class="nf">security_email</span><span class="p">(</span><span class="vi">@user</span><span class="p">,</span> <span class="vi">@ip</span><span class="p">).</span><span class="nf">deliver_later</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">first_login_from_new_ip?</span>
    <span class="o">...</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>

<p>Entonces nuestro servicio de autenticación previo se simplificaría a:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/services/authentication_service.rb</span>
<span class="k">class</span> <span class="nc">AuthenticationService</span>
  <span class="c1"># ...</span>

  <span class="k">def</span> <span class="nf">authenticate</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="vi">@email</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">user</span>
      <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="vi">@password</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">account_locked?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
        <span class="n">session</span><span class="p">[</span><span class="ss">:user_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="p">.</span><span class="nf">id</span>
        <span class="n">jwt</span> <span class="o">=</span> <span class="no">JwtManager</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>

        <span class="c1"># Send the security email if it's the first login from a new IP</span>
        <span class="no">SecurityEmailService</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="vi">@request_ip</span><span class="p">).</span><span class="nf">run</span>

        <span class="p">{</span> <span class="ss">success: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">token: </span><span class="n">jwt</span> <span class="p">}</span> <span class="p">}</span>
      <span class="k">else</span>
        <span class="n">handle_failed_login</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">else</span>
      <span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid email or password'</span> <span class="p">}</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="c1"># ...</span>
<span class="k">end</span>

</code></pre></div></div>

<p>Asi puedes utilizar el envio de emails de seguridad en otro contexto.</p>

<h1 id="dryrb">Dry.rb</h1>

<p>Ahora le toca el turno a Dry.rb, una colección de bibliotecas Ruby de última generación. En este punto, estaremos utilizando el mismo ejemplo anterior. No es necesario que revises todo el código, ya que es el mismo que se presentó anteriormente. Lo que destaca en esta sección es cómo se declaran cada uno de los pasos a realizar, cómo se pasa la entrada de un paso al siguiente y cómo se manejan los errores.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/services/authentication_service_dry.rb</span>
<span class="k">class</span> <span class="nc">AuthenticationServiceDry</span>
  <span class="kp">include</span> <span class="no">Dry</span><span class="o">::</span><span class="no">Transaction</span>

  <span class="n">step</span> <span class="ss">:find_user</span>
  <span class="n">step</span> <span class="ss">:authenticate_user</span>
  <span class="n">check</span> <span class="ss">:check_login_attempts</span>
  <span class="n">step</span> <span class="ss">:generate_token</span>
  <span class="n">step</span> <span class="ss">:handle_security_email</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">find_user</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">request</span><span class="p">)</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="n">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">])</span>

    <span class="k">if</span> <span class="n">user</span>
      <span class="no">Success</span><span class="p">(</span><span class="ss">user: </span><span class="n">user</span><span class="p">,</span> <span class="ss">params: </span><span class="n">params</span><span class="p">,</span> <span class="ss">request: </span><span class="n">request</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="no">Failure</span><span class="p">(</span><span class="s1">'Invalid email or password'</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">authenticate_user</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">input</span><span class="p">[</span><span class="ss">:user</span><span class="p">]</span>
    <span class="n">params</span> <span class="o">=</span> <span class="n">input</span><span class="p">[</span><span class="ss">:params</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">account_locked?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
      <span class="no">Success</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="n">handle_failed_login</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
      <span class="no">Failure</span><span class="p">(</span><span class="s1">'Invalid email or password'</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">generate_token</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">input</span><span class="p">[</span><span class="ss">:user</span><span class="p">]</span>
    <span class="n">jwt</span> <span class="o">=</span> <span class="no">JwtManager</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="no">Success</span><span class="p">(</span><span class="ss">token: </span><span class="n">jwt</span><span class="p">,</span> <span class="ss">request: </span><span class="n">input</span><span class="p">[</span><span class="ss">:request</span><span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">handle_security_email</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">input</span><span class="p">[</span><span class="ss">:user</span><span class="p">]</span>
    <span class="n">request</span> <span class="o">=</span> <span class="n">input</span><span class="p">[</span><span class="ss">:request</span><span class="p">]</span>

    <span class="k">if</span> <span class="n">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">)</span>
      <span class="n">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">)</span>
    <span class="k">end</span>

    <span class="no">Success</span><span class="p">(</span><span class="ss">token: </span><span class="n">input</span><span class="p">[</span><span class="ss">:token</span><span class="p">])</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">account_locked?</span><span class="p">(</span><span class="n">user</span><span class="p">);</span> <span class="o">...</span> <span class="k">end</span>

  <span class="k">def</span> <span class="nf">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">);</span> <span class="o">...</span> <span class="k">end</span>

  <span class="k">def</span> <span class="nf">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">);</span> <span class="o">...</span> <span class="k">end</span>

  <span class="k">def</span> <span class="nf">handle_failed_login</span><span class="p">(</span><span class="n">user</span><span class="p">);</span> <span class="o">...</span> <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Y en nuestro controlador se implementa de la siguiente forma:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/authentication_controller.rb</span>
<span class="k">class</span> <span class="nc">AuthenticationController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">result</span> <span class="o">=</span> <span class="no">AuthenticationServiceDry</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">params</span><span class="p">,</span> <span class="n">request</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="nf">success?</span>
      <span class="n">token</span> <span class="o">=</span> <span class="n">result</span><span class="p">.</span><span class="nf">value!</span><span class="p">[</span><span class="ss">:token</span><span class="p">]</span>
      <span class="c1"># Authentication successful</span>
      <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">success: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">token: </span><span class="n">token</span> <span class="p">}</span> <span class="p">}</span>
    <span class="k">else</span>
      <span class="n">error_message</span> <span class="o">=</span> <span class="n">result</span><span class="p">.</span><span class="nf">failure</span>
      <span class="c1"># Authentication failed</span>
      <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="n">error_message</span> <span class="p">},</span> <span class="ss">status: :unauthorized</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

</code></pre></div></div>

<p><strong>No profundizaremos mucho sobre esta librería, ya que es muy completa.</strong> Sin embargo, te recomiendo <strong>encarecidamente</strong> que explores <a href="https://dry-rb.org/gems/dry-transaction/0.15/">su documentación</a> y descubras todas las posibilidades que ofrece. Algunos de los <strong>beneficios clave</strong> de utilizar esta librería son:</p>

<ul>
  <li>La <strong>declaración inicial</strong> de cada acción que debe ejecutar tu service object te brinda una idea clara de lo que hace y donde tienes que intervenir si quieres realizar un cambio:</li>
</ul>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">step</span> <span class="ss">:find_user</span>
  <span class="n">step</span> <span class="ss">:authenticate_user</span>
  <span class="n">check</span> <span class="ss">:check_login_attempts</span>
  <span class="n">step</span> <span class="ss">:generate_token</span>
  <span class="n">step</span> <span class="ss">:handle_security_email</span>
</code></pre></div></div>

<ul>
  <li><strong>Un DSL muy efectivo</strong> que facilita la comprensión de lo que ocurre en el Service Object.</li>
  <li><strong>Validación de parámetros</strong> en tus Service Objects con <a href="https://dry-rb.org/gems/dry-validation/1.10/">dry-validations</a>.</li>
  <li>Un sólido <strong>manejo de errores</strong> para cada paso del proceso.</li>
  <li><strong>Facilidades para realizar pruebas</strong>, incluyendo la inyección de pasos.</li>
  <li>La posibilidad de <strong>desarrollar tus propios adaptadores de pasos</strong>.</li>
</ul>

<p>Si te intereso esta implementación te recomiendo este <a href="https://www.youtube.com/watch?v=YXiqzHMmv_o">video</a></p>

<h2 id="interactor">Interactor</h2>

<p><strong>Interactor</strong> es otra forma de llevar a cabo el uso de Service Objects con un nombre diferente. También es una solución bien completa para llevar a cabo nuestra implementación, introduce una clase de <strong>Objectos</strong> llamadas <strong>“Organizers”</strong>, que no son más que un <strong>Service Object</strong> que se encarga de llamar de forma secuencial a otros <strong>Interactors (Service Objects)</strong>. Veamos algo de esto en acción, tomaremos nuestro <strong>“gran ejemplo”</strong> jaja y lo separaremos en <strong>4 pequeños Interactors</strong> bajo el mando de un <strong>Organizer</strong>:</p>

<h3 id="1-interactor-para-encontrar-un-usuario">1. Interactor para encontrar un usuario</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/interactors/find_user_interactor.rb</span>
<span class="k">class</span> <span class="nc">FindUserInteractor</span>
  <span class="kp">include</span> <span class="no">Interactor</span>

  <span class="k">def</span> <span class="nf">call</span>
    <span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">email: </span><span class="n">context</span><span class="p">.</span><span class="nf">params</span><span class="p">[</span><span class="ss">:email</span><span class="p">])</span>

    <span class="k">if</span> <span class="n">user</span>
      <span class="n">context</span><span class="p">.</span><span class="nf">user</span> <span class="o">=</span> <span class="n">user</span>
    <span class="k">else</span>
      <span class="n">context</span><span class="p">.</span><span class="nf">fail!</span><span class="p">(</span><span class="ss">message: </span><span class="s1">'Invalid email or password'</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="2-interactor-para-autenticar-al-usuario">2. Interactor para autenticar al usuario</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/interactors/authenticate_user_interactor.rb</span>
<span class="k">class</span> <span class="nc">AuthenticateUserInteractor</span>
  <span class="kp">include</span> <span class="no">Interactor</span>

  <span class="k">def</span> <span class="nf">call</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">user</span>
    <span class="n">params</span> <span class="o">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">params</span>

    <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">authenticate</span><span class="p">(</span><span class="n">params</span><span class="p">[</span><span class="ss">:password</span><span class="p">])</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">account_locked?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
      <span class="c1"># Autenticación exitosa</span>
    <span class="k">else</span>
      <span class="n">handle_failed_login</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
      <span class="n">context</span><span class="p">.</span><span class="nf">fail!</span><span class="p">(</span><span class="ss">message: </span><span class="s1">'Invalid email or password'</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">account_locked?</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">failed_login_attempts</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">&gt;=</span> <span class="no">MAX_LOGIN_ATTEMPTS</span> <span class="o">&amp;&amp;</span> <span class="n">user</span><span class="p">.</span><span class="nf">locked_until</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">&gt;</span> <span class="no">Time</span><span class="p">.</span><span class="nf">now</span><span class="p">.</span><span class="nf">to_i</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">handle_failed_login</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="n">user</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">failed_login_attempts: </span><span class="n">user</span><span class="p">.</span><span class="nf">failed_login_attempts</span><span class="p">.</span><span class="nf">to_i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span>

    <span class="n">user</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">locked_until: </span><span class="mi">5</span><span class="p">.</span><span class="nf">minutes</span><span class="p">.</span><span class="nf">from_now</span><span class="p">)</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">failed_login_attempts</span> <span class="o">&gt;=</span> <span class="no">MAX_LOGIN_ATTEMPTS</span>

    <span class="p">{</span> <span class="ss">error: </span><span class="s1">'Invalid email or password'</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="3-interactor-para-generar-token-de-seguridad">3. Interactor para generar token de seguridad</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/interactors/generate_token_interactor.rb</span>
<span class="k">class</span> <span class="nc">GenerateTokenInteractor</span>
  <span class="kp">include</span> <span class="no">Interactor</span>

  <span class="k">def</span> <span class="nf">call</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">user</span>
    <span class="n">jwt</span> <span class="o">=</span> <span class="no">JwtManager</span><span class="p">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="n">context</span><span class="p">.</span><span class="nf">token</span> <span class="o">=</span> <span class="n">jwt</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="4-interactor-para-enviar-correo-de-seguridad">4. Interactor para enviar correo de seguridad</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/interactors/handle_security_email_interactor.rb</span>
<span class="k">class</span> <span class="nc">HandleSecurityEmailInteractor</span>
  <span class="kp">include</span> <span class="no">Interactor</span>

  <span class="k">def</span> <span class="nf">call</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">user</span>
    <span class="n">request</span> <span class="o">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">request</span>

    <span class="k">if</span> <span class="n">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">)</span>
      <span class="n">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="nf">remote_ip</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">first_login_from_new_ip?</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">ip</span><span class="p">)</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">user</span><span class="p">.</span><span class="nf">login_events</span><span class="p">.</span><span class="nf">exists?</span> <span class="ss">ip_address: </span><span class="n">ip</span>

    <span class="n">user</span><span class="p">.</span><span class="nf">login_events</span> <span class="o">&lt;&lt;</span> <span class="no">LoginEvent</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="ss">ip_address: </span><span class="n">ip</span><span class="p">)</span>
    <span class="kp">true</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">send_security_email</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
    <span class="no">UserMailer</span><span class="p">.</span><span class="nf">security_email</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="vi">@request_ip</span><span class="p">).</span><span class="nf">deliver_later</span>
  <span class="n">endend</span>
</code></pre></div></div>

<h3 id="el-organizer">El Organizer</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/interactors/authentication_organizer.rb</span>
<span class="k">class</span> <span class="nc">AuthenticationOrganizer</span>
  <span class="kp">include</span> <span class="no">Interactor</span><span class="o">::</span><span class="no">Organizer</span>

  <span class="n">organize</span> <span class="no">FindUserInteractor</span><span class="p">,</span>
           <span class="no">AuthenticateUserInteractor</span><span class="p">,</span>
           <span class="no">GenerateTokenInteractor</span><span class="p">,</span>
           <span class="no">HandleSecurityEmailInteractor</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="el-controller">El Controller</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/authentication_controller.rb</span>
<span class="k">class</span> <span class="nc">AuthenticationController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">create</span>
    <span class="n">result</span> <span class="o">=</span> <span class="no">AuthenticationOrganizer</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="ss">params: </span><span class="n">params</span><span class="p">,</span> <span class="ss">request: </span><span class="n">request</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="nf">success?</span>
      <span class="n">token</span> <span class="o">=</span> <span class="n">result</span><span class="p">.</span><span class="nf">token</span>
      <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">success: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">data: </span><span class="p">{</span> <span class="ss">token: </span><span class="n">token</span> <span class="p">}</span> <span class="p">}</span>
    <span class="k">else</span>
      <span class="n">error_message</span> <span class="o">=</span> <span class="n">result</span><span class="p">.</span><span class="nf">message</span>
      <span class="n">render</span> <span class="ss">json: </span><span class="p">{</span> <span class="ss">error: </span><span class="n">error_message</span> <span class="p">},</span> <span class="ss">status: :unauthorized</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><strong>Interactor</strong> es una opción más sencilla que <em>Dry.rb</em>. En esta biblioteca, se echa de menos la validación de parámetros en el <em>contexto</em>. El <em>contexto</em> puede variar significativamente desde el inicio hasta la finalización del flujo, pero se valora la simplicidad de la implementación. Te invito a revisar la <a href="https://github.com/collectiveidea/interactor">documentación</a>. Algunas funcionalidades a destacar:</p>

<ul>
  <li><strong>Hooks</strong>: Puedes ejecutar acciones antes, durante y después de la ejecución del interactor.</li>
  <li><strong>Rollback</strong>: Puedes definir un método de <em>rollback</em> en tu interactor. Si este está dentro de un organizador y alguno falla, se llama este método para cada interactor que se ejecutó. Muy útil en transacciones completas que modifican los datos.</li>
</ul>

<hr />

<p>Hasta aquí llegamos con las demostraciones de implementaciones. Puedes explorar otras gemas que te pueden ayudar con la implementación <a href="https://www.ruby-toolbox.com/categories/Service_Objects">aquí</a>.</p>

<hr />

<h2 id="lo-último">Lo último</h2>

<p>En pocas palabras, los Service Objects en Ruby on Rails son una herramienta esencial para mantener tu código limpio y organizado a medida que tu proyecto crece. Al encapsular la lógica de negocio en clases específicas, simplificas el proceso de desarrollo, haces que las pruebas sean más fáciles y aseguras que tu aplicación sea fácil de mantener y escalable.</p>

<p>Sin embargo, este viaje de mejora no termina aquí. Muchos de los conceptos que te he compartido pueden adaptarse a las necesidades específicas de tu aplicación. Espero que este artículo te haya proporcionado una valiosa perspectiva y herramientas para optimizar tu desarrollo.</p>

<p>Estoy a tu disposición para cualquier sugerencia, comentario o pregunta adicional. No dudes en escribir.</p>]]></content><author><name>Andrés</name></author><category term="[&quot;On Rails&quot;]" /><summary type="html"><![CDATA[Un Service Object es una clase de Ruby que agrupa la lógica de negocio de manera organizada en tu aplicación Ruby on Rails. Su uso simplifica el código, facilita las pruebas, fomenta la reutilización y eleva la calidad de tu aplicación.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://a-chacon.com/assets/images/service-objects.webp" /><media:content medium="image" url="https://a-chacon.com/assets/images/service-objects.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>