Consumiendo el API Rest de Marvel desde Clojure

logo-marvel

Hoy en día la necesidad de integrar unos sistemas con otros es innegable, hasta el punto de que términos como API, antes reservados a programadores, ahora están en boca de todos y se usan como una herramienta de marketing más (todos sabemos que un proyecto no es serio si no se despliega en la nube y cuenta con un API de integración).

Por suerte o por desgracia (lo siento, José Juan), la mayoría de APIs que nos encontramos actualmente son APIs REST (o algo parecido a REST) que mueven datos serializados en JSON utilizando el protocolo HTTP. Casi todos los lenguajes cuentan con librerías más o menos especializadas para consumir este tipo de APIs, y en este post vamos a ver lo sencillo que es podemos consumir un API REST desde Clojure

El API REST de Marvel

Desde hace un tiempo Marvel tiene publicada un API para acceder a la información de sus cómics. Este API es gratuita (con algunas limitaciones) y expone un montón de información para los fanáticos de los cómics (entre los que debo reconocer que no me incluyo), por lo que nos viene muy bien para jugar un poco con ella.

Para poder utilizarla, deberemos darlos de alta como desarrollador y nos proporcionará dos claves, una pública y una privada, que podremos utilizar para hacer nuestras peticiones. La documentación es muy completa y explica bastante bien todo el proceso.

Lo más importante que debemos tener en cuenta es que para realizar una petición deberemos añadir a la URL correspondiente los siguientes parámetros:

  • ts: es un timestamp que nos sirve para identificar la llamada y que debe ser único en cada llamada.
  • apikey: es nuestra clave pública.
  • hash: es el hash md5 del texto resultando de concatenar el timestamp, nuestra clave pública y nuestra clave privada.

Los datos devueltos por la API van envueltos en varios objetos JSON, pero lo que realmente interesa está dentro de la propiedad data del objeto devuelto, donde encontraremos propiedades necesarias para la paginación de resultados (offset, limit, count y total) y los resultados propiamente dichos (results).

Consumiendo el API desde Clojure

Después de esta rápida (e incompleta) introducción al API de Marvel, vamos a ver cómo podemos consumirla desde Clojure. Para consumir el API usaremos la librería clj-http que es un wrapper sobre los HttpComponentes de Apache. Deberemos añadirla como dependencia en el fichero project.clj:

(defproject ...
  :dependencies [[org.clojure/cloure "1.5.1"]
                 [clj-http "0.9.2"]])

Una vez incluida la dependencia, tendremos que referenciarla en nuestro código:

(ns marvel-clj.core
  (:require [clj-http.client :as http]))

Partiendo de la manera de usar la API que explicábamos antes, es fácil imaginarse que lo más complicado es casi construir la URL. Para ello, vamos a definir algunas funciones de apoyo:

(def public-key "YOUR_PUBLIC_KEY_HERE")
(def private-key "YOUR_PRIVATE_KEY_HERE")

(def base-url "http://gateway.marvel.com/v1/public")

(defn md5 
  (let [d (.. (doto (java.security.MessageDigest/getInstance "MD5")
                .reset
                (.update (.getBytes text)))
              digest)]
    (apply str (map (partial format "%02x") d))))

(defn build-url [path]
  (let [ts (quot (System/currentTimeMillis) 1000)
        hash (md5 (str ts private-key public-key))]
    (str base-url path "?ts=" ts "&apikey=" public-key "&hash=" hash)))

Lo más feo de todo ese código es tener que usar java interop para generar el MD5. Existen librerías «nativas» de Clojure para ello, pero he preferido no meter más dependencias externas. Seamos positivos, gracias a ello podemos ver el uso de la macro doto para realizar varias acciones sobre un mismo objeto java y la macro .. para encadenar llamadas al estilo de -> (está claro que al diseñar clojure no pensaron mucho en el SEO). Salvado el tema del MD5, construir la URL consiste en concatenar unos cuantos strings y poco más.

Para ayudarnos a invocar el API, podemos crear una función que se encargue de construir la url correctamente, parsear el json resultante y quedarse con la parte interesante.

Suena bastante más complicado de lo que es:

(defn get-json [path]
  (get-in (http/get (build-url path) {:as :json}) [:body :data]))

La función get nos permite realizar peticiones HTTP GET a una URL y podemos pasarle un map con distintas opciones. En este caso, le estamos indicando que queremos que interprete la respuesta como json con :as :json. Eso nos devolvera el json de la respuesta representando como un map de Clojure, por lo que podemos tratarlo con las funciones típicas de maps, por ejemplo get-in para acceder a valores anidados (sería algo así como hacer result.body.data en javascript).

Una vez montada esta tremenda infraestructura de unas 15 líneas de código, consumir el API es un juego de niños. Por ejemplo podríamos tener las siguientes funciones para acceder a los comics, los personajes o la información de un personaje:

(defn get-comics []
  (get-json "/comics"))

(defn get-characters []
  (get-json "/characters"))

(defn get-character [id]
  (get-json (str "/characters/" id)))

Obviamente esto no es más que un ejemplo y si quisiéramos usar esto de verdad deberíamos tener en cuenta aspectos críticos como la paginación, pero creo que es fácil hacerse una idea de cómo sería. De todas formas, si quieres ver cómo se podría implementar, puedes ver el código completo en este gist.

Resumen

Consumir un API REST desde Clojure es muy sencillo desde el punto de vista técnico. A fin de cuentas, el formato json se ajusta muy bien a las estructuras de datos de clojure (maps y secuencias) y un protocolo tan orientado a datos como REST encaja muy bien con la filosofía de datos y funciones de Clojure.

La parte complicada de cualquier API REST no es tanto consumirla, sino consumirla correctamente. Como se explica en el post de José Juan que citaba al principio, un API REST no tiene una especificación formal que pueda ser validada mecánicamente, por lo que a la hora de consumirla (y de mantenerla) es necesario ser más cuidadoso.