En mis dos anterior blog posts he presentado Ratpack y la capa ODM ligera disponible en nuestro SDK de Java. Este post se basará en ellos y mostrará una API REST para gestionar usuarios a través del repositorio Couchbase.
Quiero que mi API soporte los cuatro verbos HTTP básicos y soporte content-type. Todo ha sido pensado para eso en Ratpack. La dirección Cadena es como un constructor para componer manejadores. Así que escribí un nuevo Acción para enlazar una cadena al prefijo 'user' URL. Esto simplificará mucho mi método principal:
|
1 2 3 4 5 6 |
público estático void principal(Cadena... args) lanza Excepción { RxRatpack.inicializar(); RatpackServer.iniciar(servidor -> servidor.registro(Guice.registro(b -> b.módulo(Configurar.clase))) .manipuladores(cadena -> cadena.prefijo("usuario", UserHandler.clase))); } |
Para asegurarme de que mi instancia de UserHandler puede ser encontrada, también necesito añadirla a mi módulo Config Guice:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
público clase Configurar extiende MóduloAbstracto { protegido void configure() { CouchbaseCluster cc = CouchbaseCluster.crear(); Cubo cubo = cc.openBucket(); vincular(AsyncBucket.clase).toInstance(cubo.async()); vincular(AsyncRepository.clase).toInstance(cubo.repositorio().async()); vincular(UserHandler.clase); vincular(UserRenderer.clase); } } |
También añado la clase UserRenderer que se utilizará para manejar los diferentes tipos de contenido.
Renderizador personalizado
En Ratpack puede registrar nuevos RendererSupport . Su objetivo es especificar una forma de renderizar su objeto T basándose en la clase Contexto. En mi caso quiero renderizar un objeto Usuario basado en el tipo de contenido del contexto. El objeto Context te da un porContenido que permite componer el renderizado basándose en el tipo de contenido establecido en la solicitud. Los tipos habituales ya están predefinidos. En mi caso solo quiero soportar json y texto:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
público clase UserRenderer extiende RendererSupport<Usuario> { @Anular público void render(Contexto contexto, Usuario usuario) lanza Excepción { contexto.porContenido(byContentSpec -> byContentSpec .json(() -> contexto.render(Jackson.json(usuario))) .plainText(() -> contexto.render(usuario.toString())) ); } } |
La representación del texto es un simple toString(). La versión JSON utiliza el objeto Jackson disponible por defecto en Ratpack. Te da acceso a un atajo para conversiones JSON/Objeto. Una llamada al método json con el objeto User como argumento es suficiente. Jackson también es utilizado internamente por nuestro Java SDK. Pero hay un truco. La anotación @Field que utilicé para mi ODM Couchabse no es recogida por el mapeador Jackson por defecto utilizado en Ratpack. Así que tengo que añadir la anotación @JsonProperty de Jackson para compensar. No habría tenido que añadir nada si no hubiera utilizado la anotación @Field en primer lugar. Voy a tratar de encontrar una mejor manera de hacer que esto funcione, pero mientras tanto funciona muy bien.
|
1 2 3 4 5 6 7 8 |
@Campo("fName") @JsonProperty("fName") privado Cadena firstName; @Campo("lNombre") @JsonProperty("lNombre") privado Cadena apellido; |
Componer la API con manejadores
Ahora estoy en una buena posición para empezar a trabajar en la API. GET, PUT y DELETE necesitan un identificador de usuario para funcionar. Así que lo primero que estoy haciendo es comprobar si hay algo después de la URL /user/. Los handlers están todos encadenados y se ejecutan en el orden en que los declaras. Una vez que entras en un manejador, la cadena se detiene. Así que asegúrate de declarar /user/:userId antes de /user/. La vinculación de rutas usa regex, encontrarás ejemplos en la sección Cadena documentación.
Llamada al ruta me permite dar el path regex y un handler como argumento. En el Handler empiezo obteniendo el repositorio Couchbase y el userId de los tokens de la ruta. Luego llamo al método byMethod para definir una función para cada verbo HTTP que necesito soportar. Aquí devolveré un usuario para GET, actualizaré o crearé un usuario para PUT y eliminaré el usuario para REMOVE.
El verbo más interesante aquí es PUT, ya que requiere contenido de la solicitud. El contexto analizar toma un método Analice como argumento y devuelve una Promise. Aquí quiero parsear el JSON de la petición y mapearlo a un objeto User. Así que devuelve una Promise. Como soy usuario de RxJava mapeo esa Promise a un Observable, luego mapeo el objeto User a un EntityDocument para finalmente guardarlo en el repositorio de Couchbase. Luego convierto ese observable de nuevo en una promesa ratpack y envío de vuelta una simple cadena OK. Puede que quieras hacer algo más inteligente en casos reales :)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
cadena.ruta(":userId", ctx -> { AsyncRepository repo = ctx.consiga(AsyncRepository.clase); Cadena userId = ctx.getPathTokens().consiga("userId"); ctx.byMethod(methodSpec -> { methodSpec.consiga(() -> { Observable<DocumentoEntidad<Usuario>> usuario = repo.consiga(userId, Usuario.clase); RxRatpack.promesa(usuario).entonces(u -> ctx.render(u.consiga(0).contenido())); }).poner(() -> { Promesa<Usuario> promesa = ctx.analizar(Jackson.fromJson(Usuario.clase)); Observable<DocumentoEntidad<Usuario>> observable = RxRatpack.observe(promesa) .mapa(usuario -> DocumentoEntidad.crear(usuario)).flatMap(doc -> repo.upsert(doc)); RxRatpack.promesa(observable).entonces(doc -> ctx.render("OK")); }).borrar(() -> { Observable<DocumentoEntidad<Usuario>> usuario = repo.eliminar(userId, Usuario.clase); RxRatpack.promesa(usuario).entonces(u -> ctx.render("OK")); }); }); }); |
Una vez implementados los verbos HTTP que necesitan un userId, puedo terminar con todos los demás. Y puedo hacerlo fácilmente con el método todos. Aquí obtengo el repositorio y el bucket del registro. Luego para el método GET, como no tiene id de usuario, devuelvo la lista completa de Usuarios. Para ello ejecuto una consulta básica N1QL. Finalmente el POST es idéntico al método PUT.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
cadena.todos(ctx -> { AsyncBucket cubo = ctx.consiga(AsyncBucket.clase); AsyncRepository repo = ctx.consiga(AsyncRepository.clase); ctx.byMethod(methodSpec -> { methodSpec.consiga(() -> { N1qlQuery consulta = N1qlQuery.simple("SELECT Meta().id as username,age,fName,lName FROM default WHERE type = 'user'"); Observable<Usuario> observable = cubo.consulta(consulta) .flatMap(qRes -> qRes.filas()).mapa(fila -> { JsonObject jo = fila.valor(); Cadena nombre de usuario = jo.getString("nombre de usuario"); Entero edad = jo.getInt("edad"); Cadena fName = jo.getString("fName"); Cadena lNombre = jo.getString("lNombre"); devolver nuevo Usuario(nombre de usuario, edad, fName, lNombre); }); RxRatpack.promesa(observable).entonces(usuarios -> ctx.render(Jackson.json(usuarios))); }).Correo electrónico:(() -> { Promesa<Usuario> promesa = ctx.analizar(Jackson.fromJson(Usuario.clase)); Observable<DocumentoEntidad<Usuario>> observable = RxRatpack.observe(promesa) .mapa(usuario -> DocumentoEntidad.crear(usuario)).flatMap(doc -> repo.upsert(doc)); RxRatpack.promesa(observable).entonces(doc -> ctx.render("OK")); }); }); }); |
Puede que no sea una API totalmente RESTFUL, pero debería darte una buena idea de lo fácil que es crear una con Ratpack, Couchbase y RxJava. Por favor, házmelo saber si quieres ver más cosas sucediendo alrededor de Ratpack, como un Módulo Couchbase.