Em meus dois anterior blog Nas postagens, apresentei o Ratpack e a camada ODM leve disponível em nosso Java SDK. Esta postagem se baseará neles e apresentará uma API REST para gerenciar usuários por meio do repositório do Couchbase.
Quero que minha API seja compatível com os quatro verbos HTTP básicos e com o tipo de conteúdo. Tudo foi pensado para isso no Ratpack. O Cadeia é como um construtor para compor manipuladores. Então, escrevi um novo objeto Ação para vincular uma cadeia ao prefixo de URL 'user'. Isso simplificará bastante meu método principal:
|
1 2 3 4 5 6 |
public static void main(String... args) throws Exception { RxRatpack.initialize(); RatpackServer.start(server -> server.registry(Guice.registry(b -> b.module(Config.class))) .handlers(chain -> chain.prefix("user", UserHandler.class))); } |
Para garantir que minha instância UserHandler possa ser encontrada, também preciso adicioná-la ao meu módulo Config Guice:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Config extends AbstractModule { protected void configure() { CouchbaseCluster cc = CouchbaseCluster.create(); Bucket bucket = cc.openBucket(); bind(AsyncBucket.class).toInstance(bucket.async()); bind(AsyncRepository.class).toInstance(bucket.repository().async()); bind(UserHandler.class); bind(UserRenderer.class); } } |
Também adiciono a classe UserRenderer que será usada para lidar com os diferentes tipos de conteúdo.
Renderizador personalizado
No Ratpack, você pode registrar novos RendererSupport classes. Seu objetivo é especificar uma maneira de renderizar seu objeto T com base na classe Contexto. No meu caso, quero renderizar um objeto User com base no tipo de conteúdo do contexto. O objeto Context fornece a você um porConteúdo que permite compor a renderização com base no tipo de conteúdo definido na solicitação. Os tipos usuais já estão predefinidos. No meu caso, quero apenas oferecer suporte a json e texto:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
public class UserRenderer extends RendererSupport<User> { @Override public void render(Context context, User user) throws Exception { context.byContent(byContentSpec -> byContentSpec .json(() -> context.render(Jackson.json(user))) .plainText(() -> context.render(user.toString())) ); } } |
A renderização de texto é apenas um simples toString(). A versão JSON usa o objeto Jackson disponível por padrão no Ratpack. Ele lhe dá acesso a um atalho para conversões JSON/Objeto. Uma chamada para o método json fornecendo o objeto User como argumento é suficiente. O Jackson também é usado internamente pelo nosso Java SDK. Mas há um truque. A anotação @Field que usei no meu ODM do Couchabse não é captada pelo mapeador Jackson padrão usado no Ratpack. Portanto, preciso adicionar a anotação Jackson @JsonProperty para compensar. Eu não teria que adicionar nada se não tivesse usado a anotação @Field em primeiro lugar. Tentarei encontrar uma maneira melhor de fazer isso funcionar, mas, enquanto isso, está funcionando muito bem.
|
1 2 3 4 5 6 7 8 |
@Field("fName") @JsonProperty("fName") private String firstName; @Field("lName") @JsonProperty("lName") private String lastName; |
Composição da API com manipuladores
Agora estou em uma boa posição para começar a trabalhar na API. GET, PUT e DELETE precisam de um ID de usuário para funcionar. Portanto, a primeira coisa que estou fazendo é verificar se há algo após o URL /user/. Os manipuladores são todos encadeados e executados na ordem em que você os declara. Quando você entra em um manipulador, a cadeia é interrompida. Portanto, você deve certificar-se de declarar /user/:userId antes de /user/. A vinculação de caminho usa regex, e você encontrará exemplos na seção Cadeia documentação.
Chamando o caminho permite que eu forneça o regex do caminho e um manipulador como argumento. No manipulador, começo obtendo o repositório do Couchbase e o userId dos tokens de caminho. Em seguida, chamo o método porMétodo para definir uma função para cada verbo HTTP que preciso suportar. Aqui, retornarei um usuário para GET, atualizarei ou criarei um usuário para PUT e removerei o usuário para REMOVE.
O verbo mais interessante aqui é o PUT, pois ele requer conteúdo da solicitação. O contexto analisar recebe um Analisar como argumento e retorna uma Promise. Aqui eu quero analisar o JSON da solicitação e mapeá-lo para um objeto User. Portanto, ele retorna uma Promise. Como sou um usuário do RxJava, mapearei essa Promise para um Observable e, em seguida, mapearei o objeto User para um EntityDocument para finalmente salvá-lo no repositório do Couchbase. Em seguida, converto esse observável novamente em uma promessa do ratpack e envio de volta uma string OK simples. Talvez você queira fazer algo mais inteligente em casos reais :)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
chain.path(":userId", ctx -> { AsyncRepository repo = ctx.get(AsyncRepository.class); String userId = ctx.getPathTokens().get("userId"); ctx.byMethod(methodSpec -> { methodSpec.get(() -> { Observable<EntityDocument<User>> user = repo.get(userId, User.class); RxRatpack.promise(user).then(u -> ctx.render(u.get(0).content())); }).put(() -> { Promise<User> promise = ctx.parse(Jackson.fromJson(User.class)); Observable<EntityDocument<User>> observable = RxRatpack.observe(promise) .map(user -> EntityDocument.create(user)).flatMap(doc -> repo.upsert(doc)); RxRatpack.promise(observable).then(doc -> ctx.render("OK")); }).delete(() -> { Observable<EntityDocument<User>> user = repo.remove(userId, User.class); RxRatpack.promise(user).then(u -> ctx.render("OK")); }); }); }); |
Quando os verbos HTTP que precisam de um userId forem implementados, poderei concluir todos os outros. E posso fazer isso facilmente com o método todos. Aqui eu obtenho o repositório e o bucket do registro. Em seguida, para o método GET, como ele não tem identificação de usuário, retorno a lista completa de usuários. Para isso, executo uma consulta N1QL básica. Por fim, o método POST é idêntico ao 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 |
chain.all(ctx -> { AsyncBucket bucket = ctx.get(AsyncBucket.class); AsyncRepository repo = ctx.get(AsyncRepository.class); ctx.byMethod(methodSpec -> { methodSpec.get(() -> { N1qlQuery query = N1qlQuery.simple("SELECT Meta().id as username,age,fName,lName FROM default WHERE type = 'user'"); Observable<User> observable = bucket.query(query) .flatMap(qRes -> qRes.rows()).map(row -> { JsonObject jo = row.value(); String username = jo.getString("username"); Integer age = jo.getInt("age"); String fName = jo.getString("fName"); String lName = jo.getString("lName"); return new User(username, age, fName, lName); }); RxRatpack.promise(observable).then(users -> ctx.render(Jackson.json(users))); }).post(() -> { Promise<User> promise = ctx.parse(Jackson.fromJson(User.class)); Observable<EntityDocument<User>> observable = RxRatpack.observe(promise) .map(user -> EntityDocument.create(user)).flatMap(doc -> repo.upsert(doc)); RxRatpack.promise(observable).then(doc -> ctx.render("OK")); }); }); }); |
Essa pode não ser uma API totalmente RESTFUL, mas deve lhe dar uma boa ideia de como é fácil criar uma com o Ratpack, o Couchbase e o RxJava. Se você quiser ver mais coisas acontecendo com o Ratpack, como um módulo do Couchbase, entre em contato comigo.