# NATS APP Lectura de datos de sensores en un dispositivo IoT. Prueba técnica para optar por el puesto de programador Go. ## TL:DR - make lazy-start - abre terminal y escribe: `nats sub sensors.data.*` ## Requisitos previos - Docker - NATS CLI - Make, si prefieres la comodidad de usar Makefile ## Comandos ### Registrar un sensor - Campos obligatorios: `sensor_id` y `sensor_type`. - Campos opcionales: `sampling`, `thresholdabove` y `thresholdbelow`. `nats req sensors.register '{ "sensor_id": "sensor-001", "sensor_type": "temperature", "sampling": 3600, "thresoldabove": 50.0, "thresoldbelow": -10.0 }'` ### Actualizar configuración sensor - Campo obligatorio: `sensor_id` y la presencia de al menos un parámetro. `nats req sensors.update '{ "sensor_id": "sensor-001", "sensor_type": "temperature", "sampling": 10, "thresoldabove": 100.0, "thresoldbelow": -15.0 }'` ### Obtener información de un sensor - Campo obligatorio: `sensor_id`. `nats req sensors.get '{ "sensor_id": "sensor-001" }'` ### Obtener valores de un sensor - Campo obligatorio: `sensor_id`. - Campos opcionales: `from` y `to` en formato RFC3339. Si no se especifican, se toman los últimos 7 días. `nats req sensors.values.get '{ "sensor_id": "sensor-001", "from": "2025-10-03T00:00:00Z", "to": "2025-10-10T23:59:59Z" }'` ### Obtener listado de sensores No hay _payload_, pero hay que poner comillas dobles o si no se queda esperando una entrada de datos. `nats req sensors.list ""` ### Suscribirse a un sensor `nats sub sensors.data.sensor-001` ### Suscribirse a todos los sensores `nats sub sensors.data.*` ## Consideraciones Hay partes de códigos que son _snippets_ extraídos de una librería de autoría propia. [Repositorio GitHub](https://github.com/zepyrshut/gopher-toolbox). De las cuales son: - El _logger_ usando la _stdlib log/slog_. - La conexión con la base de datos, usando el controlador [pgx](https://github.com/jackc/pgx). ## Bitácora ### Quickstart y toma de contacto con NATS Lo primero que he hecho es un _quickstart_ del proyecto, con lo que siempre o casi siempre pongo en mis experimentos. Y lo siguiente, en lugar de empezar a construir el proyecto como loco he tratado de entender cómo funciona NATS. Al final ha sido muy sencillo, siguiendo esos pasos: 1. Levantar el servidor NATS en Docker 2. Instalar el CLI de NATS 2. Abrir un puñado de terminales, y en un par de ellas escribir: `nats sub "hello"` lo cual significa que se está suscribiendo al canal `hello`. Y en otra escribir: `nats pub "hello" "Hola mundo!"`, lo cual significa que está escribiendo el mensaje `Hola mundo!` en el canal `hello`. ![demo de NATS](./assets/nats-demo-1.png) ### Creación de la aplicación La parte fácil es hacer todo el _boilerplate_, hecho en solo _commit_, que no es lo ideal pero lo dicho, es puro relleno, mucho código ya viene escrito de la librería `gopher-toolbox`. En este punto el proyecto compila pero hay error en tiempo de ejecución por la falta de implementaciones en el repositorio. ### Empezando por el repositorio El almacenamiento de los datos se ha optado por el uso de `TimescaleDB`, una extensión de `PostgreSQL`, el motivo es porque ya he trabajado mucho con ese motor y tengo los _drivers_ ya escritos. Tengo entendido que está optimizado para trabajar con grandes cantidades de datos de series temporales, lo que viene siendo valores de sensores por ejemplo. Por otro lado también hay un sistema de caché muy rudimentario, en memoria que es un mapa de valores. Para el registro de valores y mantener ambos se ha usado el patrón decorador que bajo un mismo _struct_ se incluye las dos implementaciones y registra cambios en ambas partes. Desde la capa servicios sólo tiene que llamar al decorador sin saber los detalles de la implementación. ### Continuamos con los servicios Con el repositorio sin implementar, se puede realizar los servicios. En ese proyecto ha quedado muy básico, sirviendo solamente de enlace entre los controladores y el repositorio. Cuando se creó el _broker_ de NATS, inicialmente fue para crear una interfaz de mensajería donde se pudiese manejar _websockets_, _SSE_ y otras mensajerías pero se consideró que había otras prioridades. Así se quedó. ### Y finalmente los controladores Ahí tuve muchas dudas con el entendimiento de NATS, estaba muy arraigado en el patrón REST, y cambiar la mentalidad costó un poco, al final mirando un poco la documentación me quedé con los conceptos clave: 1. Está basado en asuntos, los canales se crean de forma jerárquica. 2. Cuando se hace un _subscribe_, se suma a un canal del asunto dado. 3. Para escribir en el canal, hay que hacer el _publish_. 4. Finalmente para solicitar un recurso, está el _request_. Esto es todo, entonces los controladores de la entidad _sensors_ están constituidos por una serie de _endpoints_ haciendo las acciones que se solicita. ### El simulador Basada en _gorutinas_ y canales, cuando se inicia el simulador, se crea un canal para detener simuladores que están en ejecución para su actualización o detención. Cuando se registra un nuevo sensor, está la función SimulateSensor, que se inicia como una _gorutina_ y usa el `SamplingInterval` para el canal `ticker`, así llamar a `generateData` cada vez que toque. Una vez que el dato está generado se hace una publicación al asunto _sensor.data_, que al mismo tiempo, el _handler_ registerData lo captura al estar registrado al mismo asunto _sensor.data_. ## Pruebas La realización de pruebas unitarias de lo que son los controladores de NATS me han sido imposible hacerlas en condiciones, podría haber usado Claude pero es que no daba pie con bola y no entendía nada, así que por la máxima transparencia he optado por no incorporarlas. Las pruebas más interesantes son las de reglas de negocio y validación, lo que viene a ser los servicios y dominio. ## Generadores y otras librerías Existen generadores de código para Golang, de hecho, se fomenta su desarrollo, hay un artículo interesante de Rob Pike [hablando sobre ello](https://go.dev/blog/generate). Muchas de las herramientas son muy interesantes usarlas ya que acelera mucho la generación de código repetitivo. Da mas confianza usar esas herramientas que la IA. Para las consultas SQL con seguridad de tipos, existe la herramienta [sqlc](https://sqlc.dev/). Para ese proyecto sólo se ha usado [GoMock](https://github.com/uber-go/mock), mantenida por Uber y sirve para usar la interfaz `Repository` sin usar una base de datos real. Por otro lado, hubiese sido interesante incorporar [ginkgo](https://onsi.github.io/ginkgo/), es un marco de trabajo de pruebas unitarias usando un lenguaje de dominio específico (DSL). > En lugar de escribir una prueba así: `Test_Validate(t *testing.T) { ... }` > se puede hacer de la siguiente manera: `var _ Describe("Models", func () { ... })`, > y dentro del cuerpo se describen las pruebas a realizar. No se ha incorporado porque hay que instalar la herramienta que ejecutan las pruebas, y no quería correr el riesgo de que no funcionase en otro equipo o no diesen los resultados esperados. Que se podría haber usado un contenedor Docker, sí, pero la prueba no consiste en eso. También se ha planteado incorporar la librería _testify_, descartado porque para comprobar si existe el error y algunas comparaciones no era necesario meter una dependencia más. ## Diagrama ![diagrama app](./assets/app-architecture.jpg) El diagrama explica básicamente la estrucutra del proyecto en términos generales, se demuestra que el dominio sensors solo se comunica al exterior mediante el NATS y el _logger_. Amarillo: exterior. Morado: infraestructura. Verde: dominio Blanco: simulador, los _handlers_ está conectado sólo para llamar a la gorutina, pero en la realidad debería ir independiente. ## Conclusión y cierre Interesante reto donde realmente lo que más me ha costado son la realización de pruebas unitarias, hay veces que me cuesta coger el concepto. Tengo la teoría muy clara, pero a la hora de la verdad se me complica un poco las cosas. Además que _mockear_ el sistema de mensajería debe tener su especial complejidad. También ha sido algo complejo entender la concurrencia y los canales, no es algo que haya trabajado en profundidad pero sí que se le ha puesto bastante empeño, cariño y nivel de detalle. He puesto mucho en valor la arquitectura limpia con un toque personal, no es DDD puro ya que hay elementos que no deberían estar en dominio, pero en el proyecto que estoy trabajando ahora mismo se está diseñando de la misma manera y está funcionando muy bien. También se ha evitado todo lo posible el uso de LLMs para la generación de código, y su uso ha sido para la toma de decisiones arquitectónicas, discusión y lectura rápida sobre los distintos funcionamientos de algunas librerías. En más de una ocasión he cuestionado las respuestas que da, teniendo que verificar con la documentación oficial. Pongo en valor mi capacidad para aprovechar la IA de la mejor forma posible, verificando la información, además recalco que justo el proyecto actual es una migración de un código en PHP completamente hecho con IA, y se puede ver patrones y errores comunes que comete. Soy consciente de que hay margen de mejora, por ejemplo con los tests o con la documentación, se ha puesto especial esfuerzo y atención a que los nombres de las funciones, variables, métodos, estructuras y paquetes sean lo más autodescriptivos posibles. Se han puesto algunos comentarios. También hay esfuerzo por permitir ejecutar el proyecto por primera vez con la mínima intervención. Un problema interesante que tuve que resolver, que como el sensor puede mandar un valor ausente, el tipo `float64` al hacer el `unmarshal` se establece a 0.0, con lo que se puede considerar válido, con lo cual su solución fue el uso de puntero, si se descubre que es `nil` se considera no válido. Digamos que este proyecto resuelve el problema que se propone, un sistema que permite registrar y actualziar un sensor. Se puede ver su estado y los datos que se recogen (simulados) se guardan en una base de datos. Espero que el proyecto sea de vuestro agrado y podamos tener una siguiente reunión.